UNPKG

boomerang-server

Version:
1,652 lines (1,528 loc) 214 kB
/** * A JavaScript implementation of the JSON-LD API. * * @author Dave Longley * * BSD 3-Clause License * Copyright (c) 2011-2014 Digital Bazaar, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Digital Bazaar, Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ (function() { // determine if in-browser or using node.js var _nodejs = ( typeof process !== 'undefined' && process.versions && process.versions.node); var _browser = !_nodejs && (typeof window !== 'undefined' || typeof self !== 'undefined'); if(_browser) { if(typeof global === 'undefined') { if(typeof window !== 'undefined') { global = window; } else if(typeof self !== 'undefined') { global = self; } else if(typeof $ !== 'undefined') { global = $; } } } // attaches jsonld API to the given object var wrapper = function(jsonld) { /* Core API */ /** * Performs JSON-LD compaction. * * @param input the JSON-LD input to compact. * @param ctx the context to compact with. * @param [options] options to use: * [base] the base IRI to use. * [compactArrays] true to compact arrays to single values when * appropriate, false not to (default: true). * [graph] true to always output a top-level graph (default: false). * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, compacted, ctx) called once the operation completes. */ jsonld.compact = function(input, ctx, options, callback) { if(arguments.length < 2) { return jsonld.nextTick(function() { callback(new TypeError('Could not compact, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; if(ctx === null) { return jsonld.nextTick(function() { callback(new JsonLdError( 'The compaction context must not be null.', 'jsonld.CompactError', {code: 'invalid local context'})); }); } // nothing to compact if(input === null) { return jsonld.nextTick(function() { callback(null, null); }); } // set default options if(!('base' in options)) { options.base = (typeof input === 'string') ? input : ''; } if(!('compactArrays' in options)) { options.compactArrays = true; } if(!('graph' in options)) { options.graph = false; } if(!('skipExpansion' in options)) { options.skipExpansion = false; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } if(!('link' in options)) { options.link = false; } if(options.link) { // force skip expansion when linking, "link" is not part of the public // API, it should only be called from framing options.skipExpansion = true; } var expand = function(input, options, callback) { jsonld.nextTick(function() { if(options.skipExpansion) { return callback(null, input); } jsonld.expand(input, options, callback); }); }; // expand input then do compaction expand(input, options, function(err, expanded) { if(err) { return callback(new JsonLdError( 'Could not expand input before compaction.', 'jsonld.CompactError', {cause: err})); } // process context var activeCtx = _getInitialContext(options); jsonld.processContext(activeCtx, ctx, options, function(err, activeCtx) { if(err) { return callback(new JsonLdError( 'Could not process context before compaction.', 'jsonld.CompactError', {cause: err})); } var compacted; try { // do compaction compacted = new Processor().compact(activeCtx, null, expanded, options); } catch(ex) { return callback(ex); } cleanup(null, compacted, activeCtx, options); }); }); // performs clean up after compaction function cleanup(err, compacted, activeCtx, options) { if(err) { return callback(err); } if(options.compactArrays && !options.graph && _isArray(compacted)) { if(compacted.length === 1) { // simplify to a single item compacted = compacted[0]; } else if(compacted.length === 0) { // simplify to an empty object compacted = {}; } } else if(options.graph && _isObject(compacted)) { // always use array if graph option is on compacted = [compacted]; } // follow @context key if(_isObject(ctx) && '@context' in ctx) { ctx = ctx['@context']; } // build output context ctx = _clone(ctx); if(!_isArray(ctx)) { ctx = [ctx]; } // remove empty contexts var tmp = ctx; ctx = []; for(var i = 0; i < tmp.length; ++i) { if(!_isObject(tmp[i]) || Object.keys(tmp[i]).length > 0) { ctx.push(tmp[i]); } } // remove array if only one context var hasContext = (ctx.length > 0); if(ctx.length === 1) { ctx = ctx[0]; } // add context and/or @graph if(_isArray(compacted)) { // use '@graph' keyword var kwgraph = _compactIri(activeCtx, '@graph'); var graph = compacted; compacted = {}; if(hasContext) { compacted['@context'] = ctx; } compacted[kwgraph] = graph; } else if(_isObject(compacted) && hasContext) { // reorder keys so @context is first var graph = compacted; compacted = {'@context': ctx}; for(var key in graph) { compacted[key] = graph[key]; } } callback(null, compacted, activeCtx); } }; /** * Performs JSON-LD expansion. * * @param input the JSON-LD input to expand. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [keepFreeFloatingNodes] true to keep free-floating nodes, * false not to, defaults to false. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, expanded) called once the operation completes. */ jsonld.expand = function(input, options, callback) { if(arguments.length < 1) { return jsonld.nextTick(function() { callback(new TypeError('Could not expand, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // set default options if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } if(!('keepFreeFloatingNodes' in options)) { options.keepFreeFloatingNodes = false; } jsonld.nextTick(function() { // if input is a string, attempt to dereference remote document if(typeof input === 'string') { var done = function(err, remoteDoc) { if(err) { return callback(err); } try { if(!remoteDoc.document) { throw new JsonLdError( 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument'); } if(typeof remoteDoc.document === 'string') { remoteDoc.document = JSON.parse(remoteDoc.document); } } catch(ex) { return callback(new JsonLdError( 'Could not retrieve a JSON-LD document from the URL. URL ' + 'dereferencing not implemented.', 'jsonld.LoadDocumentError', { code: 'loading document failed', cause: ex, remoteDoc: remoteDoc })); } expand(remoteDoc); }; var promise = options.documentLoader(input, done); if(promise && 'then' in promise) { promise.then(done.bind(null, null), done); } return; } // nothing to load expand({contextUrl: null, documentUrl: null, document: input}); }); function expand(remoteDoc) { // set default base if(!('base' in options)) { options.base = remoteDoc.documentUrl || ''; } // build meta-object and retrieve all @context URLs var input = { document: _clone(remoteDoc.document), remoteContext: {'@context': remoteDoc.contextUrl} }; if('expandContext' in options) { var expandContext = _clone(options.expandContext); if(typeof expandContext === 'object' && '@context' in expandContext) { input.expandContext = expandContext; } else { input.expandContext = {'@context': expandContext}; } } _retrieveContextUrls(input, options, function(err, input) { if(err) { return callback(err); } var expanded; try { var processor = new Processor(); var activeCtx = _getInitialContext(options); var document = input.document; var remoteContext = input.remoteContext['@context']; // process optional expandContext if(input.expandContext) { activeCtx = processor.processContext( activeCtx, input.expandContext['@context'], options); } // process remote context from HTTP Link Header if(remoteContext) { activeCtx = processor.processContext( activeCtx, remoteContext, options); } // expand document expanded = processor.expand( activeCtx, null, document, options, false); // optimize away @graph with no other properties if(_isObject(expanded) && ('@graph' in expanded) && Object.keys(expanded).length === 1) { expanded = expanded['@graph']; } else if(expanded === null) { expanded = []; } // normalize to an array if(!_isArray(expanded)) { expanded = [expanded]; } } catch(ex) { return callback(ex); } callback(null, expanded); }); } }; /** * Performs JSON-LD flattening. * * @param input the JSON-LD to flatten. * @param ctx the context to use to compact the flattened output, or null. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, flattened) called once the operation completes. */ jsonld.flatten = function(input, ctx, options, callback) { if(arguments.length < 1) { return jsonld.nextTick(function() { callback(new TypeError('Could not flatten, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } else if(typeof ctx === 'function') { callback = ctx; ctx = null; options = {}; } options = options || {}; // set default options if(!('base' in options)) { options.base = (typeof input === 'string') ? input : ''; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } // expand input jsonld.expand(input, options, function(err, _input) { if(err) { return callback(new JsonLdError( 'Could not expand input before flattening.', 'jsonld.FlattenError', {cause: err})); } var flattened; try { // do flattening flattened = new Processor().flatten(_input); } catch(ex) { return callback(ex); } if(ctx === null) { return callback(null, flattened); } // compact result (force @graph option to true, skip expansion) options.graph = true; options.skipExpansion = true; jsonld.compact(flattened, ctx, options, function(err, compacted) { if(err) { return callback(new JsonLdError( 'Could not compact flattened output.', 'jsonld.FlattenError', {cause: err})); } callback(null, compacted); }); }); }; /** * Performs JSON-LD framing. * * @param input the JSON-LD input to frame. * @param frame the JSON-LD frame to use. * @param [options] the framing options. * [base] the base IRI to use. * [expandContext] a context to expand with. * [embed] default @embed flag: '@last', '@always', '@never', '@link' * (default: '@last'). * [explicit] default @explicit flag (default: false). * [requireAll] default @requireAll flag (default: true). * [omitDefault] default @omitDefault flag (default: false). * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, framed) called once the operation completes. */ jsonld.frame = function(input, frame, options, callback) { if(arguments.length < 2) { return jsonld.nextTick(function() { callback(new TypeError('Could not frame, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // set default options if(!('base' in options)) { options.base = (typeof input === 'string') ? input : ''; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } if(!('embed' in options)) { options.embed = '@last'; } options.explicit = options.explicit || false; if(!('requireAll' in options)) { options.requireAll = true; } options.omitDefault = options.omitDefault || false; jsonld.nextTick(function() { // if frame is a string, attempt to dereference remote document if(typeof frame === 'string') { var done = function(err, remoteDoc) { if(err) { return callback(err); } try { if(!remoteDoc.document) { throw new JsonLdError( 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument'); } if(typeof remoteDoc.document === 'string') { remoteDoc.document = JSON.parse(remoteDoc.document); } } catch(ex) { return callback(new JsonLdError( 'Could not retrieve a JSON-LD document from the URL. URL ' + 'dereferencing not implemented.', 'jsonld.LoadDocumentError', { code: 'loading document failed', cause: ex, remoteDoc: remoteDoc })); } doFrame(remoteDoc); }; var promise = options.documentLoader(frame, done); if(promise && 'then' in promise) { promise.then(done.bind(null, null), done); } return; } // nothing to load doFrame({contextUrl: null, documentUrl: null, document: frame}); }); function doFrame(remoteFrame) { // preserve frame context and add any Link header context var frame = remoteFrame.document; var ctx; if(frame) { ctx = frame['@context']; if(remoteFrame.contextUrl) { if(!ctx) { ctx = remoteFrame.contextUrl; } else if(_isArray(ctx)) { ctx.push(remoteFrame.contextUrl); } else { ctx = [ctx, remoteFrame.contextUrl]; } frame['@context'] = ctx; } else { ctx = ctx || {}; } } else { ctx = {}; } // expand input jsonld.expand(input, options, function(err, expanded) { if(err) { return callback(new JsonLdError( 'Could not expand input before framing.', 'jsonld.FrameError', {cause: err})); } // expand frame var opts = _clone(options); opts.isFrame = true; opts.keepFreeFloatingNodes = true; jsonld.expand(frame, opts, function(err, expandedFrame) { if(err) { return callback(new JsonLdError( 'Could not expand frame before framing.', 'jsonld.FrameError', {cause: err})); } var framed; try { // do framing framed = new Processor().frame(expanded, expandedFrame, opts); } catch(ex) { return callback(ex); } // compact result (force @graph option to true, skip expansion, // check for linked embeds) opts.graph = true; opts.skipExpansion = true; opts.link = {}; jsonld.compact(framed, ctx, opts, function(err, compacted, ctx) { if(err) { return callback(new JsonLdError( 'Could not compact framed output.', 'jsonld.FrameError', {cause: err})); } // get graph alias var graph = _compactIri(ctx, '@graph'); // remove @preserve from results opts.link = {}; compacted[graph] = _removePreserve(ctx, compacted[graph], opts); callback(null, compacted); }); }); }); } }; /** * **Experimental** * * Links a JSON-LD document's nodes in memory. * * @param input the JSON-LD document to link. * @param ctx the JSON-LD context to apply. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, linked) called once the operation completes. */ jsonld.link = function(input, ctx, options, callback) { // API matches running frame with a wildcard frame and embed: '@link' // get arguments var frame = {}; if(ctx) { frame['@context'] = ctx; } frame['@embed'] = '@link'; jsonld.frame(input, frame, options, callback); }; /** * **Deprecated** * * Performs JSON-LD objectification. * * @param input the JSON-LD document to objectify. * @param ctx the JSON-LD context to apply. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, linked) called once the operation completes. */ jsonld.objectify = function(input, ctx, options, callback) { if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // set default options if(!('base' in options)) { options.base = (typeof input === 'string') ? input : ''; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } // expand input jsonld.expand(input, options, function(err, _input) { if(err) { return callback(new JsonLdError( 'Could not expand input before linking.', 'jsonld.LinkError', {cause: err})); } var flattened; try { // flatten the graph flattened = new Processor().flatten(_input); } catch(ex) { return callback(ex); } // compact result (force @graph option to true, skip expansion) options.graph = true; options.skipExpansion = true; jsonld.compact(flattened, ctx, options, function(err, compacted, ctx) { if(err) { return callback(new JsonLdError( 'Could not compact flattened output before linking.', 'jsonld.LinkError', {cause: err})); } // get graph alias var graph = _compactIri(ctx, '@graph'); var top = compacted[graph][0]; var recurse = function(subject) { // can't replace just a string if(!_isObject(subject) && !_isArray(subject)) { return; } // bottom out recursion on re-visit if(_isObject(subject)) { if(recurse.visited[subject['@id']]) { return; } recurse.visited[subject['@id']] = true; } // each array element *or* object key for(var k in subject) { var obj = subject[k]; var isid = (jsonld.getContextValue(ctx, k, '@type') === '@id'); // can't replace a non-object or non-array unless it's an @id if(!_isArray(obj) && !_isObject(obj) && !isid) { continue; } if(_isString(obj) && isid) { subject[k] = obj = top[obj]; recurse(obj); } else if(_isArray(obj)) { for(var i = 0; i < obj.length; ++i) { if(_isString(obj[i]) && isid) { obj[i] = top[obj[i]]; } else if(_isObject(obj[i]) && '@id' in obj[i]) { obj[i] = top[obj[i]['@id']]; } recurse(obj[i]); } } else if(_isObject(obj)) { var sid = obj['@id']; subject[k] = obj = top[sid]; recurse(obj); } } }; recurse.visited = {}; recurse(top); compacted.of_type = {}; for(var s in top) { if(!('@type' in top[s])) { continue; } var types = top[s]['@type']; if(!_isArray(types)) { types = [types]; } for(var t in types) { if(!(types[t] in compacted.of_type)) { compacted.of_type[types[t]] = []; } compacted.of_type[types[t]].push(top[s]); } } callback(null, compacted); }); }); }; /** * Performs RDF dataset normalization on the given JSON-LD input. The output * is an RDF dataset unless the 'format' option is used. * * @param input the JSON-LD input to normalize. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [format] the format if output is a string: * 'application/nquads' for N-Quads. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, normalized) called once the operation completes. */ jsonld.normalize = function(input, options, callback) { if(arguments.length < 1) { return jsonld.nextTick(function() { callback(new TypeError('Could not normalize, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // set default options if(!('base' in options)) { options.base = (typeof input === 'string') ? input : ''; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } // convert to RDF dataset then do normalization var opts = _clone(options); delete opts.format; opts.produceGeneralizedRdf = false; jsonld.toRDF(input, opts, function(err, dataset) { if(err) { return callback(new JsonLdError( 'Could not convert input to RDF dataset before normalization.', 'jsonld.NormalizeError', {cause: err})); } // do normalization new Processor().normalize(dataset, options, callback); }); }; /** * Converts an RDF dataset to JSON-LD. * * @param dataset a serialized string of RDF in a format specified by the * format option or an RDF dataset to convert. * @param [options] the options to use: * [format] the format if dataset param must first be parsed: * 'application/nquads' for N-Quads (default). * [rdfParser] a custom RDF-parser to use to parse the dataset. * [useRdfType] true to use rdf:type, false to use @type * (default: false). * [useNativeTypes] true to convert XSD types into native types * (boolean, integer, double), false not to (default: false). * @param callback(err, output) called once the operation completes. */ jsonld.fromRDF = function(dataset, options, callback) { if(arguments.length < 1) { return jsonld.nextTick(function() { callback(new TypeError('Could not convert from RDF, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // set default options if(!('useRdfType' in options)) { options.useRdfType = false; } if(!('useNativeTypes' in options)) { options.useNativeTypes = false; } if(!('format' in options) && _isString(dataset)) { // set default format to nquads if(!('format' in options)) { options.format = 'application/nquads'; } } jsonld.nextTick(function() { // handle special format var rdfParser; if(options.format) { // check supported formats rdfParser = options.rdfParser || _rdfParsers[options.format]; if(!rdfParser) { return callback(new JsonLdError( 'Unknown input format.', 'jsonld.UnknownFormat', {format: options.format})); } } else { // no-op parser, assume dataset already parsed rdfParser = function() { return dataset; }; } // rdf parser may be async or sync, always pass callback dataset = rdfParser(dataset, function(err, dataset) { if(err) { return callback(err); } fromRDF(dataset, options, callback); }); // handle synchronous or promise-based parser if(dataset) { // if dataset is actually a promise if('then' in dataset) { return dataset.then(function(dataset) { fromRDF(dataset, options, callback); }, callback); } // parser is synchronous fromRDF(dataset, options, callback); } function fromRDF(dataset, options, callback) { // convert from RDF new Processor().fromRDF(dataset, options, callback); } }); }; /** * Outputs the RDF dataset found in the given JSON-LD object. * * @param input the JSON-LD input. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [format] the format to use to output a string: * 'application/nquads' for N-Quads. * [produceGeneralizedRdf] true to output generalized RDF, false * to produce only standard RDF (default: false). * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, dataset) called once the operation completes. */ jsonld.toRDF = function(input, options, callback) { if(arguments.length < 1) { return jsonld.nextTick(function() { callback(new TypeError('Could not convert to RDF, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // set default options if(!('base' in options)) { options.base = (typeof input === 'string') ? input : ''; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } // expand input jsonld.expand(input, options, function(err, expanded) { if(err) { return callback(new JsonLdError( 'Could not expand input before serialization to RDF.', 'jsonld.RdfError', {cause: err})); } var dataset; try { // output RDF dataset dataset = Processor.prototype.toRDF(expanded, options); if(options.format) { if(options.format === 'application/nquads') { return callback(null, _toNQuads(dataset)); } throw new JsonLdError( 'Unknown output format.', 'jsonld.UnknownFormat', {format: options.format}); } } catch(ex) { return callback(ex); } callback(null, dataset); }); }; /** * **Experimental** * * Recursively flattens the nodes in the given JSON-LD input into a map of * node ID => node. * * @param input the JSON-LD input. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [namer] a jsonld.UniqueNamer to use to label blank nodes. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, nodeMap) called once the operation completes. */ jsonld.createNodeMap = function(input, options, callback) { if(arguments.length < 1) { return jsonld.nextTick(function() { callback(new TypeError('Could not create node map, too few arguments.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; // set default options if(!('base' in options)) { options.base = (typeof input === 'string') ? input : ''; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } // expand input jsonld.expand(input, options, function(err, _input) { if(err) { return callback(new JsonLdError( 'Could not expand input before creating node map.', 'jsonld.CreateNodeMapError', {cause: err})); } var nodeMap; try { nodeMap = new Processor().createNodeMap(_input, options); } catch(ex) { return callback(ex); } callback(null, nodeMap); }); }; /** * **Experimental** * * Merges two or more JSON-LD documents into a single flattened document. * * @param docs the JSON-LD documents to merge together. * @param ctx the context to use to compact the merged result, or null. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [namer] a jsonld.UniqueNamer to use to label blank nodes. * [mergeNodes] true to merge properties for nodes with the same ID, * false to ignore new properties for nodes with the same ID once * the ID has been defined; note that this may not prevent merging * new properties where a node is in the `object` position * (default: true). * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, merged) called once the operation completes. */ jsonld.merge = function(docs, ctx, options, callback) { if(arguments.length < 1) { return jsonld.nextTick(function() { callback(new TypeError('Could not merge, too few arguments.')); }); } if(!_isArray(docs)) { return jsonld.nextTick(function() { callback(new TypeError('Could not merge, "docs" must be an array.')); }); } // get arguments if(typeof options === 'function') { callback = options; options = {}; } else if(typeof ctx === 'function') { callback = ctx; ctx = null; options = {}; } options = options || {}; // expand all documents var expanded = []; var error = null; var count = docs.length; for(var i = 0; i < docs.length; ++i) { var opts = {}; for(var key in options) { opts[key] = options[key]; } jsonld.expand(docs[i], opts, expandComplete); } function expandComplete(err, _input) { if(error) { return; } if(err) { error = err; return callback(new JsonLdError( 'Could not expand input before flattening.', 'jsonld.FlattenError', {cause: err})); } expanded.push(_input); if(--count === 0) { merge(expanded); } } function merge(expanded) { var mergeNodes = true; if('mergeNodes' in options) { mergeNodes = options.mergeNodes; } var namer = options.namer || new UniqueNamer('_:b'); var graphs = {'@default': {}}; var defaultGraph; try { for(var i = 0; i < expanded.length; ++i) { // uniquely relabel blank nodes var doc = expanded[i]; doc = jsonld.relabelBlankNodes(doc, { namer: new UniqueNamer('_:b' + i + '-') }); // add nodes to the shared node map graphs if merging nodes, to a // separate graph set if not var _graphs = (mergeNodes || i === 0) ? graphs : {'@default': {}}; _createNodeMap(doc, _graphs, '@default', namer); if(_graphs !== graphs) { // merge document graphs but don't merge existing nodes for(var graphName in _graphs) { var _nodeMap = _graphs[graphName]; if(!(graphName in graphs)) { graphs[graphName] = _nodeMap; continue; } var nodeMap = graphs[graphName]; for(var key in _nodeMap) { if(!(key in nodeMap)) { nodeMap[key] = _nodeMap[key]; } } } } } // add all non-default graphs to default graph defaultGraph = _mergeNodeMaps(graphs); } catch(ex) { return callback(ex); } // produce flattened output var flattened = []; var keys = Object.keys(defaultGraph).sort(); for(var ki = 0; ki < keys.length; ++ki) { var node = defaultGraph[keys[ki]]; // only add full subjects to top-level if(!_isSubjectReference(node)) { flattened.push(node); } } if(ctx === null) { return callback(null, flattened); } // compact result (force @graph option to true, skip expansion) options.graph = true; options.skipExpansion = true; jsonld.compact(flattened, ctx, options, function(err, compacted) { if(err) { return callback(new JsonLdError( 'Could not compact merged output.', 'jsonld.MergeError', {cause: err})); } callback(null, compacted); }); } }; /** * Relabels all blank nodes in the given JSON-LD input. * * @param input the JSON-LD input. * @param [options] the options to use: * [namer] a jsonld.UniqueNamer to use. */ jsonld.relabelBlankNodes = function(input, options) { options = options || {}; var namer = options.namer || new UniqueNamer('_:b'); return _labelBlankNodes(namer, input); }; /** * The default document loader for external documents. If the environment * is node.js, a callback-continuation-style document loader is used; otherwise, * a promises-style document loader is used. * * @param url the URL to load. * @param callback(err, remoteDoc) called once the operation completes, * if using a non-promises API. * * @return a promise, if using a promises API. */ jsonld.documentLoader = function(url, callback) { var err = new JsonLdError( 'Could not retrieve a JSON-LD document from the URL. URL ' + 'dereferencing not implemented.', 'jsonld.LoadDocumentError', {code: 'loading document failed'}); if(_nodejs) { return callback(err, {contextUrl: null, documentUrl: url, document: null}); } return jsonld.promisify(function(callback) { callback(err); }); }; /** * Deprecated default document loader. Use or override jsonld.documentLoader * instead. */ jsonld.loadDocument = function(url, callback) { var promise = jsonld.documentLoader(url, callback); if(promise && 'then' in promise) { promise.then(callback.bind(null, null), callback); } }; /* Promises API */ /** * Creates a new promises API object. * * @param [options] the options to use: * [api] an object to attach the API to. * [version] 'json-ld-1.0' to output a standard JSON-LD 1.0 promises * API, 'jsonld.js' to output the same with augmented proprietary * methods (default: 'jsonld.js') * * @return the promises API object. */ jsonld.promises = function(options) { options = options || {}; var slice = Array.prototype.slice; var promisify = jsonld.promisify; // handle 'api' option as version, set defaults var api = options.api || {}; var version = options.version || 'jsonld.js'; if(typeof options.api === 'string') { if(!options.version) { version = options.api; } api = {}; } api.expand = function(input) { if(arguments.length < 1) { throw new TypeError('Could not expand, too few arguments.'); } return promisify.apply(null, [jsonld.expand].concat(slice.call(arguments))); }; api.compact = function(input, ctx) { if(arguments.length < 2) { throw new TypeError('Could not compact, too few arguments.'); } var compact = function(input, ctx, options, callback) { // ensure only one value is returned in callback jsonld.compact(input, ctx, options, function(err, compacted) { callback(err, compacted); }); }; return promisify.apply(null, [compact].concat(slice.call(arguments))); }; api.flatten = function(input) { if(arguments.length < 1) { throw new TypeError('Could not flatten, too few arguments.'); } return promisify.apply( null, [jsonld.flatten].concat(slice.call(arguments))); }; api.frame = function(input, frame) { if(arguments.length < 2) { throw new TypeError('Could not frame, too few arguments.'); } return promisify.apply(null, [jsonld.frame].concat(slice.call(arguments))); }; api.fromRDF = function(dataset) { if(arguments.length < 1) { throw new TypeError('Could not convert from RDF, too few arguments.'); } return promisify.apply( null, [jsonld.fromRDF].concat(slice.call(arguments))); }; api.toRDF = function(input) { if(arguments.length < 1) { throw new TypeError('Could not convert to RDF, too few arguments.'); } return promisify.apply(null, [jsonld.toRDF].concat(slice.call(arguments))); }; api.normalize = function(input) { if(arguments.length < 1) { throw new TypeError('Could not normalize, too few arguments.'); } return promisify.apply( null, [jsonld.normalize].concat(slice.call(arguments))); }; if(version === 'jsonld.js') { api.link = function(input, ctx) { if(arguments.length < 2) { throw new TypeError('Could not link, too few arguments.'); } return promisify.apply( null, [jsonld.link].concat(slice.call(arguments))); }; api.objectify = function(input) { return promisify.apply( null, [jsonld.objectify].concat(slice.call(arguments))); }; api.createNodeMap = function(input) { return promisify.apply( null, [jsonld.createNodeMap].concat(slice.call(arguments))); }; api.merge = function(input) { return promisify.apply( null, [jsonld.merge].concat(slice.call(arguments))); }; } try { jsonld.Promise = global.Promise || require('es6-promise').Promise; } catch(e) { var f = function() { throw new Error('Unable to find a Promise implementation.'); }; for(var method in api) { api[method] = f; } } return api; }; /** * Converts a node.js async op into a promise w/boxed resolved value(s). * * @param op the operation to convert. * * @return the promise. */ jsonld.promisify = function(op) { if(!jsonld.Promise) { try { jsonld.Promise = global.Promise || require('es6-promise').Promise; } catch(e) { throw new Error('Unable to find a Promise implementation.'); } } var args = Array.prototype.slice.call(arguments, 1); return new jsonld.Promise(function(resolve, reject) { op.apply(null, args.concat(function(err, value) { if(!err) { resolve(value); } else { reject(err); } })); }); }; // extend jsonld.promises w/jsonld.js methods jsonld.promises({api: jsonld.promises}); /* WebIDL API */ function JsonLdProcessor() {} JsonLdProcessor.prototype = jsonld.promises({version: 'json-ld-1.0'}); JsonLdProcessor.prototype.toString = function() { if(this instanceof JsonLdProcessor) { return '[object JsonLdProcessor]'; } return '[object JsonLdProcessorPrototype]'; }; jsonld.JsonLdProcessor = JsonLdProcessor; // IE8 has Object.defineProperty but it only // works on DOM nodes -- so feature detection // requires try/catch :-( var canDefineProperty = !!Object.defineProperty; if(canDefineProperty) { try { Object.defineProperty({}, 'x', {}); } catch(e) { canDefineProperty = false; } } if(canDefineProperty) { Object.defineProperty(JsonLdProcessor, 'prototype', { writable: false, enumerable: false }); Object.defineProperty(JsonLdProcessor.prototype, 'constructor', { writable: true, enumerable: false, configurable: true, value: JsonLdProcessor }); } // setup browser global JsonLdProcessor if(_browser && typeof global.JsonLdProcessor === 'undefined') { if(canDefineProperty) { Object.defineProperty(global, 'JsonLdProcessor', { writable: true, enumerable: false, configurable: true, value: JsonLdProcessor }); } else { global.JsonLdProcessor = JsonLdProcessor; } } /* Utility API */ // define setImmediate and nextTick if(typeof process === 'undefined' || !process.nextTick) { if(typeof setImmediate === 'function') { jsonld.setImmediate = jsonld.nextTick = function(callback) { return setImmediate(callback); }; } else { jsonld.setImmediate = function(callback) { setTimeout(callback, 0); }; jsonld.nextTick = jsonld.setImmediate; } } else { jsonld.nextTick = process.nextTick; if(typeof setImmediate === 'function') { jsonld.setImmediate = setImmediate; } else { jsonld.setImmediate = jsonld.nextTick; } } /** * Parses a link header. The results will be key'd by the value of "rel". * * Link: <http://json-ld.org/contexts/person.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json" * * Parses as: { * 'http://www.w3.org/ns/json-ld#context': { * target: http://json-ld.org/contexts/person.jsonld, * type: 'application/ld+json' * } * } * * If there is more than one "rel" with the same IRI, then entries in the * resulting map for that "rel" will be arrays. * * @param header the link header to parse. */ jsonld.parseLinkHeader = function(header) { var rval = {}; // split on unbracketed/unquoted commas var entries = header.match(/(?:<[^>]*?>|"[^"]*?"|[^,])+/g); var rLinkHeader = /\s*<([^>]*?)>\s*(?:;\s*(.*))?/; for(var i = 0; i < entries.length; ++i) { var match = entries[i].match(rLinkHeader); if(!match) { continue; } var result = {target: match[1]}; var params = match[2]; var rParams = /(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/g; while(match = rParams.exec(params)) { result[match[1]] = (match[2] === undefined) ? match[3] : match[2]; } var rel = result['rel'] || ''; if(_isArray(rval[rel])) { rval[rel].push(result); } else if(rel in rval) { rval[rel] = [rval[rel], result]; } else { rval[rel] = result; } } return rval; }; /** * Creates a simple document cache that retains documents for a short * period of time. * * FIXME: Implement simple HTTP caching instead. * * @param size the maximum size of the cache. */ jsonld.DocumentCache = function(size) { this.order = []; this.cache = {}; this.size = size || 50; this.expires = 30 * 1000; }; jsonld.DocumentCache.prototype.get = function(url) { if(url in this.cache) { var entry = this.cache[url]; if(entry.expires >= +new Date()) { return entry.ctx; } delete this.cache[url]; this.order.splice(this.order.indexOf(url), 1); } return null; }; jsonld.DocumentCache.prototype.set = function(url, ctx) { if(this.order.length === this.size) { delete this.cache[this.order.shift()]; } this.order.push(url); this.cache[url] = {ctx: ctx, expires: (+new Date() + this.expires)}; }; /** * Creates an active context cache. * * @param size the maximum size of the cache. */ jsonld.ActiveContextCache = function(size) { this.order = []; this.cache = {}; this.size = size || 100; }; jsonld.ActiveContextCache.prototype.get = function(activeCtx, localCtx) { var key1 = JSON.stringify(activeCtx); var key2 = JSON.stringify(localCtx); var level1 = this.cache[key1]; if(level1 && key2 in level1) { return level1[key2]; } return null; }; jsonld.ActiveContextCache.prototype.set = function( activeCtx, localCtx, result) { if(this.order.length === this.size) { var entry = this.order.shift(); delete this.cache[entry.activeCtx][entry.localCtx]; } var key1 = JSON.stringify(activeCtx); var key2 = JSON.stringify(localCtx); this.order.push({activeCtx: key1, localCtx: key2}); if(!(key1 in this.cache)) { this.cache[key1] = {}; } this.cache[key1][key2] = _clone(result); }; /** * Default JSON-LD cache. */ jsonld.cache = { activeCtx: new jsonld.ActiveContextCache() }; /** * Document loaders. */ jsonld.documentLoaders = {}; /** * Creates a built-in jquery document loader. * * @param $ the jquery instance to use. * @param options the options to use: * secure: require all URLs to use HTTPS. * usePromise: true to use a promises API, false for a * callback-continuation-style API; defaults to true if Promise * is globally defined, false if not. * * @return the jquery document loader. */ jsonld.documentLoaders.jquery = function($, options) { options = options || {}; var loader = function(url, callback) { if(url.indexOf('http:') !== 0 && url.indexOf('https:') !== 0) { return callback(new JsonLdError( 'URL could not be dereferenced; only "http" and "https" URLs are ' + 'supported.', 'jsonld.InvalidUrl', {code: 'loading document failed', url: url}), {contextUrl: null, documentUrl: url, document: null}); } if(options.secure && url.indexOf('https') !== 0) { return callback(new JsonLdError( 'URL could not be dereferenced; secure mode is enabled and ' + 'the URL\'s scheme is not "https".', 'jsonld.InvalidUrl', {code: 'loading document failed', url: url}), {contextUrl: null, documentUrl: url, document: null}); } $.ajax({ url: url, accepts: { json: 'application/ld+json, application/json' }, // ensure Accept header is very specific for JSON-LD/JSON headers: { 'Accept': 'application/ld+json, application/json' }, dataType: 'json', crossDomain: true, success: function(data, textStatus, jqXHR) { var doc = {contextUrl: null, documentUrl: url, document: data}; // handle Link Header var contentType = jqXHR.getResponseHeader('Content-Type'); var linkHeader = jqXHR.getResponseHeader('Link'); if(linkHeader && contentType !== 'application/ld+json') { // only 1 related link header permitted linkHeader = jsonld.parseLinkHeader(linkHeader)[LINK_HEADER_REL]; if(_isArray(linkHeader)) { return callback(new JsonLdError( 'URL could not be dereferenced, it has more than one ' + 'associated HTTP Link Header.', 'jsonld.InvalidUrl', {code: 'multiple context link headers', url: url}), doc); } if(linkHeader) { doc.contextUrl = linkHeader.target; } } callback(null, doc); }, error: function(jqXHR, textStatus, err) { callback(new JsonLdError( 'URL could not be dereferenced, an error occurred.', 'jsonld.LoadDocumentError', {code: 'loading document failed', url: url, cause: err}), {contextUrl: null, documentUrl: url, document: null}); } }); }; var usePromise = (typeof Promise !== 'undefined'); if('usePromise' in options) { usePromise = options.usePromise; } if(usePromise) { return function(url) { return jsonld.promisify(loader, url); }; } return loader; }; /** * Creates a built-in node document loader. * * @param options the options to use: * secure: require all URLs to use HTTPS. * strictSSL: true to require SSL certificates to be valid, * false not to (default: true). * maxRedirects: the maximum number of redirects to permit, none by * default. * usePromise: true to use a promises API, false for a * callback-continuation-style API; false by default. * * @return the node document loader. */ jsonld.documentLoaders.node = function(options) { options = options || {}; var strictSSL = ('strictSSL' in options) ? options.strictSSL : true; var maxRedirects = ('maxRedirects' in options) ? options.maxRedirects : -1; var request = require('request'); var http = require('http'); var cache = new jsonld.DocumentCache(); function loadDocument(url, redirects, callback) { if(url.indexOf('http:') !== 0 && url.indexOf('https:') !== 0) { return callback(new JsonLdError( 'URL could not be dereferenced; only "http" and "https" URLs are ' + 'supported.', 'jsonld.InvalidUrl', {code: 'loading document failed', url: u