From eca598d19e8285db521776228c75022153b8ceea Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 7 Jan 2020 16:24:06 -0800 Subject: [PATCH 1/4] Expand and Compact using base direction. --- CHANGELOG.md | 1 + lib/compact.js | 40 ++++++++++++++++++-- lib/context.js | 90 ++++++++++++++++++++++++++++++++++++++------ lib/expand.js | 58 +++++++++++++++++++++++++--- tests/test-common.js | 18 --------- 5 files changed, 169 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7fdcb6..5281ea65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Support for `"@import"`. - Added support for `@included` blocks - Skip things that have the form of a keyword, with warning. +- Support for expansion and compaction of values container `"@direction"`. ## 2.0.2 - 2020-01-17 diff --git a/lib/compact.js b/lib/compact.js index 82be80eb..5450648a 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -778,8 +778,10 @@ api.compactIri = ({ let itemLanguage = '@none'; let itemType = '@none'; if(_isValue(item)) { - if('@language' in item) { - itemLanguage = item['@language']; + if('@direction' in item) { + itemLanguage = `${item['@language']||''}_${item['@direction']}`.toLowerCase(); + } else if('@language' in item) { + itemLanguage = item['@language'].toLowerCase(); } else if('@type' in item) { itemType = item['@type']; } else { @@ -819,6 +821,11 @@ api.compactIri = ({ if('@language' in value && !('@index' in value)) { containers.push('@language', '@language@set'); typeOrLanguageValue = value['@language']; + if(value['@direction']) { + typeOrLanguageValue = `${typeOrLanguageValue}_${value['@direction']}` + } + } else if('@direction' in value && !('@index' in value)) { + typeOrLanguageValue = `_${value['@direction']}` } else if('@type' in value) { typeOrLanguage = '@type'; typeOrLanguageValue = value['@type']; @@ -947,6 +954,7 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => { // get context rules const type = _getContextValue(activeCtx, activeProperty, '@type'); const language = _getContextValue(activeCtx, activeProperty, '@language'); + const direction = _getContextValue(activeCtx, activeProperty, '@direction'); const container = _getContextValue(activeCtx, activeProperty, '@container') || []; @@ -956,7 +964,17 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => { // if there's no @index to preserve ... if(!preserveIndex && type !== '@none') { // matching @type or @language specified in context, compact value - if(value['@type'] === type || value['@language'] === language) { + if(value['@type'] === type) { + return value['@value']; + } + if('@language' in value && value['@language'] === language && + '@direction' in value && value['@direction'] == direction) { + return value['@value']; + } + if('@language' in value && value['@language'] === language) { + return value['@value']; + } + if('@direction' in value && value['@direction'] === direction) { return value['@value']; } } @@ -1006,6 +1024,15 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => { })] = value['@language']; } + if('@direction' in value) { + // alias @direction + rval[api.compactIri({ + activeCtx, + iri: '@direction', + relativeTo: {vocab: true} + })] = value['@direction']; + } + // alias @value rval[api.compactIri({ activeCtx, @@ -1168,6 +1195,13 @@ function _selectTerm( } } else { prefs.push(typeOrLanguageValue); + + // consider direction only + const langDir = prefs.find(el => el.includes('_')); + if(langDir) { + // consider _dir portion + prefs.push(langDir.replace(/^[^_]+_/, '_')); + } } prefs.push('@none'); diff --git a/lib/context.js b/lib/context.js index fc798950..fa42d034 100644 --- a/lib/context.js +++ b/lib/context.js @@ -247,6 +247,30 @@ api.process = async ({ defined.set('@language', true); } + // handle @direction + if('@direction' in ctx) { + const value = ctx['@direction']; + if(activeCtx.processingMode === 'json-ld-1.0') { + throw new JsonLdError( + 'Invalid JSON-LD syntax; @direction not compatible with ' + + activeCtx.processingMode, + 'jsonld.SyntaxError', + {code: 'invalid context member', context: ctx}); + } + if(value === null) { + delete rval['@direction']; + } else if(value !== 'ltr' && value !== 'rtl') { + throw new JsonLdError( + 'Invalid JSON-LD syntax; the value of "@direction" in a ' + + '@context must be null, "ltr", or "rtl".', + 'jsonld.SyntaxError', + {code: 'invalid base direction', context: ctx}); + } else { + rval['@direction'] = value; + } + defined.set('@direction', true); + } + // handle @propagate // note: we've already extracted it, here we just do error checking if('@propagate' in ctx) { @@ -443,7 +467,7 @@ api.createTermDefinition = ({ // JSON-LD 1.1 support if(api.processingMode(activeCtx, 1.1)) { - validKeys.push('@context', '@index', '@nest', '@prefix', '@protected'); + validKeys.push('@context', '@direction', '@index', '@nest', '@prefix', '@protected'); } for(const kw in value) { @@ -795,6 +819,18 @@ api.createTermDefinition = ({ } } + if('@direction' in value) { + const direction = value['@direction']; + if(direction !== null && direction !== 'ltr' && direction !== 'rtl') { + throw new JsonLdError( + 'Invalid JSON-LD syntax; @direction value must be ' + + 'null, "ltr", or "rtl".', + 'jsonld.SyntaxError', + {code: 'invalid base direction', context: localCtx}); + } + mapping['@direction'] = direction; + } + if('@nest' in value) { const nest = value['@nest']; if(!_isString(nest) || (nest !== '@nest' && nest.indexOf('@') === 0)) { @@ -1012,7 +1048,10 @@ api.getInitialContext = options => { const irisToTerms = {}; // handle default language - const defaultLanguage = activeCtx['@language'] || '@none'; + const defaultLanguage = (activeCtx['@language'] || '@none').toLowerCase(); + + // handle default direction + const defaultDirection = activeCtx['@direction']; // create term selections for each mapping in the context, ordered by // shortest and then lexicographically least @@ -1076,19 +1115,42 @@ api.getInitialContext = options => { } else if('@type' in mapping) { // term is preferred for values using specific type _addPreferredTerm(term, entry['@type'], mapping['@type']); + } else if('@language' in mapping && '@direction' in mapping) { + // term is preferred for values using specific language and direction + const language = mapping['@language']; + const direction = mapping['@direction']; + if(langugage && direction) { + _addPreferredTerm(term, entry['@language'], `${language}_${direction}`.toLowerCase()); + } else if(language) { + _addPreferredTerm(term, entry['@language'], language.toLowerCase()); + } else if(direction) { + _addPreferredTerm(term, entry['@language'], `_${direction}`); + } else { + _addPreferredTerm(term, entry['@language'], "@null"); + } } else if('@language' in mapping) { - // term is preferred for values using specific language - const language = mapping['@language'] || '@null'; - _addPreferredTerm(term, entry['@language'], language); + _addPreferredTerm(term, entry['@language'], (mapping['@language'] || '@null').toLowerCase()); + } else if('@direction' in mapping) { + if(mapping['@direction']) { + _addPreferredTerm(term, entry['@language'], `_${mapping['@direction']}`); + } else { + _addPreferredTerm(term, entry['@language'], '@none'); + } + //} else if(defaultLanguage && defaultDirection) { + // _addPreferredTerm(term, entry['@language'], `${defaultLanguage}_${defaultDirection}`); + // _addPreferredTerm(term, entry['@type'], '@none'); + //} else if(defaultLanguage) { + // _addPreferredTerm(term, entry['@language'], defaultLanguage); + // _addPreferredTerm(term, entry['@type'], '@none'); + } else if(defaultDirection) { + _addPreferredTerm(term, entry['@language'], `_${defaultDirection}`); + _addPreferredTerm(term, entry['@language'], '@none'); + _addPreferredTerm(term, entry['@type'], '@none'); } else { - // term is preferred for values w/default language or no type and - // no language - // add an entry for the default language - _addPreferredTerm(term, entry['@language'], defaultLanguage); - // add entries for no type and no language - _addPreferredTerm(term, entry['@type'], '@none'); + _addPreferredTerm(term, entry['@language'], defaultLanguage); _addPreferredTerm(term, entry['@language'], '@none'); + _addPreferredTerm(term, entry['@type'], '@none'); } } } @@ -1227,6 +1289,11 @@ api.getContextValue = (ctx, key, type) => { return ctx[type]; } + // get default direction + if(type === '@direction' && ctx.hasOwnProperty(type)) { + return ctx[type]; + } + if(type === '@context') { return undefined; } @@ -1266,6 +1333,7 @@ api.isKeyword = v => { case '@container': case '@context': case '@default': + case '@direction': case '@embed': case '@explicit': case '@graph': diff --git a/lib/expand.js b/lib/expand.js index 55f49460..ee3876fa 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -262,10 +262,10 @@ api.expand = async ({ if('@value' in rval) { // @value must only have @language or @type - if('@type' in rval && '@language' in rval) { + if('@type' in rval && ('@language' in rval || '@direction' in rval)) { throw new JsonLdError( 'Invalid JSON-LD syntax; an element containing "@value" may not ' + - 'contain both "@type" and "@language".', + 'contain both "@type" and either "@language" or "@direction".', 'jsonld.SyntaxError', {code: 'invalid value object', element: rval}); } let validCount = count - 1; @@ -278,11 +278,14 @@ api.expand = async ({ if('@language' in rval) { validCount -= 1; } + if('@direction' in rval) { + validCount -= 1; + } if(validCount !== 0) { throw new JsonLdError( 'Invalid JSON-LD syntax; an element containing "@value" may only ' + - 'have an "@index" property and at most one other property ' + - 'which can be "@type" or "@language".', + 'have an "@index" property and either "@type" ' + + 'or either or both "@language" or "@direction".', 'jsonld.SyntaxError', {code: 'invalid value object', element: rval}); } const values = rval['@value'] === null ? [] : _asArray(rval['@value']); @@ -571,6 +574,7 @@ async function _expandObject({ } // @language must be a string + // it should match BCP47 if(expandedProperty === '@language') { if(value === null) { // drop null @language values, they expand as if they didn't exist @@ -585,11 +589,44 @@ async function _expandObject({ // ensure language value is lowercase value = _asArray(value).map(v => _isString(v) ? v.toLowerCase() : v); + // ensure language tag matches BCP47 + for(const lang of value) { + if(_isString(lang) && !lang.match(/^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/)) { + console.warn(`@language must be valid BCP47: ${lang}`); + } + } + _addValue( expandedParent, '@language', value, {propertyIsArray: options.isFrame}); continue; } + // @direction must be "ltr" or "rtl" + if(expandedProperty === '@direction') { + if(!_isString(value) && !options.isFrame) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; "@direction" value must be a string.', + 'jsonld.SyntaxError', + {code: 'invalid base direction', value}); + } + + value = _asArray(value); + + // ensure direction is "ltr" or "rtl" + for(const dir of value) { + if(_isString(dir) && dir !== 'ltr' && dir !== 'rtl') { + throw new JsonLdError( + 'Invalid JSON-LD syntax; "@direction" must be "ltr" or "rtl".', + 'jsonld.SyntaxError', + {code: 'invalid base direction', value}); + } + } + + _addValue( + expandedParent, '@direction', value, {propertyIsArray: options.isFrame}); + continue; + } + // @index must be a string if(expandedProperty === '@index') { if(!_isString(value)) { @@ -676,8 +713,9 @@ async function _expandObject({ const container = _getContextValue(termCtx, key, '@container') || []; if(container.includes('@language') && _isObject(value)) { + const direction = _getContextValue(termCtx, key, '@direction') // handle language map container (skip if value is not an object) - expandedValue = _expandLanguageMap(termCtx, value, options); + expandedValue = _expandLanguageMap(termCtx, value, direction, options); } else if(container.includes('@index') && _isObject(value)) { // handle index container (skip if value is not an object) const asGraph = container.includes('@graph'); @@ -915,6 +953,10 @@ function _expandValue({activeCtx, activeProperty, value, options}) { if(language !== null) { rval['@language'] = language; } + const direction = _getContextValue(activeCtx, activeProperty, '@direction'); + if(direction !== null) { + rval['@direction'] = direction; + } } // do conversion of values that aren't basic JSON types to strings if(!['boolean', 'number', 'string'].includes(typeof value)) { @@ -930,11 +972,12 @@ function _expandValue({activeCtx, activeProperty, value, options}) { * * @param activeCtx the active context to use. * @param languageMap the language map to expand. + * @param direction the direction to apply to values. * @param {Object} [options] - processing options. * * @return the expanded language map. */ -function _expandLanguageMap(activeCtx, languageMap, options) { +function _expandLanguageMap(activeCtx, languageMap, direction, options) { const rval = []; const keys = Object.keys(languageMap).sort(); for(const key of keys) { @@ -958,6 +1001,9 @@ function _expandLanguageMap(activeCtx, languageMap, options) { if(expandedKey !== '@none') { val['@language'] = key.toLowerCase(); } + if(direction) { + val['@direction'] = direction; + } rval.push(val); } } diff --git a/tests/test-common.js b/tests/test-common.js index f03ddf9b..187b741e 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -33,14 +33,6 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME idRegex: [ - // direction - /compact-manifest.jsonld#tdi01$/, - /compact-manifest.jsonld#tdi02$/, - /compact-manifest.jsonld#tdi03$/, - /compact-manifest.jsonld#tdi04$/, - /compact-manifest.jsonld#tdi05$/, - /compact-manifest.jsonld#tdi06$/, - /compact-manifest.jsonld#tdi07$/, // html /html-manifest.jsonld#tc001$/, /html-manifest.jsonld#tc002$/, @@ -93,16 +85,6 @@ const TEST_TYPES = { /expand-manifest.jsonld#thc05$/, // remote /remote-doc-manifest.jsonld#t0013$/, // HTML - // direction - /expand-manifest.jsonld#tdi01$/, - /expand-manifest.jsonld#tdi02$/, - /expand-manifest.jsonld#tdi03$/, - /expand-manifest.jsonld#tdi04$/, - /expand-manifest.jsonld#tdi05$/, - /expand-manifest.jsonld#tdi06$/, - /expand-manifest.jsonld#tdi07$/, - /expand-manifest.jsonld#tdi08$/, - /expand-manifest.jsonld#tdi09$/, // unused scoped context /expand-manifest.jsonld#tc032$/, /expand-manifest.jsonld#tc033$/, From 273a526e58a6fc7b0b9203862fa24d147d721344 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 8 Jan 2020 10:46:34 -0800 Subject: [PATCH 2/4] Fix style errors. --- lib/compact.js | 13 ++++++++----- lib/context.js | 24 +++++++++++------------- lib/expand.js | 10 ++++++---- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/compact.js b/lib/compact.js index 5450648a..e0609a56 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -779,7 +779,9 @@ api.compactIri = ({ let itemType = '@none'; if(_isValue(item)) { if('@direction' in item) { - itemLanguage = `${item['@language']||''}_${item['@direction']}`.toLowerCase(); + const lang = (item['@language'] || '').toLowerCase(); + const dir = item['@direction']; + itemLanguage = `${lang}_${dir}`; } else if('@language' in item) { itemLanguage = item['@language'].toLowerCase(); } else if('@type' in item) { @@ -821,11 +823,12 @@ api.compactIri = ({ if('@language' in value && !('@index' in value)) { containers.push('@language', '@language@set'); typeOrLanguageValue = value['@language']; - if(value['@direction']) { - typeOrLanguageValue = `${typeOrLanguageValue}_${value['@direction']}` + const dir = value['@direction']; + if(dir) { + typeOrLanguageValue = `${typeOrLanguageValue}_${dir}`; } } else if('@direction' in value && !('@index' in value)) { - typeOrLanguageValue = `_${value['@direction']}` + typeOrLanguageValue = `_${value['@direction']}`; } else if('@type' in value) { typeOrLanguage = '@type'; typeOrLanguageValue = value['@type']; @@ -968,7 +971,7 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => { return value['@value']; } if('@language' in value && value['@language'] === language && - '@direction' in value && value['@direction'] == direction) { + '@direction' in value && value['@direction'] === direction) { return value['@value']; } if('@language' in value && value['@language'] === language) { diff --git a/lib/context.js b/lib/context.js index fa42d034..30077d9a 100644 --- a/lib/context.js +++ b/lib/context.js @@ -467,7 +467,8 @@ api.createTermDefinition = ({ // JSON-LD 1.1 support if(api.processingMode(activeCtx, 1.1)) { - validKeys.push('@context', '@direction', '@index', '@nest', '@prefix', '@protected'); + validKeys.push( + '@context', '@direction', '@index', '@nest', '@prefix', '@protected'); } for(const kw in value) { @@ -604,7 +605,7 @@ api.createTermDefinition = ({ // term is an absolute IRI mapping['@id'] = term; } - } else if(term == '@type') { + } else if(term === '@type') { // Special case, were we've previously determined that container is @set mapping['@id'] = term; } else { @@ -1119,29 +1120,26 @@ api.getInitialContext = options => { // term is preferred for values using specific language and direction const language = mapping['@language']; const direction = mapping['@direction']; - if(langugage && direction) { - _addPreferredTerm(term, entry['@language'], `${language}_${direction}`.toLowerCase()); + if(language && direction) { + _addPreferredTerm(term, entry['@language'], + `${language}_${direction}`.toLowerCase()); } else if(language) { _addPreferredTerm(term, entry['@language'], language.toLowerCase()); } else if(direction) { _addPreferredTerm(term, entry['@language'], `_${direction}`); } else { - _addPreferredTerm(term, entry['@language'], "@null"); + _addPreferredTerm(term, entry['@language'], '@null'); } } else if('@language' in mapping) { - _addPreferredTerm(term, entry['@language'], (mapping['@language'] || '@null').toLowerCase()); + _addPreferredTerm(term, entry['@language'], + (mapping['@language'] || '@null').toLowerCase()); } else if('@direction' in mapping) { if(mapping['@direction']) { - _addPreferredTerm(term, entry['@language'], `_${mapping['@direction']}`); + _addPreferredTerm(term, entry['@language'], + `_${mapping['@direction']}`); } else { _addPreferredTerm(term, entry['@language'], '@none'); } - //} else if(defaultLanguage && defaultDirection) { - // _addPreferredTerm(term, entry['@language'], `${defaultLanguage}_${defaultDirection}`); - // _addPreferredTerm(term, entry['@type'], '@none'); - //} else if(defaultLanguage) { - // _addPreferredTerm(term, entry['@language'], defaultLanguage); - // _addPreferredTerm(term, entry['@type'], '@none'); } else if(defaultDirection) { _addPreferredTerm(term, entry['@language'], `_${defaultDirection}`); _addPreferredTerm(term, entry['@language'], '@none'); diff --git a/lib/expand.js b/lib/expand.js index ee3876fa..0e64bfbd 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -41,6 +41,7 @@ const { const api = {}; module.exports = api; +const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; /** * Recursively expands an element using the given context. Any context in @@ -423,7 +424,7 @@ async function _expandObject({ const isJsonType = element[typeKey] && _expandIri(activeCtx, (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), - {vocab: true}, options) == '@json'; + {vocab: true}, options) === '@json'; for(const key of keys) { let value = element[key]; @@ -591,7 +592,7 @@ async function _expandObject({ // ensure language tag matches BCP47 for(const lang of value) { - if(_isString(lang) && !lang.match(/^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/)) { + if(_isString(lang) && !lang.match(REGEX_BCP47)) { console.warn(`@language must be valid BCP47: ${lang}`); } } @@ -623,7 +624,8 @@ async function _expandObject({ } _addValue( - expandedParent, '@direction', value, {propertyIsArray: options.isFrame}); + expandedParent, '@direction', value, + {propertyIsArray: options.isFrame}); continue; } @@ -713,7 +715,7 @@ async function _expandObject({ const container = _getContextValue(termCtx, key, '@container') || []; if(container.includes('@language') && _isObject(value)) { - const direction = _getContextValue(termCtx, key, '@direction') + const direction = _getContextValue(termCtx, key, '@direction'); // handle language map container (skip if value is not an object) expandedValue = _expandLanguageMap(termCtx, value, direction, options); } else if(container.includes('@index') && _isObject(value)) { From 692235001b752bd6dffedd985afbd174e4d58564 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 8 Jan 2020 12:19:11 -0800 Subject: [PATCH 3/4] Transform base direction from/toRdf. Uses `i18n-datatype` value for `rdfDirection` option only. --- CHANGELOG.md | 2 ++ lib/fromRdf.js | 25 ++++++++++++++++++++++--- lib/toRdf.js | 22 ++++++++++++++++------ tests/test-common.js | 16 ++-------------- 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5281ea65..8802f507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - Added support for `@included` blocks - Skip things that have the form of a keyword, with warning. - Support for expansion and compaction of values container `"@direction"`. +- Support for RDF transformation of `@direction` + when `rdfDirection` is 'i18n-datatype'. ## 2.0.2 - 2020-01-17 diff --git a/lib/fromRdf.js b/lib/fromRdf.js index 02217763..84986bae 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -29,6 +29,8 @@ const { XSD_STRING, } = require('./constants'); +const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; + const api = {}; module.exports = api; @@ -41,7 +43,13 @@ module.exports = api; * @return a Promise that resolves to the JSON-LD output. */ api.fromRDF = async ( - dataset, {useRdfType = false, useNativeTypes = false}) => { + dataset, + { + useRdfType = false, + useNativeTypes = false, + rdfDirection = null + } +) => { const defaultGraph = {}; const graphMap = {'@default': defaultGraph}; const referencedOnce = {}; @@ -79,7 +87,7 @@ api.fromRDF = async ( continue; } - const value = _RDFToObject(o, useNativeTypes); + const value = _RDFToObject(o, useNativeTypes, rdfDirection); util.addValue(node, p, value, {propertyIsArray: true}); // object may be an RDF list/partial list node but we can't know easily @@ -270,7 +278,7 @@ api.fromRDF = async ( * * @return the JSON-LD object. */ -function _RDFToObject(o, useNativeTypes) { +function _RDFToObject(o, useNativeTypes, rdfDirection) { // convert NamedNode/BlankNode object to JSON-LD if(o.termType.endsWith('Node')) { return {'@id': o.value}; @@ -320,6 +328,17 @@ function _RDFToObject(o, useNativeTypes) { if(![XSD_BOOLEAN, XSD_INTEGER, XSD_DOUBLE, XSD_STRING].includes(type)) { rval['@type'] = type; } + } else if(rdfDirection === 'i18n-datatype' && + type.startsWith('https://www.w3.org/ns/i18n#')) + { + const [, language, direction] = type.split(/[#_]/); + if(language.length > 0) { + rval['@language'] = language; + if(!language.match(REGEX_BCP47)) { + console.warn(`@language must be valid BCP47: ${language}`); + } + } + rval['@direction'] = direction; } else if(type !== XSD_STRING) { rval['@type'] = type; } diff --git a/lib/toRdf.js b/lib/toRdf.js index e6a3281f..61a056df 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -128,7 +128,8 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { } // convert list, value or node object to triple - const object = _objectToRDF(item, issuer, dataset, graphTerm); + const object = + _objectToRDF(item, issuer, dataset, graphTerm, options.rdfDirection); // skip null objects (they are relative IRIs) if(object) { dataset.push({ @@ -154,7 +155,7 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { * * @return the head of the list. */ -function _listToRDF(list, issuer, dataset, graphTerm) { +function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection) { const first = {termType: 'NamedNode', value: RDF_FIRST}; const rest = {termType: 'NamedNode', value: RDF_REST}; const nil = {termType: 'NamedNode', value: RDF_NIL}; @@ -165,7 +166,7 @@ function _listToRDF(list, issuer, dataset, graphTerm) { let subject = result; for(const item of list) { - const object = _objectToRDF(item, issuer, dataset, graphTerm); + const object = _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection); const next = {termType: 'BlankNode', value: issuer.getId()}; dataset.push({ subject, @@ -184,7 +185,7 @@ function _listToRDF(list, issuer, dataset, graphTerm) { // Tail of list if(last) { - const object = _objectToRDF(last, issuer, dataset, graphTerm); + const object = _objectToRDF(last, issuer, dataset, graphTerm, rdfDirection); dataset.push({ subject, predicate: first, @@ -213,7 +214,7 @@ function _listToRDF(list, issuer, dataset, graphTerm) { * * @return the RDF literal or RDF resource. */ -function _objectToRDF(item, issuer, dataset, graphTerm) { +function _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection) { const object = {}; // convert value object to RDF @@ -243,6 +244,14 @@ function _objectToRDF(item, issuer, dataset, graphTerm) { } 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']}`; + object.datatype.value = datatype; + object.value = value; } else if('@language' in item) { object.value = value; object.datatype.value = datatype || RDF_LANGSTRING; @@ -252,7 +261,8 @@ function _objectToRDF(item, issuer, dataset, graphTerm) { object.datatype.value = datatype || XSD_STRING; } } else if(graphTypes.isList(item)) { - const _list = _listToRDF(item['@list'], issuer, dataset, graphTerm); + const _list = + _listToRDF(item['@list'], issuer, dataset, graphTerm, rdfDirection); object.termType = _list.termType; object.value = _list.value; } else { diff --git a/tests/test-common.js b/tests/test-common.js index 187b741e..588830eb 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -212,9 +212,7 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME idRegex: [ - // direction - /fromRdf-manifest.jsonld#tdi05$/, - /fromRdf-manifest.jsonld#tdi06$/, + // direction (compound-literal) /fromRdf-manifest.jsonld#tdi11$/, /fromRdf-manifest.jsonld#tdi12$/, ] @@ -268,17 +266,7 @@ const TEST_TYPES = { /toRdf-manifest.jsonld#te075$/, /toRdf-manifest.jsonld#te111$/, /toRdf-manifest.jsonld#te112$/, - // direction - /toRdf-manifest.jsonld#tdi01$/, - /toRdf-manifest.jsonld#tdi02$/, - /toRdf-manifest.jsonld#tdi03$/, - /toRdf-manifest.jsonld#tdi04$/, - /toRdf-manifest.jsonld#tdi05$/, - /toRdf-manifest.jsonld#tdi06$/, - /toRdf-manifest.jsonld#tdi07$/, - /toRdf-manifest.jsonld#tdi08$/, - /toRdf-manifest.jsonld#tdi09$/, - /toRdf-manifest.jsonld#tdi10$/, + // direction (compound-literal) /toRdf-manifest.jsonld#tdi11$/, /toRdf-manifest.jsonld#tdi12$/, // unused scoped context From a718e7f86050418903b645e0ace39b4f58538bb9 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 28 Jan 2020 19:45:43 -0500 Subject: [PATCH 4/4] Changelog style. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8802f507..769d2466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,8 @@ - Added support for `@included` blocks - Skip things that have the form of a keyword, with warning. - Support for expansion and compaction of values container `"@direction"`. -- Support for RDF transformation of `@direction` - when `rdfDirection` is 'i18n-datatype'. +- Support for RDF transformation of `@direction` when `rdfDirection` is + 'i18n-datatype'. ## 2.0.2 - 2020-01-17