Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# jsonld ChangeLog

### Added
- Support list of lists.

## 1.6.2 - 2019-05-21

### Fixed
Expand Down
32 changes: 24 additions & 8 deletions lib/compact.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ api.compact = ({
return rval;
}

// if expanded property is @list and we're contained within a list
// container, recursively compact this item to an array
if(_isList(element)) {
const container = _getContextValue(
activeCtx, activeProperty, '@container') || [];
if(container.includes('@list')) {
return api.compact({
activeCtx,
activeProperty,
element: element['@list'],
options,
compactionMap
});
}
}

// FIXME: avoid misuse of active property as an expanded property?
const insideReverse = (activeProperty === '@reverse');

Expand Down Expand Up @@ -399,14 +415,14 @@ api.compact = ({
relativeTo: {vocab: true}
})] = expandedItem['@index'];
}
} else if(nestResult.hasOwnProperty(itemActiveProperty)) {
// can't use @list container for more than 1 list
throw new JsonLdError(
'JSON-LD compact error; property has a "@list" @container ' +
'rule but there is more than a single @list that matches ' +
'the compacted term in the document. Compaction might mix ' +
'unwanted items into the list.',
'jsonld.SyntaxError', {code: 'compaction to list of lists'});
// FIXME
//} else if(nestResult.hasOwnProperty(itemActiveProperty)) {
} else {
_addValue(nestResult, itemActiveProperty, compactedItem, {
valueIsArray: true,
allowDuplicate: true
});
continue;
}
}

Expand Down
12 changes: 2 additions & 10 deletions lib/expand.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,8 @@ api.expand = ({
insideIndex,
typeScopedContext
});
if(insideList && (_isArray(e) || _isList(e))) {
// lists of lists are illegal
throw new JsonLdError(
'Invalid JSON-LD syntax; lists of lists are not permitted.',
'jsonld.SyntaxError', {code: 'list of lists'});
if(insideList && _isArray(e)) {
e = {'@list': e};
}

if(e === null) {
Expand Down Expand Up @@ -672,11 +669,6 @@ function _expandObject({
insideList: isList,
expansionMap
});
if(isList && _isList(expandedValue)) {
throw new JsonLdError(
'Invalid JSON-LD syntax; lists of lists are not permitted.',
'jsonld.SyntaxError', {code: 'list of lists'});
}
} else {
// recursively expand value with key as new active property
expandedValue = api.expand({
Expand Down
33 changes: 17 additions & 16 deletions lib/fromRdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,22 +225,23 @@ api.fromRDF = async (
}

// the list is nested in another list
if(property === RDF_FIRST) {
// empty list
if(node['@id'] === RDF_NIL) {
// can't convert rdf:nil to a @list object because it would
// result in a list of lists which isn't supported
continue;
}

// preserve list head
if(RDF_REST in graphObject[head['@id']]) {
head = graphObject[head['@id']][RDF_REST][0];
}

list.pop();
listNodes.pop();
}
// FIXME
//if(property === RDF_FIRST) {
// // empty list
// if(node['@id'] === RDF_NIL) {
// // can't convert rdf:nil to a @list object because it would
// // result in a list of lists which isn't supported
// continue;
// }

// // preserve list head
// if(RDF_REST in graphObject[head['@id']]) {
// head = graphObject[head['@id']][RDF_REST][0];
// }

// list.pop();
// listNodes.pop();
//}

// transform list into @list object
delete head['@id'];
Expand Down
40 changes: 22 additions & 18 deletions lib/nodeMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ api.createMergedNodeMap = (input, options) => {
api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
// recurse through array
if(types.isArray(input)) {
for(let i = 0; i < input.length; ++i) {
api.createNodeMap(input[i], graphs, graph, issuer, undefined, list);
for(const node of input) {
api.createNodeMap(node, graphs, graph, issuer, undefined, list);
}
return;
}
Expand All @@ -64,7 +64,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
// add values to list
if(graphTypes.isValue(input)) {
if('@type' in input) {
let type = input['@type'];
const type = input['@type'];
// rename @type blank node
if(type.indexOf('_:') === 0) {
input['@type'] = type = issuer.getId(type);
Expand All @@ -74,15 +74,19 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
list.push(input);
}
return;
} else if(list && graphTypes.isList(input)) {
const _list = [];
api.createNodeMap(input['@list'], graphs, graph, issuer, name, _list);
list.push({'@list': _list});
return;
}

// Note: At this point, input must be a subject.

// spec requires @type to be named first, so assign names early
if('@type' in input) {
const types = input['@type'];
for(let i = 0; i < types.length; ++i) {
const type = types[i];
for(const type of types) {
if(type.indexOf('_:') === 0) {
issuer.getId(type);
}
Expand All @@ -105,9 +109,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
const subject = subjects[name] = subjects[name] || {};
subject['@id'] = name;
const properties = Object.keys(input).sort();
for(let pi = 0; pi < properties.length; ++pi) {
let property = properties[pi];

for(let property of properties) {
// skip @id
if(property === '@id') {
continue;
Expand All @@ -119,8 +121,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
const reverseMap = input['@reverse'];
for(const reverseProperty in reverseMap) {
const items = reverseMap[reverseProperty];
for(let ii = 0; ii < items.length; ++ii) {
const item = items[ii];
for(const item of items) {
let itemName = item['@id'];
if(graphTypes.isBlankNode(item)) {
itemName = issuer.getId(itemName);
Expand Down Expand Up @@ -171,9 +172,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
util.addValue(subject, property, [], {propertyIsArray: true});
continue;
}
for(let oi = 0; oi < objects.length; ++oi) {
let o = objects[oi];

for(var o of objects) {
if(property === '@type') {
// rename @type blank nodes
o = (o.indexOf('_:') === 0) ? issuer.getId(o) : o;
Expand All @@ -190,6 +189,13 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
subject, property, {'@id': id},
{propertyIsArray: true, allowDuplicate: false});
api.createNodeMap(o, graphs, graph, issuer, id);
} else if(graphTypes.isValue(o)) {
//if('@type' in o) {
// o['@type'] = o['@type'][0];
//}
util.addValue(
subject, property, o,
{propertyIsArray: true, allowDuplicate: false});
} else if(graphTypes.isList(o)) {
// handle @list
const _list = [];
Expand Down Expand Up @@ -249,8 +255,7 @@ api.mergeNodeMaps = graphs => {
// add all non-default graphs to default graph
const defaultGraph = graphs['@default'];
const graphNames = Object.keys(graphs).sort();
for(let i = 0; i < graphNames.length; ++i) {
const graphName = graphNames[i];
for(const graphName of graphNames) {
if(graphName === '@default') {
continue;
}
Expand All @@ -265,9 +270,8 @@ api.mergeNodeMaps = graphs => {
subject['@graph'] = [];
}
const graph = subject['@graph'];
const ids = Object.keys(nodeMap).sort();
for(let ii = 0; ii < ids.length; ++ii) {
const node = nodeMap[ids[ii]];
for(const id of Object.keys(nodeMap).sort()) {
const node = nodeMap[id];
// only add full subjects
if(!graphTypes.isSubjectReference(node)) {
graph.push(node);
Expand Down
106 changes: 58 additions & 48 deletions lib/toRdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ api.toRDF = (input, options) => {
*/
function _graphToRDF(dataset, graph, graphTerm, issuer, options) {
const ids = Object.keys(graph).sort();
for(let i = 0; i < ids.length; ++i) {
const id = ids[i];
for(const id of ids) {
const node = graph[id];
const properties = Object.keys(node).sort();
for(let property of properties) {
Expand Down Expand Up @@ -126,22 +125,16 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) {
continue;
}

// convert @list to triples
if(graphTypes.isList(item)) {
_listToRDF(
item['@list'], issuer, subject, predicate, dataset, graphTerm);
} else {
// convert value or node object to triple
const object = _objectToRDF(item);
// skip null objects (they are relative IRIs)
if(object) {
dataset.push({
subject,
predicate,
object,
graph: graphTerm
});
}
// convert list, value or node object to triple
const object = _objectToRDF(item, issuer, dataset, graphTerm);
// skip null objects (they are relative IRIs)
if(object) {
dataset.push({
subject,
predicate,
object,
graph: graphTerm
});
}
}
}
Expand All @@ -154,59 +147,72 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) {
*
* @param list the @list value.
* @param issuer a IdentifierIssuer for assigning blank node names.
* @param subject the subject for the head of the list.
* @param predicate the predicate for the head of the list.
* @param dataset the array of quads to append to.
* @param graphTerm the graph term for each quad.
*
* @return the head of the list.
*/
function _listToRDF(list, issuer, subject, predicate, dataset, graphTerm) {
function _listToRDF(list, issuer, dataset, graphTerm) {
const first = {termType: 'NamedNode', value: RDF_FIRST};
const rest = {termType: 'NamedNode', value: RDF_REST};
const nil = {termType: 'NamedNode', value: RDF_NIL};

const last = list.pop();
// Result is the head of the list
const result = last ? {termType: 'BlankNode', value: issuer.getId()} : nil;
var subject = result;

for(const item of list) {
const blankNode = {termType: 'BlankNode', value: issuer.getId()};
const object = _objectToRDF(item, issuer, dataset, graphTerm);
const next = {termType: 'BlankNode', value: issuer.getId()};
dataset.push({
subject,
predicate,
object: blankNode,
predicate: first,
object: object,
graph: graphTerm
});
dataset.push({
subject,
predicate: rest,
object: next,
graph: graphTerm
});
subject = next;
}

subject = blankNode;
predicate = first;
const object = _objectToRDF(item);

// skip null objects (they are relative IRIs)
if(object) {
dataset.push({
subject,
predicate,
object,
graph: graphTerm
});
}

predicate = rest;
// Tail of list
if(last) {
const object = _objectToRDF(last, issuer, dataset, graphTerm);
dataset.push({
subject,
predicate: first,
object: object,
graph: graphTerm
});
dataset.push({
subject,
predicate: rest,
object: nil,
graph: graphTerm
});
}

dataset.push({
subject,
predicate,
object: nil,
graph: graphTerm
});
return result;
}

/**
* Converts a JSON-LD value object to an RDF literal or a JSON-LD string or
* node object to an RDF resource.
* Converts a JSON-LD value object to an RDF literal or a JSON-LD string,
* node object to an RDF resource,
* or adds a list.
*
* @param item the JSON-LD value or node object.
* @param issuer a IdentifierIssuer for assigning blank node names.
* @param dataset the dataset to append RDF quads to.
* @param graphTerm the graph term for each quad.
*
* @return the RDF literal or RDF resource.
*/
function _objectToRDF(item) {
function _objectToRDF(item, issuer, dataset, graphTerm) {
const object = {};

// convert value object to RDF
Expand Down Expand Up @@ -241,6 +247,10 @@ function _objectToRDF(item) {
object.value = value;
object.datatype.value = datatype || XSD_STRING;
}
} else if(graphTypes.isList(item)) {
const _list = _listToRDF(item['@list'], issuer, dataset, graphTerm);
object.termType = _list.termType;
object.value = _list.value;
} else {
// convert string/node object to RDF
const id = types.isObject(item) ? item['@id'] : item;
Expand Down
Loading