From 054986347be39c276fe68e406b4643f350608d42 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 12 Mar 2020 22:34:40 -0400 Subject: [PATCH 1/9] Log unexpected errors to help with debugging. --- tests/test-common.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test-common.js b/tests/test-common.js index 3a854fe7..aa3ce50e 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -786,13 +786,15 @@ async function compareExpectedError(test, err) { result = getJsonLdErrorCode(err); assert.ok(err, 'no error present'); assert.strictEqual(result, expect); - } catch(err) { + } catch(_err) { if(options.bailOnError) { console.log('\nTEST FAILED\n'); console.log('EXPECTED: ' + expect); console.log('ACTUAL: ' + result); } - throw err; + // log the unexpected error to help with debugging + console.log('Unexpected error:', err); + throw _err; } } From 13972e67ac63857e7b9e1e24c99ec5dc2274c2e8 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 12 Mar 2020 23:17:39 -0400 Subject: [PATCH 2/9] Use asArray. --- lib/ContextResolver.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ContextResolver.js b/lib/ContextResolver.js index 9f29a8f4..613977a4 100644 --- a/lib/ContextResolver.js +++ b/lib/ContextResolver.js @@ -8,6 +8,9 @@ const { isObject: _isObject, isString: _isString, } = require('./types'); +const { + asArray: _asArray +} = require('./util'); const {prependBase} = require('./url'); const JsonLdError = require('./JsonLdError'); const ResolvedContext = require('./ResolvedContext'); @@ -32,9 +35,7 @@ module.exports = class ContextResolver { } // context is one or more contexts - if(!_isArray(context)) { - context = [context]; - } + context = _asArray(context); // resolve each context in the array const allResolved = []; From af6e38ecc1b3314b8b929c50471dd9eaf55c759d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 12 Mar 2020 23:25:59 -0400 Subject: [PATCH 3/9] Support recursive scoped contexts. - Support 1.0 recursion error code. - Support 1.1 "context overflow" error code. - Allow recursion in scoped contexts. --- lib/ContextResolver.js | 29 ++++++++++++++++++------- lib/context.js | 49 ++++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/lib/ContextResolver.js b/lib/ContextResolver.js index 613977a4..e70ba98a 100644 --- a/lib/ContextResolver.js +++ b/lib/ContextResolver.js @@ -28,7 +28,9 @@ module.exports = class ContextResolver { this.sharedCache = sharedCache; } - async resolve({context, documentLoader, base, cycles = new Set()}) { + async resolve({ + activeCtx, context, documentLoader, base, cycles = new Set() + }) { // process `@context` if(context && _isObject(context) && context['@context']) { context = context['@context']; @@ -46,7 +48,7 @@ module.exports = class ContextResolver { if(!resolved) { // not resolved yet, resolve resolved = await this._resolveRemoteContext( - {url: ctx, documentLoader, base, cycles}); + {activeCtx, url: ctx, documentLoader, base, cycles}); } // add to output and continue @@ -109,11 +111,11 @@ module.exports = class ContextResolver { return resolved; } - async _resolveRemoteContext({url, documentLoader, base, cycles}) { + async _resolveRemoteContext({activeCtx, url, documentLoader, base, cycles}) { // resolve relative URL and fetch context url = prependBase(base, url); const {context, remoteDoc} = await this._fetchContext( - {url, documentLoader, cycles}); + {activeCtx, url, documentLoader, cycles}); // update base according to remote document and resolve any relative URLs base = remoteDoc.documentUrl || url; @@ -121,26 +123,37 @@ module.exports = class ContextResolver { // resolve, cache, and return context const resolved = await this.resolve( - {context, documentLoader, base, cycles}); + {activeCtx, context, documentLoader, base, cycles}); this._cacheResolvedContext({key: url, resolved, tag: remoteDoc.tag}); return resolved; } - async _fetchContext({url, documentLoader, cycles}) { + async _fetchContext({activeCtx, url, documentLoader, cycles}) { // check for max context URLs fetched during a resolve operation if(cycles.size > MAX_CONTEXT_URLS) { throw new JsonLdError( 'Maximum number of @context URLs exceeded.', 'jsonld.ContextUrlError', - {code: 'loading remote context failed', max: MAX_CONTEXT_URLS}); + { + code: activeCtx.processingMode === 'json-ld-1.0' ? + 'loading remote context failed' : + 'context overflow', + max: MAX_CONTEXT_URLS + }); } // check for context URL cycle + // shortcut to avoid extra work that would eventually hit the max above if(cycles.has(url)) { throw new JsonLdError( 'Cyclical @context URLs detected.', 'jsonld.ContextUrlError', - {code: 'recursive context inclusion', url}); + { + code: activeCtx.processingMode === 'json-ld-1.0' ? + 'recursive context inclusion' : + 'context overflow', + url + }); } // track cycles diff --git a/lib/context.js b/lib/context.js index d83d8daa..bea8ae16 100644 --- a/lib/context.js +++ b/lib/context.js @@ -47,7 +47,8 @@ module.exports = api; api.process = async ({ activeCtx, localCtx, options, propagate = true, - overrideProtected = false + overrideProtected = false, + cycles = new Set() }) => { // normalize local context to an array of @context objects if(_isObject(localCtx) && '@context' in localCtx && @@ -63,6 +64,7 @@ api.process = async ({ // resolve contexts const resolved = await options.contextResolver.resolve({ + activeCtx, context: localCtx, documentLoader: options.documentLoader, base: options.base @@ -310,6 +312,7 @@ api.process = async ({ // resolve contexts const resolvedImport = await options.contextResolver.resolve({ + activeCtx, context: value, documentLoader: options.documentLoader, base: options.base @@ -355,23 +358,37 @@ api.process = async ({ }); if(_isObject(ctx[key]) && '@context' in ctx[key]) { + const keyCtx = ctx[key]['@context']; + let process = true; + if(_isString(keyCtx)) { + const url = prependBase(options.base, keyCtx); + // track processed contexts to avoid scoped context recursion + if(cycles.has(url)) { + process = false; + } else { + cycles.add(url); + } + } // parse context to validate - try { - await api.process({ - activeCtx: rval, - localCtx: ctx[key]['@context'], - overrideProtected: true, - options - }); - } catch(e) { - throw new JsonLdError( - 'Invalid JSON-LD syntax; invalid scoped context.', - 'jsonld.SyntaxError', - { - code: 'invalid scoped context', - context: ctx[key]['@context'], - term: key + if(process) { + try { + await api.process({ + activeCtx: rval, + localCtx: ctx[key]['@context'], + overrideProtected: true, + options, + cycles }); + } catch(e) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; invalid scoped context.', + 'jsonld.SyntaxError', + { + code: 'invalid scoped context', + context: ctx[key]['@context'], + term: key + }); + } } } } From ad73110093af125c41681af2e2b188811f3c622d Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 2 Apr 2020 12:12:57 -0700 Subject: [PATCH 4/9] Fix manifest base for EARL reporting. (cherry picked from commit 020e51b57236f5496bceea6d3429a265fb4c836f) --- tests/test-common.js | 140 +++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/tests/test-common.js b/tests/test-common.js index aa3ce50e..93b314e0 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -33,13 +33,13 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: - //MMM-manifest.jsonld#tNNN$/, + //MMM-manifest#tNNN$/, idRegex: [ // html - /html-manifest.jsonld#tc001$/, - /html-manifest.jsonld#tc002$/, - /html-manifest.jsonld#tc003$/, - /html-manifest.jsonld#tc004$/, + /html-manifest#tc001$/, + /html-manifest#tc002$/, + /html-manifest#tc003$/, + /html-manifest#tc004$/, ] }, fn: 'compact', @@ -57,38 +57,38 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: - //MMM-manifest.jsonld#tNNN$/, + //MMM-manifest#tNNN$/, idRegex: [ // html - /html-manifest.jsonld#te001$/, - /html-manifest.jsonld#te002$/, - /html-manifest.jsonld#te003$/, - /html-manifest.jsonld#te004$/, - /html-manifest.jsonld#te005$/, - /html-manifest.jsonld#te006$/, - /html-manifest.jsonld#te007$/, - /html-manifest.jsonld#te010$/, - /html-manifest.jsonld#te011$/, - /html-manifest.jsonld#te012$/, - /html-manifest.jsonld#te013$/, - /html-manifest.jsonld#te014$/, - /html-manifest.jsonld#te015$/, - /html-manifest.jsonld#te016$/, - /html-manifest.jsonld#te017$/, - /html-manifest.jsonld#te018$/, - /html-manifest.jsonld#te019$/, - /html-manifest.jsonld#te020$/, - /html-manifest.jsonld#te021$/, - /html-manifest.jsonld#te022$/, - /html-manifest.jsonld#tex01$/, + /html-manifest#te001$/, + /html-manifest#te002$/, + /html-manifest#te003$/, + /html-manifest#te004$/, + /html-manifest#te005$/, + /html-manifest#te006$/, + /html-manifest#te007$/, + /html-manifest#te010$/, + /html-manifest#te011$/, + /html-manifest#te012$/, + /html-manifest#te013$/, + /html-manifest#te014$/, + /html-manifest#te015$/, + /html-manifest#te016$/, + /html-manifest#te017$/, + /html-manifest#te018$/, + /html-manifest#te019$/, + /html-manifest#te020$/, + /html-manifest#te021$/, + /html-manifest#te022$/, + /html-manifest#tex01$/, // HTML extraction - /expand-manifest.jsonld#thc01$/, - /expand-manifest.jsonld#thc02$/, - /expand-manifest.jsonld#thc03$/, - /expand-manifest.jsonld#thc04$/, - /expand-manifest.jsonld#thc05$/, + /expand-manifest#thc01$/, + /expand-manifest#thc02$/, + /expand-manifest#thc03$/, + /expand-manifest#thc04$/, + /expand-manifest#thc05$/, // remote - /remote-doc-manifest.jsonld#t0013$/, // HTML + /remote-doc-manifest#t0013$/, // HTML ] }, fn: 'expand', @@ -105,13 +105,13 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: - //MMM-manifest.jsonld#tNNN$/, + //MMM-manifest#tNNN$/, idRegex: [ // html - /html-manifest.jsonld#tf001$/, - /html-manifest.jsonld#tf002$/, - /html-manifest.jsonld#tf003$/, - /html-manifest.jsonld#tf004$/, + /html-manifest#tf001$/, + /html-manifest#tf002$/, + /html-manifest#tf003$/, + /html-manifest#tf004$/, ] }, fn: 'flatten', @@ -129,7 +129,7 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: - //MMM-manifest.jsonld#tNNN$/, + //MMM-manifest#tNNN$/, idRegex: [ ] }, @@ -148,11 +148,11 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: - //MMM-manifest.jsonld#tNNN$/, + //MMM-manifest#tNNN$/, idRegex: [ // direction (compound-literal) - /fromRdf-manifest.jsonld#tdi11$/, - /fromRdf-manifest.jsonld#tdi12$/, + /fromRdf-manifest#tdi11$/, + /fromRdf-manifest#tdi12$/, ] }, fn: 'fromRDF', @@ -177,38 +177,38 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: - //MMM-manifest.jsonld#tNNN$/, + //MMM-manifest#tNNN$/, idRegex: [ // well formed - /toRdf-manifest.jsonld#twf05$/, + /toRdf-manifest#twf05$/, // html - /html-manifest.jsonld#tr001$/, - /html-manifest.jsonld#tr002$/, - /html-manifest.jsonld#tr003$/, - /html-manifest.jsonld#tr004$/, - /html-manifest.jsonld#tr005$/, - /html-manifest.jsonld#tr006$/, - /html-manifest.jsonld#tr007$/, - /html-manifest.jsonld#tr010$/, - /html-manifest.jsonld#tr011$/, - /html-manifest.jsonld#tr012$/, - /html-manifest.jsonld#tr013$/, - /html-manifest.jsonld#tr014$/, - /html-manifest.jsonld#tr015$/, - /html-manifest.jsonld#tr016$/, - /html-manifest.jsonld#tr017$/, - /html-manifest.jsonld#tr018$/, - /html-manifest.jsonld#tr019$/, - /html-manifest.jsonld#tr020$/, - /html-manifest.jsonld#tr021$/, - /html-manifest.jsonld#tr022$/, + /html-manifest#tr001$/, + /html-manifest#tr002$/, + /html-manifest#tr003$/, + /html-manifest#tr004$/, + /html-manifest#tr005$/, + /html-manifest#tr006$/, + /html-manifest#tr007$/, + /html-manifest#tr010$/, + /html-manifest#tr011$/, + /html-manifest#tr012$/, + /html-manifest#tr013$/, + /html-manifest#tr014$/, + /html-manifest#tr015$/, + /html-manifest#tr016$/, + /html-manifest#tr017$/, + /html-manifest#tr018$/, + /html-manifest#tr019$/, + /html-manifest#tr020$/, + /html-manifest#tr021$/, + /html-manifest#tr022$/, // Invalid Statement - /toRdf-manifest.jsonld#te075$/, - /toRdf-manifest.jsonld#te111$/, - /toRdf-manifest.jsonld#te112$/, + /toRdf-manifest#te075$/, + /toRdf-manifest#te111$/, + /toRdf-manifest#te112$/, // direction (compound-literal) - /toRdf-manifest.jsonld#tdi11$/, - /toRdf-manifest.jsonld#tdi12$/, + /toRdf-manifest#tdi11$/, + /toRdf-manifest#tdi12$/, ] }, fn: 'toRDF', @@ -387,7 +387,7 @@ function addTest(manifest, test, tests) { // expand @id and input base const test_id = test['@id'] || test['id']; //var number = test_id.substr(2); - test['@id'] = manifest.baseIri + basename(manifest.filename) + test_id; + test['@id'] = manifest.baseIri + basename(manifest.filename).replace('.jsonld', '') + test_id; test.base = manifest.baseIri + test.input; test.manifest = manifest; const description = test_id + ' ' + (test.purpose || test.name); From 5459cae2169d6c85e055ebc099e8982290b1a15d Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 2 Apr 2020 12:30:27 -0700 Subject: [PATCH 5/9] Add doap:release/revision to EARL output. FIXME: hard-codes version number. (cherry picked from commit 811f16c6ddd1e618de9306bd100e9c88634b82da) --- tests/earl-report.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/earl-report.js b/tests/earl-report.js index ab3cffd4..fbdaa403 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -36,7 +36,8 @@ function EarlReport(options) { 'earl:mode': {'@type': '@id'}, 'earl:test': {'@type': '@id'}, 'earl:outcome': {'@type': '@id'}, - 'dc:date': {'@type': 'xsd:date'} + 'dc:date': {'@type': 'xsd:dateTime'}, + 'doap:created': {'@type': 'xsd:date'} }, '@id': 'https://github.com/digitalbazaar/jsonld.js', '@type': [ @@ -61,16 +62,21 @@ function EarlReport(options) { 'foaf:name': 'Dave Longley', 'foaf:homepage': 'https://github.com/dlongley' }, - 'dc:date': { - '@value': today, - '@type': 'xsd:date' + 'doap:release': { + 'doap:name': '', + 'doap:revision': '', + 'doap:created': today }, 'subjectOf': [] }; /* eslint-enable quote-props */ + // FIXME: read this from somewhere + version = 'v3.0.1' this._report['@id'] += '#' + this.id; this._report['doap:name'] += ' ' + this.id; this._report['dc:title'] += ' ' + this.id; + this._report['doap:release']['doap:name'] = this._report['doap:name'] + ' ' + version; + this._report['doap:release']['doap:revision'] = version; } EarlReport.prototype.addAssertion = function(test, pass) { From 2b0bb946b77d7788241eb9764b289743858c4b55 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 2 Apr 2020 12:35:36 -0700 Subject: [PATCH 6/9] Ignore test fixed in PR #384. --- tests/test-common.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test-common.js b/tests/test-common.js index 93b314e0..b51c7555 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -59,6 +59,9 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ + // IRI resolution (PR #384) + /expand-manifest#t0129$/, + // html /html-manifest#te001$/, /html-manifest#te002$/, @@ -179,8 +182,12 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ + // IRI resolution (PR #384) + /toRdf-manifest#te129$/, + // well formed /toRdf-manifest#twf05$/, + // html /html-manifest#tr001$/, /html-manifest#tr002$/, From b0d5991c16adbc61cdaa7f037770a423488df5db Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 2 Apr 2020 12:46:32 -0700 Subject: [PATCH 7/9] Better support for using a processed context for `null` and caching `@import`. --- lib/context.js | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/context.js b/lib/context.js index bea8ae16..0e6bf4a7 100644 --- a/lib/context.js +++ b/lib/context.js @@ -111,6 +111,14 @@ api.process = async ({ } else if(protectedMode === 'warn') { // FIXME: remove logging and use a handler console.warn('WARNING: invalid context nullification'); + + // get processed context from cache if available + const processed = resolvedContext.getProcessed(activeCtx); + if(processed) { + rval = activeCtx = processed; + continue; + } + const oldActiveCtx = activeCtx; // copy all protected term definitions to fresh initial context rval = activeCtx = api.getInitialContext(options).clone(); @@ -323,19 +331,33 @@ api.process = async ({ 'jsonld.SyntaxError', {code: 'invalid remote context', context: localCtx}); } - const importCtx = resolvedImport[0].document; - if('@import' in importCtx) { - throw new JsonLdError( - 'Invalid JSON-LD syntax; imported context must not include @import.', - 'jsonld.SyntaxError', - {code: 'invalid context entry', context: localCtx}); - } + const processedImport = resolvedImport[0].getProcessed(activeCtx); + if(processedImport) { + // Note: if the same context were used in this active context + // as a reference context, then processed_input might not + // be a dict. + ctx = processedImport; + } else { + const importCtx = resolvedImport[0].document; + if('@import' in importCtx) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; imported context must not include @import.', + 'jsonld.SyntaxError', + {code: 'invalid context entry', context: localCtx}); + } - // merge ctx into importCtx and replace rval with the result - for(const key in importCtx) { - if(!ctx.hasOwnProperty(key)) { - ctx[key] = importCtx[key]; + // merge ctx into importCtx and replace rval with the result + for(const key in importCtx) { + if(!ctx.hasOwnProperty(key)) { + ctx[key] = importCtx[key]; + } } + + // Note: this could potenially conflict if the import + // were used in the same active context as a referenced + // context and an import. In this case, we + // could override the cached result, but seems unlikely. + resolvedImport[0].setProcessed(activeCtx, ctx); } defined.set('@import', true); From 4dbecc813c4a6b15b70c17c58a90f3f23328b701 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 9 Apr 2020 17:41:41 -0400 Subject: [PATCH 8/9] Update changelog. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd43fbf..11fe957b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # jsonld ChangeLog +### Fixed +- Support recusrive scoped contexts. +- Various EARL report updates. + +### Changed +- Better support for using a processed context for `null` and caching + `@import`. + ## 3.0.1 - 2020-03-10 ### Fixed From a99b4071fd84d7808c7550696aac37dba7411983 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 6 Apr 2020 15:49:35 -0700 Subject: [PATCH 9/9] Change `@base` handling to improve cachability. Don't set `@base` in initial context and don't resolve a relative IRI when setting `@base` in a context, so that the document location can be kept separate from the context itself. This makes the initial context the same for all processingModes, allowing much more cachability of remote and resolved contexts. --- CHANGELOG.md | 3 +++ lib/compact.js | 41 ++++++++++++++++++++++++++++++----------- lib/context.js | 26 +++++++++++++++----------- lib/url.js | 4 ++-- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11fe957b..5039de4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ ### Changed - Better support for using a processed context for `null` and caching `@import`. +- Don't set `@base` in initial context and don't resolve a relative IRI + when setting `@base` in a context, so that the document location can + be kept separate from the context itself. ## 3.0.1 - 2020-03-10 diff --git a/lib/compact.js b/lib/compact.js index 994a4ba5..1bcdb4bb 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -29,7 +29,8 @@ const { } = require('./context'); const { - removeBase: _removeBase + removeBase: _removeBase, + prependBase: _prependBase } = require('./url'); const { @@ -226,7 +227,8 @@ api.compact = async ({ expandedIri => api.compactIri({ activeCtx, iri: expandedIri, - relativeTo: {vocab: false} + relativeTo: {vocab: false}, + base: options.base })); if(compactedValue.length === 1) { compactedValue = compactedValue[0]; @@ -485,7 +487,8 @@ api.compact = async ({ // index on @id or @index or alias of @none const key = (container.includes('@id') ? expandedItem['@id'] : expandedItem['@index']) || - api.compactIri({activeCtx, iri: '@none', vocab: true}); + api.compactIri({activeCtx, iri: '@none', + relativeTo: {vocab: true}}); // add compactedItem to map, using value of `@id` or a new blank // node identifier @@ -570,7 +573,7 @@ api.compact = async ({ const indexKey = _getContextValue( activeCtx, itemActiveProperty, '@index') || '@index'; const containerKey = api.compactIri( - {activeCtx, iri: indexKey, vocab: true}); + {activeCtx, iri: indexKey, relativeTo: {vocab: true}}); if(indexKey === '@index') { key = expandedItem['@index']; delete compactedItem[containerKey]; @@ -595,14 +598,15 @@ api.compact = async ({ } } } else if(container.includes('@id')) { - const idKey = api.compactIri({activeCtx, iri: '@id', vocab: true}); + 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', - vocab: true + relativeTo: {vocab: true} }); let types; [key, ...types] = _asArray(compactedItem[typeKey] || []); @@ -634,7 +638,8 @@ api.compact = async ({ // if compacting this value which has no key, index on @none if(!key) { - key = api.compactIri({activeCtx, iri: '@none', vocab: true}); + 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 @@ -676,6 +681,7 @@ api.compact = async ({ * @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. */ @@ -684,7 +690,8 @@ api.compactIri = ({ iri, value = null, relativeTo = {vocab: false}, - reverse = false + reverse = false, + base = null }) => { // can't compact null if(iri === null) { @@ -933,7 +940,16 @@ api.compactIri = ({ // compact IRI relative to base if(!relativeTo.vocab) { - return _removeBase(activeCtx['@base'], iri); + if('@base' in activeCtx) { + if(!activeCtx['@base']) { + // The None case preserves rval as potentially relative + return iri; + } else { + return _removeBase(_prependBase(base, activeCtx['@base']), iri); + } + } else { + return _removeBase(base, iri); + } } // return IRI as is @@ -1050,8 +1066,11 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => { 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'}}); + 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') { diff --git a/lib/context.js b/lib/context.js index 0e6bf4a7..672b80fa 100644 --- a/lib/context.js +++ b/lib/context.js @@ -201,12 +201,10 @@ api.process = async ({ if('@base' in ctx) { let base = ctx['@base']; - if(base === null) { + if(base === null || _isAbsoluteIri(base)) { // no action - } else if(_isAbsoluteIri(base)) { - base = parseUrl(base); } else if(_isRelativeIri(base)) { - base = parseUrl(prependBase(rval['@base'].href, base)); + base = prependBase(rval['@base'], base); } else { throw new JsonLdError( 'Invalid JSON-LD syntax; the value of "@base" in a ' + @@ -341,7 +339,8 @@ api.process = async ({ const importCtx = resolvedImport[0].document; if('@import' in importCtx) { throw new JsonLdError( - 'Invalid JSON-LD syntax; imported context must not include @import.', + 'Invalid JSON-LD syntax: ' + + 'imported context must not include @import.', 'jsonld.SyntaxError', {code: 'invalid context entry', context: localCtx}); } @@ -1048,8 +1047,13 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { } // prepend base - if(relativeTo.base) { - return prependBase(activeCtx['@base'], value); + if(relativeTo.base && '@base' in activeCtx) { + if(activeCtx['@base']) { + // The null case preserves value as potentially relative + return prependBase(prependBase(options.base, activeCtx['@base']), value); + } + } else if(relativeTo.base) { + return prependBase(options.base, value); } return value; @@ -1064,15 +1068,13 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { * @return the initial context. */ api.getInitialContext = options => { - const base = parseUrl(options.base || ''); - const key = JSON.stringify({base, processingMode: options.processingMode}); + const key = JSON.stringify({processingMode: options.processingMode}); const cached = INITIAL_CONTEXT_CACHE.get(key); if(cached) { return cached; } const initialContext = { - '@base': base, processingMode: options.processingMode, mappings: new Map(), inverse: null, @@ -1278,7 +1280,6 @@ api.getInitialContext = options => { */ function _cloneActiveContext() { const child = {}; - child['@base'] = this['@base']; child.mappings = util.clone(this.mappings); child.clone = this.clone; child.inverse = null; @@ -1288,6 +1289,9 @@ api.getInitialContext = options => { child.previousContext = this.previousContext.clone(); } child.revertToPreviousContext = this.revertToPreviousContext; + if('@base' in this) { + child['@base'] = this['@base']; + } if('@language' in this) { child['@language'] = this['@language']; } diff --git a/lib/url.js b/lib/url.js index 07741914..98e2b848 100644 --- a/lib/url.js +++ b/lib/url.js @@ -71,7 +71,7 @@ api.prependBase = (base, iri) => { } // parse base if it is a string - if(types.isString(base)) { + if(!base || types.isString(base)) { base = api.parse(base || ''); } @@ -158,7 +158,7 @@ api.removeBase = (base, iri) => { return iri; } - if(types.isString(base)) { + if(!base || types.isString(base)) { base = api.parse(base || ''); }