--- a/playground/jsonld.js Sun Apr 29 19:33:19 2012 -0700
+++ b/playground/jsonld.js Sun Apr 29 22:45:57 2012 -0400
@@ -43,7 +43,7 @@
/**
* Performs JSON-LD compaction.
*
- * @param input the JSON-LD object to compact.
+ * @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.
@@ -194,7 +194,7 @@
/**
* Performs JSON-LD expansion.
*
- * @param input the JSON-LD object to expand.
+ * @param input the JSON-LD input to expand.
* @param [options] the options to use:
* [base] the base IRI to use.
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
@@ -250,7 +250,7 @@
/**
* Performs JSON-LD framing.
*
- * @param input the JSON-LD object to frame.
+ * @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.
@@ -333,7 +333,7 @@
/**
* Performs JSON-LD normalization.
*
- * @param input the JSON-LD object to normalize.
+ * @param input the JSON-LD input to normalize.
* @param [options] the options to use:
* [base] the base IRI to use.
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
@@ -372,15 +372,62 @@
};
/**
+ * Converts RDF statements into JSON-LD.
+ *
+ * @param statements a serialized string of RDF statements in a format
+ * specified by the format option or an array of the RDF statements
+ * to convert.
+ * @param [options] the options to use:
+ * [format] the format if input is a string:
+ * 'text/x-nquads' for N-Quads (default).
+ * [notType] true to use rdf:type, false to use @type (default).
+ * @param callback(err, output) called once the operation completes.
+ */
+jsonld.fromRDF = function(statements) {
+ // get arguments
+ var options = {};
+ var callback;
+ var callbackArg = 1;
+ if(arguments.length > 2) {
+ options = arguments[1] || {};
+ callbackArg += 1;
+ }
+ callback = arguments[callbackArg];
+
+ // set default options
+ if(!('format' in options)) {
+ options.format = 'text/x-nquads';
+ }
+ if(!('notType' in options)) {
+ options.notType = false;
+ }
+
+ if(_isString(statements)) {
+ // supported formats
+ if(options.format === 'text/x-nquads') {
+ statements = _parseNQuads(statements);
+ }
+ else {
+ throw new JsonLdError(
+ 'Unknown input format.',
+ 'jsonld.UnknownFormat', {format: options.format});
+ }
+ }
+
+ // convert from RDF
+ new Processor().fromRDF(statements, options, callback);
+};
+
+/**
* Outputs the RDF statements found in the given JSON-LD object.
*
- * @param input the JSON-LD object.
+ * @param input the JSON-LD input.
* @param [options] the options to use:
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
* @param callback(err, statement) called when a statement is output, with the
* last statement as null.
*/
-jsonld.toRDF = function(input, callback) {
+jsonld.toRDF = function(input) {
// get arguments
var options = {};
var callback;
@@ -399,14 +446,17 @@
options.resolver = jsonld.urlResolver;
}
- // resolve all @context URLs in the input
- input = _clone(input);
- _resolveUrls(input, options.resolver, function(err, input) {
+ // expand input
+ jsonld.expand(input, options, function(err, expanded) {
if(err) {
- return callback(err);
+ return callback(new JsonLdError(
+ 'Could not expand input before conversion to RDF.',
+ 'jsonld.RdfError', {cause: err}));
}
+
// output RDF statements
- return new Processor().toRDF(input, callback);
+ var namer = new UniqueNamer('_:t');
+ new Processor().toRDF(expanded, namer, null, null, null, callback);
});
};
@@ -842,17 +892,14 @@
}
// constants
-var XSD = {
- 'boolean': 'http://www.w3.org/2001/XMLSchema#boolean',
- 'double': 'http://www.w3.org/2001/XMLSchema#double',
- 'integer': 'http://www.w3.org/2001/XMLSchema#integer'
-};
-var RDF = {
- 'first': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first',
- 'rest': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest',
- 'nil': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil',
- 'type': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
-};
+var XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
+var XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
+var XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
+
+var RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
+var RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest';
+var RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil';
+var RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
/**
* A JSON-LD Error.
@@ -924,13 +971,13 @@
element = element['@value'];
// use native datatypes for certain xsd types
- if(type === XSD['boolean']) {
+ if(type === XSD_BOOLEAN) {
element = !(element === 'false' || element === '0');
}
- else if(type === XSD['integer']) {
+ else if(type === XSD_INTEGER) {
element = parseInt(element);
}
- else if(type === XSD['double']) {
+ else if(type === XSD_DOUBLE) {
element = parseFloat(element);
}
}
@@ -1461,15 +1508,337 @@
};
/**
- * Outputs the RDF statements found in the given JSON-LD object.
+ * Converts RDF statements into JSON-LD.
*
- * @param input the JSON-LD object.
+ * @param statements the RDF statements.
+ * @param options the RDF conversion options.
+ * @param callback(err, output) called once the operation completes.
+ */
+Processor.prototype.fromRDF = function(statements, options, callback) {
+ // prepare graph map (maps graph name => subjects, lists, etc)
+ var defaultGraph = {subjects: {}, listMap: {}};
+ var graphs = {'': defaultGraph};
+
+ for(var i in statements) {
+ var statement = statements[i];
+
+ // get subject, property, object, and graph name (default to '')
+ var s = statement.subject.nominalValue;
+ var p = statement.property.nominalValue;
+ var o = statement.object;
+ var name = ('name' in statement) ? statement.name.nominalValue : '';
+
+ // create a graph entry as needed
+ if(!(name in graphs)) {
+ var graph = graphs[name] = {subjects: {}, listMap: {}};
+ }
+ else {
+ var graph = graphs[name];
+ }
+
+ // handle element in @list
+ if(p === RDF_FIRST) {
+ // create list entry as needed
+ var listMap = graphs[name].listMap;
+ if(!(s in listMap)) {
+ var entry = listMap[s] = {};
+ }
+ else {
+ var entry = listMap[s];
+ }
+ // set object value
+ entry.first = _rdfToObject(o);
+ continue;
+ }
+
+ // handle other element in @list
+ if(p === RDF_REST) {
+ // create list entry as needed
+ var listMap = graphs[name].listMap;
+ if(!(s in listMap)) {
+ var entry = listMap[s] = {};
+ }
+ else {
+ var entry = listMap[s];
+ }
+ // set next in list
+ if(o.interfaceName === 'BlankNode') {
+ entry.rest = o.nominalValue;
+ }
+ continue;
+ }
+
+ // prepare to assign next JSON-LD value
+ var value;
+
+ // if graph is not the default graph
+ if(name !== '') {
+ // add graph subject to default graph as needed
+ if(!(name in defaultGraph.subjects)) {
+ value = defaultGraph.subjects[name] = {'@id': name};
+ }
+ else {
+ value = defaultGraph.subjects[name];
+ }
+ }
+
+ // add subject to graph as needed
+ var subjects = graphs[name].subjects;
+ if(!(s in subjects)) {
+ value = subjects[s] = {'@id': s};
+ }
+ // use existing subject value
+ else {
+ value = subjects[s];
+ }
+
+ // convert to @type unless options indicate to treat rdf:type as property
+ if(p === RDF_TYPE && !options.notType) {
+ // add value of object as @type
+ jsonld.addValue(value, '@type', o.nominalValue, true);
+ }
+ else {
+ // add property to value as needed
+ var object = _rdfToObject(o);
+ jsonld.addValue(value, p, object, true);
+
+ // a bnode might be the beginning of a list, so add it to the list map
+ if(o.interfaceName === 'BlankNode') {
+ var id = object['@id'];
+ var listMap = graphs[name].listMap;
+ if(!(id in listMap)) {
+ var entry = listMap[id] = {};
+ }
+ else {
+ var entry = listMap[id];
+ }
+ entry.head = object;
+ }
+ }
+ }
+
+ // build @lists
+ for(var name in graphs) {
+ var graph = graphs[name];
+
+ // find list head
+ var listMap = graph.listMap;
+ for(subject in listMap) {
+ var entry = listMap[subject];
+
+ // head found, build lists
+ if('head' in entry && 'first' in entry) {
+ // replace bnode @id with @list
+ delete entry.head['@id'];
+ var list = entry.head['@list'] = [entry.first];
+ while('rest' in entry) {
+ var rest = entry.rest;
+ entry = listMap[rest];
+ if(!('first' in entry)) {
+ throw new JsonLdError(
+ 'Invalid RDF list entry.',
+ 'jsonld.RdfError', {bnode: rest});
+ }
+ list.push(entry.first);
+ }
+ }
+ }
+ }
+
+ // build default graph in subject @id order
+ var output = [];
+ var subjects = defaultGraph.subjects;
+ var ids = Object.keys(subjects).sort();
+ for(var i in ids) {
+ var id = ids[i];
+
+ // add subject to default graph
+ var subject = subjects[id];
+ output.push(subject);
+
+ // output named graph in subject @id order
+ if(id in graphs) {
+ var graph = subject['@graph'] = [];
+ var _subjects = graphs[id].subjects;
+ var _ids = Object.keys(_subjects).sort();
+ for(var _i in _ids) {
+ graph.push(_subjects[_ids[_i]]);
+ }
+ }
+ }
+ callback(null, output);
+};
+
+/**
+ * Outputs the RDF statements found in the given JSON-LD element.
+ *
+ * @param element the JSON-LD element.
+ * @param namer the UniqueNamer for assigning bnode names.
+ * @param subject the active subject.
+ * @param property the active property.
+ * @param graph the graph name.
* @param callback(err, statement) called when a statement is output, with the
* last statement as null.
*/
-Processor.prototype.toRDF = function(input, callback) {
- // FIXME: implement
- callback(new JsonLdError('Not implemented', 'jsonld.NotImplemented'), null);
+Processor.prototype.toRDF = function(
+ element, namer, subject, property, graph, callback) {
+ // recurse into arrays
+ if(_isArray(element)) {
+ for(var i in element) {
+ this.toRDF(element[i], namer, subject, property, graph, callback);
+ }
+ return;
+ }
+
+ if(_isObject(element)) {
+ // convert @value to object
+ if(_isValue(element)) {
+ var object = {
+ nominalValue: element['@value'],
+ interfaceName: 'LiteralNode'
+ };
+
+ if('@type' in element) {
+ object.datatype = {
+ nominalValue: element['@type'],
+ interfaceName: 'IRI'
+ };
+ }
+ else if('@language' in element) {
+ object.language = element['@language'];
+ }
+
+ // emit literal
+ var statement = {
+ subject: _clone(subject),
+ property: _clone(property),
+ object: object
+ };
+ if(graph !== null) {
+ statement.name = graph;
+ }
+ return callback(null, statement);
+ }
+
+ // convert @list
+ if(_isList(element)) {
+ var list = _makeLinkedList(element);
+ return this.toRDF(list, namer, subject, property, graph, callback);
+ }
+
+ // Note: element must be a subject
+
+ // get subject @id (generate one if it is a bnode)
+ var isBnode = _isBlankNode(element);
+ var id = isBnode ? namer.getName(input['@id']) : input['@id'];
+
+ // create object
+ var object = {
+ nominalValue: id,
+ interfaceName: isBnode ? 'BlankNode' : 'IRI'
+ };
+
+ // emit statement if subject isn't null
+ if(subject !== null) {
+ var statement = {
+ subject: _clone(subject),
+ property: _clone(property),
+ object: _clone(object)
+ };
+ if(graph !== null) {
+ statement.name = graph;
+ }
+ callback(null, statement);
+ }
+
+ // set new active subject to object
+ subject = object;
+
+ // recurse over subject properties in order
+ var props = Object.keys(element).sort();
+ for(var pi in props) {
+ var prop = props[pi];
+
+ // convert @type to rdf:type
+ if(prop === '@type') {
+ prop = RDF_TYPE;
+ }
+
+ // recurse into @graph
+ if(prop === '@graph') {
+ this.toRDF(element[prop], namer, null, null, subject, callback);
+ continue;
+ }
+
+ // skip keywords
+ if(_isKeyword(prop)) {
+ continue;
+ }
+
+ // create new active property
+ property = {
+ nominalValue: id,
+ interfaceName: 'IRI'
+ };
+
+ // recurse into value
+ this.toRDF(element[prop], namer, subject, property, graph, callback);
+ }
+
+ return;
+ }
+
+ if(_isString(element)) {
+ // emit plain literal
+ var statement = {
+ subject: _clone(subject),
+ property: _clone(property),
+ object: {
+ nominalValue: element,
+ interfaceName: 'LiteralNode'
+ }
+ };
+ if(graph !== null) {
+ statement.name = graph;
+ }
+ return callback(null, statement);
+ }
+
+ if(_isBoolean(element) || _isNumber(element)) {
+ // convert to XSD datatype
+ if(_isBoolean(element)) {
+ var datatype = XSD_BOOLEAN;
+ var value = String(element);
+ }
+ else if(_isInteger(element)) {
+ var datatype = XSD_INTEGER;
+ var value = String(element);
+ }
+ else {
+ var datatype = XSD_DOUBLE;
+ // printf('%1.16e') equivalent
+ var value = element.toExponential(16).replace(
+ /(e(?:\+|-))([0-9])$/, '$10$2');
+ }
+
+ // emit typed literal
+ var statement = {
+ subject: _clone(subject),
+ property: _clone(property),
+ object: {
+ nominalValue: value,
+ interfaceName: 'LiteralNode',
+ datatype: {
+ nominalValue: datatype,
+ interfaceName: 'IRI'
+ }
+ }
+ };
+ if(graph !== null) {
+ statement.name = graph;
+ }
+ return callback(null, statement);
+ }
};
/**
@@ -1566,7 +1935,40 @@
}
return rval;
-};
+}
+
+/**
+ * Converts an RDF statement object to a JSON-LD object.
+ *
+ * @param o the RDF statement object to convert.
+ *
+ * @return the JSON-LD object.
+ */
+function _rdfToObject(o) {
+ // convert empty list
+ if(o.interfaceName === 'IRI' && o.nominalValue === RDF_NIL) {
+ return {'@list': []};
+ }
+
+ // convert IRI/BlankNode object to JSON-LD
+ if(o.interfaceName === 'IRI' || o.interfaceName === 'BlankNode') {
+ return {'@id': o.nominalValue};
+ }
+
+ // convert literal object to JSON-LD
+ var rval = {'@value': o.nominalValue};
+
+ // add datatype
+ if('datatype' in o) {
+ rval['@type'] = o.datatype.nominalValue;
+ }
+ // add language
+ else if('language' in o) {
+ rval['@language'] = o.language;
+ }
+
+ return rval;
+}
/**
* Recursively gets all statements from the given expanded JSON-LD input.
@@ -1628,17 +2030,17 @@
// convert boolean to @value
if(_isBoolean(o)) {
- o = {'@value': String(o), '@type': XSD['boolean']};
+ o = {'@value': String(o), '@type': XSD_BOOLEAN};
}
// convert double to @value
else if(_isDouble(o)) {
// do special JSON-LD double format, printf('%1.16e') JS equivalent
o = o.toExponential(16).replace(/(e(?:\+|-))([0-9])$/, '$10$2');
- o = {'@value': o, '@type': XSD['double']};
+ o = {'@value': o, '@type': XSD_DOUBLE};
}
// convert integer to @value
else if(_isNumber(o)) {
- o = {'@value': String(o), '@type': XSD['integer']};
+ o = {'@value': String(o), '@type': XSD_INTEGER};
}
// object is a blank node
@@ -1676,7 +2078,7 @@
}
}
}
-};
+}
/**
* Converts a @list value into an embedded linked list of blank nodes in
@@ -1688,19 +2090,14 @@
* @return the head of the linked list of blank nodes.
*/
function _makeLinkedList(value) {
- // convert @list array into embedded blank node linked list
+ // convert @list array into embedded blank node linked list in reverse
var list = value['@list'];
- var first = RDF['first'];
- var rest = RDF['rest'];
- var nil = RDF['nil'];
-
- // build linked list in reverse
var len = list.length;
- var tail = {'@id': nil};
+ var tail = {'@id': RDF_NIL};
for(var i = len - 1; i >= 0; --i) {
var e = {};
- e[first] = [list[i]];
- e[rest] = [tail];
+ e[RDF_FIRST] = [list[i]];
+ e[RDF_REST] = [tail];
tail = e;
}
@@ -1756,7 +2153,7 @@
}
// serialize property
- var p = (statement.p === '@type') ? RDF.type : statement.p;
+ var p = (statement.p === '@type') ? RDF_TYPE : statement.p;
triple += ' <' + p + '> ';
// serialize object
@@ -1859,7 +2256,7 @@
// hash direction, property, and bnode name/hash
var md = sha1.create();
md.update(direction);
- md.update((statement.p === '@type') ? RDF.type : statement.p);
+ md.update((statement.p === '@type') ? RDF_TYPE : statement.p);
md.update(name);
var groupHash = md.digest();
@@ -2250,7 +2647,7 @@
function _getFrameFlag(frame, options, name) {
var flag = '@' + name;
return (flag in frame) ? frame[flag][0] : options[name];
-};
+}
/**
* Validates a JSON-LD frame, throwing an exception if the frame is invalid.
@@ -2552,13 +2949,13 @@
if(_isBoolean(value) || _isNumber(value)) {
var type;
if(_isBoolean(value)) {
- type = XSD['boolean'];
+ type = XSD_BOOLEAN;
}
else if(_isDouble(value)) {
- type = XSD['double'];
+ type = XSD_DOUBLE;
}
else {
- type = XSD['integer'];
+ type = XSD_INTEGER;
}
if(entry['@type'] === type) {
return 3;
@@ -3392,7 +3789,7 @@
* of @context in the input that refers to a URL will be replaced with the
* JSON @context found at that URL.
*
- * @param input the JSON-LD object with possible contexts.
+ * @param input the JSON-LD input with possible contexts.
* @param resolver(url, callback(err, jsonCtx)) the URL resolver to use.
* @param callback(err, input) called once the operation completes.
*/
@@ -3540,6 +3937,112 @@
}
/**
+ * Parses statements in the form of N-Quads.
+ *
+ * @param input the N-Quads input to parse.
+ *
+ * @return an array of RDF statements.
+ */
+function _parseNQuads(input) {
+ // define partial regexes
+ var iri = '(?:<([^:]+:[^>]*)>)';
+ var bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))';
+ var plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"';
+ var datatype = '(?:\\^\\^' + iri + ')';
+ var language = '(?:@([a-z]+(?:-[a-z0-9]+)*))';
+ var literal = '(?:' + plain + '(?:' + datatype + '|' + language + ')?)';
+ var ws = '[ \t]+';
+ var wso = '[ \t]*';
+ var eoln = /(?:\r\n)|(?:\n)|(?:\r)/g;
+ var empty = new RegExp('^' + wso + '$');
+
+ // define quad part regexes
+ var subject = '(?:' + iri + '|' + bnode + ')' + ws;
+ var property = iri + ws;
+ var object = '(?:' + iri + '|' + bnode + '|' + literal + ')' + wso;
+ var graph = '(?:\\.|(?:(?:' + iri + '|' + bnode + ')' + wso + '\\.))';
+
+ // full quad regex
+ var quad = new RegExp(
+ '^' + wso + subject + property + object + graph + wso + '$');
+
+ // build RDF statements
+ var statements = [];
+
+ // split N-Quad input into lines
+ var lines = input.split(eoln);
+ var lineNumber = 0;
+ for(var i in lines) {
+ var line = lines[i];
+ lineNumber++;
+
+ // skip empty lines
+ if(empty.test(line)) {
+ continue;
+ }
+
+ // parse quad
+ var match = line.match(quad);
+ if(match === null) {
+ throw new JsonLdError(
+ 'Error while parsing N-Quads; invalid quad.',
+ 'jsonld.ParseError', {line: lineNumber});
+ }
+
+ // create RDF statement
+ var s = {subject: {}, property: {}, object: {}};
+
+ // get subject
+ if(_isUndefined(match[2])) {
+ s.subject.nominalValue = match[1];
+ s.subject.interfaceName = 'IRI';
+ }
+ else {
+ s.subject.nominalValue = match[2];
+ s.subject.interfaceName = 'BlankNode';
+ }
+
+ // get property
+ s.property = {nominalValue: match[3], interfaceName: 'IRI'};
+
+ // get object
+ if(_isUndefined(match[6])) {
+ if(_isUndefined(match[5])) {
+ s.object.nominalValue = match[4];
+ s.object.interfaceName = 'IRI';
+ }
+ else {
+ s.object.nominalValue = match[5];
+ s.object.interfaceName = 'BlankNode';
+ }
+ }
+ else {
+ s.object.nominalValue = match[6];
+ s.object.interfaceName = 'LiteralNode';
+ if(!_isUndefined(match[7])) {
+ s.object.datatype = {nominalValue: match[7], interfaceName: 'IRI'};
+ }
+ else if(!_isUndefined(match[8])) {
+ s.object.language = match[8];
+ }
+ }
+
+ // get graph
+ if(!_isUndefined(match[9])) {
+ s.name = {nominalValue: match[9], interfaceName: 'IRI'};
+ }
+ else if(!_isUndefined(match[10])) {
+ s.name = {nominalValue: match[10], interfaceName: 'BlankNode'};
+ }
+
+ // add statement
+ statements.push(s);
+ }
+
+ return statements;
+}
+
+/**
* Creates a new UniqueNamer. A UniqueNamer issues unique names, keeping
* track of any previously issued names.
*