Skip to content

Commit cad4768

Browse files
committed
Expand and Compact using base direction.
1 parent edae3d9 commit cad4768

File tree

5 files changed

+169
-39
lines changed

5 files changed

+169
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
### Added
2424
- Support for `"@import"`.
25+
- Support for expansion and compaction of values container `"@direction"`.
2526

2627
## 2.0.2 - 2020-01-17
2728

lib/compact.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -776,8 +776,10 @@ api.compactIri = ({
776776
let itemLanguage = '@none';
777777
let itemType = '@none';
778778
if(_isValue(item)) {
779-
if('@language' in item) {
780-
itemLanguage = item['@language'];
779+
if('@direction' in item) {
780+
itemLanguage = `${item['@language']||''}_${item['@direction']}`.toLowerCase();
781+
} else if('@language' in item) {
782+
itemLanguage = item['@language'].toLowerCase();
781783
} else if('@type' in item) {
782784
itemType = item['@type'];
783785
} else {
@@ -817,6 +819,11 @@ api.compactIri = ({
817819
if('@language' in value && !('@index' in value)) {
818820
containers.push('@language', '@language@set');
819821
typeOrLanguageValue = value['@language'];
822+
if(value['@direction']) {
823+
typeOrLanguageValue = `${typeOrLanguageValue}_${value['@direction']}`
824+
}
825+
} else if('@direction' in value && !('@index' in value)) {
826+
typeOrLanguageValue = `_${value['@direction']}`
820827
} else if('@type' in value) {
821828
typeOrLanguage = '@type';
822829
typeOrLanguageValue = value['@type'];
@@ -945,6 +952,7 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => {
945952
// get context rules
946953
const type = _getContextValue(activeCtx, activeProperty, '@type');
947954
const language = _getContextValue(activeCtx, activeProperty, '@language');
955+
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
948956
const container =
949957
_getContextValue(activeCtx, activeProperty, '@container') || [];
950958

@@ -954,7 +962,17 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => {
954962
// if there's no @index to preserve ...
955963
if(!preserveIndex && type !== '@none') {
956964
// matching @type or @language specified in context, compact value
957-
if(value['@type'] === type || value['@language'] === language) {
965+
if(value['@type'] === type) {
966+
return value['@value'];
967+
}
968+
if('@language' in value && value['@language'] === language &&
969+
'@direction' in value && value['@direction'] == direction) {
970+
return value['@value'];
971+
}
972+
if('@language' in value && value['@language'] === language) {
973+
return value['@value'];
974+
}
975+
if('@direction' in value && value['@direction'] === direction) {
958976
return value['@value'];
959977
}
960978
}
@@ -1004,6 +1022,15 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => {
10041022
})] = value['@language'];
10051023
}
10061024

1025+
if('@direction' in value) {
1026+
// alias @direction
1027+
rval[api.compactIri({
1028+
activeCtx,
1029+
iri: '@direction',
1030+
relativeTo: {vocab: true}
1031+
})] = value['@direction'];
1032+
}
1033+
10071034
// alias @value
10081035
rval[api.compactIri({
10091036
activeCtx,
@@ -1166,6 +1193,13 @@ function _selectTerm(
11661193
}
11671194
} else {
11681195
prefs.push(typeOrLanguageValue);
1196+
1197+
// consider direction only
1198+
const langDir = prefs.find(el => el.includes('_'));
1199+
if(langDir) {
1200+
// consider _dir portion
1201+
prefs.push(langDir.replace(/^[^_]+_/, '_'));
1202+
}
11691203
}
11701204
prefs.push('@none');
11711205

lib/context.js

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,30 @@ api.process = async ({
246246
defined.set('@language', true);
247247
}
248248

249+
// handle @direction
250+
if('@direction' in ctx) {
251+
const value = ctx['@direction'];
252+
if(activeCtx.processingMode === 'json-ld-1.0') {
253+
throw new JsonLdError(
254+
'Invalid JSON-LD syntax; @direction not compatible with ' +
255+
activeCtx.processingMode,
256+
'jsonld.SyntaxError',
257+
{code: 'invalid context member', context: ctx});
258+
}
259+
if(value === null) {
260+
delete rval['@direction'];
261+
} else if(value !== 'ltr' && value !== 'rtl') {
262+
throw new JsonLdError(
263+
'Invalid JSON-LD syntax; the value of "@direction" in a ' +
264+
'@context must be null, "ltr", or "rtl".',
265+
'jsonld.SyntaxError',
266+
{code: 'invalid base direction', context: ctx});
267+
} else {
268+
rval['@direction'] = value;
269+
}
270+
defined.set('@direction', true);
271+
}
272+
249273
// handle @propagate
250274
// note: we've already extracted it, here we just do error checking
251275
if('@propagate' in ctx) {
@@ -437,7 +461,7 @@ api.createTermDefinition = ({
437461

438462
// JSON-LD 1.1 support
439463
if(api.processingMode(activeCtx, 1.1)) {
440-
validKeys.push('@context', '@index', '@nest', '@prefix', '@protected');
464+
validKeys.push('@context', '@direction', '@index', '@nest', '@prefix', '@protected');
441465
}
442466

443467
for(const kw in value) {
@@ -760,6 +784,18 @@ api.createTermDefinition = ({
760784
}
761785
}
762786

787+
if('@direction' in value) {
788+
const direction = value['@direction'];
789+
if(direction !== null && direction !== 'ltr' && direction !== 'rtl') {
790+
throw new JsonLdError(
791+
'Invalid JSON-LD syntax; @direction value must be ' +
792+
'null, "ltr", or "rtl".',
793+
'jsonld.SyntaxError',
794+
{code: 'invalid base direction', context: localCtx});
795+
}
796+
mapping['@direction'] = direction;
797+
}
798+
763799
if('@nest' in value) {
764800
const nest = value['@nest'];
765801
if(!_isString(nest) || (nest !== '@nest' && nest.indexOf('@') === 0)) {
@@ -972,7 +1008,10 @@ api.getInitialContext = options => {
9721008
const irisToTerms = {};
9731009

9741010
// handle default language
975-
const defaultLanguage = activeCtx['@language'] || '@none';
1011+
const defaultLanguage = (activeCtx['@language'] || '@none').toLowerCase();
1012+
1013+
// handle default direction
1014+
const defaultDirection = activeCtx['@direction'];
9761015

9771016
// create term selections for each mapping in the context, ordered by
9781017
// shortest and then lexicographically least
@@ -1036,19 +1075,42 @@ api.getInitialContext = options => {
10361075
} else if('@type' in mapping) {
10371076
// term is preferred for values using specific type
10381077
_addPreferredTerm(term, entry['@type'], mapping['@type']);
1078+
} else if('@language' in mapping && '@direction' in mapping) {
1079+
// term is preferred for values using specific language and direction
1080+
const language = mapping['@language'];
1081+
const direction = mapping['@direction'];
1082+
if(langugage && direction) {
1083+
_addPreferredTerm(term, entry['@language'], `${language}_${direction}`.toLowerCase());
1084+
} else if(language) {
1085+
_addPreferredTerm(term, entry['@language'], language.toLowerCase());
1086+
} else if(direction) {
1087+
_addPreferredTerm(term, entry['@language'], `_${direction}`);
1088+
} else {
1089+
_addPreferredTerm(term, entry['@language'], "@null");
1090+
}
10391091
} else if('@language' in mapping) {
1040-
// term is preferred for values using specific language
1041-
const language = mapping['@language'] || '@null';
1042-
_addPreferredTerm(term, entry['@language'], language);
1092+
_addPreferredTerm(term, entry['@language'], (mapping['@language'] || '@null').toLowerCase());
1093+
} else if('@direction' in mapping) {
1094+
if(mapping['@direction']) {
1095+
_addPreferredTerm(term, entry['@language'], `_${mapping['@direction']}`);
1096+
} else {
1097+
_addPreferredTerm(term, entry['@language'], '@none');
1098+
}
1099+
//} else if(defaultLanguage && defaultDirection) {
1100+
// _addPreferredTerm(term, entry['@language'], `${defaultLanguage}_${defaultDirection}`);
1101+
// _addPreferredTerm(term, entry['@type'], '@none');
1102+
//} else if(defaultLanguage) {
1103+
// _addPreferredTerm(term, entry['@language'], defaultLanguage);
1104+
// _addPreferredTerm(term, entry['@type'], '@none');
1105+
} else if(defaultDirection) {
1106+
_addPreferredTerm(term, entry['@language'], `_${defaultDirection}`);
1107+
_addPreferredTerm(term, entry['@language'], '@none');
1108+
_addPreferredTerm(term, entry['@type'], '@none');
10431109
} else {
1044-
// term is preferred for values w/default language or no type and
1045-
// no language
1046-
// add an entry for the default language
1047-
_addPreferredTerm(term, entry['@language'], defaultLanguage);
1048-
10491110
// add entries for no type and no language
1050-
_addPreferredTerm(term, entry['@type'], '@none');
1111+
_addPreferredTerm(term, entry['@language'], defaultLanguage);
10511112
_addPreferredTerm(term, entry['@language'], '@none');
1113+
_addPreferredTerm(term, entry['@type'], '@none');
10521114
}
10531115
}
10541116
}
@@ -1187,6 +1249,11 @@ api.getContextValue = (ctx, key, type) => {
11871249
return ctx[type];
11881250
}
11891251

1252+
// get default direction
1253+
if(type === '@direction' && ctx.hasOwnProperty(type)) {
1254+
return ctx[type];
1255+
}
1256+
11901257
if(type === '@context') {
11911258
return undefined;
11921259
}
@@ -1226,6 +1293,7 @@ api.isKeyword = v => {
12261293
case '@container':
12271294
case '@context':
12281295
case '@default':
1296+
case '@direction':
12291297
case '@embed':
12301298
case '@explicit':
12311299
case '@graph':

lib/expand.js

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ api.expand = async ({
261261

262262
if('@value' in rval) {
263263
// @value must only have @language or @type
264-
if('@type' in rval && '@language' in rval) {
264+
if('@type' in rval && ('@language' in rval || '@direction' in rval)) {
265265
throw new JsonLdError(
266266
'Invalid JSON-LD syntax; an element containing "@value" may not ' +
267-
'contain both "@type" and "@language".',
267+
'contain both "@type" and either "@language" or "@direction".',
268268
'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
269269
}
270270
let validCount = count - 1;
@@ -277,11 +277,14 @@ api.expand = async ({
277277
if('@language' in rval) {
278278
validCount -= 1;
279279
}
280+
if('@direction' in rval) {
281+
validCount -= 1;
282+
}
280283
if(validCount !== 0) {
281284
throw new JsonLdError(
282285
'Invalid JSON-LD syntax; an element containing "@value" may only ' +
283-
'have an "@index" property and at most one other property ' +
284-
'which can be "@type" or "@language".',
286+
'have an "@index" property and either "@type" ' +
287+
'or either or both "@language" or "@direction".',
285288
'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
286289
}
287290
const values = rval['@value'] === null ? [] : _asArray(rval['@value']);
@@ -543,6 +546,7 @@ async function _expandObject({
543546
}
544547

545548
// @language must be a string
549+
// it should match BCP47
546550
if(expandedProperty === '@language') {
547551
if(value === null) {
548552
// drop null @language values, they expand as if they didn't exist
@@ -557,11 +561,44 @@ async function _expandObject({
557561
// ensure language value is lowercase
558562
value = _asArray(value).map(v => _isString(v) ? v.toLowerCase() : v);
559563

564+
// ensure language tag matches BCP47
565+
for(const lang of value) {
566+
if(_isString(lang) && !lang.match(/^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/)) {
567+
console.warn(`@language must be valid BCP47: ${lang}`);
568+
}
569+
}
570+
560571
_addValue(
561572
expandedParent, '@language', value, {propertyIsArray: options.isFrame});
562573
continue;
563574
}
564575

576+
// @direction must be "ltr" or "rtl"
577+
if(expandedProperty === '@direction') {
578+
if(!_isString(value) && !options.isFrame) {
579+
throw new JsonLdError(
580+
'Invalid JSON-LD syntax; "@direction" value must be a string.',
581+
'jsonld.SyntaxError',
582+
{code: 'invalid base direction', value});
583+
}
584+
585+
value = _asArray(value);
586+
587+
// ensure direction is "ltr" or "rtl"
588+
for(const dir of value) {
589+
if(_isString(dir) && dir !== 'ltr' && dir !== 'rtl') {
590+
throw new JsonLdError(
591+
'Invalid JSON-LD syntax; "@direction" must be "ltr" or "rtl".',
592+
'jsonld.SyntaxError',
593+
{code: 'invalid base direction', value});
594+
}
595+
}
596+
597+
_addValue(
598+
expandedParent, '@direction', value, {propertyIsArray: options.isFrame});
599+
continue;
600+
}
601+
565602
// @index must be a string
566603
if(expandedProperty === '@index') {
567604
if(!_isString(value)) {
@@ -648,8 +685,9 @@ async function _expandObject({
648685
const container = _getContextValue(termCtx, key, '@container') || [];
649686

650687
if(container.includes('@language') && _isObject(value)) {
688+
const direction = _getContextValue(termCtx, key, '@direction')
651689
// handle language map container (skip if value is not an object)
652-
expandedValue = _expandLanguageMap(termCtx, value, options);
690+
expandedValue = _expandLanguageMap(termCtx, value, direction, options);
653691
} else if(container.includes('@index') && _isObject(value)) {
654692
// handle index container (skip if value is not an object)
655693
const asGraph = container.includes('@graph');
@@ -886,6 +924,10 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
886924
if(language !== null) {
887925
rval['@language'] = language;
888926
}
927+
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
928+
if(direction !== null) {
929+
rval['@direction'] = direction;
930+
}
889931
}
890932
// do conversion of values that aren't basic JSON types to strings
891933
if(!['boolean', 'number', 'string'].includes(typeof value)) {
@@ -901,11 +943,12 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
901943
*
902944
* @param activeCtx the active context to use.
903945
* @param languageMap the language map to expand.
946+
* @param direction the direction to apply to values.
904947
* @param {Object} [options] - processing options.
905948
*
906949
* @return the expanded language map.
907950
*/
908-
function _expandLanguageMap(activeCtx, languageMap, options) {
951+
function _expandLanguageMap(activeCtx, languageMap, direction, options) {
909952
const rval = [];
910953
const keys = Object.keys(languageMap).sort();
911954
for(const key of keys) {
@@ -929,6 +972,9 @@ function _expandLanguageMap(activeCtx, languageMap, options) {
929972
if(expandedKey !== '@none') {
930973
val['@language'] = key.toLowerCase();
931974
}
975+
if(direction) {
976+
val['@direction'] = direction;
977+
}
932978
rval.push(val);
933979
}
934980
}

tests/test-common.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,6 @@ const TEST_TYPES = {
3939
/compact-manifest.jsonld#tin03$/,
4040
/compact-manifest.jsonld#tin04$/,
4141
/compact-manifest.jsonld#tin05$/,
42-
// direction
43-
/compact-manifest.jsonld#tdi01$/,
44-
/compact-manifest.jsonld#tdi02$/,
45-
/compact-manifest.jsonld#tdi03$/,
46-
/compact-manifest.jsonld#tdi04$/,
47-
/compact-manifest.jsonld#tdi05$/,
48-
/compact-manifest.jsonld#tdi06$/,
49-
/compact-manifest.jsonld#tdi07$/,
5042
// html
5143
/html-manifest.jsonld#tc001$/,
5244
/html-manifest.jsonld#tc002$/,
@@ -125,19 +117,8 @@ const TEST_TYPES = {
125117
/expand-manifest.jsonld#tpr36$/,
126118
/expand-manifest.jsonld#tpr37$/,
127119
/expand-manifest.jsonld#tpr39$/,
128-
// direction
129-
/expand-manifest.jsonld#tdi01$/,
130-
/expand-manifest.jsonld#tdi02$/,
131-
/expand-manifest.jsonld#tdi03$/,
132-
/expand-manifest.jsonld#tdi04$/,
133-
/expand-manifest.jsonld#tdi05$/,
134-
/expand-manifest.jsonld#tdi06$/,
135-
/expand-manifest.jsonld#tdi07$/,
136-
/expand-manifest.jsonld#tdi08$/,
137-
/expand-manifest.jsonld#tdi09$/,
138120
// unused scoped context
139121
/expand-manifest.jsonld#tc032$/,
140-
/expand-manifest.jsonld#tc033$/,
141122
]
142123
},
143124
fn: 'expand',

0 commit comments

Comments
 (0)