From 9f0fa72566646336e43a5e821776da752447842d Mon Sep 17 00:00:00 2001 From: Shaun Date: Sun, 13 Sep 2026 13:04:04 +0800 Subject: [PATCH] Copy @direction when cloning the active context. The default base direction is part of the active context (JSON-LD 1.1 API, Context Processing) and must survive cloning like the default language does. Previously any context layer processed on top of an existing active context - a second document-level layer, a scoped context, an embedded node context, or a remote context - silently dropped the inherited @direction while @language survived. Fixes #586. --- CHANGELOG.md | 12 +++++++++ lib/context.js | 3 +++ tests/misc.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a69cfe..1446241d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # jsonld ChangeLog +## Unreleased + +### Fixed +- Copy the default base direction (`@direction`) when cloning the active + context, matching the existing handling of the default language. Previously + any context layer processed on top of an existing active context — a second + document-level layer, a property-/type-scoped context, an embedded node + `@context`, or a remote context — silently dropped the inherited + `@direction` (while `@language` survived), changing expanded output and, + under `rdfDirection`, the emitted N-Quads. + ([#586](https://github.com/digitalbazaar/jsonld.js/issues/586)) + ## 9.0.0 - 2025-11-20 ### Added diff --git a/lib/context.js b/lib/context.js index f9fdcc84..ea93b1f2 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1432,6 +1432,9 @@ api.getInitialContext = options => { if('@language' in this) { child['@language'] = this['@language']; } + if('@direction' in this) { + child['@direction'] = this['@direction']; + } if('@vocab' in this) { child['@vocab'] = this['@vocab']; } diff --git a/tests/misc.js b/tests/misc.js index 2d8c8cf7..284ac77a 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -4374,3 +4374,72 @@ describe('safe canonize defaults', () => { assert.deepStrictEqual(result, expected); }); }); + +describe('@direction inheritance', () => { + // The default base direction is part of the active context and must + // survive cloning like the default language does (JSON-LD 1.1 API §4.1, + // Context Processing step 1). See issue #586. + it('should keep the default @direction across context layers', async () => { + const output = await jsonld.expand({ + "@context": [ + {"@language": "en", "@direction": "rtl"}, + {"dummy": "http://example.com/dummy"} + ], + "http://example.com/p": "v" + }); + assert.deepStrictEqual(output, [{ + "http://example.com/p": [ + {"@language": "en", "@direction": "rtl", "@value": "v"} + ] + }]); + }); + + it('should inherit the default @direction into a property-scoped context', + async () => { + const output = await jsonld.expand({ + "@context": { + "@language": "en", + "@direction": "rtl", + "thing": { + "@id": "http://example.com/thing", + "@context": {"other": "http://example.com/other"} + } + }, + "thing": {"http://example.com/label": "hello"} + }); + assert.deepStrictEqual(output, [{ + "http://example.com/thing": [{ + "http://example.com/label": [ + {"@language": "en", "@direction": "rtl", "@value": "hello"} + ] + }] + }]); + }); + + it('should still allow a scoped context to override or clear @direction', + async () => { + const output = await jsonld.expand({ + "@context": { + "@direction": "rtl", + "ltr": { + "@id": "http://example.com/ltr", + "@context": {"@direction": "ltr"} + }, + "none": { + "@id": "http://example.com/none", + "@context": {"@direction": null} + } + }, + "ltr": {"http://example.com/label": "a"}, + "none": {"http://example.com/label": "b"} + }); + assert.deepStrictEqual(output, [{ + "http://example.com/ltr": [{ + "http://example.com/label": [{"@direction": "ltr", "@value": "a"}] + }], + "http://example.com/none": [{ + "http://example.com/label": [{"@value": "b"}] + }] + }]); + }); +});