UNPKG

ldapp

Version:

JavaScript Linked Data Stack

1,672 lines (1,555 loc) 199 kB
/** * A JavaScript implementation of the JSON-LD API. * * @author Dave Longley * * BSD 3-Clause License * Copyright (c) 2011-2013 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; } 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)) { // simplify to a single item if(compacted.length === 1) { compacted = compacted[0]; } // simplify to an empty object else if(compacted.length === 0) { compacted = {}; } } // always use array if graph option is on else if(options.graph && _isObject(compacted)) { 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 ' + 'derefencing 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 (default: true). * [explicit] default @explicit flag (default: false). * [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 = true; } options.explicit = options.explicit || false; 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 ' + 'derefencing 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 = {}; } // 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) opts.graph = true; opts.skipExpansion = true; 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 compacted[graph] = _removePreserve(ctx, compacted[graph], opts); callback(null, compacted); }); }); }); } }; /** * Performs JSON-LD objectification. * * @param input the JSON-LD input to objectify. * @param ctx the JSON-LD context to apply. * @param [options] the framing options. * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, objectified) called once the operation completes. */ jsonld.objectify = function(input, ctx, options, callback) { // 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 framing.', 'jsonld.FrameError', {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.', 'jsonld.FrameError', {cause: err})); } // get graph alias var graph = _compactIri(ctx, '@graph'); // remove @preserve from results (named graphs?) compacted[graph] = _removePreserve(ctx, compacted[graph], options); 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) { throw 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); }); }; /** * Relabels all blank nodes in the given JSON-LD input. * * @param input the JSON-LD input. */ jsonld.relabelBlankNodes = function(input) { _labelBlankNodes(new UniqueNamer('_:b', 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 derefencing 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 */ jsonld.promises = function() { var slice = Array.prototype.slice; var promisify = jsonld.promisify; var 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))); }; 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) { var Promise = _nodejs ? require('./Promise').Promise : global.Promise; var args = Array.prototype.slice.call(arguments, 1); return new Promise(function(resolver) { op.apply(null, args.concat(function(err, value) { if(err) { resolver.reject(err); } else { resolver.resolve(value); } })); }); }; /* WebIDL API */ function JsonLdProcessor() {} JsonLdProcessor.prototype = jsonld.promises(); 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 = 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 cache = new jsonld.DocumentCache(); var loader = function(url, callback) { 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}); } var doc = cache.get(url); if(doc !== null) { return callback(null, doc); } $.ajax({ url: url, 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; } } cache.set(url, doc); 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. * 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 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(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}); } var doc = cache.get(url); if(doc !== null) { return callback(null, doc); } request({ url: url, strictSSL: true, followRedirect: false }, function(err, res, body) { doc = {contextUrl: null, documentUrl: url, document: body || null}; // handle error if(err) { return callback(new JsonLdError( 'URL could not be dereferenced, an error occurred.', 'jsonld.LoadDocumentError', {code: 'loading document failed', url: url, cause: err}), doc); } var statusText = http.STATUS_CODES[res.statusCode]; if(res.statusCode >= 400) { return callback(new JsonLdError( 'URL could not be dereferenced: ' + statusText, 'jsonld.InvalidUrl', { code: 'loading document failed', url: url, httpStatusCode: res.statusCode }), doc); } // handle Link Header if(res.headers.link && res.headers['content-type'] !== 'application/ld+json') { // only 1 related link header permitted var linkHeader = jsonld.parseLinkHeader( res.headers.link)[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; } } // handle redirect if(res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { if(redirects.length === maxRedirects) { return callback(new JsonLdError( 'URL could not be dereferenced; there were too many redirects.', 'jsonld.TooManyRedirects', { code: 'loading document failed', url: url, httpStatusCode: res.statusCode, redirects: redirects }), doc); } if(redirects.indexOf(url) !== -1) { return callback(new JsonLdError( 'URL could not be dereferenced; infinite redirection was detected.', 'jsonld.InfiniteRedirectDetected', { code: 'recursive context inclusion', url: url, httpStatusCode: res.statusCode, redirects: redirects }), doc); } redirects.push(url); return loadDocument(res.headers.location, redirects, callback); } // cache for each redirected URL redirects.push(url); for(var i = 0; i < redirects.length; ++i) { cache.set( redirects[i], {contextUrl: null, documentUrl: redirects[i], document: body}); } callback(err, doc); }); } var loader = function(url, callback) { loadDocument(url, [], callback); }; if(options.usePromise) { return function(url) { return jsonld.promisify(loader, url); }; } return loader; }; /** * Creates a built-in XMLHttpRequest document loader. * * @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. * [xhr]: the XMLHttpRequest API to use. * * @return the XMLHttpRequest document loader. */ jsonld.documentLoaders.xhr = function(options) { var rlink = /(^|(\r\n))link:/i; options = options || {}; var cache = new jsonld.DocumentCache(); var loader = function(url, callback) { 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}); } var doc = cache.get(url); if(doc !== null) { return callback(null, doc); } var xhr = options.xhr || XMLHttpRequest; var req = new xhr(); req.onload = function(e) { var doc = {contextUrl: null, documentUrl: url, document: req.response}; // handle Link Header (avoid unsafe header warning by existence testing) var contentType = req.getResponseHeader('Content-Type'); var linkHeader; if(rlink.test(req.getAllResponseHeaders())) { linkHeader = req.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; } } cache.set(url, doc); callback(null, doc); }; req.onerror = function() { callback(new JsonLdError( 'URL could not be dereferenced, an error occurred.', 'jsonld.LoadDocumentError', {code: 'loading document failed', url: url}), {contextUrl: null, documentUrl: url, document: null}); }; req.open('GET', url, true); req.send(); }; var usePromise = (typeof Promise !== 'undefined'); if('usePromise' in options) { usePromise = options.usePromise; } if(usePromise) { return function(url) { return jsonld.promisify(loader, url); }; } return loader; }; /** * Assigns the default document loader for external document URLs to a built-in * default. Supported types currently include: 'jquery' and 'node'. * * To use the jquery document loader, the first parameter must be a reference * to the main jquery object. * * @param type the type to set. * @param [params] the parameters required to use the document loader. */ jsonld.useDocumentLoader = function(type) { if(!(type in jsonld.documentLoaders)) { throw new JsonLdError( 'Unknown document loader type: "' + type + '"', 'jsonld.UnknownDocumentLoader', {type: type}); } // set document loader jsonld.documentLoader = jsonld.documentLoaders[type].apply( jsonld, Array.prototype.slice.call(arguments, 1)); }; /** * Processes a local context, resolving any URLs as necessary, and returns a * new active context in its callback. * * @param activeCtx the current active context. * @param localCtx the local context to process. * @param [options] the options to use: * [documentLoader(url, callback(err, remoteDoc))] the document loader. * @param callback(err, ctx) called once the operation completes. */ jsonld.processContext = function(activeCtx, localCtx) { // get arguments var options = {}; var callbackArg = 2; if(arguments.length > 3) { options = arguments[2] || {}; callbackArg += 1; } var callback = arguments[callbackArg]; // set default options if(!('base' in options)) { options.base = ''; } if(!('documentLoader' in options)) { options.documentLoader = jsonld.loadDocument; } // return initial context early for null context if(localCtx === null) { return callback(null, _getInitialContext(options)); } // retrieve URLs in localCtx localCtx = _clone(localCtx); if(_isString(localCtx) || (_isObject(localCtx) && !('@context' in localCtx))) { localCtx = {'@context': localCtx}; } _retrieveContextUrls(localCtx, options, function(err, ctx) { if(err) { return callback(err); } try { // process context ctx = new Processor().processContext(activeCtx, ctx, options); } catch(ex) { return callback(ex); } callback(null, ctx); }); }; /** * Returns true if the given subject has the given property. * * @param subject the subject to check. * @param property the property to look for. * * @return true if the subject has the given property, false if not. */ jsonld.hasProperty = function(subject, property) { var rval = false; if(property in subject) { var value = subject[property]; rval = (!_isArray(value) || value.length > 0); } return rval; }; /** * Determines if the given value is a property of the given subject. * * @param subject the subject to check. * @param property the property to check. * @param value the value to check. * * @return true if the value exists, false if not. */ jsonld.hasValue = function(subject, property, value) { var rval = false; if(jsonld.hasProperty(subject, property)) { var val = subject[property]; var isList = _isList(val); if(_isArray(val) || isList) { if(isList) { val = val['@list']; } for(var i = 0; i < val.length; ++i) { if(jsonld.compareValues(value, val[i])) { rval = true; break; } } } // avoid matching the set of values with an array value parameter else if(!_isArray(value)) { rval = jsonld.compareValues(value, val); } } return rval; }; /** * Adds a value to a subject. If the