jsonld
Version:
A JSON-LD Processor and API implementation in JavaScript.
1,163 lines (1,062 loc) • 38.4 kB
JavaScript
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';
const JsonLdError = require('./JsonLdError');
const {
isArray: _isArray,
isObject: _isObject,
isString: _isString,
isUndefined: _isUndefined
} = require('./types');
const {
isList: _isList,
isValue: _isValue,
isGraph: _isGraph,
isSimpleGraph: _isSimpleGraph,
isSubjectReference: _isSubjectReference
} = require('./graphTypes');
const {
expandIri: _expandIri,
getContextValue: _getContextValue,
isKeyword: _isKeyword,
process: _processContext,
processingMode: _processingMode
} = require('./context');
const {
removeBase: _removeBase,
prependBase: _prependBase
} = require('./url');
const {
REGEX_KEYWORD,
addValue: _addValue,
asArray: _asArray,
compareShortestLeast: _compareShortestLeast
} = require('./util');
const api = {};
module.exports = api;
/**
* Recursively compacts an element using the given active context. All values
* must be in expanded form before this method is called.
*
* @param activeCtx the active context to use.
* @param activeProperty the compacted property associated with the element
* to compact, null for none.
* @param element the element to compact.
* @param options the compaction options.
*
* @return a promise that resolves to the compacted value.
*/
api.compact = async ({
activeCtx,
activeProperty = null,
element,
options = {}
}) => {
// recursively compact array
if(_isArray(element)) {
let rval = [];
for(let i = 0; i < element.length; ++i) {
const compacted = await api.compact({
activeCtx,
activeProperty,
element: element[i],
options
});
if(compacted === null) {
// FIXME: need event?
continue;
}
rval.push(compacted);
}
if(options.compactArrays && rval.length === 1) {
// use single element if no container is specified
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
if(container.length === 0) {
rval = rval[0];
}
}
return rval;
}
// use any scoped context on activeProperty
const ctx = _getContextValue(activeCtx, activeProperty, '@context');
if(!_isUndefined(ctx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: ctx,
propagate: true,
overrideProtected: true,
options
});
}
// recursively compact object
if(_isObject(element)) {
if(options.link && '@id' in element &&
options.link.hasOwnProperty(element['@id'])) {
// check for a linked element to reuse
const linked = options.link[element['@id']];
for(let i = 0; i < linked.length; ++i) {
if(linked[i].expanded === element) {
return linked[i].compacted;
}
}
}
// do value compaction on @values and subject references
if(_isValue(element) || _isSubjectReference(element)) {
const rval =
api.compactValue({activeCtx, activeProperty, value: element, options});
if(options.link && _isSubjectReference(element)) {
// store linked element
if(!(options.link.hasOwnProperty(element['@id']))) {
options.link[element['@id']] = [];
}
options.link[element['@id']].push({expanded: element, compacted: rval});
}
return rval;
}
// if expanded property is @list and we're contained within a list
// container, recursively compact this item to an array
if(_isList(element)) {
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
if(container.includes('@list')) {
return api.compact({
activeCtx,
activeProperty,
element: element['@list'],
options
});
}
}
// FIXME: avoid misuse of active property as an expanded property?
const insideReverse = (activeProperty === '@reverse');
const rval = {};
// original context before applying property-scoped and local contexts
const inputCtx = activeCtx;
// revert to previous context, if there is one,
// and element is not a value object or a node reference
if(!_isValue(element) && !_isSubjectReference(element)) {
activeCtx = activeCtx.revertToPreviousContext();
}
// apply property-scoped context after reverting term-scoped context
const propertyScopedCtx =
_getContextValue(inputCtx, activeProperty, '@context');
if(!_isUndefined(propertyScopedCtx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: propertyScopedCtx,
propagate: true,
overrideProtected: true,
options
});
}
if(options.link && '@id' in element) {
// store linked element
if(!options.link.hasOwnProperty(element['@id'])) {
options.link[element['@id']] = [];
}
options.link[element['@id']].push({expanded: element, compacted: rval});
}
// apply any context defined on an alias of @type
// if key is @type and any compacted value is a term having a local
// context, overlay that context
let types = element['@type'] || [];
if(types.length > 1) {
types = Array.from(types).sort();
}
// find all type-scoped contexts based on current context, prior to
// updating it
const typeContext = activeCtx;
for(const type of types) {
const compactedType = api.compactIri(
{activeCtx: typeContext, iri: type, relativeTo: {vocab: true}});
// Use any type-scoped context defined on this value
const ctx = _getContextValue(inputCtx, compactedType, '@context');
if(!_isUndefined(ctx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: ctx,
options,
propagate: false
});
}
}
// process element keys in order
const keys = Object.keys(element).sort();
for(const expandedProperty of keys) {
const expandedValue = element[expandedProperty];
// compact @id
if(expandedProperty === '@id') {
let compactedValue = _asArray(expandedValue).map(
expandedIri => api.compactIri({
activeCtx,
iri: expandedIri,
relativeTo: {vocab: false},
base: options.base
}));
if(compactedValue.length === 1) {
compactedValue = compactedValue[0];
}
// use keyword alias and add value
const alias = api.compactIri(
{activeCtx, iri: '@id', relativeTo: {vocab: true}});
rval[alias] = compactedValue;
continue;
}
// compact @type(s)
if(expandedProperty === '@type') {
// resolve type values against previous context
let compactedValue = _asArray(expandedValue).map(
expandedIri => api.compactIri({
activeCtx: inputCtx,
iri: expandedIri,
relativeTo: {vocab: true}
}));
if(compactedValue.length === 1) {
compactedValue = compactedValue[0];
}
// use keyword alias and add value
const alias = api.compactIri(
{activeCtx, iri: '@type', relativeTo: {vocab: true}});
const container = _getContextValue(
activeCtx, alias, '@container') || [];
// treat as array for @type if @container includes @set
const typeAsSet =
container.includes('@set') &&
_processingMode(activeCtx, 1.1);
const isArray =
typeAsSet || (_isArray(compactedValue) && expandedValue.length === 0);
_addValue(rval, alias, compactedValue, {propertyIsArray: isArray});
continue;
}
// handle @reverse
if(expandedProperty === '@reverse') {
// recursively compact expanded value
const compactedValue = await api.compact({
activeCtx,
activeProperty: '@reverse',
element: expandedValue,
options
});
// handle double-reversed properties
for(const compactedProperty in compactedValue) {
if(activeCtx.mappings.has(compactedProperty) &&
activeCtx.mappings.get(compactedProperty).reverse) {
const value = compactedValue[compactedProperty];
const container = _getContextValue(
activeCtx, compactedProperty, '@container') || [];
const useArray = (
container.includes('@set') || !options.compactArrays);
_addValue(
rval, compactedProperty, value, {propertyIsArray: useArray});
delete compactedValue[compactedProperty];
}
}
if(Object.keys(compactedValue).length > 0) {
// use keyword alias and add value
const alias = api.compactIri({
activeCtx,
iri: expandedProperty,
relativeTo: {vocab: true}
});
_addValue(rval, alias, compactedValue);
}
continue;
}
if(expandedProperty === '@preserve') {
// compact using activeProperty
const compactedValue = await api.compact({
activeCtx,
activeProperty,
element: expandedValue,
options
});
if(!(_isArray(compactedValue) && compactedValue.length === 0)) {
_addValue(rval, expandedProperty, compactedValue);
}
continue;
}
// handle @index property
if(expandedProperty === '@index') {
// drop @index if inside an @index container
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
if(container.includes('@index')) {
continue;
}
// use keyword alias and add value
const alias = api.compactIri({
activeCtx,
iri: expandedProperty,
relativeTo: {vocab: true}
});
_addValue(rval, alias, expandedValue);
continue;
}
// skip array processing for keywords that aren't
// @graph, @list, or @included
if(expandedProperty !== '@graph' && expandedProperty !== '@list' &&
expandedProperty !== '@included' &&
_isKeyword(expandedProperty)) {
// use keyword alias and add value as is
const alias = api.compactIri({
activeCtx,
iri: expandedProperty,
relativeTo: {vocab: true}
});
_addValue(rval, alias, expandedValue);
continue;
}
// Note: expanded value must be an array due to expansion algorithm.
if(!_isArray(expandedValue)) {
throw new JsonLdError(
'JSON-LD expansion error; expanded value must be an array.',
'jsonld.SyntaxError');
}
// preserve empty arrays
if(expandedValue.length === 0) {
const itemActiveProperty = api.compactIri({
activeCtx,
iri: expandedProperty,
value: expandedValue,
relativeTo: {vocab: true},
reverse: insideReverse
});
const nestProperty = activeCtx.mappings.has(itemActiveProperty) ?
activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
let nestResult = rval;
if(nestProperty) {
_checkNestProperty(activeCtx, nestProperty, options);
if(!_isObject(rval[nestProperty])) {
rval[nestProperty] = {};
}
nestResult = rval[nestProperty];
}
_addValue(
nestResult, itemActiveProperty, expandedValue, {
propertyIsArray: true
});
}
// recursively process array values
for(const expandedItem of expandedValue) {
// compact property and get container type
const itemActiveProperty = api.compactIri({
activeCtx,
iri: expandedProperty,
value: expandedItem,
relativeTo: {vocab: true},
reverse: insideReverse
});
// if itemActiveProperty is a @nest property, add values to nestResult,
// otherwise rval
const nestProperty = activeCtx.mappings.has(itemActiveProperty) ?
activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
let nestResult = rval;
if(nestProperty) {
_checkNestProperty(activeCtx, nestProperty, options);
if(!_isObject(rval[nestProperty])) {
rval[nestProperty] = {};
}
nestResult = rval[nestProperty];
}
const container = _getContextValue(
activeCtx, itemActiveProperty, '@container') || [];
// get simple @graph or @list value if appropriate
const isGraph = _isGraph(expandedItem);
const isList = _isList(expandedItem);
let inner;
if(isList) {
inner = expandedItem['@list'];
} else if(isGraph) {
inner = expandedItem['@graph'];
}
// recursively compact expanded item
let compactedItem = await api.compact({
activeCtx,
activeProperty: itemActiveProperty,
element: (isList || isGraph) ? inner : expandedItem,
options
});
// handle @list
if(isList) {
// ensure @list value is an array
if(!_isArray(compactedItem)) {
compactedItem = [compactedItem];
}
if(!container.includes('@list')) {
// wrap using @list alias
compactedItem = {
[api.compactIri({
activeCtx,
iri: '@list',
relativeTo: {vocab: true}
})]: compactedItem
};
// include @index from expanded @list, if any
if('@index' in expandedItem) {
compactedItem[api.compactIri({
activeCtx,
iri: '@index',
relativeTo: {vocab: true}
})] = expandedItem['@index'];
}
} else {
_addValue(nestResult, itemActiveProperty, compactedItem, {
valueIsArray: true,
allowDuplicate: true
});
continue;
}
}
// Graph object compaction cases
if(isGraph) {
if(container.includes('@graph') && (container.includes('@id') ||
container.includes('@index') && _isSimpleGraph(expandedItem))) {
// get or create the map object
let mapObject;
if(nestResult.hasOwnProperty(itemActiveProperty)) {
mapObject = nestResult[itemActiveProperty];
} else {
nestResult[itemActiveProperty] = mapObject = {};
}
// index on @id or @index or alias of @none
const key = (container.includes('@id') ?
expandedItem['@id'] : expandedItem['@index']) ||
api.compactIri({activeCtx, iri: '@none',
relativeTo: {vocab: true}});
// add compactedItem to map, using value of `@id` or a new blank
// node identifier
_addValue(
mapObject, key, compactedItem, {
propertyIsArray:
(!options.compactArrays || container.includes('@set'))
});
} else if(container.includes('@graph') &&
_isSimpleGraph(expandedItem)) {
// container includes @graph but not @id or @index and value is a
// simple graph object add compact value
// if compactedItem contains multiple values, it is wrapped in
// `@included`
if(_isArray(compactedItem) && compactedItem.length > 1) {
compactedItem = {'@included': compactedItem};
}
_addValue(
nestResult, itemActiveProperty, compactedItem, {
propertyIsArray:
(!options.compactArrays || container.includes('@set'))
});
} else {
// wrap using @graph alias, remove array if only one item and
// compactArrays not set
if(_isArray(compactedItem) && compactedItem.length === 1 &&
options.compactArrays) {
compactedItem = compactedItem[0];
}
compactedItem = {
[api.compactIri({
activeCtx,
iri: '@graph',
relativeTo: {vocab: true}
})]: compactedItem
};
// include @id from expanded graph, if any
if('@id' in expandedItem) {
compactedItem[api.compactIri({
activeCtx,
iri: '@id',
relativeTo: {vocab: true}
})] = expandedItem['@id'];
}
// include @index from expanded graph, if any
if('@index' in expandedItem) {
compactedItem[api.compactIri({
activeCtx,
iri: '@index',
relativeTo: {vocab: true}
})] = expandedItem['@index'];
}
_addValue(
nestResult, itemActiveProperty, compactedItem, {
propertyIsArray:
(!options.compactArrays || container.includes('@set'))
});
}
} else if(container.includes('@language') ||
container.includes('@index') || container.includes('@id') ||
container.includes('@type')) {
// handle language and index maps
// get or create the map object
let mapObject;
if(nestResult.hasOwnProperty(itemActiveProperty)) {
mapObject = nestResult[itemActiveProperty];
} else {
nestResult[itemActiveProperty] = mapObject = {};
}
let key;
if(container.includes('@language')) {
// if container is a language map, simplify compacted value to
// a simple string
if(_isValue(compactedItem)) {
compactedItem = compactedItem['@value'];
}
key = expandedItem['@language'];
} else if(container.includes('@index')) {
const indexKey = _getContextValue(
activeCtx, itemActiveProperty, '@index') || '@index';
const containerKey = api.compactIri(
{activeCtx, iri: indexKey, relativeTo: {vocab: true}});
if(indexKey === '@index') {
key = expandedItem['@index'];
delete compactedItem[containerKey];
} else {
let others;
[key, ...others] = _asArray(compactedItem[indexKey] || []);
if(!_isString(key)) {
// Will use @none if it isn't a string.
key = null;
} else {
switch(others.length) {
case 0:
delete compactedItem[indexKey];
break;
case 1:
compactedItem[indexKey] = others[0];
break;
default:
compactedItem[indexKey] = others;
break;
}
}
}
} else if(container.includes('@id')) {
const idKey = api.compactIri({activeCtx, iri: '@id',
relativeTo: {vocab: true}});
key = compactedItem[idKey];
delete compactedItem[idKey];
} else if(container.includes('@type')) {
const typeKey = api.compactIri({
activeCtx,
iri: '@type',
relativeTo: {vocab: true}
});
let types;
[key, ...types] = _asArray(compactedItem[typeKey] || []);
switch(types.length) {
case 0:
delete compactedItem[typeKey];
break;
case 1:
compactedItem[typeKey] = types[0];
break;
default:
compactedItem[typeKey] = types;
break;
}
// If compactedItem contains a single entry
// whose key maps to @id, recompact without @type
if(Object.keys(compactedItem).length === 1 &&
'@id' in expandedItem) {
compactedItem = await api.compact({
activeCtx,
activeProperty: itemActiveProperty,
element: {'@id': expandedItem['@id']},
options
});
}
}
// if compacting this value which has no key, index on @none
if(!key) {
key = api.compactIri({activeCtx, iri: '@none',
relativeTo: {vocab: true}});
}
// add compact value to map object using key from expanded value
// based on the container type
_addValue(
mapObject, key, compactedItem, {
propertyIsArray: container.includes('@set')
});
} else {
// use an array if: compactArrays flag is false,
// @container is @set or @list , value is an empty
// array, or key is @graph
const isArray = (!options.compactArrays ||
container.includes('@set') || container.includes('@list') ||
(_isArray(compactedItem) && compactedItem.length === 0) ||
expandedProperty === '@list' || expandedProperty === '@graph');
// add compact value
_addValue(
nestResult, itemActiveProperty, compactedItem,
{propertyIsArray: isArray});
}
}
}
return rval;
}
// only primitives remain which are already compact
return element;
};
/**
* Compacts an IRI or keyword into a term or prefix if it can be. If the
* IRI has an associated value it may be passed.
*
* @param activeCtx the active context to use.
* @param iri the IRI to compact.
* @param value the value to check or null.
* @param relativeTo options for how to compact IRIs:
* vocab: true to split after @vocab, false not to.
* @param reverse true if a reverse property is being compacted, false if not.
* @param base the absolute URL to use for compacting document-relative IRIs.
*
* @return the compacted term, prefix, keyword alias, or the original IRI.
*/
api.compactIri = ({
activeCtx,
iri,
value = null,
relativeTo = {vocab: false},
reverse = false,
base = null
}) => {
// can't compact null
if(iri === null) {
return iri;
}
// if context is from a property term scoped context composed with a
// type-scoped context, then use the previous context instead
if(activeCtx.isPropertyTermScoped && activeCtx.previousContext) {
activeCtx = activeCtx.previousContext;
}
const inverseCtx = activeCtx.getInverse();
// if term is a keyword, it may be compacted to a simple alias
if(_isKeyword(iri) &&
iri in inverseCtx &&
'@none' in inverseCtx[iri] &&
'@type' in inverseCtx[iri]['@none'] &&
'@none' in inverseCtx[iri]['@none']['@type']) {
return inverseCtx[iri]['@none']['@type']['@none'];
}
// use inverse context to pick a term if iri is relative to vocab
if(relativeTo.vocab && iri in inverseCtx) {
const defaultLanguage = activeCtx['@language'] || '@none';
// prefer @index if available in value
const containers = [];
if(_isObject(value) && '@index' in value && !('@graph' in value)) {
containers.push('@index', '@index@set');
}
// if value is a preserve object, use its value
if(_isObject(value) && '@preserve' in value) {
value = value['@preserve'][0];
}
// prefer most specific container including @graph, preferring @set
// variations
if(_isGraph(value)) {
// favor indexmap if the graph is indexed
if('@index' in value) {
containers.push(
'@graph@index', '@graph@index@set', '@index', '@index@set');
}
// favor idmap if the graph is has an @id
if('@id' in value) {
containers.push(
'@graph@id', '@graph@id@set');
}
containers.push('@graph', '@graph@set', '@set');
// allow indexmap if the graph is not indexed
if(!('@index' in value)) {
containers.push(
'@graph@index', '@graph@index@set', '@index', '@index@set');
}
// allow idmap if the graph does not have an @id
if(!('@id' in value)) {
containers.push('@graph@id', '@graph@id@set');
}
} else if(_isObject(value) && !_isValue(value)) {
containers.push('@id', '@id@set', '@type', '@set@type');
}
// defaults for term selection based on type/language
let typeOrLanguage = '@language';
let typeOrLanguageValue = '@null';
if(reverse) {
typeOrLanguage = '@type';
typeOrLanguageValue = '@reverse';
containers.push('@set');
} else if(_isList(value)) {
// choose the most specific term that works for all elements in @list
// only select @list containers if @index is NOT in value
if(!('@index' in value)) {
containers.push('@list');
}
const list = value['@list'];
if(list.length === 0) {
// any empty list can be matched against any term that uses the
// @list container regardless of @type or @language
typeOrLanguage = '@any';
typeOrLanguageValue = '@none';
} else {
let commonLanguage = (list.length === 0) ? defaultLanguage : null;
let commonType = null;
for(let i = 0; i < list.length; ++i) {
const item = list[i];
let itemLanguage = '@none';
let itemType = '@none';
if(_isValue(item)) {
if('@direction' in item) {
const lang = (item['@language'] || '').toLowerCase();
const dir = item['@direction'];
itemLanguage = `${lang}_${dir}`;
} else if('@language' in item) {
itemLanguage = item['@language'].toLowerCase();
} else if('@type' in item) {
itemType = item['@type'];
} else {
// plain literal
itemLanguage = '@null';
}
} else {
itemType = '@id';
}
if(commonLanguage === null) {
commonLanguage = itemLanguage;
} else if(itemLanguage !== commonLanguage && _isValue(item)) {
commonLanguage = '@none';
}
if(commonType === null) {
commonType = itemType;
} else if(itemType !== commonType) {
commonType = '@none';
}
// there are different languages and types in the list, so choose
// the most generic term, no need to keep iterating the list
if(commonLanguage === '@none' && commonType === '@none') {
break;
}
}
commonLanguage = commonLanguage || '@none';
commonType = commonType || '@none';
if(commonType !== '@none') {
typeOrLanguage = '@type';
typeOrLanguageValue = commonType;
} else {
typeOrLanguageValue = commonLanguage;
}
}
} else {
if(_isValue(value)) {
if('@language' in value && !('@index' in value)) {
containers.push('@language', '@language@set');
typeOrLanguageValue = value['@language'];
const dir = value['@direction'];
if(dir) {
typeOrLanguageValue = `${typeOrLanguageValue}_${dir}`;
}
} else if('@direction' in value && !('@index' in value)) {
typeOrLanguageValue = `_${value['@direction']}`;
} else if('@type' in value) {
typeOrLanguage = '@type';
typeOrLanguageValue = value['@type'];
}
} else {
typeOrLanguage = '@type';
typeOrLanguageValue = '@id';
}
containers.push('@set');
}
// do term selection
containers.push('@none');
// an index map can be used to index values using @none, so add as a low
// priority
if(_isObject(value) && !('@index' in value)) {
// allow indexing even if no @index present
containers.push('@index', '@index@set');
}
// values without type or language can use @language map
if(_isValue(value) && Object.keys(value).length === 1) {
// allow indexing even if no @index present
containers.push('@language', '@language@set');
}
const term = _selectTerm(
activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue);
if(term !== null) {
return term;
}
}
// no term match, use @vocab if available
if(relativeTo.vocab) {
if('@vocab' in activeCtx) {
// determine if vocab is a prefix of the iri
const vocab = activeCtx['@vocab'];
if(iri.indexOf(vocab) === 0 && iri !== vocab) {
// use suffix as relative iri if it is not a term in the active context
const suffix = iri.substr(vocab.length);
if(!activeCtx.mappings.has(suffix)) {
return suffix;
}
}
}
}
// no term or @vocab match, check for possible CURIEs
let choice = null;
// TODO: make FastCurieMap a class with a method to do this lookup
const partialMatches = [];
let iriMap = activeCtx.fastCurieMap;
// check for partial matches of against `iri`, which means look until
// iri.length - 1, not full length
const maxPartialLength = iri.length - 1;
for(let i = 0; i < maxPartialLength && iri[i] in iriMap; ++i) {
iriMap = iriMap[iri[i]];
if('' in iriMap) {
partialMatches.push(iriMap[''][0]);
}
}
// check partial matches in reverse order to prefer longest ones first
for(let i = partialMatches.length - 1; i >= 0; --i) {
const entry = partialMatches[i];
const terms = entry.terms;
for(const term of terms) {
// a CURIE is usable if:
// 1. it has no mapping, OR
// 2. value is null, which means we're not compacting an @value, AND
// the mapping matches the IRI
const curie = term + ':' + iri.substr(entry.iri.length);
const isUsableCurie = (activeCtx.mappings.get(term)._prefix &&
(!activeCtx.mappings.has(curie) ||
(value === null && activeCtx.mappings.get(curie)['@id'] === iri)));
// select curie if it is shorter or the same length but lexicographically
// less than the current choice
if(isUsableCurie && (choice === null ||
_compareShortestLeast(curie, choice) < 0)) {
choice = curie;
}
}
}
// return chosen curie
if(choice !== null) {
return choice;
}
// If iri could be confused with a compact IRI using a term in this context,
// signal an error
for(const [term, td] of activeCtx.mappings) {
if(td && td._prefix && iri.startsWith(term + ':')) {
throw new JsonLdError(
`Absolute IRI "${iri}" confused with prefix "${term}".`,
'jsonld.SyntaxError',
{code: 'IRI confused with prefix', context: activeCtx});
}
}
// compact IRI relative to base
if(!relativeTo.vocab) {
if('@base' in activeCtx) {
if(!activeCtx['@base']) {
// The None case preserves rval as potentially relative
return iri;
} else {
const _iri = _removeBase(_prependBase(base, activeCtx['@base']), iri);
return REGEX_KEYWORD.test(_iri) ? `./${_iri}` : _iri;
}
} else {
return _removeBase(base, iri);
}
}
// return IRI as is
return iri;
};
/**
* Performs value compaction on an object with '@value' or '@id' as the only
* property.
*
* @param activeCtx the active context.
* @param activeProperty the active property that points to the value.
* @param value the value to compact.
* @param {Object} [options] - processing options.
*
* @return the compaction result.
*/
api.compactValue = ({activeCtx, activeProperty, value, options}) => {
// value is a @value
if(_isValue(value)) {
// get context rules
const type = _getContextValue(activeCtx, activeProperty, '@type');
const language = _getContextValue(activeCtx, activeProperty, '@language');
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
const container =
_getContextValue(activeCtx, activeProperty, '@container') || [];
// whether or not the value has an @index that must be preserved
const preserveIndex = '@index' in value && !container.includes('@index');
// if there's no @index to preserve ...
if(!preserveIndex && type !== '@none') {
// matching @type or @language specified in context, compact value
if(value['@type'] === type) {
return value['@value'];
}
if('@language' in value && value['@language'] === language &&
'@direction' in value && value['@direction'] === direction) {
return value['@value'];
}
if('@language' in value && value['@language'] === language) {
return value['@value'];
}
if('@direction' in value && value['@direction'] === direction) {
return value['@value'];
}
}
// return just the value of @value if all are true:
// 1. @value is the only key or @index isn't being preserved
// 2. there is no default language or @value is not a string or
// the key has a mapping with a null @language
const keyCount = Object.keys(value).length;
const isValueOnlyKey = (keyCount === 1 ||
(keyCount === 2 && '@index' in value && !preserveIndex));
const hasDefaultLanguage = ('@language' in activeCtx);
const isValueString = _isString(value['@value']);
const hasNullMapping = (activeCtx.mappings.has(activeProperty) &&
activeCtx.mappings.get(activeProperty)['@language'] === null);
if(isValueOnlyKey &&
type !== '@none' &&
(!hasDefaultLanguage || !isValueString || hasNullMapping)) {
return value['@value'];
}
const rval = {};
// preserve @index
if(preserveIndex) {
rval[api.compactIri({
activeCtx,
iri: '@index',
relativeTo: {vocab: true}
})] = value['@index'];
}
if('@type' in value) {
// compact @type IRI
rval[api.compactIri({
activeCtx,
iri: '@type',
relativeTo: {vocab: true}
})] = api.compactIri(
{activeCtx, iri: value['@type'], relativeTo: {vocab: true}});
} else if('@language' in value) {
// alias @language
rval[api.compactIri({
activeCtx,
iri: '@language',
relativeTo: {vocab: true}
})] = value['@language'];
}
if('@direction' in value) {
// alias @direction
rval[api.compactIri({
activeCtx,
iri: '@direction',
relativeTo: {vocab: true}
})] = value['@direction'];
}
// alias @value
rval[api.compactIri({
activeCtx,
iri: '@value',
relativeTo: {vocab: true}
})] = value['@value'];
return rval;
}
// value is a subject reference
const expandedProperty = _expandIri(activeCtx, activeProperty, {vocab: true},
options);
const type = _getContextValue(activeCtx, activeProperty, '@type');
const compacted = api.compactIri({
activeCtx,
iri: value['@id'],
relativeTo: {vocab: type === '@vocab'},
base: options.base});
// compact to scalar
if(type === '@id' || type === '@vocab' || expandedProperty === '@graph') {
return compacted;
}
return {
[api.compactIri({
activeCtx,
iri: '@id',
relativeTo: {vocab: true}
})]: compacted
};
};
/**
* Picks the preferred compaction term from the given inverse context entry.
*
* @param activeCtx the active context.
* @param iri the IRI to pick the term for.
* @param value the value to pick the term for.
* @param containers the preferred containers.
* @param typeOrLanguage either '@type' or '@language'.
* @param typeOrLanguageValue the preferred value for '@type' or '@language'.
*
* @return the preferred term.
*/
function _selectTerm(
activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue) {
if(typeOrLanguageValue === null) {
typeOrLanguageValue = '@null';
}
// preferences for the value of @type or @language
const prefs = [];
// determine prefs for @id based on whether or not value compacts to a term
if((typeOrLanguageValue === '@id' || typeOrLanguageValue === '@reverse') &&
_isObject(value) && '@id' in value) {
// prefer @reverse first
if(typeOrLanguageValue === '@reverse') {
prefs.push('@reverse');
}
// try to compact value to a term
const term = api.compactIri(
{activeCtx, iri: value['@id'], relativeTo: {vocab: true}});
if(activeCtx.mappings.has(term) &&
activeCtx.mappings.get(term) &&
activeCtx.mappings.get(term)['@id'] === value['@id']) {
// prefer @vocab
prefs.push.apply(prefs, ['@vocab', '@id']);
} else {
// prefer @id
prefs.push.apply(prefs, ['@id', '@vocab']);
}
} else {
prefs.push(typeOrLanguageValue);
// consider direction only
const langDir = prefs.find(el => el.includes('_'));
if(langDir) {
// consider _dir portion
prefs.push(langDir.replace(/^[^_]+_/, '_'));
}
}
prefs.push('@none');
const containerMap = activeCtx.inverse[iri];
for(const container of containers) {
// if container not available in the map, continue
if(!(container in containerMap)) {
continue;
}
const typeOrLanguageValueMap = containerMap[container][typeOrLanguage];
for(const pref of prefs) {
// if type/language option not available in the map, continue
if(!(pref in typeOrLanguageValueMap)) {
continue;
}
// select term
return typeOrLanguageValueMap[pref];
}
}
return null;
}
/**
* The value of `@nest` in the term definition must either be `@nest`, or a term
* which resolves to `@nest`.
*
* @param activeCtx the active context.
* @param nestProperty a term in the active context or `@nest`.
* @param {Object} [options] - processing options.
*/
function _checkNestProperty(activeCtx, nestProperty, options) {
if(_expandIri(activeCtx, nestProperty, {vocab: true}, options) !== '@nest') {
throw new JsonLdError(
'JSON-LD compact error; nested property must have an @nest value ' +
'resolving to @nest.',
'jsonld.SyntaxError', {code: 'invalid @nest value'});
}
}