Skip to content

Commit

Permalink
Improve @direction support.
Browse files Browse the repository at this point in the history
- Support `compound-literal` `rdfDirection` option.
- Emit `toRdf` warning if `@direction` is used and `rdfDirection` is not
  set.
- Add safe mode support for `@direction`. Using `@direction` without
  `rdfDirection` set will cause a safe mode failure.
- Add tests.
  • Loading branch information
davidlehn committed Sep 1, 2023
1 parent 78d1ab6 commit 24046f7
Show file tree
Hide file tree
Showing 7 changed files with 704 additions and 27 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# jsonld ChangeLog

## 8.3.0 - 2023-09-xx

### Added
- Support `compound-literal` `rdfDirection` option.
- Emit `toRdf` warning if `@direction` is used and `rdfDirection` is not set.

### Fixed
- Add safe mode support for `@direction`. Using `@direction` without
`rdfDirection` set will cause a safe mode failure.

## 8.2.1 - 2023-08-31

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ const _notSafeEventCodes = new Set([
'relative graph reference',
'relative object reference',
'relative predicate reference',
'relative subject reference'
'relative subject reference',
// toRDF / fromRDF
'rdfDirection not set'
]);

// safe handler that rejects unsafe warning conditions
Expand Down
97 changes: 91 additions & 6 deletions lib/fromRdf.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

Expand All @@ -18,7 +18,7 @@ const {

// constants
const {
// RDF,
RDF,
RDF_LIST,
RDF_FIRST,
RDF_REST,
Expand Down Expand Up @@ -52,14 +52,30 @@ api.fromRDF = async (
dataset,
options
) => {
const defaultGraph = {};
const graphMap = {'@default': defaultGraph};
const referencedOnce = {};
const {
useRdfType = false,
useNativeTypes = false,
rdfDirection = null
} = options;
// FIXME: use Maps?
const defaultGraph = {};
const graphMap = {'@default': defaultGraph};
const referencedOnce = {};
let processCompoundLiterals = false;
if(rdfDirection) {
if(rdfDirection === 'compound-literal') {
processCompoundLiterals = true;
} else if(rdfDirection !== 'i18n-datatype') {
throw new JsonLdError(
'Unknown rdfDirection value.',
'jsonld.InvalidRdfDirection',
{value: rdfDirection});
}
}
let compoundLiteralSubjects;
if(processCompoundLiterals) {
compoundLiteralSubjects = {};
}

for(const quad of dataset) {
// TODO: change 'name' to 'graph'
Expand All @@ -68,11 +84,18 @@ api.fromRDF = async (
if(!(name in graphMap)) {
graphMap[name] = {};
}
if(processCompoundLiterals && !(name in compoundLiteralSubjects)) {
compoundLiteralSubjects[name] = {};
}
if(name !== '@default' && !(name in defaultGraph)) {
defaultGraph[name] = {'@id': name};
}

const nodeMap = graphMap[name];
let compoundMap;
if(processCompoundLiterals) {
compoundMap = compoundLiteralSubjects[name];
}

// get subject, predicate, object
const s = quad.subject.value;
Expand All @@ -83,6 +106,9 @@ api.fromRDF = async (
nodeMap[s] = {'@id': s};
}
const node = nodeMap[s];
if(processCompoundLiterals && p === RDF + 'direction') {
compoundMap[s] = true;
}

const objectIsNode = o.termType.endsWith('Node');
if(objectIsNode && !(o.value in nodeMap)) {
Expand Down Expand Up @@ -194,6 +220,64 @@ api.fromRDF = async (
for(const name in graphMap) {
const graphObject = graphMap[name];

if(processCompoundLiterals) {
if(name in compoundLiteralSubjects) {
const cls = compoundLiteralSubjects[name];
for(const cl of Object.keys(cls)) {
const clEntry = referencedOnce[cl];
if(!clEntry) {
continue;
}
const node = clEntry.node;
const property = clEntry.property;
//const value = clEntry.value;
const clNode = graphObject[cl];
if(!types.isObject(clNode)) {
continue;
}
delete graphObject[cl];
for(const clReference of node[property]) {
if(clReference['@id'] === cl) {
delete clReference['@id'];
}
const value = clNode[RDF + 'value'];
// FIXME: error on !== 1 value
clReference['@value'] = value[0]['@value'];
const language = clNode[RDF + 'language'];
if(language) {
// FIXME: error on !== 1 language value
const v = language[0]['@value'];
if(!v.match(REGEX_BCP47)) {
throw new JsonLdError(
'Invalid RDF syntax; rdf:language must be valid BCP47.',
'jsonld.SyntaxError',
{
code: 'invalid language-tagged string',
value: v
});
}
clReference['@language'] = v;
}
const direction = clNode[RDF + 'direction'];
if(direction) {
// FIXME: error on !== 1 direction value
const v = direction[0]['@value'];
if(!(v === 'ltr' || v === 'rtl')) {
throw new JsonLdError(
'Invalid RDF syntax; rdf:direction must be "ltr" or "rtl".',
'jsonld.SyntaxError',
{
code: 'invalid base direction',
value: v
});
}
clReference['@direction'] = v;
}
}
}
}
}

// no @lists to be converted, continue
if(!(RDF_NIL in graphObject)) {
continue;
Expand Down Expand Up @@ -282,7 +366,8 @@ api.fromRDF = async (
*
* @param o the RDF triple object to convert.
* @param useNativeTypes true to output native types, false not to.
* @param rdfDirection text direction mode [null, i18n-datatype]
* @param rdfDirection text direction mode [null, i18n-datatype,
* compound-literal]
* @param options top level API options
*
* @return the JSON-LD object.
Expand Down
17 changes: 12 additions & 5 deletions lib/jsonld.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE});
* [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.
* expansion, false not to, defaults to false. Some well-formed
* and safe-mode checks may be omitted.
* [documentLoader(url, options)] the document loader.
* [framing] true if compaction is occuring during a framing operation.
* [safe] true to use safe mode. (default: false)
Expand Down Expand Up @@ -538,13 +539,16 @@ jsonld.link = async function(input, ctx, options) {
* [base] the base IRI to use (default: `null`).
* [expandContext] a context to expand with.
* [skipExpansion] true to assume the input is expanded and skip
* expansion, false not to, defaults to false.
* expansion, false not to, defaults to false. Some well-formed
* and safe-mode checks may be omitted.
* [inputFormat] the format if input is not JSON-LD:
* 'application/n-quads' for N-Quads.
* [format] the format if output is a string:
* 'application/n-quads' for N-Quads.
* [documentLoader(url, options)] the document loader.
* [useNative] true to use a native canonize algorithm
* [rdfDirection] 'i18n-datatype' or 'compound-literal' to support RDF
* transformation of @direction (default: null).
* [safe] true to use safe mode. (default: true).
* [contextResolver] internal use only.
*
Expand Down Expand Up @@ -601,8 +605,8 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
* (default: false).
* [useNativeTypes] true to convert XSD types into native types
* (boolean, integer, double), false not to (default: false).
* [rdfDirection] 'i18n-datatype' to support RDF transformation of
* @direction (default: null).
* [rdfDirection] 'i18n-datatype' or 'compound-literal' to support RDF
* transformation of @direction (default: null).
* [safe] true to use safe mode. (default: false)
*
* @return a Promise that resolves to the JSON-LD document.
Expand Down Expand Up @@ -647,13 +651,16 @@ jsonld.fromRDF = async function(dataset, options) {
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [skipExpansion] true to assume the input is expanded and skip
* expansion, false not to, defaults to false.
* expansion, false not to, defaults to false. Some well-formed
* and safe-mode checks may be omitted.
* [format] the format to use to output a string:
* 'application/n-quads' for N-Quads.
* [produceGeneralizedRdf] true to output generalized RDF, false
* to produce only standard RDF (default: false).
* [documentLoader(url, options)] the document loader.
* [safe] true to use safe mode. (default: false)
* [rdfDirection] 'i18n-datatype' or 'compound-literal' to support RDF
* transformation of @direction (default: null).
* [contextResolver] internal use only.
*
* @return a Promise that resolves to the RDF dataset.
Expand Down
122 changes: 115 additions & 7 deletions lib/toRdf.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

const {createNodeMap} = require('./nodeMap');
const {isKeyword} = require('./context');
const graphTypes = require('./graphTypes');
const jsonCanonicalize = require('canonicalize');
const JsonLdError = require('./JsonLdError');
const types = require('./types');
const util = require('./util');

Expand All @@ -15,7 +16,7 @@ const {
} = require('./events');

const {
// RDF,
RDF,
// RDF_LIST,
RDF_FIRST,
RDF_REST,
Expand Down Expand Up @@ -312,18 +313,125 @@ function _objectToRDF(
} else if(types.isNumber(value)) {
object.value = value.toFixed(0);
object.datatype.value = datatype || XSD_INTEGER;
} else if(rdfDirection === 'i18n-datatype' &&
'@direction' in item) {
const datatype = 'https://www.w3.org/ns/i18n#' +
(item['@language'] || '') +
`_${item['@direction']}`;
} else if('@direction' in item && rdfDirection === 'i18n-datatype') {
const language = (item['@language'] || '').toLowerCase();
const direction = item['@direction'];
const datatype = `https://www.w3.org/ns/i18n#${language}_${direction}`;
object.datatype.value = datatype;
object.value = value;
} else if('@direction' in item && rdfDirection === 'compound-literal') {
const language = (item['@language'] || '').toLowerCase();
const direction = item['@direction'];
// blank node
object.termType = 'BlankNode';
object.value = issuer.getId();
object.datatype = undefined;
// value
dataset.push({
subject: {
termType: object.termType,
value: object.value
},
predicate: {
termType: 'NamedNode',
value: RDF + 'value'
},
object: {
termType: 'Literal',
value,
datatype: {
termType: 'NamedNode',
value: XSD_STRING
}
},
graph: graphTerm
});
// language if preset
if(language !== '') {
dataset.push({
subject: {
termType: object.termType,
value: object.value
},
predicate: {
termType: 'NamedNode',
value: RDF + 'language'
},
object: {
termType: 'Literal',
value: language,
datatype: {
termType: 'NamedNode',
value: XSD_STRING
}
},
graph: graphTerm
});
}
// direction
dataset.push({
subject: {
termType: object.termType,
value: object.value
},
predicate: {
termType: 'NamedNode',
value: RDF + 'direction'
},
object: {
termType: 'Literal',
value: direction,
datatype: {
termType: 'NamedNode',
value: XSD_STRING
}
},
graph: graphTerm
});
} else if('@direction' in item && rdfDirection) {
throw new JsonLdError(
'Unknown rdfDirection value.',
'jsonld.InvalidRdfDirection',
{value: rdfDirection});
} else if('@language' in item) {
if('@direction' in item && rdfDirection === null) {
if(options.eventHandler) {
// FIXME: only emit once?
_handleEvent({
event: {
type: ['JsonLdEvent'],
code: 'rdfDirection not set',
level: 'warning',
message: 'rdfDirection not set for @direction.',
details: {
object: object.value
}
},
options
});
}
}
object.value = value;
object.datatype.value = datatype || RDF_LANGSTRING;
object.language = item['@language'];
} else {
if('@direction' in item && rdfDirection === null) {
if(options.eventHandler) {
// FIXME: only emit once?
_handleEvent({
event: {
type: ['JsonLdEvent'],
code: 'rdfDirection not set',
level: 'warning',
message: 'rdfDirection not set for @direction.',
details: {
object: object.value
}
},
options
});
}
}
object.value = value;
object.datatype.value = datatype || XSD_STRING;
}
Expand Down
Loading

0 comments on commit 24046f7

Please sign in to comment.