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
Expand Up @@ -25,6 +25,9 @@
- 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"`.
- Support for RDF transformation of `@direction` when `rdfDirection` is
'i18n-datatype'.

## 2.0.2 - 2020-01-17

Expand Down
43 changes: 40 additions & 3 deletions lib/compact.js
Original file line number Diff line number Diff line change
Expand Up @@ -778,8 +778,12 @@ api.compactIri = ({
let itemLanguage = '@none';
let itemType = '@none';
if(_isValue(item)) {
if('@language' in item) {
itemLanguage = item['@language'];
if('@direction' in item) {
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) {
itemType = item['@type'];
} else {
Expand Down Expand Up @@ -819,6 +823,12 @@ api.compactIri = ({
if('@language' in value && !('@index' in value)) {
containers.push('@language', '@language@set');
typeOrLanguageValue = value['@language'];
const dir = value['@direction'];
if(dir) {
typeOrLanguageValue = `${typeOrLanguageValue}_${dir}`;
}
} else if('@direction' in value && !('@index' in value)) {
typeOrLanguageValue = `_${value['@direction']}`;
} else if('@type' in value) {
typeOrLanguage = '@type';
typeOrLanguageValue = value['@type'];
Expand Down Expand Up @@ -947,6 +957,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') || [];

Expand All @@ -956,7 +967,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'];
}
}
Expand Down Expand Up @@ -1006,6 +1027,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,
Expand Down Expand Up @@ -1168,6 +1198,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');

Expand Down
90 changes: 78 additions & 12 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -443,7 +467,8 @@ 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) {
Expand Down Expand Up @@ -580,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 {
Expand Down Expand Up @@ -795,6 +820,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)) {
Expand Down Expand Up @@ -1012,7 +1049,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
Expand Down Expand Up @@ -1076,19 +1116,39 @@ 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(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');
}
} 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(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');
}
}
}
Expand Down Expand Up @@ -1227,6 +1287,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;
}
Expand Down Expand Up @@ -1266,6 +1331,7 @@ api.isKeyword = v => {
case '@container':
case '@context':
case '@default':
case '@direction':
case '@embed':
case '@explicit':
case '@graph':
Expand Down
62 changes: 55 additions & 7 deletions lib/expand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -262,10 +263,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;
Expand All @@ -278,11 +279,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']);
Expand Down Expand Up @@ -420,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];
Expand Down Expand Up @@ -571,6 +575,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
Expand All @@ -585,11 +590,45 @@ 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(REGEX_BCP47)) {
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)) {
Expand Down Expand Up @@ -676,8 +715,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');
Expand Down Expand Up @@ -915,6 +955,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)) {
Expand All @@ -930,11 +974,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) {
Expand All @@ -958,6 +1003,9 @@ function _expandLanguageMap(activeCtx, languageMap, options) {
if(expandedKey !== '@none') {
val['@language'] = key.toLowerCase();
}
if(direction) {
val['@direction'] = direction;
}
rval.push(val);
}
}
Expand Down
Loading