Skip to content

Commit 1e656fe

Browse files
gkelloggdavidlehn
authored andcommitted
Expand and Compact using base direction.
1 parent dc40f6e commit 1e656fe

File tree

5 files changed

+169
-38
lines changed

5 files changed

+169
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Support for `"@import"`.
2626
- Added support for `@included` blocks
2727
- Skip things that have the form of a keyword, with warning.
28+
- Support for expansion and compaction of values container `"@direction"`.
2829

2930
## 2.0.2 - 2020-01-17
3031

lib/compact.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -778,8 +778,10 @@ api.compactIri = ({
778778
let itemLanguage = '@none';
779779
let itemType = '@none';
780780
if(_isValue(item)) {
781-
if('@language' in item) {
782-
itemLanguage = item['@language'];
781+
if('@direction' in item) {
782+
itemLanguage = `${item['@language']||''}_${item['@direction']}`.toLowerCase();
783+
} else if('@language' in item) {
784+
itemLanguage = item['@language'].toLowerCase();
783785
} else if('@type' in item) {
784786
itemType = item['@type'];
785787
} else {
@@ -819,6 +821,11 @@ api.compactIri = ({
819821
if('@language' in value && !('@index' in value)) {
820822
containers.push('@language', '@language@set');
821823
typeOrLanguageValue = value['@language'];
824+
if(value['@direction']) {
825+
typeOrLanguageValue = `${typeOrLanguageValue}_${value['@direction']}`
826+
}
827+
} else if('@direction' in value && !('@index' in value)) {
828+
typeOrLanguageValue = `_${value['@direction']}`
822829
} else if('@type' in value) {
823830
typeOrLanguage = '@type';
824831
typeOrLanguageValue = value['@type'];
@@ -947,6 +954,7 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => {
947954
// get context rules
948955
const type = _getContextValue(activeCtx, activeProperty, '@type');
949956
const language = _getContextValue(activeCtx, activeProperty, '@language');
957+
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
950958
const container =
951959
_getContextValue(activeCtx, activeProperty, '@container') || [];
952960

@@ -956,7 +964,17 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => {
956964
// if there's no @index to preserve ...
957965
if(!preserveIndex && type !== '@none') {
958966
// matching @type or @language specified in context, compact value
959-
if(value['@type'] === type || value['@language'] === language) {
967+
if(value['@type'] === type) {
968+
return value['@value'];
969+
}
970+
if('@language' in value && value['@language'] === language &&
971+
'@direction' in value && value['@direction'] == direction) {
972+
return value['@value'];
973+
}
974+
if('@language' in value && value['@language'] === language) {
975+
return value['@value'];
976+
}
977+
if('@direction' in value && value['@direction'] === direction) {
960978
return value['@value'];
961979
}
962980
}
@@ -1006,6 +1024,15 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => {
10061024
})] = value['@language'];
10071025
}
10081026

1027+
if('@direction' in value) {
1028+
// alias @direction
1029+
rval[api.compactIri({
1030+
activeCtx,
1031+
iri: '@direction',
1032+
relativeTo: {vocab: true}
1033+
})] = value['@direction'];
1034+
}
1035+
10091036
// alias @value
10101037
rval[api.compactIri({
10111038
activeCtx,
@@ -1168,6 +1195,13 @@ function _selectTerm(
11681195
}
11691196
} else {
11701197
prefs.push(typeOrLanguageValue);
1198+
1199+
// consider direction only
1200+
const langDir = prefs.find(el => el.includes('_'));
1201+
if(langDir) {
1202+
// consider _dir portion
1203+
prefs.push(langDir.replace(/^[^_]+_/, '_'));
1204+
}
11711205
}
11721206
prefs.push('@none');
11731207

lib/context.js

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

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

444468
// JSON-LD 1.1 support
445469
if(api.processingMode(activeCtx, 1.1)) {
446-
validKeys.push('@context', '@index', '@nest', '@prefix', '@protected');
470+
validKeys.push('@context', '@direction', '@index', '@nest', '@prefix', '@protected');
447471
}
448472

449473
for(const kw in value) {
@@ -795,6 +819,18 @@ api.createTermDefinition = ({
795819
}
796820
}
797821

822+
if('@direction' in value) {
823+
const direction = value['@direction'];
824+
if(direction !== null && direction !== 'ltr' && direction !== 'rtl') {
825+
throw new JsonLdError(
826+
'Invalid JSON-LD syntax; @direction value must be ' +
827+
'null, "ltr", or "rtl".',
828+
'jsonld.SyntaxError',
829+
{code: 'invalid base direction', context: localCtx});
830+
}
831+
mapping['@direction'] = direction;
832+
}
833+
798834
if('@nest' in value) {
799835
const nest = value['@nest'];
800836
if(!_isString(nest) || (nest !== '@nest' && nest.indexOf('@') === 0)) {
@@ -1012,7 +1048,10 @@ api.getInitialContext = options => {
10121048
const irisToTerms = {};
10131049

10141050
// handle default language
1015-
const defaultLanguage = activeCtx['@language'] || '@none';
1051+
const defaultLanguage = (activeCtx['@language'] || '@none').toLowerCase();
1052+
1053+
// handle default direction
1054+
const defaultDirection = activeCtx['@direction'];
10161055

10171056
// create term selections for each mapping in the context, ordered by
10181057
// shortest and then lexicographically least
@@ -1076,19 +1115,42 @@ api.getInitialContext = options => {
10761115
} else if('@type' in mapping) {
10771116
// term is preferred for values using specific type
10781117
_addPreferredTerm(term, entry['@type'], mapping['@type']);
1118+
} else if('@language' in mapping && '@direction' in mapping) {
1119+
// term is preferred for values using specific language and direction
1120+
const language = mapping['@language'];
1121+
const direction = mapping['@direction'];
1122+
if(langugage && direction) {
1123+
_addPreferredTerm(term, entry['@language'], `${language}_${direction}`.toLowerCase());
1124+
} else if(language) {
1125+
_addPreferredTerm(term, entry['@language'], language.toLowerCase());
1126+
} else if(direction) {
1127+
_addPreferredTerm(term, entry['@language'], `_${direction}`);
1128+
} else {
1129+
_addPreferredTerm(term, entry['@language'], "@null");
1130+
}
10791131
} else if('@language' in mapping) {
1080-
// term is preferred for values using specific language
1081-
const language = mapping['@language'] || '@null';
1082-
_addPreferredTerm(term, entry['@language'], language);
1132+
_addPreferredTerm(term, entry['@language'], (mapping['@language'] || '@null').toLowerCase());
1133+
} else if('@direction' in mapping) {
1134+
if(mapping['@direction']) {
1135+
_addPreferredTerm(term, entry['@language'], `_${mapping['@direction']}`);
1136+
} else {
1137+
_addPreferredTerm(term, entry['@language'], '@none');
1138+
}
1139+
//} else if(defaultLanguage && defaultDirection) {
1140+
// _addPreferredTerm(term, entry['@language'], `${defaultLanguage}_${defaultDirection}`);
1141+
// _addPreferredTerm(term, entry['@type'], '@none');
1142+
//} else if(defaultLanguage) {
1143+
// _addPreferredTerm(term, entry['@language'], defaultLanguage);
1144+
// _addPreferredTerm(term, entry['@type'], '@none');
1145+
} else if(defaultDirection) {
1146+
_addPreferredTerm(term, entry['@language'], `_${defaultDirection}`);
1147+
_addPreferredTerm(term, entry['@language'], '@none');
1148+
_addPreferredTerm(term, entry['@type'], '@none');
10831149
} else {
1084-
// term is preferred for values w/default language or no type and
1085-
// no language
1086-
// add an entry for the default language
1087-
_addPreferredTerm(term, entry['@language'], defaultLanguage);
1088-
10891150
// add entries for no type and no language
1090-
_addPreferredTerm(term, entry['@type'], '@none');
1151+
_addPreferredTerm(term, entry['@language'], defaultLanguage);
10911152
_addPreferredTerm(term, entry['@language'], '@none');
1153+
_addPreferredTerm(term, entry['@type'], '@none');
10921154
}
10931155
}
10941156
}
@@ -1227,6 +1289,11 @@ api.getContextValue = (ctx, key, type) => {
12271289
return ctx[type];
12281290
}
12291291

1292+
// get default direction
1293+
if(type === '@direction' && ctx.hasOwnProperty(type)) {
1294+
return ctx[type];
1295+
}
1296+
12301297
if(type === '@context') {
12311298
return undefined;
12321299
}
@@ -1266,6 +1333,7 @@ api.isKeyword = v => {
12661333
case '@container':
12671334
case '@context':
12681335
case '@default':
1336+
case '@direction':
12691337
case '@embed':
12701338
case '@explicit':
12711339
case '@graph':

lib/expand.js

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

263263
if('@value' in rval) {
264264
// @value must only have @language or @type
265-
if('@type' in rval && '@language' in rval) {
265+
if('@type' in rval && ('@language' in rval || '@direction' in rval)) {
266266
throw new JsonLdError(
267267
'Invalid JSON-LD syntax; an element containing "@value" may not ' +
268-
'contain both "@type" and "@language".',
268+
'contain both "@type" and either "@language" or "@direction".',
269269
'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
270270
}
271271
let validCount = count - 1;
@@ -278,11 +278,14 @@ api.expand = async ({
278278
if('@language' in rval) {
279279
validCount -= 1;
280280
}
281+
if('@direction' in rval) {
282+
validCount -= 1;
283+
}
281284
if(validCount !== 0) {
282285
throw new JsonLdError(
283286
'Invalid JSON-LD syntax; an element containing "@value" may only ' +
284-
'have an "@index" property and at most one other property ' +
285-
'which can be "@type" or "@language".',
287+
'have an "@index" property and either "@type" ' +
288+
'or either or both "@language" or "@direction".',
286289
'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
287290
}
288291
const values = rval['@value'] === null ? [] : _asArray(rval['@value']);
@@ -571,6 +574,7 @@ async function _expandObject({
571574
}
572575

573576
// @language must be a string
577+
// it should match BCP47
574578
if(expandedProperty === '@language') {
575579
if(value === null) {
576580
// drop null @language values, they expand as if they didn't exist
@@ -585,11 +589,44 @@ async function _expandObject({
585589
// ensure language value is lowercase
586590
value = _asArray(value).map(v => _isString(v) ? v.toLowerCase() : v);
587591

592+
// ensure language tag matches BCP47
593+
for(const lang of value) {
594+
if(_isString(lang) && !lang.match(/^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/)) {
595+
console.warn(`@language must be valid BCP47: ${lang}`);
596+
}
597+
}
598+
588599
_addValue(
589600
expandedParent, '@language', value, {propertyIsArray: options.isFrame});
590601
continue;
591602
}
592603

604+
// @direction must be "ltr" or "rtl"
605+
if(expandedProperty === '@direction') {
606+
if(!_isString(value) && !options.isFrame) {
607+
throw new JsonLdError(
608+
'Invalid JSON-LD syntax; "@direction" value must be a string.',
609+
'jsonld.SyntaxError',
610+
{code: 'invalid base direction', value});
611+
}
612+
613+
value = _asArray(value);
614+
615+
// ensure direction is "ltr" or "rtl"
616+
for(const dir of value) {
617+
if(_isString(dir) && dir !== 'ltr' && dir !== 'rtl') {
618+
throw new JsonLdError(
619+
'Invalid JSON-LD syntax; "@direction" must be "ltr" or "rtl".',
620+
'jsonld.SyntaxError',
621+
{code: 'invalid base direction', value});
622+
}
623+
}
624+
625+
_addValue(
626+
expandedParent, '@direction', value, {propertyIsArray: options.isFrame});
627+
continue;
628+
}
629+
593630
// @index must be a string
594631
if(expandedProperty === '@index') {
595632
if(!_isString(value)) {
@@ -676,8 +713,9 @@ async function _expandObject({
676713
const container = _getContextValue(termCtx, key, '@container') || [];
677714

678715
if(container.includes('@language') && _isObject(value)) {
716+
const direction = _getContextValue(termCtx, key, '@direction')
679717
// handle language map container (skip if value is not an object)
680-
expandedValue = _expandLanguageMap(termCtx, value, options);
718+
expandedValue = _expandLanguageMap(termCtx, value, direction, options);
681719
} else if(container.includes('@index') && _isObject(value)) {
682720
// handle index container (skip if value is not an object)
683721
const asGraph = container.includes('@graph');
@@ -915,6 +953,10 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
915953
if(language !== null) {
916954
rval['@language'] = language;
917955
}
956+
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
957+
if(direction !== null) {
958+
rval['@direction'] = direction;
959+
}
918960
}
919961
// do conversion of values that aren't basic JSON types to strings
920962
if(!['boolean', 'number', 'string'].includes(typeof value)) {
@@ -930,11 +972,12 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
930972
*
931973
* @param activeCtx the active context to use.
932974
* @param languageMap the language map to expand.
975+
* @param direction the direction to apply to values.
933976
* @param {Object} [options] - processing options.
934977
*
935978
* @return the expanded language map.
936979
*/
937-
function _expandLanguageMap(activeCtx, languageMap, options) {
980+
function _expandLanguageMap(activeCtx, languageMap, direction, options) {
938981
const rval = [];
939982
const keys = Object.keys(languageMap).sort();
940983
for(const key of keys) {
@@ -958,6 +1001,9 @@ function _expandLanguageMap(activeCtx, languageMap, options) {
9581001
if(expandedKey !== '@none') {
9591002
val['@language'] = key.toLowerCase();
9601003
}
1004+
if(direction) {
1005+
val['@direction'] = direction;
1006+
}
9611007
rval.push(val);
9621008
}
9631009
}

tests/test-common.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,6 @@ const TEST_TYPES = {
3333
specVersion: ['json-ld-1.0'],
3434
// FIXME
3535
idRegex: [
36-
// direction
37-
/compact-manifest.jsonld#tdi01$/,
38-
/compact-manifest.jsonld#tdi02$/,
39-
/compact-manifest.jsonld#tdi03$/,
40-
/compact-manifest.jsonld#tdi04$/,
41-
/compact-manifest.jsonld#tdi05$/,
42-
/compact-manifest.jsonld#tdi06$/,
43-
/compact-manifest.jsonld#tdi07$/,
4436
// html
4537
/html-manifest.jsonld#tc001$/,
4638
/html-manifest.jsonld#tc002$/,
@@ -93,16 +85,6 @@ const TEST_TYPES = {
9385
/expand-manifest.jsonld#thc05$/,
9486
// remote
9587
/remote-doc-manifest.jsonld#t0013$/, // HTML
96-
// direction
97-
/expand-manifest.jsonld#tdi01$/,
98-
/expand-manifest.jsonld#tdi02$/,
99-
/expand-manifest.jsonld#tdi03$/,
100-
/expand-manifest.jsonld#tdi04$/,
101-
/expand-manifest.jsonld#tdi05$/,
102-
/expand-manifest.jsonld#tdi06$/,
103-
/expand-manifest.jsonld#tdi07$/,
104-
/expand-manifest.jsonld#tdi08$/,
105-
/expand-manifest.jsonld#tdi09$/,
10688
// unused scoped context
10789
/expand-manifest.jsonld#tc032$/,
10890
/expand-manifest.jsonld#tc033$/,

0 commit comments

Comments
 (0)