jsonld
Version:
A JSON-LD Processor and API implementation in JavaScript.
1,598 lines (1,426 loc) • 792 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["jsonld"] = factory();
else
root["jsonld"] = factory();
})(this, () => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./lib/ContextResolver.js":
/*!********************************!*\
!*** ./lib/ContextResolver.js ***!
\********************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
/*
* Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved.
*/
const {
isArray: _isArray,
isObject: _isObject,
isString: _isString,
} = __webpack_require__(/*! ./types */ "./lib/types.js");
const {
asArray: _asArray
} = __webpack_require__(/*! ./util */ "./lib/util.js");
const {prependBase} = __webpack_require__(/*! ./url */ "./lib/url.js");
const JsonLdError = __webpack_require__(/*! ./JsonLdError */ "./lib/JsonLdError.js");
const ResolvedContext = __webpack_require__(/*! ./ResolvedContext */ "./lib/ResolvedContext.js");
const MAX_CONTEXT_URLS = 10;
module.exports = class ContextResolver {
/**
* Creates a ContextResolver.
*
* @param sharedCache a shared LRU cache with `get` and `set` APIs.
*/
constructor({sharedCache}) {
this.perOpCache = new Map();
this.sharedCache = sharedCache;
}
async resolve({
activeCtx, context, documentLoader, base, cycles = new Set()
}) {
// process `@context`
if(context && _isObject(context) && context['@context']) {
context = context['@context'];
}
// context is one or more contexts
context = _asArray(context);
// resolve each context in the array
const allResolved = [];
for(const ctx of context) {
if(_isString(ctx)) {
// see if `ctx` has been resolved before...
let resolved = this._get(ctx);
if(!resolved) {
// not resolved yet, resolve
resolved = await this._resolveRemoteContext(
{activeCtx, url: ctx, documentLoader, base, cycles});
}
// add to output and continue
if(_isArray(resolved)) {
allResolved.push(...resolved);
} else {
allResolved.push(resolved);
}
continue;
}
if(ctx === null) {
// handle `null` context, nothing to cache
allResolved.push(new ResolvedContext({document: null}));
continue;
}
if(!_isObject(ctx)) {
_throwInvalidLocalContext(context);
}
// context is an object, get/create `ResolvedContext` for it
const key = JSON.stringify(ctx);
let resolved = this._get(key);
if(!resolved) {
// create a new static `ResolvedContext` and cache it
resolved = new ResolvedContext({document: ctx});
this._cacheResolvedContext({key, resolved, tag: 'static'});
}
allResolved.push(resolved);
}
return allResolved;
}
_get(key) {
// get key from per operation cache; no `tag` is used with this cache so
// any retrieved context will always be the same during a single operation
let resolved = this.perOpCache.get(key);
if(!resolved) {
// see if the shared cache has a `static` entry for this URL
const tagMap = this.sharedCache.get(key);
if(tagMap) {
resolved = tagMap.get('static');
if(resolved) {
this.perOpCache.set(key, resolved);
}
}
}
return resolved;
}
_cacheResolvedContext({key, resolved, tag}) {
this.perOpCache.set(key, resolved);
if(tag !== undefined) {
let tagMap = this.sharedCache.get(key);
if(!tagMap) {
tagMap = new Map();
this.sharedCache.set(key, tagMap);
}
tagMap.set(tag, resolved);
}
return resolved;
}
async _resolveRemoteContext({activeCtx, url, documentLoader, base, cycles}) {
// resolve relative URL and fetch context
url = prependBase(base, url);
const {context, remoteDoc} = await this._fetchContext(
{activeCtx, url, documentLoader, cycles});
// update base according to remote document and resolve any relative URLs
base = remoteDoc.documentUrl || url;
_resolveContextUrls({context, base});
// resolve, cache, and return context
const resolved = await this.resolve(
{activeCtx, context, documentLoader, base, cycles});
this._cacheResolvedContext({key: url, resolved, tag: remoteDoc.tag});
return resolved;
}
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: 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: activeCtx.processingMode === 'json-ld-1.0' ?
'recursive context inclusion' :
'context overflow',
url
});
}
// track cycles
cycles.add(url);
let context;
let remoteDoc;
try {
remoteDoc = await documentLoader(url);
context = remoteDoc.document || null;
// parse string context as JSON
if(_isString(context)) {
context = JSON.parse(context);
}
} catch(e) {
throw new JsonLdError(
'Dereferencing a URL did not result in a valid JSON-LD object. ' +
'Possible causes are an inaccessible URL perhaps due to ' +
'a same-origin policy (ensure the server uses CORS if you are ' +
'using client-side JavaScript), too many redirects, a ' +
'non-JSON response, or more than one HTTP Link Header was ' +
'provided for a remote context. ' +
`URL: "${url}".`,
'jsonld.InvalidUrl',
{code: 'loading remote context failed', url, cause: e});
}
// ensure ctx is an object
if(!_isObject(context)) {
throw new JsonLdError(
'Dereferencing a URL did not result in a JSON object. The ' +
'response was valid JSON, but it was not a JSON object. ' +
`URL: "${url}".`,
'jsonld.InvalidUrl', {code: 'invalid remote context', url});
}
// use empty context if no @context key is present
if(!('@context' in context)) {
context = {'@context': {}};
} else {
context = {'@context': context['@context']};
}
// append @context URL to context if given
if(remoteDoc.contextUrl) {
if(!_isArray(context['@context'])) {
context['@context'] = [context['@context']];
}
context['@context'].push(remoteDoc.contextUrl);
}
return {context, remoteDoc};
}
};
function _throwInvalidLocalContext(ctx) {
throw new JsonLdError(
'Invalid JSON-LD syntax; @context must be an object.',
'jsonld.SyntaxError', {
code: 'invalid local context', context: ctx
});
}
/**
* Resolve all relative `@context` URLs in the given context by inline
* replacing them with absolute URLs.
*
* @param context the context.
* @param base the base IRI to use to resolve relative IRIs.
*/
function _resolveContextUrls({context, base}) {
if(!context) {
return;
}
const ctx = context['@context'];
if(_isString(ctx)) {
context['@context'] = prependBase(base, ctx);
return;
}
if(_isArray(ctx)) {
for(let i = 0; i < ctx.length; ++i) {
const element = ctx[i];
if(_isString(element)) {
ctx[i] = prependBase(base, element);
continue;
}
if(_isObject(element)) {
_resolveContextUrls({context: {'@context': element}, base});
}
}
return;
}
if(!_isObject(ctx)) {
// no @context URLs can be found in non-object
return;
}
// ctx is an object, resolve any context URLs in terms
for(const term in ctx) {
_resolveContextUrls({context: ctx[term], base});
}
}
/***/ }),
/***/ "./lib/JsonLdError.js":
/*!****************************!*\
!*** ./lib/JsonLdError.js ***!
\****************************/
/***/ ((module) => {
"use strict";
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
*/
module.exports = class JsonLdError extends Error {
/**
* Creates a JSON-LD Error.
*
* @param msg the error message.
* @param type the error type.
* @param details the error details.
*/
constructor(
message = 'An unspecified JSON-LD error occurred.',
name = 'jsonld.Error',
details = {}) {
super(message);
this.name = name;
this.message = message;
this.details = details;
}
};
/***/ }),
/***/ "./lib/JsonLdProcessor.js":
/*!********************************!*\
!*** ./lib/JsonLdProcessor.js ***!
\********************************/
/***/ ((module) => {
"use strict";
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
*/
module.exports = jsonld => {
class JsonLdProcessor {
toString() {
return '[object JsonLdProcessor]';
}
}
Object.defineProperty(JsonLdProcessor, 'prototype', {
writable: false,
enumerable: false
});
Object.defineProperty(JsonLdProcessor.prototype, 'constructor', {
writable: true,
enumerable: false,
configurable: true,
value: JsonLdProcessor
});
// The Web IDL test harness will check the number of parameters defined in
// the functions below. The number of parameters must exactly match the
// required (non-optional) parameters of the JsonLdProcessor interface as
// defined here:
// https://www.w3.org/TR/json-ld-api/#the-jsonldprocessor-interface
JsonLdProcessor.compact = function(input, ctx) {
if(arguments.length < 2) {
return Promise.reject(
new TypeError('Could not compact, too few arguments.'));
}
return jsonld.compact(input, ctx);
};
JsonLdProcessor.expand = function(input) {
if(arguments.length < 1) {
return Promise.reject(
new TypeError('Could not expand, too few arguments.'));
}
return jsonld.expand(input);
};
JsonLdProcessor.flatten = function(input) {
if(arguments.length < 1) {
return Promise.reject(
new TypeError('Could not flatten, too few arguments.'));
}
return jsonld.flatten(input);
};
return JsonLdProcessor;
};
/***/ }),
/***/ "./lib/NQuads.js":
/*!***********************!*\
!*** ./lib/NQuads.js ***!
\***********************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
*/
// TODO: move `NQuads` to its own package
module.exports = __webpack_require__(/*! rdf-canonize */ "./node_modules/rdf-canonize/index.js").NQuads;
/***/ }),
/***/ "./lib/RequestQueue.js":
/*!*****************************!*\
!*** ./lib/RequestQueue.js ***!
\*****************************/
/***/ ((module) => {
"use strict";
/*
* Copyright (c) 2017-2019 Digital Bazaar, Inc. All rights reserved.
*/
module.exports = class RequestQueue {
/**
* Creates a simple queue for requesting documents.
*/
constructor() {
this._requests = {};
}
wrapLoader(loader) {
const self = this;
self._loader = loader;
return function(/* url */) {
return self.add.apply(self, arguments);
};
}
async add(url) {
let promise = this._requests[url];
if(promise) {
// URL already queued, wait for it to load
return Promise.resolve(promise);
}
// queue URL and load it
promise = this._requests[url] = this._loader(url);
try {
return await promise;
} finally {
delete this._requests[url];
}
}
};
/***/ }),
/***/ "./lib/ResolvedContext.js":
/*!********************************!*\
!*** ./lib/ResolvedContext.js ***!
\********************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
/*
* Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved.
*/
const LRU = __webpack_require__(/*! lru-cache */ "./node_modules/lru-cache/index.js");
const MAX_ACTIVE_CONTEXTS = 10;
module.exports = class ResolvedContext {
/**
* Creates a ResolvedContext.
*
* @param document the context document.
*/
constructor({document}) {
this.document = document;
// TODO: enable customization of processed context cache
// TODO: limit based on size of processed contexts vs. number of them
this.cache = new LRU({max: MAX_ACTIVE_CONTEXTS});
}
getProcessed(activeCtx) {
return this.cache.get(activeCtx);
}
setProcessed(activeCtx, processedCtx) {
this.cache.set(activeCtx, processedCtx);
}
};
/***/ }),
/***/ "./lib/compact.js":
/*!************************!*\
!*** ./lib/compact.js ***!
\************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
*/
const JsonLdError = __webpack_require__(/*! ./JsonLdError */ "./lib/JsonLdError.js");
const {
isArray: _isArray,
isObject: _isObject,
isString: _isString,
isUndefined: _isUndefined
} = __webpack_require__(/*! ./types */ "./lib/types.js");
const {
isList: _isList,
isValue: _isValue,
isGraph: _isGraph,
isSimpleGraph: _isSimpleGraph,
isSubjectReference: _isSubjectReference
} = __webpack_require__(/*! ./graphTypes */ "./lib/graphTypes.js");
const {
expandIri: _expandIri,
getContextValue: _getContextValue,
isKeyword: _isKeyword,
process: _processContext,
processingMode: _processingMode
} = __webpack_require__(/*! ./context */ "./lib/context.js");
const {
removeBase: _removeBase,
prependBase: _prependBase
} = __webpack_require__(/*! ./url */ "./lib/url.js");
const {
REGEX_KEYWORD,
addValue: _addValue,
asArray: _asArray,
compareShortestLeast: _compareShortestLeast
} = __webpack_require__(/*! ./util */ "./lib/util.js");
const api = {};
module.exports = api;
/**
* Recursively compacts an element using the given active context. All values
* must be in expanded form before this method is called.
*
* @param activeCtx the active context to use.
* @param activeProperty the compacted property associated with the element
* to compact, null for none.
* @param element the element to compact.
* @param options the compaction options.
*
* @return a promise that resolves to the compacted value.
*/
api.compact = async ({
activeCtx,
activeProperty = null,
element,
options = {}
}) => {
// recursively compact array
if(_isArray(element)) {
let rval = [];
for(let i = 0; i < element.length; ++i) {
const compacted = await api.compact({
activeCtx,
activeProperty,
element: element[i],
options
});
if(compacted === null) {
// FIXME: need event?
continue;
}
rval.push(compacted);
}
if(options.compactArrays && rval.length === 1) {
// use single element if no container is specified
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
if(container.length === 0) {
rval = rval[0];
}
}
return rval;
}
// use any scoped context on activeProperty
const ctx = _getContextValue(activeCtx, activeProperty, '@context');
if(!_isUndefined(ctx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: ctx,
propagate: true,
overrideProtected: true,
options
});
}
// recursively compact object
if(_isObject(element)) {
if(options.link && '@id' in element &&
options.link.hasOwnProperty(element['@id'])) {
// check for a linked element to reuse
const linked = options.link[element['@id']];
for(let i = 0; i < linked.length; ++i) {
if(linked[i].expanded === element) {
return linked[i].compacted;
}
}
}
// do value compaction on @values and subject references
if(_isValue(element) || _isSubjectReference(element)) {
const rval =
api.compactValue({activeCtx, activeProperty, value: element, options});
if(options.link && _isSubjectReference(element)) {
// store linked element
if(!(options.link.hasOwnProperty(element['@id']))) {
options.link[element['@id']] = [];
}
options.link[element['@id']].push({expanded: element, compacted: rval});
}
return rval;
}
// if expanded property is @list and we're contained within a list
// container, recursively compact this item to an array
if(_isList(element)) {
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
if(container.includes('@list')) {
return api.compact({
activeCtx,
activeProperty,
element: element['@list'],
options
});
}
}
// FIXME: avoid misuse of active property as an expanded property?
const insideReverse = (activeProperty === '@reverse');
const rval = {};
// original context before applying property-scoped and local contexts
const inputCtx = activeCtx;
// revert to previous context, if there is one,
// and element is not a value object or a node reference
if(!_isValue(element) && !_isSubjectReference(element)) {
activeCtx = activeCtx.revertToPreviousContext();
}
// apply property-scoped context after reverting term-scoped context
const propertyScopedCtx =
_getContextValue(inputCtx, activeProperty, '@context');
if(!_isUndefined(propertyScopedCtx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: propertyScopedCtx,
propagate: true,
overrideProtected: true,
options
});
}
if(options.link && '@id' in element) {
// store linked element
if(!options.link.hasOwnProperty(element['@id'])) {
options.link[element['@id']] = [];
}
options.link[element['@id']].push({expanded: element, compacted: rval});
}
// apply any context defined on an alias of @type
// if key is @type and any compacted value is a term having a local
// context, overlay that context
let types = element['@type'] || [];
if(types.length > 1) {
types = Array.from(types).sort();
}
// find all type-scoped contexts based on current context, prior to
// updating it
const typeContext = activeCtx;
for(const type of types) {
const compactedType = api.compactIri(
{activeCtx: typeContext, iri: type, relativeTo: {vocab: true}});
// Use any type-scoped context defined on this value
const ctx = _getContextValue(inputCtx, compactedType, '@context');
if(!_isUndefined(ctx)) {
activeCtx = await _processContext({
activeCtx,
localCtx: ctx,
options,
propagate: false
});
}
}
// process element keys in order
const keys = Object.keys(element).sort();
for(const expandedProperty of keys) {
const expandedValue = element[expandedProperty];
// compact @id
if(expandedProperty === '@id') {
let compactedValue = _asArray(expandedValue).map(
expandedIri => api.compactIri({
activeCtx,
iri: expandedIri,
relativeTo: {vocab: false},
base: options.base
}));
if(compactedValue.length === 1) {
compactedValue = compactedValue[0];
}
// use keyword alias and add value
const alias = api.compactIri(
{activeCtx, iri: '@id', relativeTo: {vocab: true}});
rval[alias] = compactedValue;
continue;
}
// compact @type(s)
if(expandedProperty === '@type') {
// resolve type values against previous context
let compactedValue = _asArray(expandedValue).map(
expandedIri => api.compactIri({
activeCtx: inputCtx,
iri: expandedIri,
relativeTo: {vocab: true}
}));
if(compactedValue.length === 1) {
compactedValue = compactedValue[0];
}
// use keyword alias and add value
const alias = api.compactIri(
{activeCtx, iri: '@type', relativeTo: {vocab: true}});
const container = _getContextValue(
activeCtx, alias, '@container') || [];
// treat as array for @type if @container includes @set
const typeAsSet =
container.includes('@set') &&
_processingMode(activeCtx, 1.1);
const isArray =
typeAsSet || (_isArray(compactedValue) && expandedValue.length === 0);
_addValue(rval, alias, compactedValue, {propertyIsArray: isArray});
continue;
}
// handle @reverse
if(expandedProperty === '@reverse') {
// recursively compact expanded value
const compactedValue = await api.compact({
activeCtx,
activeProperty: '@reverse',
element: expandedValue,
options
});
// handle double-reversed properties
for(const compactedProperty in compactedValue) {
if(activeCtx.mappings.has(compactedProperty) &&
activeCtx.mappings.get(compactedProperty).reverse) {
const value = compactedValue[compactedProperty];
const container = _getContextValue(
activeCtx, compactedProperty, '@container') || [];
const useArray = (
container.includes('@set') || !options.compactArrays);
_addValue(
rval, compactedProperty, value, {propertyIsArray: useArray});
delete compactedValue[compactedProperty];
}
}
if(Object.keys(compactedValue).length > 0) {
// use keyword alias and add value
const alias = api.compactIri({
activeCtx,
iri: expandedProperty,
relativeTo: {vocab: true}
});
_addValue(rval, alias, compactedValue);
}
continue;
}
if(expandedProperty === '@preserve') {
// compact using activeProperty
const compactedValue = await api.compact({
activeCtx,
activeProperty,
element: expandedValue,
options
});
if(!(_isArray(compactedValue) && compactedValue.length === 0)) {
_addValue(rval, expandedProperty, compactedValue);
}
continue;
}
// handle @index property
if(expandedProperty === '@index') {
// drop @index if inside an @index container
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
if(container.includes('@index')) {
continue;
}
// use keyword alias and add value
const alias = api.compactIri({
activeCtx,
iri: expandedProperty,
relativeTo: {vocab: true}
});
_addValue(rval, alias, expandedValue);
continue;
}
// skip array processing for keywords that aren't
// @graph, @list, or @included
if(expandedProperty !== '@graph' && expandedProperty !== '@list' &&
expandedProperty !== '@included' &&
_isKeyword(expandedProperty)) {
// use keyword alias and add value as is
const alias = api.compactIri({
activeCtx,
iri: expandedProperty,
relativeTo: {vocab: true}
});
_addValue(rval, alias, expandedValue);
continue;
}
// Note: expanded value must be an array due to expansion algorithm.
if(!_isArray(expandedValue)) {
throw new JsonLdError(
'JSON-LD expansion error; expanded value must be an array.',
'jsonld.SyntaxError');
}
// preserve empty arrays
if(expandedValue.length === 0) {
const itemActiveProperty = api.compactIri({
activeCtx,
iri: expandedProperty,
value: expandedValue,
relativeTo: {vocab: true},
reverse: insideReverse
});
const nestProperty = activeCtx.mappings.has(itemActiveProperty) ?
activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
let nestResult = rval;
if(nestProperty) {
_checkNestProperty(activeCtx, nestProperty, options);
if(!_isObject(rval[nestProperty])) {
rval[nestProperty] = {};
}
nestResult = rval[nestProperty];
}
_addValue(
nestResult, itemActiveProperty, expandedValue, {
propertyIsArray: true
});
}
// recursively process array values
for(const expandedItem of expandedValue) {
// compact property and get container type
const itemActiveProperty = api.compactIri({
activeCtx,
iri: expandedProperty,
value: expandedItem,
relativeTo: {vocab: true},
reverse: insideReverse
});
// if itemActiveProperty is a @nest property, add values to nestResult,
// otherwise rval
const nestProperty = activeCtx.mappings.has(itemActiveProperty) ?
activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
let nestResult = rval;
if(nestProperty) {
_checkNestProperty(activeCtx, nestProperty, options);
if(!_isObject(rval[nestProperty])) {
rval[nestProperty] = {};
}
nestResult = rval[nestProperty];
}
const container = _getContextValue(
activeCtx, itemActiveProperty, '@container') || [];
// get simple @graph or @list value if appropriate
const isGraph = _isGraph(expandedItem);
const isList = _isList(expandedItem);
let inner;
if(isList) {
inner = expandedItem['@list'];
} else if(isGraph) {
inner = expandedItem['@graph'];
}
// recursively compact expanded item
let compactedItem = await api.compact({
activeCtx,
activeProperty: itemActiveProperty,
element: (isList || isGraph) ? inner : expandedItem,
options
});
// handle @list
if(isList) {
// ensure @list value is an array
if(!_isArray(compactedItem)) {
compactedItem = [compactedItem];
}
if(!container.includes('@list')) {
// wrap using @list alias
compactedItem = {
[api.compactIri({
activeCtx,
iri: '@list',
relativeTo: {vocab: true}
})]: compactedItem
};
// include @index from expanded @list, if any
if('@index' in expandedItem) {
compactedItem[api.compactIri({
activeCtx,
iri: '@index',
relativeTo: {vocab: true}
})] = expandedItem['@index'];
}
} else {
_addValue(nestResult, itemActiveProperty, compactedItem, {
valueIsArray: true,
allowDuplicate: true
});
continue;
}
}
// Graph object compaction cases
if(isGraph) {
if(container.includes('@graph') && (container.includes('@id') ||
container.includes('@index') && _isSimpleGraph(expandedItem))) {
// get or create the map object
let mapObject;
if(nestResult.hasOwnProperty(itemActiveProperty)) {
mapObject = nestResult[itemActiveProperty];
} else {
nestResult[itemActiveProperty] = mapObject = {};
}
// index on @id or @index or alias of @none
const key = (container.includes('@id') ?
expandedItem['@id'] : expandedItem['@index']) ||
api.compactIri({activeCtx, iri: '@none',
relativeTo: {vocab: true}});
// add compactedItem to map, using value of `@id` or a new blank
// node identifier
_addValue(
mapObject, key, compactedItem, {
propertyIsArray:
(!options.compactArrays || container.includes('@set'))
});
} else if(container.includes('@graph') &&
_isSimpleGraph(expandedItem)) {
// container includes @graph but not @id or @index and value is a
// simple graph object add compact value
// if compactedItem contains multiple values, it is wrapped in
// `@included`
if(_isArray(compactedItem) && compactedItem.length > 1) {
compactedItem = {'@included': compactedItem};
}
_addValue(
nestResult, itemActiveProperty, compactedItem, {
propertyIsArray:
(!options.compactArrays || container.includes('@set'))
});
} else {
// wrap using @graph alias, remove array if only one item and
// compactArrays not set
if(_isArray(compactedItem) && compactedItem.length === 1 &&
options.compactArrays) {
compactedItem = compactedItem[0];
}
compactedItem = {
[api.compactIri({
activeCtx,
iri: '@graph',
relativeTo: {vocab: true}
})]: compactedItem
};
// include @id from expanded graph, if any
if('@id' in expandedItem) {
compactedItem[api.compactIri({
activeCtx,
iri: '@id',
relativeTo: {vocab: true}
})] = expandedItem['@id'];
}
// include @index from expanded graph, if any
if('@index' in expandedItem) {
compactedItem[api.compactIri({
activeCtx,
iri: '@index',
relativeTo: {vocab: true}
})] = expandedItem['@index'];
}
_addValue(
nestResult, itemActiveProperty, compactedItem, {
propertyIsArray:
(!options.compactArrays || container.includes('@set'))
});
}
} else if(container.includes('@language') ||
container.includes('@index') || container.includes('@id') ||
container.includes('@type')) {
// handle language and index maps
// get or create the map object
let mapObject;
if(nestResult.hasOwnProperty(itemActiveProperty)) {
mapObject = nestResult[itemActiveProperty];
} else {
nestResult[itemActiveProperty] = mapObject = {};
}
let key;
if(container.includes('@language')) {
// if container is a language map, simplify compacted value to
// a simple string
if(_isValue(compactedItem)) {
compactedItem = compactedItem['@value'];
}
key = expandedItem['@language'];
} else if(container.includes('@index')) {
const indexKey = _getContextValue(
activeCtx, itemActiveProperty, '@index') || '@index';
const containerKey = api.compactIri(
{activeCtx, iri: indexKey, relativeTo: {vocab: true}});
if(indexKey === '@index') {
key = expandedItem['@index'];
delete compactedItem[containerKey];
} else {
let others;
[key, ...others] = _asArray(compactedItem[indexKey] || []);
if(!_isString(key)) {
// Will use @none if it isn't a string.
key = null;
} else {
switch(others.length) {
case 0:
delete compactedItem[indexKey];
break;
case 1:
compactedItem[indexKey] = others[0];
break;
default:
compactedItem[indexKey] = others;
break;
}
}
}
} else if(container.includes('@id')) {
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',
relativeTo: {vocab: true}
});
let types;
[key, ...types] = _asArray(compactedItem[typeKey] || []);
switch(types.length) {
case 0:
delete compactedItem[typeKey];
break;
case 1:
compactedItem[typeKey] = types[0];
break;
default:
compactedItem[typeKey] = types;
break;
}
// If compactedItem contains a single entry
// whose key maps to @id, recompact without @type
if(Object.keys(compactedItem).length === 1 &&
'@id' in expandedItem) {
compactedItem = await api.compact({
activeCtx,
activeProperty: itemActiveProperty,
element: {'@id': expandedItem['@id']},
options
});
}
}
// if compacting this value which has no key, index on @none
if(!key) {
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
_addValue(
mapObject, key, compactedItem, {
propertyIsArray: container.includes('@set')
});
} else {
// use an array if: compactArrays flag is false,
// @container is @set or @list , value is an empty
// array, or key is @graph
const isArray = (!options.compactArrays ||
container.includes('@set') || container.includes('@list') ||
(_isArray(compactedItem) && compactedItem.length === 0) ||
expandedProperty === '@list' || expandedProperty === '@graph');
// add compact value
_addValue(
nestResult, itemActiveProperty, compactedItem,
{propertyIsArray: isArray});
}
}
}
return rval;
}
// only primitives remain which are already compact
return element;
};
/**
* Compacts an IRI or keyword into a term or prefix if it can be. If the
* IRI has an associated value it may be passed.
*
* @param activeCtx the active context to use.
* @param iri the IRI to compact.
* @param value the value to check or null.
* @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.
*/
api.compactIri = ({
activeCtx,
iri,
value = null,
relativeTo = {vocab: false},
reverse = false,
base = null
}) => {
// can't compact null
if(iri === null) {
return iri;
}
// if context is from a property term scoped context composed with a
// type-scoped context, then use the previous context instead
if(activeCtx.isPropertyTermScoped && activeCtx.previousContext) {
activeCtx = activeCtx.previousContext;
}
const inverseCtx = activeCtx.getInverse();
// if term is a keyword, it may be compacted to a simple alias
if(_isKeyword(iri) &&
iri in inverseCtx &&
'@none' in inverseCtx[iri] &&
'@type' in inverseCtx[iri]['@none'] &&
'@none' in inverseCtx[iri]['@none']['@type']) {
return inverseCtx[iri]['@none']['@type']['@none'];
}
// use inverse context to pick a term if iri is relative to vocab
if(relativeTo.vocab && iri in inverseCtx) {
const defaultLanguage = activeCtx['@language'] || '@none';
// prefer @index if available in value
const containers = [];
if(_isObject(value) && '@index' in value && !('@graph' in value)) {
containers.push('@index', '@index@set');
}
// if value is a preserve object, use its value
if(_isObject(value) && '@preserve' in value) {
value = value['@preserve'][0];
}
// prefer most specific container including @graph, preferring @set
// variations
if(_isGraph(value)) {
// favor indexmap if the graph is indexed
if('@index' in value) {
containers.push(
'@graph@index', '@graph@index@set', '@index', '@index@set');
}
// favor idmap if the graph is has an @id
if('@id' in value) {
containers.push(
'@graph@id', '@graph@id@set');
}
containers.push('@graph', '@graph@set', '@set');
// allow indexmap if the graph is not indexed
if(!('@index' in value)) {
containers.push(
'@graph@index', '@graph@index@set', '@index', '@index@set');
}
// allow idmap if the graph does not have an @id
if(!('@id' in value)) {
containers.push('@graph@id', '@graph@id@set');
}
} else if(_isObject(value) && !_isValue(value)) {
containers.push('@id', '@id@set', '@type', '@set@type');
}
// defaults for term selection based on type/language
let typeOrLanguage = '@language';
let typeOrLanguageValue = '@null';
if(reverse) {
typeOrLanguage = '@type';
typeOrLanguageValue = '@reverse';
containers.push('@set');
} else if(_isList(value)) {
// choose the most specific term that works for all elements in @list
// only select @list containers if @index is NOT in value
if(!('@index' in value)) {
containers.push('@list');
}
const list = value['@list'];
if(list.length === 0) {
// any empty list can be matched against any term that uses the
// @list container regardless of @type or @language
typeOrLanguage = '@any';
typeOrLanguageValue = '@none';
} else {
let commonLanguage = (list.length === 0) ? defaultLanguage : null;
let commonType = null;
for(let i = 0; i < list.length; ++i) {
const item = list[i];
let itemLanguage = '@none';
let itemType = '@none';
if(_isValue(item)) {
if('@direction' in item) {
const lang = (item['@language'] || '').toLowerCase();
const dir = item['@direction'];
itemLanguage = `${lang}_${dir}`;
} else if('@language' in item) {
itemLanguage = item['@language'].toLowerCase();
} else if('@type' in item) {
itemType = item['@type'];
} else {
// plain literal
itemLanguage = '@null';
}
} else {
itemType = '@id';
}
if(commonLanguage === null) {
commonLanguage = itemLanguage;
} else if(itemLanguage !== commonLanguage && _isValue(item)) {
commonLanguage = '@none';
}
if(commonType === null) {
commonType = itemType;
} else if(itemType !== commonType) {
commonType = '@none';
}
// there are different languages and types in the list, so choose
// the most generic term, no need to keep iterating the list
if(commonLanguage === '@none' && commonType === '@none') {
break;
}
}
commonLanguage = commonLanguage || '@none';
commonType = commonType || '@none';
if(commonType !== '@none') {
typeOrLanguage = '@type';
typeOrLanguageValue = commonType;
} else {
typeOrLanguageValue = commonLanguage;
}
}
} else {
if(_isValue(value)) {
if('@language' in value && !('@index' in value)) {
containers.push('@language', '@language@set');
typeOrLanguageValue = value['@language'];
const dir = value['@direction'];
if(dir) {
typeOrLanguageValue = `${typeOrLanguageValue}_${dir}`;
}
} else if('@direction' in value && !('@index' in value)) {
typeOrLanguageValue = `_${value['@direction']}`;
} else if('@type' in value) {
typeOrLanguage = '@type';
typeOrLanguageValue = value['@type'];
}
} else {
typeOrLanguage = '@type';
typeOrLanguageValue = '@id';
}
containers.push('@set');
}
// do term selection
containers.push('@none');
// an index map can be used to index values using @none, so add as a low
// priority
if(_isObject(value) && !('@index' in value)) {
// allow indexing even if no @index present
containers.push('@index', '@index@set');
}
// values without type or language can use @language map
if(_isValue(value) && Object.keys(value).length === 1) {
// allow indexing even if no @index present
containers.push('@language', '@language@set');
}
const term = _selectTerm(
activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue);
if(term !== null) {
return term;
}
}
// no term match, use @vocab if available
if(relativeTo.vocab) {
if('@vocab' in activeCtx) {
// determine if vocab is a prefix of the iri
const vocab = activeCtx['@vocab'];
if(iri.indexOf(vocab) === 0 && iri !== vocab) {
// use suffix as relative iri if it is not a term in the active context
const suffix = iri.substr(vocab.length);
if(!activeCtx.mappings.has(suffix)) {
return suffix;
}
}
}
}
// no term or @vocab match, check for possible CURIEs
let choice = null;
// TODO: make FastCurieMap a class with a method to do this lookup
const partialMatches = [];
let iriMap = activeCtx.fastCurieMap;
// check for partial matches of against `iri`, which means look until
// iri.length - 1, not full length
const maxPartialLength = iri.length - 1;
for(let i = 0; i < maxPartialLength && iri[i] in iriMap; ++i) {
iriMap = iriMap[iri[i]];
if('' in iriMap) {
partialMatches.push(iriMap[''][0]);
}
}
// check partial matches in reverse order to prefer longest ones first
for(let i = partialMatches.length - 1; i >= 0; --i) {
const entry = partialMatches[i];
const terms = entry.terms;
for(const term of terms) {
// a CURIE is usable if:
// 1. it has no mapping, OR
// 2. value is null, which means we're not compacting an @value, AND
// the mapping matches the IRI
const curie = term + ':' + iri.substr(entry.iri.length);
const isUsableCurie = (activeCtx.mappings.get(term)._prefix &&
(!activeCtx.mappings.has(curie) ||
(value === null && activeCtx.mappings.get(curie)['@id'] === iri)));
// select curie if it is shorter or the same length but lexicographically
// less than the current choice
if(isUsableCurie && (choice === null ||
_compareShortestLeast(curie, choice) < 0)) {
choice = curie;
}
}
}
// return chosen curie
if(choice !== null) {
return choice;
}
// If iri could be confused with a compact IRI using a term in this context,
// signal an error
for(const [term, td] of activeCtx.mappings) {
if(td && td._prefix && iri.startsWith(term + ':')) {
throw new JsonLdError(
`Absolute IRI "${iri}" confused with prefix "${term}".`,
'jsonld.SyntaxError',
{code: 'IRI confused with prefix', context: activeCtx});
}
}
// compact IRI relative to base
if(!relativeTo.vocab) {
if('@base' in activeCtx) {
if(!activeCtx['@base']) {
// The None case preserves rval as potentially relative
return iri;
} else {
const _iri = _removeBase(_prependBase(base, activeCtx['@base']), iri);
return REGEX_KEYWORD.test(_iri) ? `./${_iri}` : _iri;
}
} else {
return _removeBase(base, iri);
}
}
// return IRI as is
return iri;
};
/**
* Performs value compaction on an object with '@value' or '@id' as the only
* property.
*
* @param activeCtx the active context.
* @param activeProperty the active property that points to the value.
* @param value the value to compact.
* @param {Object} [options] - processing options.
*
* @return the compaction result.
*/
api.compactValue = ({activeCtx, activeProperty, value, options}) => {
// value is a @value
if(_isValue(value)) {
// get context rules
const type = _getContextValue(activeCtx, activeProperty, '@type');
const language = _getContextValue(activeCtx, activeProperty, '@language');
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
const container =
_getContextValue(activeCtx, activeProperty, '@container') || [];
// whether or not the value has an @index that must be preserved
const preserveIndex = '@index' in value && !container.includes('@index');
// if there's no @index to preserve ...
if(!preserveIndex && type !== '@none') {
// matching @type or @language specified in context, compact value
if(value['@type'] === type) {
return value['@value'];
}
if('@language' in value && value['@language'] === language &&
'@direction' in value && value['@direction'] === direction) {
return value['@value'];
}
if('@language' in value && value['@language'] === language) {
return value['@value'];
}
if('@direction' in value && value['@direction'] === direction) {
return value['@value'];
}
}
// return just the value of @value if all are true:
// 1. @value is the only key or @index isn't being preserved
// 2. there is no default language or @value is not a string or
// the key has a mapping with a null @language
const keyCount = Object.keys(value).length;
const isValueOnlyKey = (keyCount === 1 ||
(keyCount === 2 && '@index' in value && !preserveIndex));
const hasDefaultLanguage = ('@language' in activeCtx);
const isValueString = _isString(value['@value']);
const hasNullMapping = (activeCtx.mappings.has(activeProperty) &&
activeCtx.mappings.get(activeProperty)['@language'] === null);
if(isValueOnlyKey &&
type !== '@none' &&
(!hasDefaultLanguage || !isValueString || hasNullMapping)) {
return value['@value'];
}
const rval = {};
// preserve @index
if(preserveIndex) {
rval[api.compactIri({
activeCtx,
iri: '@index',
relativeTo: {vocab: true}
})] = value['@index'];
}
if('@type' in value) {
// compact @type IRI
rval[api.compactIri({
activeCtx,
iri: '@type',
relativeTo: {vocab: true}
})] = api.compactIri(
{activeCtx, iri: value['@type'], relativeTo: {vocab: true}});
} else if('@language' in value) {
// alias @language
rval[api.compactIri({
activeCtx,
iri: '@language',
relativeTo: {vocab: true}
})] = value['@language'];
}
if('@direction' in value) {
// alias @direction
rval[api.compactIri({
activeCtx,
iri: '@direction',
relativeTo: {vocab: true}
})] = value['@direction'];
}
// alias @value
rval[api.compactIri({
activeCtx,
iri: '@value',
relativeTo: {vocab: true}
})] = value['@value'];
return rval;
}
// value is a subject reference
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'},
base: options.base});
// compact to scalar
if(type === '@id' || type === '@vocab' || expandedProperty === '@graph') {
return compacted;
}
return {
[api.compactIri({
activeCtx,
iri: '@id',
relativeTo: {vocab: true}
})]: compacted
};
};
/**
* Picks the preferred compaction term from the given inverse context entry.
*
* @param activeCtx the active context.
* @param iri the IRI to pick the term for.
* @param value the value to pick the term for.
* @param containers the preferred containers.
* @param typeOrLanguage either '@type' or '@language'.
* @param typeOrLanguageValue the preferred value for '@type' or '@language'.
*
* @return the preferred term.
*/
function _selectTerm(
activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue) {
if(typeOrLanguageValue === null) {
typeOrLanguageValue = '@null';
}
// preferences for the value of @type or @language
const prefs = [];
// determine prefs for @id based on whether or not value compacts to a term
if((typeOrLanguageValue === '@id' || typeOrLanguageValue === '@reverse') &&
_isObject(value) && '@id' in value) {
// prefer @reverse first
if(typeOrLanguageValue === '@reverse') {
prefs.pus