Comparing version 6.0.0 to 7.0.0
@@ -37,2 +37,3 @@ /* | ||
const { | ||
REGEX_KEYWORD, | ||
addValue: _addValue, | ||
@@ -55,3 +56,2 @@ asArray: _asArray, | ||
* @param options the compaction options. | ||
* @param compactionMap the compaction map to use. | ||
* | ||
@@ -64,4 +64,3 @@ * @return a promise that resolves to the compacted value. | ||
element, | ||
options = {}, | ||
compactionMap = () => undefined | ||
options = {} | ||
}) => { | ||
@@ -72,22 +71,11 @@ // recursively compact array | ||
for(let i = 0; i < element.length; ++i) { | ||
// compact, dropping any null values unless custom mapped | ||
let compacted = await api.compact({ | ||
const compacted = await api.compact({ | ||
activeCtx, | ||
activeProperty, | ||
element: element[i], | ||
options, | ||
compactionMap | ||
options | ||
}); | ||
if(compacted === null) { | ||
compacted = await compactionMap({ | ||
unmappedValue: element[i], | ||
activeCtx, | ||
activeProperty, | ||
parent: element, | ||
index: i, | ||
options | ||
}); | ||
if(compacted === undefined) { | ||
continue; | ||
} | ||
// FIXME: need event? | ||
continue; | ||
} | ||
@@ -156,4 +144,3 @@ rval.push(compacted); | ||
element: element['@list'], | ||
options, | ||
compactionMap | ||
options | ||
}); | ||
@@ -286,4 +273,3 @@ } | ||
element: expandedValue, | ||
options, | ||
compactionMap | ||
options | ||
}); | ||
@@ -325,4 +311,3 @@ | ||
element: expandedValue, | ||
options, | ||
compactionMap | ||
options | ||
}); | ||
@@ -444,4 +429,3 @@ | ||
element: (isList || isGraph) ? inner : expandedItem, | ||
options, | ||
compactionMap | ||
options | ||
}); | ||
@@ -641,4 +625,3 @@ | ||
element: {'@id': expandedItem['@id']}, | ||
options, | ||
compactionMap | ||
options | ||
}); | ||
@@ -957,3 +940,4 @@ } | ||
} else { | ||
return _removeBase(_prependBase(base, activeCtx['@base']), iri); | ||
const _iri = _removeBase(_prependBase(base, activeCtx['@base']), iri); | ||
return REGEX_KEYWORD.test(_iri) ? `./${_iri}` : _iri; | ||
} | ||
@@ -960,0 +944,0 @@ } else { |
@@ -23,2 +23,8 @@ /* | ||
const { | ||
handleEvent: _handleEvent | ||
} = require('./events'); | ||
const { | ||
REGEX_BCP47, | ||
REGEX_KEYWORD, | ||
asArray: _asArray, | ||
@@ -30,3 +36,2 @@ compareShortestLeast: _compareShortestLeast | ||
const INITIAL_CONTEXT_CACHE_MAX_SIZE = 10000; | ||
const KEYWORD_PATTERN = /^@[a-zA-Z]+$/; | ||
@@ -66,2 +71,19 @@ const api = {}; | ||
// event handler for capturing events to replay when using a cached context | ||
const events = []; | ||
const eventCaptureHandler = [ | ||
({event, next}) => { | ||
events.push(event); | ||
next(); | ||
} | ||
]; | ||
// chain to original handler | ||
if(options.eventHandler) { | ||
eventCaptureHandler.push(options.eventHandler); | ||
} | ||
// store original options to use when replaying events | ||
const originalOptions = options; | ||
// shallow clone options with event capture handler | ||
options = {...options, eventHandler: eventCaptureHandler}; | ||
// resolve contexts | ||
@@ -104,42 +126,8 @@ const resolved = await options.contextResolver.resolve({ | ||
// not allowing overrides (e.g. processing a property term scoped context) | ||
if(!overrideProtected && | ||
Object.keys(activeCtx.protected).length !== 0) { | ||
const protectedMode = (options && options.protectedMode) || 'error'; | ||
if(protectedMode === 'error') { | ||
throw new JsonLdError( | ||
'Tried to nullify a context with protected terms outside of ' + | ||
'a term definition.', | ||
'jsonld.SyntaxError', | ||
{code: 'invalid context nullification'}); | ||
} 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(); | ||
for(const [term, _protected] of | ||
Object.entries(oldActiveCtx.protected)) { | ||
if(_protected) { | ||
activeCtx.mappings[term] = | ||
util.clone(oldActiveCtx.mappings[term]); | ||
} | ||
} | ||
activeCtx.protected = util.clone(oldActiveCtx.protected); | ||
// cache processed result | ||
resolvedContext.setProcessed(oldActiveCtx, rval); | ||
continue; | ||
} | ||
if(!overrideProtected && Object.keys(activeCtx.protected).length !== 0) { | ||
throw new JsonLdError( | ||
'Invalid protectedMode.', | ||
'Tried to nullify a context with protected terms outside of ' + | ||
'a term definition.', | ||
'jsonld.SyntaxError', | ||
{code: 'invalid protected mode', context: localCtx, protectedMode}); | ||
{code: 'invalid context nullification'}); | ||
} | ||
@@ -153,3 +141,10 @@ rval = activeCtx = api.getInitialContext(options).clone(); | ||
if(processed) { | ||
rval = activeCtx = processed; | ||
if(originalOptions.eventHandler) { | ||
// replay events with original non-capturing options | ||
for(const event of processed.events) { | ||
_handleEvent({event, options: originalOptions}); | ||
} | ||
} | ||
rval = activeCtx = processed.context; | ||
continue; | ||
@@ -239,4 +234,21 @@ } | ||
} else { | ||
rval['@vocab'] = _expandIri(rval, value, {vocab: true, base: true}, | ||
const vocab = _expandIri(rval, value, {vocab: true, base: true}, | ||
undefined, undefined, options); | ||
if(!_isAbsoluteIri(vocab)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative @vocab reference', | ||
level: 'warning', | ||
message: 'Relative @vocab reference found.', | ||
details: { | ||
vocab | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
rval['@vocab'] = vocab; | ||
} | ||
@@ -258,2 +270,18 @@ defined.set('@vocab', true); | ||
} else { | ||
if(!value.match(REGEX_BCP47)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'invalid @language value', | ||
level: 'warning', | ||
message: '@language value must be valid BCP47.', | ||
details: { | ||
language: value | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
rval['@language'] = value.toLowerCase(); | ||
@@ -424,3 +452,6 @@ } | ||
// cache processed result | ||
resolvedContext.setProcessed(activeCtx, rval); | ||
resolvedContext.setProcessed(activeCtx, { | ||
context: rval, | ||
events | ||
}); | ||
} | ||
@@ -440,5 +471,2 @@ | ||
* @param {Object} [options] - creation options. | ||
* @param {string} [options.protectedMode="error"] - "error" to throw error | ||
* on `@protected` constraint violation, "warn" to allow violations and | ||
* signal a warning. | ||
* @param overrideProtected `false` allows protected terms to be modified. | ||
@@ -493,6 +521,19 @@ */ | ||
{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}); | ||
} else if(term.match(REGEX_KEYWORD)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'reserved term', | ||
level: 'warning', | ||
message: | ||
'Terms beginning with "@" are ' + | ||
'reserved for future use and dropped.', | ||
details: { | ||
term | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
return; | ||
@@ -577,6 +618,19 @@ } else if(term === '') { | ||
if(!api.isKeyword(reverse) && 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(reverse.match(REGEX_KEYWORD)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'reserved @reverse value', | ||
level: 'warning', | ||
message: | ||
'@reverse values beginning with "@" are ' + | ||
'reserved for future use and dropped.', | ||
details: { | ||
reverse | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
if(previousMapping) { | ||
@@ -614,6 +668,19 @@ activeCtx.mappings.set(term, previousMapping); | ||
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}); | ||
} else if(!api.isKeyword(id) && id.match(REGEX_KEYWORD)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'reserved @id value', | ||
level: 'warning', | ||
message: | ||
'@id values beginning with "@" are ' + | ||
'reserved for future use and dropped.', | ||
details: { | ||
id | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
if(previousMapping) { | ||
@@ -933,19 +1000,6 @@ activeCtx.mappings.set(term, previousMapping); | ||
if(!_deepCompare(previousMapping, mapping)) { | ||
const protectedMode = (options && options.protectedMode) || 'error'; | ||
if(protectedMode === 'error') { | ||
throw new JsonLdError( | ||
`Invalid JSON-LD syntax; tried to redefine "${term}" which is a ` + | ||
'protected term.', | ||
'jsonld.SyntaxError', | ||
{code: 'protected term redefinition', context: localCtx, term}); | ||
} else if(protectedMode === 'warn') { | ||
// FIXME: remove logging and use a handler | ||
console.warn('WARNING: protected term redefinition', {term}); | ||
return; | ||
} | ||
throw new JsonLdError( | ||
'Invalid protectedMode.', | ||
'Invalid JSON-LD syntax; tried to redefine a protected term.', | ||
'jsonld.SyntaxError', | ||
{code: 'invalid protected mode', context: localCtx, term, | ||
protectedMode}); | ||
{code: 'protected term redefinition', context: localCtx, term}); | ||
} | ||
@@ -999,3 +1053,3 @@ } | ||
// ignore non-keyword things that look like a keyword | ||
if(value.match(KEYWORD_PATTERN)) { | ||
if(value.match(REGEX_KEYWORD)) { | ||
return null; | ||
@@ -1058,17 +1112,100 @@ } | ||
// prepend vocab | ||
// A flag that captures whether the iri being expanded is | ||
// the value for an @type | ||
//let typeExpansion = false; | ||
//if(options !== undefined && options.typeExpansion !== undefined) { | ||
// typeExpansion = options.typeExpansion; | ||
//} | ||
if(relativeTo.vocab && '@vocab' in activeCtx) { | ||
return activeCtx['@vocab'] + value; | ||
} | ||
// prepend base | ||
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); | ||
// prepend vocab | ||
const prependedResult = activeCtx['@vocab'] + value; | ||
// FIXME: needed? may be better as debug event. | ||
/* | ||
if(options && options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'prepending @vocab during expansion', | ||
level: 'info', | ||
message: 'Prepending @vocab during expansion.', | ||
details: { | ||
type: '@vocab', | ||
vocab: activeCtx['@vocab'], | ||
value, | ||
result: prependedResult, | ||
typeExpansion | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
*/ | ||
// the null case preserves value as potentially relative | ||
value = prependedResult; | ||
} else if(relativeTo.base) { | ||
return prependBase(options.base, value); | ||
// prepend base | ||
let prependedResult; | ||
let base; | ||
if('@base' in activeCtx) { | ||
if(activeCtx['@base']) { | ||
base = prependBase(options.base, activeCtx['@base']); | ||
prependedResult = prependBase(base, value); | ||
} else { | ||
base = activeCtx['@base']; | ||
prependedResult = value; | ||
} | ||
} else { | ||
base = options.base; | ||
prependedResult = prependBase(options.base, value); | ||
} | ||
// FIXME: needed? may be better as debug event. | ||
/* | ||
if(options && options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'prepending @base during expansion', | ||
level: 'info', | ||
message: 'Prepending @base during expansion.', | ||
details: { | ||
type: '@base', | ||
base, | ||
value, | ||
result: prependedResult, | ||
typeExpansion | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
*/ | ||
// the null case preserves value as potentially relative | ||
value = prependedResult; | ||
} | ||
// FIXME: duplicate? needed? maybe just enable in a verbose debug mode | ||
/* | ||
if(!_isAbsoluteIri(value) && options && options.eventHandler) { | ||
// emit event indicating a relative IRI was found, which can result in it | ||
// being dropped when converting to other RDF representations | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative IRI after expansion', | ||
// FIXME: what level? | ||
level: 'warning', | ||
message: 'Relative IRI after expansion.', | ||
details: { | ||
relativeIri: value, | ||
typeExpansion | ||
} | ||
}, | ||
options | ||
}); | ||
// NOTE: relative reference events emitted at calling sites as needed | ||
} | ||
*/ | ||
return value; | ||
@@ -1075,0 +1212,0 @@ } |
@@ -36,2 +36,4 @@ /* | ||
const { | ||
REGEX_BCP47, | ||
REGEX_KEYWORD, | ||
addValue: _addValue, | ||
@@ -43,5 +45,8 @@ asArray: _asArray, | ||
const { | ||
handleEvent: _handleEvent | ||
} = require('./events'); | ||
const api = {}; | ||
module.exports = api; | ||
const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; | ||
@@ -63,6 +68,2 @@ /** | ||
* a type-scoped context. | ||
* @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 | ||
* will be used. | ||
* | ||
@@ -78,4 +79,3 @@ * @return a Promise that resolves to the expanded value. | ||
insideIndex = false, | ||
typeScopedContext = null, | ||
expansionMap = () => undefined | ||
typeScopedContext = null | ||
}) => { | ||
@@ -93,17 +93,24 @@ // nothing to expand | ||
if(!_isArray(element) && !_isObject(element)) { | ||
// drop free-floating scalars that are not in lists unless custom mapped | ||
// drop free-floating scalars that are not in lists | ||
if(!insideList && (activeProperty === null || | ||
_expandIri(activeCtx, activeProperty, {vocab: true}, | ||
options) === '@graph')) { | ||
const mapped = await expansionMap({ | ||
unmappedValue: element, | ||
activeCtx, | ||
activeProperty, | ||
options, | ||
insideList | ||
}); | ||
if(mapped === undefined) { | ||
return null; | ||
// FIXME | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'free-floating scalar', | ||
level: 'warning', | ||
message: 'Dropping free-floating scalar not in a list.', | ||
details: { | ||
value: element | ||
//activeProperty | ||
//insideList | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
return mapped; | ||
return null; | ||
} | ||
@@ -128,3 +135,2 @@ | ||
options, | ||
expansionMap, | ||
insideIndex, | ||
@@ -138,15 +144,12 @@ typeScopedContext | ||
if(e === null) { | ||
e = await expansionMap({ | ||
unmappedValue: element[i], | ||
activeCtx, | ||
activeProperty, | ||
parent: element, | ||
index: i, | ||
options, | ||
expandedParent: rval, | ||
insideList | ||
}); | ||
if(e === undefined) { | ||
continue; | ||
} | ||
// FIXME: add debug event? | ||
//unmappedValue: element[i], | ||
//activeProperty, | ||
//parent: element, | ||
//index: i, | ||
//expandedParent: rval, | ||
//insideList | ||
// NOTE: no-value events emitted at calling sites as needed | ||
continue; | ||
} | ||
@@ -264,4 +267,4 @@ | ||
typeKey, | ||
typeScopedContext, | ||
expansionMap}); | ||
typeScopedContext | ||
}); | ||
@@ -303,3 +306,3 @@ // get property count on expanded output | ||
// drop null @values unless custom mapped | ||
// drop null @values | ||
if(_processingMode(activeCtx, 1.1) && types.includes('@json') && | ||
@@ -309,15 +312,18 @@ types.length === 1) { | ||
} else if(values.length === 0) { | ||
const mapped = await expansionMap({ | ||
unmappedValue: rval, | ||
activeCtx, | ||
activeProperty, | ||
element, | ||
options, | ||
insideList | ||
}); | ||
if(mapped !== undefined) { | ||
rval = mapped; | ||
} else { | ||
rval = null; | ||
// FIXME | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'null @value value', | ||
level: 'warning', | ||
message: 'Dropping null @value value.', | ||
details: { | ||
value: rval | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
rval = null; | ||
} else if(!values.every(v => (_isString(v) || _isEmptyObject(v))) && | ||
@@ -357,20 +363,22 @@ '@language' in rval) { | ||
} else if(count === 1 && '@language' in rval) { | ||
// drop objects with only @language unless custom mapped | ||
const mapped = await expansionMap(rval, { | ||
unmappedValue: rval, | ||
activeCtx, | ||
activeProperty, | ||
element, | ||
options, | ||
insideList | ||
}); | ||
if(mapped !== undefined) { | ||
rval = mapped; | ||
} else { | ||
rval = null; | ||
// drop objects with only @language | ||
// FIXME | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'object with only @language', | ||
level: 'warning', | ||
message: 'Dropping object with only @language.', | ||
details: { | ||
value: rval | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
rval = null; | ||
} | ||
// drop certain top-level objects that do not occur in lists, unless custom | ||
// mapped | ||
// drop certain top-level objects that do not occur in lists | ||
if(_isObject(rval) && | ||
@@ -382,15 +390,34 @@ !options.keepFreeFloatingNodes && !insideList && | ||
(count === 1 && '@id' in rval)) { | ||
const mapped = await expansionMap({ | ||
unmappedValue: rval, | ||
activeCtx, | ||
activeProperty, | ||
element, | ||
options, | ||
insideList | ||
}); | ||
if(mapped !== undefined) { | ||
rval = mapped; | ||
} else { | ||
rval = null; | ||
// FIXME | ||
if(options.eventHandler) { | ||
// FIXME: one event or diff event for empty, @v/@l, {@id}? | ||
let code; | ||
let message; | ||
if(count === 0) { | ||
code = 'empty object'; | ||
message = 'Dropping empty object.'; | ||
} else if('@value' in rval) { | ||
code = 'object with only @value'; | ||
message = 'Dropping object with only @value.'; | ||
} else if('@list' in rval) { | ||
code = 'object with only @list'; | ||
message = 'Dropping object with only @list.'; | ||
} else if(count === 1 && '@id' in rval) { | ||
code = 'object with only @id'; | ||
message = 'Dropping object with only @id.'; | ||
} | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code, | ||
level: 'warning', | ||
message, | ||
details: { | ||
value: rval | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
rval = null; | ||
} | ||
@@ -414,6 +441,2 @@ } | ||
* @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); | ||
* if this function returns `undefined` then the default behavior | ||
* will be used. | ||
*/ | ||
@@ -429,4 +452,3 @@ async function _expandObject({ | ||
typeKey, | ||
typeScopedContext, | ||
expansionMap | ||
typeScopedContext | ||
}) { | ||
@@ -441,3 +463,6 @@ const keys = Object.keys(element).sort(); | ||
(_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), | ||
{vocab: true}, options) === '@json'; | ||
{vocab: true}, { | ||
...options, | ||
typeExpansion: true | ||
}) === '@json'; | ||
@@ -454,21 +479,24 @@ for(const key of keys) { | ||
// expand property | ||
let expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options); | ||
const expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options); | ||
// drop non-absolute IRI keys that aren't keywords unless custom mapped | ||
// drop non-absolute IRI keys that aren't keywords | ||
if(expandedProperty === null || | ||
!(_isAbsoluteIri(expandedProperty) || _isKeyword(expandedProperty))) { | ||
// TODO: use `await` to support async | ||
expandedProperty = expansionMap({ | ||
unmappedProperty: key, | ||
activeCtx, | ||
activeProperty, | ||
parent: element, | ||
options, | ||
insideList, | ||
value, | ||
expandedParent | ||
}); | ||
if(expandedProperty === undefined) { | ||
continue; | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'invalid property', | ||
level: 'warning', | ||
message: 'Dropping property that did not expand into an ' + | ||
'absolute IRI or keyword.', | ||
details: { | ||
property: key, | ||
expandedProperty | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
continue; | ||
} | ||
@@ -526,4 +554,57 @@ | ||
expandedParent, '@id', | ||
_asArray(value).map(v => | ||
_isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v), | ||
_asArray(value).map(v => { | ||
if(_isString(v)) { | ||
const ve = _expandIri(activeCtx, v, {base: true}, options); | ||
if(options.eventHandler) { | ||
if(ve === null) { | ||
// NOTE: spec edge case | ||
// See https://github.com/w3c/json-ld-api/issues/480 | ||
if(v === null) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'null @id value', | ||
level: 'warning', | ||
message: 'Null @id found.', | ||
details: { | ||
id: v | ||
} | ||
}, | ||
options | ||
}); | ||
} else { | ||
// matched KEYWORD regex | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'reserved @id value', | ||
level: 'warning', | ||
message: 'Reserved @id found.', | ||
details: { | ||
id: v | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} else if(!_isAbsoluteIri(ve)) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative @id reference', | ||
level: 'warning', | ||
message: 'Relative @id reference found.', | ||
details: { | ||
id: v, | ||
expandedId: ve | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
return ve; | ||
} | ||
return v; | ||
}), | ||
{propertyIsArray: options.isFrame}); | ||
@@ -540,3 +621,4 @@ continue; | ||
_asArray(v).map(vv => | ||
_expandIri(typeScopedContext, vv, {base: true, vocab: true}) | ||
_expandIri(typeScopedContext, vv, {base: true, vocab: true}, | ||
{...options, typeExpansion: true}) | ||
) | ||
@@ -548,6 +630,27 @@ ])); | ||
expandedParent, '@type', | ||
_asArray(value).map(v => | ||
_isString(v) ? | ||
_expandIri(typeScopedContext, v, | ||
{base: true, vocab: true}, options) : v), | ||
_asArray(value).map(v => { | ||
if(_isString(v)) { | ||
const ve = _expandIri(typeScopedContext, v, | ||
{base: true, vocab: true}, | ||
{...options, typeExpansion: true}); | ||
if(!_isAbsoluteIri(ve)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative @type reference', | ||
level: 'warning', | ||
message: 'Relative @type reference found.', | ||
details: { | ||
type: v | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
return ve; | ||
} | ||
return v; | ||
}), | ||
{propertyIsArray: options.isFrame}); | ||
@@ -565,4 +668,3 @@ continue; | ||
element: value, | ||
options, | ||
expansionMap | ||
options | ||
})); | ||
@@ -623,5 +725,18 @@ | ||
// ensure language tag matches BCP47 | ||
for(const lang of value) { | ||
if(_isString(lang) && !lang.match(REGEX_BCP47)) { | ||
console.warn(`@language must be valid BCP47: ${lang}`); | ||
for(const language of value) { | ||
if(_isString(language) && !language.match(REGEX_BCP47)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'invalid @language value', | ||
level: 'warning', | ||
message: '@language value must be valid BCP47.', | ||
details: { | ||
language | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
@@ -687,4 +802,3 @@ } | ||
element: value, | ||
options, | ||
expansionMap | ||
options | ||
}); | ||
@@ -764,3 +878,2 @@ // properties double-reversed | ||
value, | ||
expansionMap, | ||
asGraph, | ||
@@ -778,3 +891,2 @@ indexKey, | ||
value, | ||
expansionMap, | ||
asGraph, | ||
@@ -791,3 +903,2 @@ indexKey: '@id' | ||
value, | ||
expansionMap, | ||
asGraph: false, | ||
@@ -809,4 +920,3 @@ indexKey: '@type' | ||
options, | ||
insideList: isList, | ||
expansionMap | ||
insideList: isList | ||
}); | ||
@@ -826,4 +936,3 @@ } else if( | ||
options, | ||
insideList: false, | ||
expansionMap | ||
insideList: false | ||
}); | ||
@@ -835,17 +944,7 @@ } | ||
if(expandedValue === null && expandedProperty !== '@value') { | ||
// TODO: use `await` to support async | ||
expandedValue = expansionMap({ | ||
unmappedValue: value, | ||
expandedProperty, | ||
activeCtx: termCtx, | ||
activeProperty, | ||
parent: element, | ||
options, | ||
insideList, | ||
key, | ||
expandedParent | ||
}); | ||
if(expandedValue === undefined) { | ||
continue; | ||
} | ||
// FIXME: event? | ||
//unmappedValue: value, | ||
//expandedProperty, | ||
//key, | ||
continue; | ||
} | ||
@@ -932,4 +1031,4 @@ | ||
typeScopedContext, | ||
typeKey, | ||
expansionMap}); | ||
typeKey | ||
}); | ||
} | ||
@@ -962,3 +1061,4 @@ } | ||
} else if(expandedProperty === '@type') { | ||
return _expandIri(activeCtx, value, {vocab: true, base: true}, options); | ||
return _expandIri(activeCtx, value, {vocab: true, base: true}, | ||
{...options, typeExpansion: true}); | ||
} | ||
@@ -971,3 +1071,21 @@ | ||
if((type === '@id' || expandedProperty === '@graph') && _isString(value)) { | ||
return {'@id': _expandIri(activeCtx, value, {base: true}, options)}; | ||
const expandedValue = _expandIri(activeCtx, value, {base: true}, options); | ||
// NOTE: handle spec edge case and avoid invalid {"@id": null} | ||
if(expandedValue === null && value.match(REGEX_KEYWORD)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'reserved @id value', | ||
level: 'warning', | ||
message: 'Reserved @id found.', | ||
details: { | ||
id: activeProperty | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
return {'@id': expandedValue}; | ||
} | ||
@@ -1043,2 +1161,18 @@ // do @id expansion w/vocab | ||
if(expandedKey !== '@none') { | ||
if(!key.match(REGEX_BCP47)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'invalid @language value', | ||
level: 'warning', | ||
message: '@language value must be valid BCP47.', | ||
details: { | ||
language: key | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
val['@language'] = key.toLowerCase(); | ||
@@ -1055,5 +1189,5 @@ } | ||
async function _expandIndexMap( | ||
{activeCtx, options, activeProperty, value, expansionMap, asGraph, | ||
indexKey, propertyIndex}) { | ||
async function _expandIndexMap({ | ||
activeCtx, options, activeProperty, value, asGraph, indexKey, propertyIndex | ||
}) { | ||
const rval = []; | ||
@@ -1087,4 +1221,3 @@ const keys = Object.keys(value).sort(); | ||
insideList: false, | ||
insideIndex: true, | ||
expansionMap | ||
insideIndex: true | ||
}); | ||
@@ -1091,0 +1224,0 @@ |
@@ -9,4 +9,12 @@ /* | ||
const types = require('./types'); | ||
const util = require('./util'); | ||
const { | ||
REGEX_BCP47, | ||
addValue: _addValue | ||
} = require('./util'); | ||
const { | ||
handleEvent: _handleEvent | ||
} = require('./events'); | ||
// constants | ||
@@ -33,4 +41,2 @@ const { | ||
const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; | ||
const api = {}; | ||
@@ -49,7 +55,3 @@ module.exports = api; | ||
dataset, | ||
{ | ||
useRdfType = false, | ||
useNativeTypes = false, | ||
rdfDirection = null | ||
} | ||
options | ||
) => { | ||
@@ -59,2 +61,7 @@ const defaultGraph = {}; | ||
const referencedOnce = {}; | ||
const { | ||
useRdfType = false, | ||
useNativeTypes = false, | ||
rdfDirection = null | ||
} = options; | ||
@@ -90,8 +97,8 @@ for(const quad of dataset) { | ||
if(p === RDF_TYPE && !useRdfType && objectIsNode) { | ||
util.addValue(node, '@type', o.value, {propertyIsArray: true}); | ||
_addValue(node, '@type', o.value, {propertyIsArray: true}); | ||
continue; | ||
} | ||
const value = _RDFToObject(o, useNativeTypes, rdfDirection); | ||
util.addValue(node, p, value, {propertyIsArray: true}); | ||
const value = _RDFToObject(o, useNativeTypes, rdfDirection, options); | ||
_addValue(node, p, value, {propertyIsArray: true}); | ||
@@ -155,3 +162,3 @@ // object may be an RDF list/partial list node but we can't know easily | ||
if(p === RDF_TYPE && !useRdfType && objectIsId) { | ||
util.addValue(node, '@type', o.value, {propertyIsArray: true}); | ||
_addValue(node, '@type', o.value, {propertyIsArray: true}); | ||
continue; | ||
@@ -161,3 +168,3 @@ } | ||
const value = _RDFToObject(o, useNativeTypes); | ||
util.addValue(node, p, value, {propertyIsArray: true}); | ||
_addValue(node, p, value, {propertyIsArray: true}); | ||
@@ -285,6 +292,8 @@ // object may be an RDF list/partial list node but we can't know easily | ||
* @param useNativeTypes true to output native types, false not to. | ||
* @param rdfDirection text direction mode [null, i18n-datatype] | ||
* @param options top level API options | ||
* | ||
* @return the JSON-LD object. | ||
*/ | ||
function _RDFToObject(o, useNativeTypes, rdfDirection) { | ||
function _RDFToObject(o, useNativeTypes, rdfDirection, options) { | ||
// convert NamedNode/BlankNode object to JSON-LD | ||
@@ -300,2 +309,18 @@ if(o.termType.endsWith('Node')) { | ||
if(o.language) { | ||
if(!o.language.match(REGEX_BCP47)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'invalid @language value', | ||
level: 'warning', | ||
message: '@language value must be valid BCP47.', | ||
details: { | ||
language: o.language | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
rval['@language'] = o.language; | ||
@@ -346,3 +371,16 @@ } else { | ||
if(!language.match(REGEX_BCP47)) { | ||
console.warn(`@language must be valid BCP47: ${language}`); | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'invalid @language value', | ||
level: 'warning', | ||
message: '@language value must be valid BCP47.', | ||
details: { | ||
language | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
} | ||
@@ -349,0 +387,0 @@ } |
@@ -109,7 +109,8 @@ /* | ||
// 1. It is an Object. | ||
// 2. If it has an @id key its value begins with '_:'. | ||
// 2. If it has an @id key that is not a string OR begins with '_:'. | ||
// 3. It has no keys OR is not a @value, @set, or @list. | ||
if(types.isObject(v)) { | ||
if('@id' in v) { | ||
return (v['@id'].indexOf('_:') === 0); | ||
const id = v['@id']; | ||
return !types.isString(id) || id.indexOf('_:') === 0; | ||
} | ||
@@ -116,0 +117,0 @@ return (Object.keys(v).length === 0 || |
@@ -7,3 +7,3 @@ /** | ||
* @license BSD 3-Clause License | ||
* Copyright (c) 2011-2019 Digital Bazaar, Inc. | ||
* Copyright (c) 2011-2022 Digital Bazaar, Inc. | ||
* All rights reserved. | ||
@@ -84,2 +84,12 @@ * | ||
const { | ||
logEventHandler: _logEventHandler, | ||
logWarningEventHandler: _logWarningEventHandler, | ||
safeEventHandler: _safeEventHandler, | ||
setDefaultEventHandler: _setDefaultEventHandler, | ||
setupEventHandler: _setupEventHandler, | ||
strictEventHandler: _strictEventHandler, | ||
unhandledEventHandler: _unhandledEventHandler | ||
} = require('./events'); | ||
/* eslint-disable indent */ | ||
@@ -115,11 +125,4 @@ // attaches jsonld API to the given object | ||
* [documentLoader(url, options)] the document loader. | ||
* [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 | ||
* will be used. | ||
* [framing] true if compaction is occuring during a framing operation. | ||
* [compactionMap(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 | ||
* will be used. | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -182,4 +185,3 @@ * | ||
element: expanded, | ||
options, | ||
compactionMap: options.compactionMap | ||
options | ||
}); | ||
@@ -260,6 +262,3 @@ | ||
* [documentLoader(url, options)] the document loader. | ||
* [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 | ||
* will be used. | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -280,5 +279,2 @@ * | ||
}); | ||
if(options.expansionMap === false) { | ||
options.expansionMap = undefined; | ||
} | ||
@@ -334,4 +330,3 @@ // build set of objects that may have @contexts to resolve | ||
element: toResolve.input, | ||
options, | ||
expansionMap: options.expansionMap | ||
options | ||
}); | ||
@@ -419,2 +414,3 @@ | ||
* [documentLoader(url, options)] the document loader. | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -518,2 +514,3 @@ * | ||
* [documentLoader(url, options)] the document loader. | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -554,2 +551,3 @@ * | ||
* [useNative] true to use a native canonize algorithm | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -609,2 +607,5 @@ * | ||
* (boolean, integer, double), false not to (default: false). | ||
* [rdfDirection] 'i18n-datatype' to support RDF transformation of | ||
* @direction (default: null). | ||
* [safe] true to use safe mode. (default: false) | ||
* | ||
@@ -659,2 +660,3 @@ * @return a Promise that resolves to the JSON-LD document. | ||
* [documentLoader(url, options)] the document loader. | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -752,2 +754,3 @@ * | ||
* [documentLoader(url, options)] the document loader. | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -915,2 +918,3 @@ * | ||
* [documentLoader(url, options)] the document loader. | ||
* [safe] true to use safe mode. (default: false) | ||
* [contextResolver] internal use only. | ||
@@ -1001,2 +1005,10 @@ * | ||
/* Events API and handlers */ | ||
jsonld.logEventHandler = _logEventHandler; | ||
jsonld.logWarningEventHandler = _logWarningEventHandler; | ||
jsonld.safeEventHandler = _safeEventHandler; | ||
jsonld.setDefaultEventHandler = _setDefaultEventHandler; | ||
jsonld.strictEventHandler = _strictEventHandler; | ||
jsonld.unhandledEventHandler = _unhandledEventHandler; | ||
/* Utility API */ | ||
@@ -1023,3 +1035,20 @@ jsonld.util = util; | ||
}) { | ||
return Object.assign({}, {documentLoader}, defaults, options); | ||
// fail if obsolete options present | ||
if(options && 'compactionMap' in options) { | ||
throw new JsonLdError( | ||
'"compactionMap" not supported.', | ||
'jsonld.OptionsError'); | ||
} | ||
if(options && 'expansionMap' in options) { | ||
throw new JsonLdError( | ||
'"expansionMap" not supported.', | ||
'jsonld.OptionsError'); | ||
} | ||
return Object.assign( | ||
{}, | ||
{documentLoader}, | ||
defaults, | ||
options, | ||
{eventHandler: _setupEventHandler({options})} | ||
); | ||
} | ||
@@ -1026,0 +1055,0 @@ |
@@ -14,2 +14,6 @@ /* | ||
const { | ||
handleEvent: _handleEvent | ||
} = require('./events'); | ||
const { | ||
// RDF, | ||
@@ -70,2 +74,16 @@ // RDF_LIST, | ||
// skip relative IRIs (not valid RDF) | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative graph reference', | ||
level: 'warning', | ||
message: 'Relative graph reference found.', | ||
details: { | ||
graph: graphName | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
continue; | ||
@@ -112,2 +130,16 @@ } | ||
if(!_isAbsoluteIri(id)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative subject reference', | ||
level: 'warning', | ||
message: 'Relative subject reference found.', | ||
details: { | ||
subject: id | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
continue; | ||
@@ -124,2 +156,16 @@ } | ||
if(!_isAbsoluteIri(property)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative property reference', | ||
level: 'warning', | ||
message: 'Relative property reference found.', | ||
details: { | ||
property | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
continue; | ||
@@ -131,2 +177,18 @@ } | ||
!options.produceGeneralizedRdf) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'blank node predicate', | ||
level: 'warning', | ||
message: 'Dropping blank node predicate.', | ||
details: { | ||
// FIXME: add better issuer API to get reverse mapping | ||
property: issuer.getOldIds() | ||
.find(key => issuer.getId(key) === property) | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
continue; | ||
@@ -136,4 +198,4 @@ } | ||
// convert list, value or node object to triple | ||
const object = | ||
_objectToRDF(item, issuer, dataset, graphTerm, options.rdfDirection); | ||
const object = _objectToRDF( | ||
item, issuer, dataset, graphTerm, options.rdfDirection, options); | ||
// skip null objects (they are relative IRIs) | ||
@@ -161,6 +223,7 @@ if(object) { | ||
* @param graphTerm the graph term for each quad. | ||
* @param options the RDF serialization options. | ||
* | ||
* @return the head of the list. | ||
*/ | ||
function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection) { | ||
function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection, options) { | ||
const first = {termType: 'NamedNode', value: RDF_FIRST}; | ||
@@ -176,3 +239,4 @@ const rest = {termType: 'NamedNode', value: RDF_REST}; | ||
for(const item of list) { | ||
const object = _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection); | ||
const object = _objectToRDF( | ||
item, issuer, dataset, graphTerm, rdfDirection, options); | ||
const next = {termType: 'BlankNode', value: issuer.getId()}; | ||
@@ -196,3 +260,4 @@ dataset.push({ | ||
if(last) { | ||
const object = _objectToRDF(last, issuer, dataset, graphTerm, rdfDirection); | ||
const object = _objectToRDF( | ||
last, issuer, dataset, graphTerm, rdfDirection, options); | ||
dataset.push({ | ||
@@ -223,6 +288,9 @@ subject, | ||
* @param graphTerm the graph term for each quad. | ||
* @param options the RDF serialization options. | ||
* | ||
* @return the RDF literal or RDF resource. | ||
*/ | ||
function _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection) { | ||
function _objectToRDF( | ||
item, issuer, dataset, graphTerm, rdfDirection, options | ||
) { | ||
const object = {}; | ||
@@ -273,4 +341,4 @@ | ||
} else if(graphTypes.isList(item)) { | ||
const _list = | ||
_listToRDF(item['@list'], issuer, dataset, graphTerm, rdfDirection); | ||
const _list = _listToRDF( | ||
item['@list'], issuer, dataset, graphTerm, rdfDirection, options); | ||
object.termType = _list.termType; | ||
@@ -287,2 +355,16 @@ object.value = _list.value; | ||
if(object.termType === 'NamedNode' && !_isAbsoluteIri(object.value)) { | ||
if(options.eventHandler) { | ||
_handleEvent({ | ||
event: { | ||
type: ['JsonLdEvent'], | ||
code: 'relative type reference', | ||
level: 'warning', | ||
message: 'Relative type reference found.', | ||
details: { | ||
type: object.value | ||
} | ||
}, | ||
options | ||
}); | ||
} | ||
return null; | ||
@@ -289,0 +371,0 @@ } |
@@ -13,2 +13,3 @@ /* | ||
// constants | ||
const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; | ||
const REGEX_LINK_HEADERS = /(?:<[^>]*?>|"[^"]*?"|[^,])+/g; | ||
@@ -18,2 +19,3 @@ const REGEX_LINK_HEADER = /\s*<([^>]*?)>\s*(?:;\s*(.*))?/; | ||
/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/g; | ||
const REGEX_KEYWORD = /^@[a-zA-Z]+$/; | ||
@@ -29,2 +31,4 @@ const DEFAULTS = { | ||
api.IdentifierIssuer = IdentifierIssuer; | ||
api.REGEX_BCP47 = REGEX_BCP47; | ||
api.REGEX_KEYWORD = REGEX_KEYWORD; | ||
@@ -31,0 +35,0 @@ /** |
{ | ||
"name": "jsonld", | ||
"version": "6.0.0", | ||
"version": "7.0.0", | ||
"description": "A JSON-LD Processor and API implementation in JavaScript.", | ||
@@ -52,4 +52,4 @@ "homepage": "https://github.com/digitalbazaar/jsonld.js", | ||
"envify": "^4.1.0", | ||
"eslint": "^7.23.0", | ||
"eslint-config-digitalbazaar": "^2.6.1", | ||
"eslint": "^8.17.0", | ||
"eslint-config-digitalbazaar": "^3.0.0", | ||
"esmify": "^2.1.1", | ||
@@ -56,0 +56,0 @@ "express": "^4.16.4", |
@@ -349,2 +349,20 @@ jsonld.js | ||
### Safe Mode | ||
A common use case is to avoid JSON-LD constructs that will result in lossy | ||
behavior. The JSON-LD specifications have notes about when data is dropped. | ||
This can be especially important when calling [`canonize`][] in order to | ||
digitally sign data. A special "safe mode" is available that will detect these | ||
situations and cause processing to fail. | ||
**Note**: This mode is designed to be the common way that digital signing and | ||
similar applications use this library. | ||
The `safe` options flag set to `true` enables this behavior: | ||
```js | ||
// expand a document in safe mode | ||
const expanded = await jsonld.expand(data, {safe: true}); | ||
``` | ||
Related Modules | ||
@@ -459,2 +477,12 @@ --------------- | ||
EARL reports with benchmark data can be generated with an optional environment | ||
details: | ||
JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test | ||
See `tests/test.js` for more `TEST_ENV` control and options. | ||
These reports can be compared with the `benchmarks/compare/` tool and at the | ||
[JSON-LD Benchmarks][] site. | ||
[Digital Bazaar]: https://digitalbazaar.com/ | ||
@@ -482,2 +510,3 @@ | ||
[JSON-LD Benchmarks]: https://json-ld.org/benchmarks/ | ||
[JSON-LD Processor Conformance]: https://w3c.github.io/json-ld-api/reports | ||
@@ -484,0 +513,0 @@ [JSON-LD WG]: https://www.w3.org/2018/json-ld-wg/ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
2781569
35
20816
525