Skip to content

Commit 1ed8825

Browse files
committed
Expand and Compact using base direction.
1 parent 1cc722a commit 1ed8825

File tree

5 files changed

+172
-38
lines changed

5 files changed

+172
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# jsonld ChangeLog
22

3+
=======
4+
### Added
5+
- Support for expansion and compaction of values container `"@direction"`.
6+
37
## 2.0.2 - 2020-01-17
48

59
### Fixed

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) {
@@ -397,7 +421,7 @@ api.createTermDefinition = ({
397421

398422
// JSON-LD 1.1 support
399423
if(api.processingMode(activeCtx, 1.1)) {
400-
validKeys.push('@context', '@index', '@nest', '@prefix', '@protected');
424+
validKeys.push('@context', '@direction', '@index', '@nest', '@prefix', '@protected');
401425
}
402426

403427
for(const kw in value) {
@@ -701,6 +725,18 @@ api.createTermDefinition = ({
701725
}
702726
}
703727

728+
if('@direction' in value) {
729+
const direction = value['@direction'];
730+
if(direction !== null && direction !== 'ltr' && direction !== 'rtl') {
731+
throw new JsonLdError(
732+
'Invalid JSON-LD syntax; @direction value must be ' +
733+
'null, "ltr", or "rtl".',
734+
'jsonld.SyntaxError',
735+
{code: 'invalid base direction', context: localCtx});
736+
}
737+
mapping['@direction'] = direction;
738+
}
739+
704740
if('@nest' in value) {
705741
const nest = value['@nest'];
706742
if(!_isString(nest) || (nest !== '@nest' && nest.indexOf('@') === 0)) {
@@ -913,7 +949,10 @@ api.getInitialContext = options => {
913949
const irisToTerms = {};
914950

915951
// handle default language
916-
const defaultLanguage = activeCtx['@language'] || '@none';
952+
const defaultLanguage = (activeCtx['@language'] || '@none').toLowerCase();
953+
954+
// handle default direction
955+
const defaultDirection = activeCtx['@direction'];
917956

918957
// create term selections for each mapping in the context, ordered by
919958
// shortest and then lexicographically least
@@ -974,19 +1013,42 @@ api.getInitialContext = options => {
9741013
} else if('@type' in mapping) {
9751014
// term is preferred for values using specific type
9761015
_addPreferredTerm(term, entry['@type'], mapping['@type']);
1016+
} else if('@language' in mapping && '@direction' in mapping) {
1017+
// term is preferred for values using specific language and direction
1018+
const language = mapping['@language'];
1019+
const direction = mapping['@direction'];
1020+
if(langugage && direction) {
1021+
_addPreferredTerm(term, entry['@language'], `${language}_${direction}`.toLowerCase());
1022+
} else if(language) {
1023+
_addPreferredTerm(term, entry['@language'], language.toLowerCase());
1024+
} else if(direction) {
1025+
_addPreferredTerm(term, entry['@language'], `_${direction}`);
1026+
} else {
1027+
_addPreferredTerm(term, entry['@language'], "@null");
1028+
}
9771029
} else if('@language' in mapping) {
978-
// term is preferred for values using specific language
979-
const language = mapping['@language'] || '@null';
980-
_addPreferredTerm(term, entry['@language'], language);
1030+
_addPreferredTerm(term, entry['@language'], (mapping['@language'] || '@null').toLowerCase());
1031+
} else if('@direction' in mapping) {
1032+
if(mapping['@direction']) {
1033+
_addPreferredTerm(term, entry['@language'], `_${mapping['@direction']}`);
1034+
} else {
1035+
_addPreferredTerm(term, entry['@language'], '@none');
1036+
}
1037+
//} else if(defaultLanguage && defaultDirection) {
1038+
// _addPreferredTerm(term, entry['@language'], `${defaultLanguage}_${defaultDirection}`);
1039+
// _addPreferredTerm(term, entry['@type'], '@none');
1040+
//} else if(defaultLanguage) {
1041+
// _addPreferredTerm(term, entry['@language'], defaultLanguage);
1042+
// _addPreferredTerm(term, entry['@type'], '@none');
1043+
} else if(defaultDirection) {
1044+
_addPreferredTerm(term, entry['@language'], `_${defaultDirection}`);
1045+
_addPreferredTerm(term, entry['@language'], '@none');
1046+
_addPreferredTerm(term, entry['@type'], '@none');
9811047
} else {
982-
// term is preferred for values w/default language or no type and
983-
// no language
984-
// add an entry for the default language
985-
_addPreferredTerm(term, entry['@language'], defaultLanguage);
986-
9871048
// add entries for no type and no language
988-
_addPreferredTerm(term, entry['@type'], '@none');
1049+
_addPreferredTerm(term, entry['@language'], defaultLanguage);
9891050
_addPreferredTerm(term, entry['@language'], '@none');
1051+
_addPreferredTerm(term, entry['@type'], '@none');
9901052
}
9911053
}
9921054
}
@@ -1125,6 +1187,11 @@ api.getContextValue = (ctx, key, type) => {
11251187
return ctx[type];
11261188
}
11271189

1190+
// get default direction
1191+
if(type === '@direction' && ctx.hasOwnProperty(type)) {
1192+
return ctx[type];
1193+
}
1194+
11281195
if(type === '@context') {
11291196
return undefined;
11301197
}
@@ -1164,6 +1231,7 @@ api.isKeyword = v => {
11641231
case '@container':
11651232
case '@context':
11661233
case '@default':
1234+
case '@direction':
11671235
case '@embed':
11681236
case '@explicit':
11691237
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']);
@@ -541,6 +544,7 @@ async function _expandObject({
541544
}
542545

543546
// @language must be a string
547+
// it should match BCP47
544548
if(expandedProperty === '@language') {
545549
if(value === null) {
546550
// drop null @language values, they expand as if they didn't exist
@@ -555,11 +559,44 @@ async function _expandObject({
555559
// ensure language value is lowercase
556560
value = _asArray(value).map(v => _isString(v) ? v.toLowerCase() : v);
557561

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

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

648685
if(container.includes('@language') && _isObject(value)) {
686+
const direction = _getContextValue(termCtx, key, '@direction')
649687
// handle language map container (skip if value is not an object)
650-
expandedValue = _expandLanguageMap(termCtx, value, options);
688+
expandedValue = _expandLanguageMap(termCtx, value, direction, options);
651689
} else if(container.includes('@index') && _isObject(value)) {
652690
// handle index container (skip if value is not an object)
653691
const asGraph = container.includes('@graph');
@@ -883,6 +921,10 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
883921
if(language !== null) {
884922
rval['@language'] = language;
885923
}
924+
const direction = _getContextValue(activeCtx, activeProperty, '@direction');
925+
if(direction !== null) {
926+
rval['@direction'] = direction;
927+
}
886928
}
887929
// do conversion of values that aren't basic JSON types to strings
888930
if(!['boolean', 'number', 'string'].includes(typeof value)) {
@@ -898,11 +940,12 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
898940
*
899941
* @param activeCtx the active context to use.
900942
* @param languageMap the language map to expand.
943+
* @param direction the direction to apply to values.
901944
* @param {Object} [options] - processing options.
902945
*
903946
* @return the expanded language map.
904947
*/
905-
function _expandLanguageMap(activeCtx, languageMap, options) {
948+
function _expandLanguageMap(activeCtx, languageMap, direction, options) {
906949
const rval = [];
907950
const keys = Object.keys(languageMap).sort();
908951
for(const key of keys) {
@@ -926,6 +969,9 @@ function _expandLanguageMap(activeCtx, languageMap, options) {
926969
if(expandedKey !== '@none') {
927970
val['@language'] = key.toLowerCase();
928971
}
972+
if(direction) {
973+
val['@direction'] = direction;
974+
}
929975
rval.push(val);
930976
}
931977
}

tests/test-common.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,6 @@ const TEST_TYPES = {
3838
/compact-manifest.jsonld#tin03$/,
3939
/compact-manifest.jsonld#tin04$/,
4040
/compact-manifest.jsonld#tin05$/,
41-
// direction
42-
/compact-manifest.jsonld#tdi01$/,
43-
/compact-manifest.jsonld#tdi02$/,
44-
/compact-manifest.jsonld#tdi03$/,
45-
/compact-manifest.jsonld#tdi04$/,
46-
/compact-manifest.jsonld#tdi05$/,
47-
/compact-manifest.jsonld#tdi06$/,
48-
/compact-manifest.jsonld#tdi07$/,
4941
// html
5042
/html-manifest.jsonld#tc001$/,
5143
/html-manifest.jsonld#tc002$/,
@@ -155,16 +147,6 @@ const TEST_TYPES = {
155147
/expand-manifest.jsonld#tpr36$/,
156148
/expand-manifest.jsonld#tpr37$/,
157149
/expand-manifest.jsonld#tpr39$/,
158-
// direction
159-
/expand-manifest.jsonld#tdi01$/,
160-
/expand-manifest.jsonld#tdi02$/,
161-
/expand-manifest.jsonld#tdi03$/,
162-
/expand-manifest.jsonld#tdi04$/,
163-
/expand-manifest.jsonld#tdi05$/,
164-
/expand-manifest.jsonld#tdi06$/,
165-
/expand-manifest.jsonld#tdi07$/,
166-
/expand-manifest.jsonld#tdi08$/,
167-
/expand-manifest.jsonld#tdi09$/,
168150
]
169151
},
170152
fn: 'expand',

0 commit comments

Comments
 (0)