/* * 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 }); } // recusively 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, prefering @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'}); } }