diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d38094c..4d7fdcb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Terms of the form of an IRI must map to the same IRI. - Terms of the form of a relative IRI may not be used as prefixes. - Match spec error code "invalid context entry" vs "invalid context member". +- Keywords may not be used as prefixes. ### Changed - Keep term definitions mapping to null so they may be protected. @@ -22,6 +23,8 @@ ### Added - Support for `"@import"`. +- Added support for `@included` blocks +- Skip things that have the form of a keyword, with warning. ## 2.0.2 - 2020-01-17 diff --git a/lib/compact.js b/lib/compact.js index 4f12bb63..82be80eb 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -343,8 +343,10 @@ api.compact = async ({ continue; } - // skip array processing for keywords that aren't @graph or @list + // 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({ diff --git a/lib/context.js b/lib/context.js index 82e78f67..fc798950 100644 --- a/lib/context.js +++ b/lib/context.js @@ -27,6 +27,7 @@ const { const INITIAL_CONTEXT_CACHE = new Map(); const INITIAL_CONTEXT_CACHE_MAX_SIZE = 10000; +const KEYWORD_PATTERN = /^@[a-zA-Z]+$/; const api = {}; module.exports = api; @@ -382,7 +383,7 @@ api.createTermDefinition = ({ if(term === '@type' && _isObject(value) && - value['@container'] === '@set' && + (value['@container'] || '@set') === '@set' && api.processingMode(activeCtx, 1.1)) { const validKeys = ['@container', '@id', '@protected']; @@ -397,6 +398,11 @@ api.createTermDefinition = ({ 'Invalid JSON-LD syntax; keywords cannot be overridden.', 'jsonld.SyntaxError', {code: 'keyword redefinition', context: localCtx, term}); + } else if(term.match(KEYWORD_PATTERN)) { + // FIXME: remove logging and use a handler + console.warn('WARNING: terms beginning with "@" are reserved' + + ' for future use and ignored', {term}); + return; } else if(term === '') { throw new JsonLdError( 'Invalid JSON-LD syntax; a term cannot be an empty string.', @@ -484,6 +490,19 @@ api.createTermDefinition = ({ 'absolute IRI or a blank node identifier.', 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } + + if(reverse.match(KEYWORD_PATTERN)) { + // FIXME: remove logging and use a handler + console.warn('WARNING: values beginning with "@" are reserved' + + ' for future use and ignored', {reverse}); + if(previousMapping) { + activeCtx.mappings.set(term, previousMapping); + } else { + activeCtx.mappings.delete(term); + } + return; + } + mapping['@id'] = id; mapping.reverse = true; } else if('@id' in value) { @@ -497,6 +516,16 @@ api.createTermDefinition = ({ if(id === null) { // reserve a null term, which may be protected mapping['@id'] = null; + } else if(!api.isKeyword(id) && id.match(KEYWORD_PATTERN)) { + // FIXME: remove logging and use a handler + console.warn('WARNING: values beginning with "@" are reserved' + + ' for future use and ignored', {id}); + if(previousMapping) { + activeCtx.mappings.set(term, previousMapping); + } else { + activeCtx.mappings.delete(term); + } + return; } else if(id !== term) { // expand and add @id mapping id = _expandIri( @@ -750,6 +779,12 @@ api.createTermDefinition = ({ 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } + if(api.isKeyword(mapping['@id'])) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; keywords may not be used as prefixes', + 'jsonld.SyntaxError', + {code: 'invalid term definition', context: localCtx}); + } if(typeof value['@prefix'] === 'boolean') { mapping._prefix = value['@prefix'] === true; } else { @@ -850,6 +885,11 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { return value; } + // ignore non-keyword things that look like a keyword + if(value.match(KEYWORD_PATTERN)) { + return null; + } + // define term dependency if not defined if(localCtx && localCtx.hasOwnProperty(value) && defined.get(value) !== true) { @@ -1230,6 +1270,7 @@ api.isKeyword = v => { case '@explicit': case '@graph': case '@id': + case '@included': case '@index': case '@json': case '@language': diff --git a/lib/expand.js b/lib/expand.js index fd7ce9c7..55f49460 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -16,7 +16,8 @@ const { const { isList: _isList, isValue: _isValue, - isGraph: _isGraph + isGraph: _isGraph, + isSubject: _isSubject } = require('./graphTypes'); const { @@ -251,8 +252,8 @@ api.expand = async ({ expandedParent: rval, options, insideList, - typeScopedContext, typeKey, + typeScopedContext, expansionMap}); // get property count on expanded output @@ -392,6 +393,7 @@ api.expand = async ({ * @param expandedParent the expanded result into which to add values. * @param options the expansion options. * @param insideList true if the element is a list, false if not. + * @param typeKey first key found expanding to @type. * @param typeScopedContext the context before reverting. * @param expansionMap(info) a function that can be used to custom map * unmappable values (or to throw an error when they are detected); @@ -406,8 +408,8 @@ async function _expandObject({ expandedParent, options = {}, insideList, - typeScopedContext, typeKey, + typeScopedContext, expansionMap }) { const keys = Object.keys(element).sort(); @@ -459,8 +461,9 @@ async function _expandObject({ {code: 'invalid reverse property map', value}); } if(expandedProperty in expandedParent && - expandedProperty !== '@included' && - expandedProperty !== '@type') { + expandedProperty !== '@included' && + expandedProperty !== '@type') + { throw new JsonLdError( 'Invalid JSON-LD syntax; colliding keywords detected.', 'jsonld.SyntaxError', @@ -519,6 +522,31 @@ async function _expandObject({ continue; } + // Included blocks are treated as an array of separate object nodes sharing + // the same referencing active_property. + // For 1.0, it is skipped as are other unknown keywords + if(expandedProperty === '@included' && _processingMode(activeCtx, 1.1)) { + const includedResult = _asArray(await api.expand({ + activeCtx, + activeProperty, + element: value, + options, + expansionMap + })); + + // Expanded values must be node objects + if(!includedResult.every(v => _isSubject(v))) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; ' + + 'values of @included must expand to node objects.', + 'jsonld.SyntaxError', {code: 'invalid @included value', value}); + } + + _addValue( + expandedParent, '@included', includedResult, {propertyIsArray: true}); + continue; + } + // @graph must be an array or an object if(expandedProperty === '@graph' && !(_isObject(value) || _isArray(value))) { @@ -825,6 +853,7 @@ async function _expandObject({ insideList, typeScopedContext, typeKey, + typeScopedContext, expansionMap}); } } diff --git a/lib/frame.js b/lib/frame.js index 7d1b5fc2..c33ad57c 100644 --- a/lib/frame.js +++ b/lib/frame.js @@ -167,6 +167,13 @@ api.frame = (state, subjects, frame, parent, property = null) => { } } + // if frame has @included, recurse over its sub-frame + if('@included' in frame) { + api.frame( + state, + subjects, frame['@included'], output, '@included'); + } + // iterate over subject properties for(const prop of Object.keys(subject).sort()) { // copy keywords to output diff --git a/lib/nodeMap.js b/lib/nodeMap.js index b9e7da2b..9fe58e50 100644 --- a/lib/nodeMap.js +++ b/lib/nodeMap.js @@ -145,6 +145,12 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => { continue; } + // recurse into included + if(property === '@included') { + api.createNodeMap(input[property], graphs, graph, issuer); + continue; + } + // copy non-@type keywords if(property !== '@type' && isKeyword(property)) { if(property === '@index' && property in subject && @@ -180,6 +186,11 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => { // handle embedded subject or subject reference if(graphTypes.isSubject(o) || graphTypes.isSubjectReference(o)) { + // skip null @id + if('@id' in o && !o['@id']) { + continue; + } + // relabel blank node @id const id = graphTypes.isBlankNode(o) ? issuer.getId(o['@id']) : o['@id']; diff --git a/tests/test-common.js b/tests/test-common.js index 09367f15..f03ddf9b 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -33,12 +33,6 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME idRegex: [ - // included - /compact-manifest.jsonld#tin01$/, - /compact-manifest.jsonld#tin02$/, - /compact-manifest.jsonld#tin03$/, - /compact-manifest.jsonld#tin04$/, - /compact-manifest.jsonld#tin05$/, // direction /compact-manifest.jsonld#tdi01$/, /compact-manifest.jsonld#tdi02$/, @@ -69,10 +63,6 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME idRegex: [ - // terms having form of keyword - /expand-manifest.jsonld#t0119$/, - /expand-manifest.jsonld#t0120$/, - /expand-manifest.jsonld#t0122$/, // html /html-manifest.jsonld#te001$/, /html-manifest.jsonld#te002$/, @@ -103,28 +93,6 @@ const TEST_TYPES = { /expand-manifest.jsonld#thc05$/, // remote /remote-doc-manifest.jsonld#t0013$/, // HTML - // colliding keywords - /expand-manifest.jsonld#t0114$/, - // included - /expand-manifest.jsonld#tin01$/, - /expand-manifest.jsonld#tin02$/, - /expand-manifest.jsonld#tin03$/, - /expand-manifest.jsonld#tin04$/, - /expand-manifest.jsonld#tin05$/, - /expand-manifest.jsonld#tin06$/, - /expand-manifest.jsonld#tin07$/, - /expand-manifest.jsonld#tin08$/, - /expand-manifest.jsonld#tin09$/, - // keywords - /expand-manifest.jsonld#tpr30$/, - /expand-manifest.jsonld#tpr31$/, - /expand-manifest.jsonld#tpr32$/, - /expand-manifest.jsonld#tpr33$/, - /expand-manifest.jsonld#tpr34$/, - /expand-manifest.jsonld#tpr35$/, - /expand-manifest.jsonld#tpr36$/, - /expand-manifest.jsonld#tpr37$/, - /expand-manifest.jsonld#tpr39$/, // direction /expand-manifest.jsonld#tdi01$/, /expand-manifest.jsonld#tdi02$/, @@ -159,13 +127,6 @@ const TEST_TYPES = { /html-manifest.jsonld#tf002$/, /html-manifest.jsonld#tf003$/, /html-manifest.jsonld#tf004$/, - // included - /flatten-manifest.jsonld#tin01$/, - /flatten-manifest.jsonld#tin02$/, - /flatten-manifest.jsonld#tin03$/, - /flatten-manifest.jsonld#tin04$/, - /flatten-manifest.jsonld#tin05$/, - /flatten-manifest.jsonld#tin06$/, ] }, fn: 'flatten', @@ -298,10 +259,6 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME idRegex: [ - // terms having form of keyword - /toRdf-manifest.jsonld#te119$/, - /toRdf-manifest.jsonld#te120$/, - /toRdf-manifest.jsonld#te122$/, // well formed /toRdf-manifest.jsonld#twf05$/, // html @@ -329,25 +286,6 @@ const TEST_TYPES = { /toRdf-manifest.jsonld#te075$/, /toRdf-manifest.jsonld#te111$/, /toRdf-manifest.jsonld#te112$/, - // colliding keyword - /toRdf-manifest.jsonld#te114$/, - // included - /toRdf-manifest.jsonld#tin01$/, - /toRdf-manifest.jsonld#tin02$/, - /toRdf-manifest.jsonld#tin03$/, - /toRdf-manifest.jsonld#tin04$/, - /toRdf-manifest.jsonld#tin05$/, - /toRdf-manifest.jsonld#tin06$/, - // keywords - /toRdf-manifest.jsonld#tpr30$/, - /toRdf-manifest.jsonld#tpr31$/, - /toRdf-manifest.jsonld#tpr32$/, - /toRdf-manifest.jsonld#tpr33$/, - /toRdf-manifest.jsonld#tpr34$/, - /toRdf-manifest.jsonld#tpr35$/, - /toRdf-manifest.jsonld#tpr36$/, - /toRdf-manifest.jsonld#tpr37$/, - /toRdf-manifest.jsonld#tpr39$/, // direction /toRdf-manifest.jsonld#tdi01$/, /toRdf-manifest.jsonld#tdi02$/,