Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 22 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,12 @@ 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'});
} 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
18 changes: 0 additions & 18 deletions lib/fromRdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,24 +224,6 @@ 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();
}

// transform list into @list object
delete head['@id'];
head['@list'] = list.reverse();
Expand Down
35 changes: 18 additions & 17 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 @@ -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(let o of objects) {
if(property === '@type') {
// rename @type blank nodes
o = (o.indexOf('_:') === 0) ? issuer.getId(o) : o;
Expand All @@ -190,6 +189,10 @@ 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)) {
util.addValue(
subject, property, o,
{propertyIsArray: true, allowDuplicate: false});
} else if(graphTypes.isList(o)) {
// handle @list
const _list = [];
Expand Down Expand Up @@ -249,8 +252,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 +267,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
105 changes: 57 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,71 @@ 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;
let 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,
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,
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 +246,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
9 changes: 8 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ api.hasValue = (subject, property, value) => {
* @param [options] the options to use:
* [propertyIsArray] true if the property is always an array, false
* if not (default: false).
* [valueIsArray] true if the value to be added should be preserved as
* an array (lists) (default: false).
* [allowDuplicate] true to allow duplicates, false not to (uses a
* simple shallow comparison of subject ID or value) (default: true).
*/
Expand All @@ -257,11 +259,16 @@ api.addValue = (subject, property, value, options) => {
if(!('propertyIsArray' in options)) {
options.propertyIsArray = false;
}
if(!('valueIsArray' in options)) {
options.valueIsArray = false;
}
if(!('allowDuplicate' in options)) {
options.allowDuplicate = true;
}

if(types.isArray(value)) {
if(options.valueIsArray) {
subject[property] = value;
} else if(types.isArray(value)) {
if(value.length === 0 && options.propertyIsArray &&
!subject.hasOwnProperty(property)) {
subject[property] = [];
Expand Down
Loading