From c5af42025fc179715182bbfdab692c407348ca62 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Sep 2023 17:48:43 -0400 Subject: [PATCH] Support `compound-literal` `rdfDirection` option. --- CHANGELOG.md | 5 ++ lib/fromRdf.js | 83 ++++++++++++++++++++++++++-- lib/jsonld.js | 9 ++- lib/toRdf.js | 74 +++++++++++++++++++++++-- tests/misc.js | 147 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.js | 11 +--- 6 files changed, 305 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2046c8e..409e547b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # jsonld ChangeLog +## 8.x.x - 2023-xx-xx + +### Added +- Support `compound-literal` `rdfDirection` option. + ## 8.3.0 - 2023-09-06 ### Added diff --git a/lib/fromRdf.js b/lib/fromRdf.js index 01098353..518b4e8c 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -18,7 +18,7 @@ const { // constants const { - // RDF, + RDF, RDF_LIST, RDF_FIRST, RDF_REST, @@ -61,12 +61,10 @@ api.fromRDF = async ( const defaultGraph = {}; const graphMap = {'@default': defaultGraph}; const referencedOnce = {}; + let processCompoundLiterals = false; if(rdfDirection) { if(rdfDirection === 'compound-literal') { - throw new JsonLdError( - 'Unsupported rdfDirection value.', - 'jsonld.InvalidRdfDirection', - {value: rdfDirection}); + processCompoundLiterals = true; } else if(rdfDirection !== 'i18n-datatype') { throw new JsonLdError( 'Unknown rdfDirection value.', @@ -74,6 +72,10 @@ api.fromRDF = async ( {value: rdfDirection}); } } + let compoundLiteralSubjects; + if(processCompoundLiterals) { + compoundLiteralSubjects = {}; + } for(const quad of dataset) { // TODO: change 'name' to 'graph' @@ -82,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; @@ -97,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)) { @@ -208,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; @@ -296,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. diff --git a/lib/jsonld.js b/lib/jsonld.js index c6931aeb..cd5deb73 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -547,7 +547,8 @@ jsonld.link = async function(input, ctx, options) { * 'application/n-quads' for N-Quads. * [documentLoader(url, options)] the document loader. * [useNative] true to use a native canonize algorithm - * [rdfDirection] null or 'i18n-datatype' to support RDF + * [rdfDirection] Mode for RDF transformation of @direction. null, + * 'i18n-datatype', or 'compound-literal' (default: null). * transformation of @direction (default: null). * [safe] true to use safe mode. (default: true). * [contextResolver] internal use only. @@ -605,7 +606,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] null or 'i18n-datatype' to support RDF + * [rdfDirection] Mode for RDF transformation of @direction. null, + * 'i18n-datatype', or 'compound-literal' (default: null). * transformation of @direction (default: null). * [safe] true to use safe mode. (default: false) * @@ -659,7 +661,8 @@ jsonld.fromRDF = async function(dataset, options) { * to produce only standard RDF (default: false). * [documentLoader(url, options)] the document loader. * [safe] true to use safe mode. (default: false) - * [rdfDirection] null or 'i18n-datatype' to support RDF + * [rdfDirection] Mode for RDF transformation of @direction. null, + * 'i18n-datatype', or 'compound-literal' (default: null). * transformation of @direction (default: null). * [contextResolver] internal use only. * diff --git a/lib/toRdf.js b/lib/toRdf.js index f576f6d8..7c960f76 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -16,7 +16,7 @@ const { } = require('./events'); const { - // RDF, + RDF, // RDF_LIST, RDF_FIRST, RDF_REST, @@ -320,10 +320,74 @@ function _objectToRDF( object.datatype.value = datatype; object.value = value; } else if('@direction' in item && rdfDirection === 'compound-literal') { - throw new JsonLdError( - 'Unsupported rdfDirection value.', - 'jsonld.InvalidRdfDirection', - {value: rdfDirection}); + 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.', diff --git a/tests/misc.js b/tests/misc.js index a552d7f9..a0c1a559 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -3695,6 +3695,17 @@ _:b0 "[null]"^^ . `; const _nq_dir_l_d_i18n = `\ "v"^^ . +`; + const _nq_dir_nl_d_cl = `\ + _:b0 . +_:b0 "ltr" . +_:b0 "v" . +`; + const _nq_dir_l_d_cl = `\ + _:b0 . +_:b0 "ltr" . +_:b0 "en-us" . +_:b0 "v" . `; describe('fromRDF', () => { @@ -3811,6 +3822,20 @@ _:b0 "[null]"^^ . }); }); + it('should handle no @lang, no @dir, rdfDirection=c-l', async () => { + const input = _nq_dir_nl_nd; + const expected = _json_dir_nl_nd; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle no @lang, @dir, rdfDirection=i18n', async () => { const input = _nq_dir_nl_d_i18n; const expected = _json_dir_nl_d; @@ -3825,6 +3850,20 @@ _:b0 "[null]"^^ . }); }); + it('should handle no @lang, @dir, rdfDirection=c-l', async () => { + const input = _nq_dir_nl_d_cl; + const expected = _json_dir_nl_d; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle @lang, no @dir, rdfDirection=i18n', async () => { const input = _nq_dir_l_nd_ls; const expected = _json_dir_l_nd; @@ -3839,6 +3878,20 @@ _:b0 "[null]"^^ . }); }); + it('should handle @lang, no @dir, rdfDirection=c-l', async () => { + const input = _nq_dir_l_nd_ls; + const expected = _json_dir_l_nd; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle @lang, @dir, rdfDirection=i18n', async () => { const input = _nq_dir_l_d_i18n; const expected = _json_dir_l_d; @@ -3853,6 +3906,20 @@ _:b0 "[null]"^^ . }); }); + it('should handle @lang, @dir, rdfDirection=c-l', async () => { + const input = _nq_dir_l_d_cl; + const expected = _json_dir_l_d; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle bad rdfDirection', async () => { const input = _nq_dir_l_d_i18n; @@ -4101,6 +4168,20 @@ _:b0 "v" . }); }); + it('should handle no @lang, no @dir, rdfDirection=c-l', async () => { + const input = _json_dir_nl_nd; + const nq = _nq_dir_nl_nd; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle no @lang, @dir, rdfDirection=null', async () => { const input = _json_dir_nl_d; const nq = _nq_dir_nl_nd; @@ -4131,6 +4212,20 @@ _:b0 "v" . }); }); + it('should handle no @lang, @dir, rdfDirection=c-l', async () => { + const input = _json_dir_nl_d; + const nq = _nq_dir_nl_d_cl; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle @lang, no @dir, rdfDirection=null', async () => { const input = _json_dir_l_nd; const nq = _nq_dir_l_nd_ls; @@ -4159,6 +4254,20 @@ _:b0 "v" . }); }); + it('should handle @lang, no @dir, rdfDirection=c-l', async () => { + const input = _json_dir_l_nd; + const nq = _nq_dir_l_nd_ls; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle @lang, @dir, rdfDirection=null', async () => { const input = _json_dir_l_d; const nq = _nq_dir_l_nd_ls; @@ -4189,6 +4298,20 @@ _:b0 "v" . }); }); + it('should handle @lang, @dir, rdfDirection=c-l', async () => { + const input = _json_dir_l_d; + const nq = _nq_dir_l_d_cl; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'compound-literal'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + it('should handle bad rdfDirection', async () => { const input = _json_dir_l_d; @@ -4258,6 +4381,30 @@ _:b0 "RTL"^^ . testSafe: true }); }); + + it('should handle ctx @lang/@dir/rdfDirection=c-l', async () => { + const input = _ctx_dir_input; + const nq = `\ +_:b0 "NULL"@ar-eg . +_:b0 _:b1 . +_:b0 _:b2 . +_:b1 "rtl" . +_:b1 "ar-eg" . +_:b1 "RTL" . +_:b2 "ltr" . +_:b2 "en" . +_:b2 "LTR" . +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: false, rdfDirection: 'compound-literal'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); }); describe('various', () => { diff --git a/tests/test.js b/tests/test.js index a0af0eba..c463bb64 100644 --- a/tests/test.js +++ b/tests/test.js @@ -264,13 +264,7 @@ const TEST_TYPES = { // NOTE: idRegex format: // /MMM-manifest#tNNN$/, // FIXME - idRegex: [ - // direction (compound-literal) - /fromRdf-manifest#tdi09$/, - /fromRdf-manifest#tdi10$/, - /fromRdf-manifest#tdi11$/, - /fromRdf-manifest#tdi12$/, - ] + idRegex: [] }, fn: 'fromRDF', params: [ @@ -337,9 +331,6 @@ const TEST_TYPES = { /toRdf-manifest#te075$/, /toRdf-manifest#te111$/, /toRdf-manifest#te112$/, - // direction (compound-literal) - /toRdf-manifest#tdi11$/, - /toRdf-manifest#tdi12$/, ] }, fn: 'toRDF',