jsonld
Version:
A JSON-LD Processor and API implementation in JavaScript.
1,282 lines (1,194 loc) • 39.9 kB
JavaScript
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';
const JsonLdError = require('./JsonLdError');
const {
isArray: _isArray,
isObject: _isObject,
isEmptyObject: _isEmptyObject,
isString: _isString,
isUndefined: _isUndefined
} = require('./types');
const {
isList: _isList,
isValue: _isValue,
isGraph: _isGraph,
isSubject: _isSubject
} = require('./graphTypes');
const {
expandIri: _expandIri,
getContextValue: _getContextValue,
isKeyword: _isKeyword,
process: _processContext,
processingMode: _processingMode
} = require('./context');
const {
isAbsolute: _isAbsoluteIri
} = require('./url');
const {
REGEX_BCP47,
REGEX_KEYWORD,
addValue: _addValue,
asArray: _asArray,
getValues: _getValues,
validateTypeValue: _validateTypeValue
} = require('./util');
const {
handleEvent: _handleEvent
} = require('./events');
const api = {};
module.exports = api;
/**
* Recursively expands an element using the given context. Any context in
* the element will be removed. All context URLs must have been retrieved
* before calling this method.
*
* @param activeCtx the context to use.
* @param activeProperty the property for the element, null for none.
* @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 typeScopedContext an optional type-scoped active context for
* expanding values of nodes that were expressed according to
* a type-scoped context.
*
* @return a Promise that resolves to the expanded value.
*/
api.expand = async ({
activeCtx,
activeProperty = null,
element,
options = {},
insideList = false,
insideIndex = false,
typeScopedContext = null
}) => {
// nothing to expand
if(element === null || element === undefined) {
return null;
}
// disable framing if activeProperty is @default
if(activeProperty === '@default') {
options = Object.assign({}, options, {isFrame: false});
}
if(!_isArray(element) && !_isObject(element)) {
// drop free-floating scalars that are not in lists
if(!insideList && (activeProperty === null ||
_expandIri(activeCtx, activeProperty, {vocab: true},
options) === '@graph')) {
// 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 null;
}
// expand element according to value expansion rules
return _expandValue({activeCtx, activeProperty, value: element, options});
}
// recursively expand array
if(_isArray(element)) {
let rval = [];
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
insideList = insideList || container.includes('@list');
for(let i = 0; i < element.length; ++i) {
// expand element
let e = await api.expand({
activeCtx,
activeProperty,
element: element[i],
options,
insideIndex,
typeScopedContext
});
if(insideList && _isArray(e)) {
e = {'@list': e};
}
if(e === null) {
// 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;
}
if(_isArray(e)) {
rval = rval.concat(e);
} else {
rval.push(e);
}
}
return rval;
}
// recursively expand object:
// first, expand the active property
const expandedActiveProperty = _expandIri(
activeCtx, activeProperty, {vocab: true}, options);
// Get any property-scoped context for activeProperty
const propertyScopedCtx =
_getContextValue(activeCtx, activeProperty, '@context');
// second, determine if any type-scoped context should be reverted; it
// should only be reverted when the following are all true:
// 1. `element` is not a value or subject reference
// 2. `insideIndex` is false
typeScopedContext = typeScopedContext ||
(activeCtx.previousContext ? activeCtx : null);
let keys = Object.keys(element).sort();
let mustRevert = !insideIndex;
if(mustRevert && typeScopedContext && keys.length <= 2 &&
!keys.includes('@context')) {
for(const key of keys) {
const expandedProperty = _expandIri(
typeScopedContext, key, {vocab: true}, options);
if(expandedProperty === '@value') {
// value found, ensure type-scoped context is used to expand it
mustRevert = false;
activeCtx = typeScopedContext;
break;
}
if(expandedProperty === '@id' && keys.length === 1) {
// subject reference found, do not revert
mustRevert = false;
break;
}
}
}
if(mustRevert) {
// revert type scoped context
activeCtx = activeCtx.revertToPreviousContext();
}
// apply property-scoped context after reverting term-scoped context
if(!_isUndefined(propertyScopedCtx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: propertyScopedCtx,
propagate: true,
overrideProtected: true,
options
});
}
// if element has a context, process it
if('@context' in element) {
activeCtx = await _processContext(
{activeCtx, localCtx: element['@context'], options});
}
// set the type-scoped context to the context on input, for use later
typeScopedContext = activeCtx;
// Remember the first key found expanding to @type
let typeKey = null;
// look for scoped contexts on `@type`
for(const key of keys) {
const expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options);
if(expandedProperty === '@type') {
// set scoped contexts from @type
// avoid sorting if possible
typeKey = typeKey || key;
const value = element[key];
const types =
Array.isArray(value) ?
(value.length > 1 ? value.slice().sort() : value) : [value];
for(const type of types) {
const ctx = _getContextValue(typeScopedContext, type, '@context');
if(!_isUndefined(ctx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: ctx,
options,
propagate: false
});
}
}
}
}
// process each key and value in element, ignoring @nest content
let rval = {};
await _expandObject({
activeCtx,
activeProperty,
expandedActiveProperty,
element,
expandedParent: rval,
options,
insideList,
typeKey,
typeScopedContext
});
// get property count on expanded output
keys = Object.keys(rval);
let count = keys.length;
if('@value' in rval) {
// @value must only have @language or @type
if('@type' in rval && ('@language' in rval || '@direction' in rval)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; an element containing "@value" may not ' +
'contain both "@type" and either "@language" or "@direction".',
'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
}
let validCount = count - 1;
if('@type' in rval) {
validCount -= 1;
}
if('@index' in rval) {
validCount -= 1;
}
if('@language' in rval) {
validCount -= 1;
}
if('@direction' in rval) {
validCount -= 1;
}
if(validCount !== 0) {
throw new JsonLdError(
'Invalid JSON-LD syntax; an element containing "@value" may only ' +
'have an "@index" property and either "@type" ' +
'or either or both "@language" or "@direction".',
'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
}
const values = rval['@value'] === null ? [] : _asArray(rval['@value']);
const types = _getValues(rval, '@type');
// drop null @values
if(_processingMode(activeCtx, 1.1) && types.includes('@json') &&
types.length === 1) {
// Any value of @value is okay if @type: @json
} else if(values.length === 0) {
// 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))) &&
'@language' in rval) {
// if @language is present, @value must be a string
throw new JsonLdError(
'Invalid JSON-LD syntax; only strings may be language-tagged.',
'jsonld.SyntaxError',
{code: 'invalid language-tagged value', element: rval});
} else if(!types.every(t =>
(_isAbsoluteIri(t) && !(_isString(t) && t.indexOf('_:') === 0) ||
_isEmptyObject(t)))) {
throw new JsonLdError(
'Invalid JSON-LD syntax; an element containing "@value" and "@type" ' +
'must have an absolute IRI for the value of "@type".',
'jsonld.SyntaxError', {code: 'invalid typed value', element: rval});
}
} else if('@type' in rval && !_isArray(rval['@type'])) {
// convert @type to an array
rval['@type'] = [rval['@type']];
} else if('@set' in rval || '@list' in rval) {
// handle @set and @list
if(count > 1 && !(count === 2 && '@index' in rval)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; if an element has the property "@set" ' +
'or "@list", then it can have at most one other property that is ' +
'"@index".', 'jsonld.SyntaxError',
{code: 'invalid set or list object', element: rval});
}
// optimize away @set
if('@set' in rval) {
rval = rval['@set'];
keys = Object.keys(rval);
count = keys.length;
}
} else if(count === 1 && '@language' in rval) {
// 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
if(_isObject(rval) &&
!options.keepFreeFloatingNodes && !insideList &&
(activeProperty === null ||
expandedActiveProperty === '@graph' ||
(_getContextValue(activeCtx, activeProperty, '@container') || [])
.includes('@graph')
)) {
// drop empty object, top-level @value/@list, or object with only @id
rval = _dropUnsafeObject({value: rval, count, options});
}
return rval;
};
/**
* Drop empty object, top-level @value/@list, or object with only @id
*
* @param value Value to check.
* @param count Number of properties in object.
* @param options The expansion options.
*
* @return null if dropped, value otherwise.
*/
function _dropUnsafeObject({
value,
count,
options
}) {
if(count === 0 || '@value' in value || '@list' in value ||
(count === 1 && '@id' in value)) {
// 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 value) {
code = 'object with only @value';
message = 'Dropping object with only @value.';
} else if('@list' in value) {
code = 'object with only @list';
message = 'Dropping object with only @list.';
} else if(count === 1 && '@id' in value) {
code = 'object with only @id';
message = 'Dropping object with only @id.';
}
_handleEvent({
event: {
type: ['JsonLdEvent'],
code,
level: 'warning',
message,
details: {
value
}
},
options
});
}
return null;
}
return value;
}
/**
* Expand each key and value of element adding to result
*
* @param activeCtx the context to use.
* @param activeProperty the property for the element.
* @param expandedActiveProperty the expansion of activeProperty
* @param element the element to expand.
* @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.
*/
async function _expandObject({
activeCtx,
activeProperty,
expandedActiveProperty,
element,
expandedParent,
options = {},
insideList,
typeKey,
typeScopedContext
}) {
const keys = Object.keys(element).sort();
const nests = [];
let unexpandedValue;
// Figure out if this is the type for a JSON literal
const isJsonType = element[typeKey] &&
_expandIri(activeCtx,
(_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]),
{vocab: true}, {
...options,
typeExpansion: true
}) === '@json';
for(const key of keys) {
let value = element[key];
let expandedValue;
// skip @context
if(key === '@context') {
continue;
}
// expand property
const expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options);
// drop non-absolute IRI keys that aren't keywords
if(expandedProperty === null ||
!(_isAbsoluteIri(expandedProperty) || _isKeyword(expandedProperty))) {
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;
}
if(_isKeyword(expandedProperty)) {
if(expandedActiveProperty === '@reverse') {
throw new JsonLdError(
'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' +
'property.', 'jsonld.SyntaxError',
{code: 'invalid reverse property map', value});
}
if(expandedProperty in expandedParent &&
expandedProperty !== '@included' &&
expandedProperty !== '@type') {
throw new JsonLdError(
'Invalid JSON-LD syntax; colliding keywords detected.',
'jsonld.SyntaxError',
{code: 'colliding keywords', keyword: expandedProperty});
}
}
// syntax error if @id is not a string
if(expandedProperty === '@id') {
if(!_isString(value)) {
if(!options.isFrame) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@id" value must a string.',
'jsonld.SyntaxError', {code: 'invalid @id value', value});
}
if(_isObject(value)) {
// empty object is a wildcard
if(!_isEmptyObject(value)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@id" value an empty object or array ' +
'of strings, if framing',
'jsonld.SyntaxError', {code: 'invalid @id value', value});
}
} else if(_isArray(value)) {
if(!value.every(v => _isString(v))) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@id" value an empty object or array ' +
'of strings, if framing',
'jsonld.SyntaxError', {code: 'invalid @id value', value});
}
} else {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@id" value an empty object or array ' +
'of strings, if framing',
'jsonld.SyntaxError', {code: 'invalid @id value', value});
}
}
_addValue(
expandedParent, '@id',
_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});
continue;
}
if(expandedProperty === '@type') {
// if framing, can be a default object, but need to expand
// key to determine that
if(_isObject(value)) {
value = Object.fromEntries(Object.entries(value).map(([k, v]) => [
_expandIri(typeScopedContext, k, {vocab: true}),
_asArray(v).map(vv =>
_expandIri(typeScopedContext, vv, {base: true, vocab: true},
{...options, typeExpansion: true})
)
]));
}
_validateTypeValue(value, options.isFrame);
_addValue(
expandedParent, '@type',
_asArray(value).map(v => {
if(_isString(v)) {
const ve = _expandIri(typeScopedContext, v,
{base: true, vocab: true},
{...options, typeExpansion: true});
if(ve !== '@json' && !_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});
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
}));
// 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))) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@graph" value must not be an ' +
'object or an array.',
'jsonld.SyntaxError', {code: 'invalid @graph value', value});
}
if(expandedProperty === '@value') {
// capture value for later
// "colliding keywords" check prevents this from being set twice
unexpandedValue = value;
if(isJsonType && _processingMode(activeCtx, 1.1)) {
// no coercion to array, and retain all values
expandedParent['@value'] = value;
} else {
_addValue(
expandedParent, '@value', value, {propertyIsArray: options.isFrame});
}
continue;
}
// @language must be a string
// it should match BCP47
if(expandedProperty === '@language') {
if(value === null) {
// drop null @language values, they expand as if they didn't exist
continue;
}
if(!_isString(value) && !options.isFrame) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@language" value must be a string.',
'jsonld.SyntaxError',
{code: 'invalid language-tagged string', value});
}
// ensure language value is lowercase
value = _asArray(value).map(v => _isString(v) ? v.toLowerCase() : v);
// ensure language tag matches BCP47
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
});
}
}
}
_addValue(
expandedParent, '@language', value, {propertyIsArray: options.isFrame});
continue;
}
// @direction must be "ltr" or "rtl"
if(expandedProperty === '@direction') {
if(!_isString(value) && !options.isFrame) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@direction" value must be a string.',
'jsonld.SyntaxError',
{code: 'invalid base direction', value});
}
value = _asArray(value);
// ensure direction is "ltr" or "rtl"
for(const dir of value) {
if(_isString(dir) && dir !== 'ltr' && dir !== 'rtl') {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@direction" must be "ltr" or "rtl".',
'jsonld.SyntaxError',
{code: 'invalid base direction', value});
}
}
_addValue(
expandedParent, '@direction', value,
{propertyIsArray: options.isFrame});
continue;
}
// @index must be a string
if(expandedProperty === '@index') {
if(!_isString(value)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@index" value must be a string.',
'jsonld.SyntaxError',
{code: 'invalid @index value', value});
}
_addValue(expandedParent, '@index', value);
continue;
}
// @reverse must be an object
if(expandedProperty === '@reverse') {
if(!_isObject(value)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@reverse" value must be an object.',
'jsonld.SyntaxError', {code: 'invalid @reverse value', value});
}
expandedValue = await api.expand({
activeCtx,
activeProperty: '@reverse',
element: value,
options
});
// properties double-reversed
if('@reverse' in expandedValue) {
for(const property in expandedValue['@reverse']) {
_addValue(
expandedParent, property, expandedValue['@reverse'][property],
{propertyIsArray: true});
}
}
// FIXME: can this be merged with code below to simplify?
// merge in all reversed properties
let reverseMap = expandedParent['@reverse'] || null;
for(const property in expandedValue) {
if(property === '@reverse') {
continue;
}
if(reverseMap === null) {
reverseMap = expandedParent['@reverse'] = {};
}
_addValue(reverseMap, property, [], {propertyIsArray: true});
const items = expandedValue[property];
for(let ii = 0; ii < items.length; ++ii) {
const item = items[ii];
if(_isValue(item) || _isList(item)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
'@value or an @list.', 'jsonld.SyntaxError',
{code: 'invalid reverse property value', value: expandedValue});
}
_addValue(reverseMap, property, item, {propertyIsArray: true});
}
}
continue;
}
// nested keys
if(expandedProperty === '@nest') {
nests.push(key);
continue;
}
// use potential scoped context for key
let termCtx = activeCtx;
const ctx = _getContextValue(activeCtx, key, '@context');
if(!_isUndefined(ctx)) {
termCtx = await _processContext({
activeCtx,
localCtx: ctx,
propagate: true,
overrideProtected: true,
options
});
}
const container = _getContextValue(activeCtx, key, '@container') || [];
if(container.includes('@language') && _isObject(value)) {
const direction = _getContextValue(termCtx, key, '@direction');
// handle language map container (skip if value is not an object)
expandedValue = _expandLanguageMap(termCtx, value, direction, options);
} else if(container.includes('@index') && _isObject(value)) {
// handle index container (skip if value is not an object)
const asGraph = container.includes('@graph');
const indexKey = _getContextValue(termCtx, key, '@index') || '@index';
const propertyIndex = indexKey !== '@index' &&
_expandIri(activeCtx, indexKey, {vocab: true}, options);
expandedValue = await _expandIndexMap({
activeCtx: termCtx,
options,
activeProperty: key,
value,
asGraph,
indexKey,
propertyIndex
});
} else if(container.includes('@id') && _isObject(value)) {
// handle id container (skip if value is not an object)
const asGraph = container.includes('@graph');
expandedValue = await _expandIndexMap({
activeCtx: termCtx,
options,
activeProperty: key,
value,
asGraph,
indexKey: '@id'
});
} else if(container.includes('@type') && _isObject(value)) {
// handle type container (skip if value is not an object)
expandedValue = await _expandIndexMap({
// since container is `@type`, revert type scoped context when expanding
activeCtx: termCtx.revertToPreviousContext(),
options,
activeProperty: key,
value,
asGraph: false,
indexKey: '@type'
});
} else {
// recurse into @list or @set
const isList = expandedProperty === '@list';
if(isList || expandedProperty === '@set') {
let nextActiveProperty = activeProperty;
if(isList && expandedActiveProperty === '@graph') {
nextActiveProperty = null;
}
expandedValue = await api.expand({
activeCtx: termCtx,
activeProperty: nextActiveProperty,
element: value,
options,
insideList: isList
});
} else if(
_getContextValue(activeCtx, key, '@type') === '@json') {
expandedValue = {
'@type': '@json',
'@value': value
};
} else {
// recursively expand value with key as new active property
expandedValue = await api.expand({
activeCtx: termCtx,
activeProperty: key,
element: value,
options,
insideList: false
});
}
}
// drop null values if property is not @value
if(expandedValue === null && expandedProperty !== '@value') {
// FIXME: event?
//unmappedValue: value,
//expandedProperty,
//key,
continue;
}
// convert expanded value to @list if container specifies it
if(expandedProperty !== '@list' && !_isList(expandedValue) &&
container.includes('@list')) {
// ensure expanded value in @list is an array
expandedValue = {'@list': _asArray(expandedValue)};
}
// convert expanded value to @graph if container specifies it
// and value is not, itself, a graph
// index cases handled above
if(container.includes('@graph') &&
!container.some(key => key === '@id' || key === '@index')) {
// ensure expanded values are in an array
expandedValue = _asArray(expandedValue);
if(!options.isFrame) {
// drop items if needed
expandedValue = expandedValue.filter(v => {
const count = Object.keys(v).length;
return _dropUnsafeObject({value: v, count, options}) !== null;
});
}
if(expandedValue.length === 0) {
// all items dropped, skip adding and continue
continue;
}
// convert to graph
expandedValue = expandedValue.map(v => ({'@graph': _asArray(v)}));
}
// FIXME: can this be merged with code above to simplify?
// merge in reverse properties
if(termCtx.mappings.has(key) && termCtx.mappings.get(key).reverse) {
const reverseMap =
expandedParent['@reverse'] = expandedParent['@reverse'] || {};
expandedValue = _asArray(expandedValue);
for(let ii = 0; ii < expandedValue.length; ++ii) {
const item = expandedValue[ii];
if(_isValue(item) || _isList(item)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
'@value or an @list.', 'jsonld.SyntaxError',
{code: 'invalid reverse property value', value: expandedValue});
}
_addValue(reverseMap, expandedProperty, item, {propertyIsArray: true});
}
continue;
}
// add value for property
// special keywords handled above
_addValue(expandedParent, expandedProperty, expandedValue, {
propertyIsArray: true
});
}
// @value must not be an object or an array (unless framing) or if @type is
// @json
if('@value' in expandedParent) {
if(expandedParent['@type'] === '@json' && _processingMode(activeCtx, 1.1)) {
// allow any value, to be verified when the object is fully expanded and
// the @type is @json.
} else if((_isObject(unexpandedValue) || _isArray(unexpandedValue)) &&
!options.isFrame) {
throw new JsonLdError(
'Invalid JSON-LD syntax; "@value" value must not be an ' +
'object or an array.',
'jsonld.SyntaxError',
{code: 'invalid value object value', value: unexpandedValue});
}
}
// expand each nested key
for(const key of nests) {
const nestedValues = _isArray(element[key]) ? element[key] : [element[key]];
for(const nv of nestedValues) {
if(!_isObject(nv) || Object.keys(nv).some(k =>
_expandIri(activeCtx, k, {vocab: true}, options) === '@value')) {
throw new JsonLdError(
'Invalid JSON-LD syntax; nested value must be a node object.',
'jsonld.SyntaxError',
{code: 'invalid @nest value', value: nv});
}
await _expandObject({
activeCtx,
activeProperty,
expandedActiveProperty,
element: nv,
expandedParent,
options,
insideList,
typeScopedContext,
typeKey
});
}
}
}
/**
* Expands the given value by using the coercion and keyword rules in the
* given context.
*
* @param activeCtx the active context to use.
* @param activeProperty the active property the value is associated with.
* @param value the value to expand.
* @param {Object} [options] - processing options.
*
* @return the expanded value.
*/
function _expandValue({activeCtx, activeProperty, value, options}) {
// nothing to expand
if(value === null || value === undefined) {
return null;
}
// special-case expand @id and @type (skips '@id' expansion)
const expandedProperty = _expandIri(
activeCtx, activeProperty, {vocab: true}, options);
if(expandedProperty === '@id') {
return _expandIri(activeCtx, value, {base: true}, options);
} else if(expandedProperty === '@type') {
return _expandIri(activeCtx, value, {vocab: true, base: true},
{...options, typeExpansion: true});
}
// get type definition from context
const type = _getContextValue(activeCtx, activeProperty, '@type');
// do @id expansion (automatic for @graph)
if((type === '@id' || expandedProperty === '@graph') && _isString(value)) {
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};
}
// do @id expansion w/vocab
if(type === '@vocab' && _isString(value)) {
return {
'@id': _expandIri(activeCtx, value, {vocab: true, base: true}, options)
};
}
// do not expand keyword values
if(_isKeyword(expandedProperty)) {
return value;
}
const rval = {};
if(type && !['@id', '@vocab', '@none'].includes(type)) {
// other type
rval['@type'] = type;
} else if(_isString(value)) {
// check for language tagging for strings
const language = _getContextValue(activeCtx, activeProperty, '@language');
if(language !== null) {
rval['@language'] = language;
}
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
if(direction !== null) {
rval['@direction'] = direction;
}
}
// do conversion of values that aren't basic JSON types to strings
if(!['boolean', 'number', 'string'].includes(typeof value)) {
value = value.toString();
}
rval['@value'] = value;
return rval;
}
/**
* Expands a language map.
*
* @param activeCtx the active context to use.
* @param languageMap the language map to expand.
* @param direction the direction to apply to values.
* @param {Object} [options] - processing options.
*
* @return the expanded language map.
*/
function _expandLanguageMap(activeCtx, languageMap, direction, options) {
const rval = [];
const keys = Object.keys(languageMap).sort();
for(const key of keys) {
const expandedKey = _expandIri(activeCtx, key, {vocab: true}, options);
let val = languageMap[key];
if(!_isArray(val)) {
val = [val];
}
for(const item of val) {
if(item === null) {
// null values are allowed (8.5) but ignored (3.1)
continue;
}
if(!_isString(item)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; language map values must be strings.',
'jsonld.SyntaxError',
{code: 'invalid language map value', languageMap});
}
const val = {'@value': item};
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();
}
if(direction) {
val['@direction'] = direction;
}
rval.push(val);
}
}
return rval;
}
async function _expandIndexMap({
activeCtx, options, activeProperty, value, asGraph, indexKey, propertyIndex
}) {
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
if(isTypeIndex) {
const ctx = _getContextValue(activeCtx, key, '@context');
if(!_isUndefined(ctx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: ctx,
propagate: false,
options
});
}
}
let val = value[key];
if(!_isArray(val)) {
val = [val];
}
val = await api.expand({
activeCtx,
activeProperty,
element: val,
options,
insideList: false,
insideIndex: true
});
// expand for @type, but also for @none
let expandedKey;
if(propertyIndex) {
if(key === '@none') {
expandedKey = '@none';
} else {
expandedKey = _expandValue(
{activeCtx, activeProperty: indexKey, value: key, options});
}
} else {
expandedKey = _expandIri(activeCtx, key, {vocab: true}, options);
}
if(indexKey === '@id') {
// expand document relative
key = _expandIri(activeCtx, key, {base: true}, options);
} else if(isTypeIndex) {
key = expandedKey;
}
for(let item of val) {
// If this is also a @graph container, turn items into graphs
if(asGraph && !_isGraph(item)) {
item = {'@graph': [item]};
}
if(indexKey === '@type') {
if(expandedKey === '@none') {
// ignore @none
} else if(item['@type']) {
item['@type'] = [key].concat(item['@type']);
} else {
item['@type'] = [key];
}
} else if(_isValue(item) &&
!['@language', '@type', '@index'].includes(indexKey)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; Attempt to add illegal key to value ' +
`object: "${indexKey}".`,
'jsonld.SyntaxError',
{code: 'invalid value object', value: item});
} else if(propertyIndex) {
// index is a property to be expanded, and values interpreted for that
// property
if(expandedKey !== '@none') {
// expand key as a value
_addValue(item, propertyIndex, expandedKey, {
propertyIsArray: true,
prependValue: true
});
}
} else if(expandedKey !== '@none' && !(indexKey in item)) {
item[indexKey] = key;
}
rval.push(item);
}
}
return rval;
}