ldapp
Version:
JavaScript Linked Data Stack
1,672 lines (1,555 loc) • 199 kB
JavaScript
/**
* 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