diff --git a/lib/compact.js b/lib/compact.js index 7e52fa85..ce990f2f 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -143,6 +143,9 @@ api.compact = ({ const rval = {}; + // revert type scoped terms + activeCtx = activeCtx.revertTypeScopedTerms(); + if(options.link && '@id' in element) { // store linked element if(!options.link.hasOwnProperty(element['@id'])) { @@ -162,10 +165,15 @@ api.compact = ({ const compactedType = api.compactIri( {activeCtx, iri: type, relativeTo: {vocab: true}}); - // Use any scoped context defined on this value + // Use any type-scoped context defined on this value const ctx = _getContextValue(activeCtx, compactedType, '@context'); if(!_isUndefined(ctx)) { - activeCtx = _processContext({activeCtx, localCtx: ctx, options}); + activeCtx = _processContext({ + activeCtx, + localCtx: ctx, + options, + isTypeScopedContext: true + }); } } diff --git a/lib/context.js b/lib/context.js index 792c70f5..31696483 100644 --- a/lib/context.js +++ b/lib/context.js @@ -44,11 +44,16 @@ api.cache = new ActiveContextCache(); * @param options the context processing options. * @param isPropertyTermScopedContext `true` if `localCtx` is a scoped context * from a property term. + * @param isTypeScopedContext `true` if `localCtx` is a scoped context + * from a type. * * @return the new active context. */ -api.process = ( - {activeCtx, localCtx, options, isPropertyTermScopedContext = false}) => { +api.process = ({ + activeCtx, localCtx, options, + isPropertyTermScopedContext = false, + isTypeScopedContext = false +}) => { // normalize local context to an array of @context objects if(_isObject(localCtx) && '@context' in localCtx && _isArray(localCtx['@context'])) { @@ -233,7 +238,8 @@ api.process = ( // process all other keys for(const key in ctx) { api.createTermDefinition( - rval, ctx, key, defined, options, isPropertyTermScopedContext); + rval, ctx, key, defined, options, + isPropertyTermScopedContext, isTypeScopedContext); } // cache result @@ -259,10 +265,13 @@ api.process = ( * signal a warning. * @param isPropertyTermScopedContext `true` if `localCtx` is a scoped context * from a property term. + * @param isTypeScopedContext `true` if `localCtx` is a scoped context + * from a type. */ api.createTermDefinition = ( activeCtx, localCtx, term, defined, options, - isPropertyTermScopedContext = false) => { + isPropertyTermScopedContext = false, + isTypeScopedContext = false) => { if(defined.has(term)) { // term already defined if(defined.get(term)) { @@ -314,7 +323,11 @@ api.createTermDefinition = ( } // remove old mapping + let previousMapping = null; if(activeCtx.mappings.has(term)) { + if(isTypeScopedContext) { + previousMapping = activeCtx.mappings.get(term); + } activeCtx.mappings.delete(term); } @@ -349,6 +362,11 @@ api.createTermDefinition = ( // create new mapping const mapping = {}; activeCtx.mappings.set(term, mapping); + if(isTypeScopedContext) { + activeCtx.hasTypeScopedTerms = true; + mapping.isTypeScopedTerm = true; + mapping.previousMapping = previousMapping; + } mapping.reverse = false; // make sure term definition only has expected keywords @@ -466,6 +484,7 @@ api.createTermDefinition = ( if(value['@protected'] === true || (defined.get('@protected') === true && value['@protected'] !== false)) { activeCtx.protected[term] = true; + mapping.protected = true; } // IRI mapping now defined @@ -762,6 +781,7 @@ api.getInitialContext = options => { inverse: null, getInverse: _createInverseContext, clone: _cloneActiveContext, + revertTypeScopedTerms: _revertTypeScopedTerms, protected: {} }; // TODO: consider using LRU cache instead @@ -937,6 +957,7 @@ api.getInitialContext = options => { child.inverse = null; child.getInverse = this.getInverse; child.protected = util.clone(this.protected); + child.revertTypeScopedTerms = this.revertTypeScopedTerms; if('@language' in this) { child['@language'] = this['@language']; } @@ -945,6 +966,38 @@ api.getInitialContext = options => { } return child; } + + /** + * Reverts any type-scoped terms in this active context to their previous + * mappings. + */ + function _revertTypeScopedTerms() { + // optimization: no type-scoped terms to remove, reuse active context + if(!this.hasTypeScopedTerms) { + return this; + } + // create clone without type scoped terms + const child = this.clone(); + const entries = child.mappings.entries(); + for(const [term, mapping] of entries) { + if(mapping.isTypeScopedTerm) { + if(mapping.previousMapping) { + child.mappings.set(term, mapping.previousMapping); + if(mapping.previousMapping.protected) { + child.protected[term] = true; + } else { + delete child.protected[term]; + } + } else { + child.mappings.delete(term); + if(child.protected[term]) { + delete child.protected[term]; + } + } + } + } + return child; + } }; /** diff --git a/lib/expand.js b/lib/expand.js index 6dcb4990..4619a0d3 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -50,6 +50,8 @@ module.exports = api; * @param element the element to expand. * @param options the expansion options. * @param insideList true if the element is a list, false if not. + * @param insideIndex true if the element is inside an index container, + * false if not. * @param expansionMap(info) a function that can be used to custom map * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior @@ -63,6 +65,7 @@ api.expand = ({ element, options = {}, insideList = false, + insideIndex = false, expansionMap = () => undefined }) => { // nothing to expand @@ -111,7 +114,8 @@ api.expand = ({ activeProperty, element: element[i], options, - expansionMap + expansionMap, + insideIndex }); if(insideList && (_isArray(e) || _isList(e))) { // lists of lists are illegal @@ -148,6 +152,11 @@ api.expand = ({ // recursively expand object: + if(!insideIndex) { + // revert type scoped terms + activeCtx = activeCtx.revertTypeScopedTerms(); + } + // if element has a context, process it if('@context' in element) { activeCtx = _processContext( @@ -159,7 +168,7 @@ api.expand = ({ for(const key of keys) { const expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options); if(expandedProperty === '@type') { - // set scopped contexts from @type + // set scoped contexts from @type // avoid sorting if possible const value = element[key]; const types = @@ -168,7 +177,12 @@ api.expand = ({ for(const type of types) { const ctx = _getContextValue(activeCtx, type, '@context'); if(!_isUndefined(ctx)) { - activeCtx = _processContext({activeCtx, localCtx: ctx, options}); + activeCtx = _processContext({ + activeCtx, + localCtx: ctx, + options, + isTypeScopedContext: true + }); } } } @@ -599,7 +613,8 @@ function _expandObject({ } else if(container.includes('@type') && _isObject(value)) { // handle type container (skip if value is not an object) expandedValue = _expandIndexMap({ - activeCtx: termCtx, + // since container is `@type`, revert type scoped terms when expanding + activeCtx: termCtx.revertTypeScopedTerms(), options, activeProperty: key, value, @@ -840,11 +855,19 @@ function _expandIndexMap( indexKey}) { const rval = []; const keys = Object.keys(value).sort(); + const isTypeIndex = indexKey === '@type'; for(let key of keys) { // if indexKey is @type, there may be a context defined for it - const ctx = _getContextValue(activeCtx, key, '@context'); - if(!_isUndefined(ctx)) { - activeCtx = _processContext({activeCtx, localCtx: ctx, options}); + if(isTypeIndex) { + const ctx = _getContextValue(activeCtx, key, '@context'); + if(!_isUndefined(ctx)) { + activeCtx = _processContext({ + activeCtx, + localCtx: ctx, + isTypeScopedContext: true, + options + }); + } } let val = value[key]; @@ -857,7 +880,7 @@ function _expandIndexMap( if(indexKey === '@id') { // expand document relative key = _expandIri(activeCtx, key, {base: true}, options); - } else if(indexKey === '@type') { + } else if(isTypeIndex) { key = expandedKey; } @@ -867,6 +890,7 @@ function _expandIndexMap( element: val, options, insideList: false, + insideIndex: true, expansionMap }); for(let item of val) {