@nuggetslife/vc 0.0.30 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuggetslife/vc",
3
- "version": "0.0.30",
3
+ "version": "0.1.0",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
@@ -41,11 +41,11 @@
41
41
  },
42
42
  "packageManager": "yarn@4.3.1",
43
43
  "optionalDependencies": {
44
- "@nuggetslife/vc-darwin-arm64": "0.0.30",
45
- "@nuggetslife/vc-linux-arm64-gnu": "0.0.30",
46
- "@nuggetslife/vc-linux-arm64-musl": "0.0.30",
47
- "@nuggetslife/vc-linux-x64-gnu": "0.0.30",
48
- "@nuggetslife/vc-linux-x64-musl": "0.0.30"
44
+ "@nuggetslife/vc-darwin-arm64": "0.1.0",
45
+ "@nuggetslife/vc-linux-arm64-gnu": "0.1.0",
46
+ "@nuggetslife/vc-linux-arm64-musl": "0.1.0",
47
+ "@nuggetslife/vc-linux-x64-gnu": "0.1.0",
48
+ "@nuggetslife/vc-linux-x64-musl": "0.1.0"
49
49
  },
50
50
  "dependencies": {}
51
51
  }
@@ -12,6 +12,9 @@ JLD_API_SHA="04a4eb7dc7cbc313f3f5be7ad9a3b06e87741693"
12
12
  RDF_CANON_REPO="https://github.com/w3c/rdf-canon.git"
13
13
  RDF_CANON_SHA="15619df2fda7a4ca88308733789b6774517f9638"
14
14
 
15
+ JLD_FRAMING_REPO="https://github.com/w3c/json-ld-framing.git"
16
+ JLD_FRAMING_SHA="fa228743e890499c35bc61aabf01e44cf5bbc3bc"
17
+
15
18
  mkdir -p tmp-w3c-tests
16
19
  cd tmp-w3c-tests
17
20
 
@@ -25,4 +28,9 @@ if [ ! -d rdf-canon ]; then
25
28
  fi
26
29
  (cd rdf-canon && git fetch --depth=1 origin "$RDF_CANON_SHA" && git checkout --detach "$RDF_CANON_SHA")
27
30
 
31
+ if [ ! -d json-ld-framing ]; then
32
+ git clone --filter=blob:none "$JLD_FRAMING_REPO"
33
+ fi
34
+ (cd json-ld-framing && git fetch --depth=1 origin "$JLD_FRAMING_SHA" && git checkout --detach "$JLD_FRAMING_SHA")
35
+
28
36
  echo "W3C suites fetched at pinned SHAs."
package/src/jsonld.rs CHANGED
@@ -1,12 +1,13 @@
1
1
  use serde_json::{json, Value};
2
+ use std::collections::HashMap;
2
3
  use std::sync::Arc;
3
4
 
4
5
  use vc::{
5
- document_loader::nuggets::DocumentLoader,
6
- jsonld::{self, context_resolver::ContextResolver},
6
+ document_loader::nuggets::DocumentLoader,
7
+ jsonld::{self, context_resolver::ContextResolver},
7
8
  };
8
9
 
9
- use crate::ld_signatures::loader_from_contexts;
10
+ use crate::ld_signatures::create_document_loader;
10
11
 
11
12
  /// JSON-LD processor — drop-in replacement for the jsonld.js API.
12
13
  ///
@@ -20,159 +21,228 @@ use crate::ld_signatures::loader_from_contexts;
20
21
  /// ```
21
22
  #[napi]
22
23
  pub struct JsonLd {
23
- loader: Arc<DocumentLoader>,
24
- cr: Arc<ContextResolver>,
24
+ loader: Arc<DocumentLoader>,
25
+ cr: Arc<ContextResolver>,
26
+ additional_contexts: Arc<HashMap<String, Value>>,
27
+ }
28
+
29
+ impl JsonLd {
30
+ fn additional_contexts(&self) -> Arc<HashMap<String, Value>> {
31
+ Arc::clone(&self.additional_contexts)
32
+ }
25
33
  }
26
34
 
27
35
  #[napi]
28
36
  impl JsonLd {
29
- /// Create a new JSON-LD processor.
30
- ///
31
- /// Accepts optional `{ contexts?: Record<string, any> }` to register
32
- /// additional URL → document mappings in the document loader (on top
33
- /// of the built-in nuggets contexts).
34
- #[napi(constructor)]
35
- pub fn new(options: Option<Value>) -> Self {
36
- let contexts = options.as_ref().and_then(|o| o.get("contexts"));
37
- let (loader, cr) = loader_from_contexts(contexts);
38
- Self { loader, cr }
37
+ /// Create a new JSON-LD processor.
38
+ ///
39
+ /// Accepts optional `{ contexts?: Record<string, any> }` to register
40
+ /// additional URL → document mappings in the document loader (on top
41
+ /// of the built-in nuggets contexts).
42
+ #[napi(constructor)]
43
+ pub fn new(options: Option<Value>) -> Self {
44
+ let additional_contexts: HashMap<String, Value> = options
45
+ .as_ref()
46
+ .and_then(|o| o.get("contexts"))
47
+ .and_then(|v| v.as_object())
48
+ .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
49
+ .unwrap_or_default();
50
+ let (loader, cr) = create_document_loader(additional_contexts.clone());
51
+ Self {
52
+ loader,
53
+ cr,
54
+ additional_contexts: Arc::new(additional_contexts),
39
55
  }
56
+ }
40
57
 
41
- /// Expand a JSON-LD document.
42
- ///
43
- /// `options` can include: `base`, `expandContext`, `keepFreeFloatingNodes`,
44
- /// `processingMode`.
45
- ///
46
- /// Returns an array of expanded JSON-LD objects.
47
- #[napi]
48
- pub async fn expand(
49
- &self,
50
- input: Value,
51
- options: Option<Value>,
52
- ) -> napi::Result<Value> {
53
- let opts = options.unwrap_or(json!({}));
54
- jsonld::expand(input, self.loader.clone(), self.cr.clone(), opts)
55
- .await
56
- .map_err(|e| napi::Error::from_reason(format!("expand failed: {e}")))
58
+ /// Expand a JSON-LD document.
59
+ ///
60
+ /// `options` can include: `base`, `expandContext`, `keepFreeFloatingNodes`,
61
+ /// `processingMode`.
62
+ ///
63
+ /// Returns an array of expanded JSON-LD objects.
64
+ #[napi]
65
+ pub async fn expand(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
66
+ let opts = options.unwrap_or(json!({}));
67
+ if vc::jsonld_v2::use_v2() {
68
+ let additional = self.additional_contexts();
69
+ vc::jsonld_v2::expand_v2_sync(input, &additional, opts)
70
+ .map_err(|e| napi::Error::from_reason(format!("expand (v2) failed: {e}")))
71
+ } else {
72
+ jsonld::expand(input, self.loader.clone(), self.cr.clone(), opts)
73
+ .await
74
+ .map_err(|e| napi::Error::from_reason(format!("expand failed: {e}")))
57
75
  }
76
+ }
58
77
 
59
- /// Compact a JSON-LD document using a context.
60
- ///
61
- /// `ctx` — the compaction context (object, array, or string URL).
62
- /// `options` can include: `base`, `compactArrays`, `graph`,
63
- /// `skipExpansion`, `processingMode`.
64
- ///
65
- /// Returns the compacted JSON-LD object.
66
- #[napi]
67
- pub async fn compact(
68
- &self,
69
- input: Value,
70
- ctx: Value,
71
- options: Option<Value>,
72
- ) -> napi::Result<Value> {
73
- let opts = options.unwrap_or(json!({}));
74
- jsonld::compact_api(input, ctx, self.loader.clone(), self.cr.clone(), opts)
75
- .await
76
- .map_err(|e| napi::Error::from_reason(format!("compact failed: {e}")))
78
+ /// Compact a JSON-LD document using a context.
79
+ ///
80
+ /// `ctx` — the compaction context (object, array, or string URL).
81
+ /// `options` can include: `base`, `compactArrays`, `graph`,
82
+ /// `skipExpansion`, `processingMode`.
83
+ ///
84
+ /// Returns the compacted JSON-LD object.
85
+ #[napi]
86
+ pub async fn compact(
87
+ &self,
88
+ input: Value,
89
+ ctx: Value,
90
+ options: Option<Value>,
91
+ ) -> napi::Result<Value> {
92
+ let opts = options.unwrap_or(json!({}));
93
+ if vc::jsonld_v2::use_v2() {
94
+ let additional = self.additional_contexts();
95
+ vc::jsonld_v2::compact_v2_sync(input, ctx, &additional, opts)
96
+ .map_err(|e| napi::Error::from_reason(format!("compact (v2) failed: {e}")))
97
+ } else {
98
+ jsonld::compact_api(input, ctx, self.loader.clone(), self.cr.clone(), opts)
99
+ .await
100
+ .map_err(|e| napi::Error::from_reason(format!("compact failed: {e}")))
77
101
  }
102
+ }
78
103
 
79
- /// Flatten a JSON-LD document.
80
- ///
81
- /// `ctx` — optional context for compacting the flattened result.
82
- /// Pass `null` to get the flattened expanded form.
83
- /// `options` can include: `base`, `expandContext`, `processingMode`.
84
- ///
85
- /// Returns flattened array (no context) or compacted object (with context).
86
- #[napi]
87
- pub async fn flatten(
88
- &self,
89
- input: Value,
90
- ctx: Option<Value>,
91
- options: Option<Value>,
92
- ) -> napi::Result<Value> {
93
- let opts = options.unwrap_or(json!({}));
94
- // Filter out null ctx (JS passes null, Rust expects None)
95
- let ctx = ctx.filter(|v| !v.is_null());
96
- jsonld::flatten(input, ctx, self.loader.clone(), self.cr.clone(), opts)
97
- .await
98
- .map_err(|e| napi::Error::from_reason(format!("flatten failed: {e}")))
104
+ /// Flatten a JSON-LD document.
105
+ ///
106
+ /// `ctx` — optional context for compacting the flattened result.
107
+ /// Pass `null` to get the flattened expanded form.
108
+ /// `options` can include: `base`, `expandContext`, `processingMode`.
109
+ ///
110
+ /// Returns flattened array (no context) or compacted object (with context).
111
+ #[napi]
112
+ pub async fn flatten(
113
+ &self,
114
+ input: Value,
115
+ ctx: Option<Value>,
116
+ options: Option<Value>,
117
+ ) -> napi::Result<Value> {
118
+ let opts = options.unwrap_or(json!({}));
119
+ // Filter out null ctx (JS passes null, Rust expects None)
120
+ let ctx = ctx.filter(|v| !v.is_null());
121
+ if vc::jsonld_v2::use_v2() {
122
+ let additional = self.additional_contexts();
123
+ vc::jsonld_v2::flatten_v2_sync(input, ctx, &additional, opts)
124
+ .map_err(|e| napi::Error::from_reason(format!("flatten (v2) failed: {e}")))
125
+ } else {
126
+ jsonld::flatten(input, ctx, self.loader.clone(), self.cr.clone(), opts)
127
+ .await
128
+ .map_err(|e| napi::Error::from_reason(format!("flatten failed: {e}")))
99
129
  }
130
+ }
100
131
 
101
- /// Frame a JSON-LD document.
102
- ///
103
- /// `frame` — the framing template.
104
- /// `options` can include: `base`, `embed`, `explicit`, `requireAll`,
105
- /// `omitDefault`, `omitGraph`, `processingMode`.
106
- ///
107
- /// Returns the framed JSON-LD object.
108
- #[napi]
109
- pub async fn frame(
110
- &self,
111
- input: Value,
112
- frame: Value,
113
- options: Option<Value>,
114
- ) -> napi::Result<Value> {
115
- let opts = options.unwrap_or(json!({}));
116
- jsonld::frame(input, frame, self.loader.clone(), self.cr.clone(), opts)
117
- .await
118
- .map_err(|e| napi::Error::from_reason(format!("frame failed: {e}")))
132
+ /// Frame a JSON-LD document.
133
+ ///
134
+ /// `frame` — the framing template.
135
+ /// `options` can include: `base`, `embed`, `explicit`, `requireAll`,
136
+ /// `omitDefault`, `omitGraph`, `processingMode`.
137
+ ///
138
+ /// Returns the framed JSON-LD object.
139
+ #[napi]
140
+ pub async fn frame(
141
+ &self,
142
+ input: Value,
143
+ frame: Value,
144
+ options: Option<Value>,
145
+ ) -> napi::Result<Value> {
146
+ let opts = options.unwrap_or(json!({}));
147
+ if vc::jsonld_v2::use_v2() {
148
+ // Sprint 3 Phase D cutover: V2 frame is always the native typed-tree
149
+ // walker. CLIENTFFI_FRAME_NATIVE / CLIENTFFI_FRAME_FAST flags are no
150
+ // longer consulted — frame_v2 / frame_v2_fast public APIs were
151
+ // retired in this commit.
152
+ let loader = self.loader.clone();
153
+ let cr = self.cr.clone();
154
+ let join = tokio::task::spawn_blocking(move || {
155
+ vc::jsonld_v2::frame_native_sync(input, frame, loader, cr, opts)
156
+ .map_err(|e| napi::Error::from_reason(format!("frame (native) failed: {e}")))
157
+ });
158
+ join
159
+ .await
160
+ .map_err(|e| napi::Error::from_reason(format!("frame (v2) join: {e}")))?
161
+ } else {
162
+ jsonld::frame(input, frame, self.loader.clone(), self.cr.clone(), opts)
163
+ .await
164
+ .map_err(|e| napi::Error::from_reason(format!("frame failed: {e}")))
119
165
  }
166
+ }
120
167
 
121
- /// Convert a JSON-LD document to an RDF dataset.
122
- ///
123
- /// `options` can include: `base`, `expandContext`, `skipExpansion`,
124
- /// `format` (`"application/n-quads"` for string output), `processingMode`.
125
- ///
126
- /// Returns an array of quads or an N-Quads string (depending on `format`).
127
- #[napi(js_name = "toRDF")]
128
- pub async fn to_rdf(
129
- &self,
130
- input: Value,
131
- options: Option<Value>,
132
- ) -> napi::Result<Value> {
133
- let opts = options.unwrap_or(json!({}));
134
- jsonld::to_rdf(input, self.loader.clone(), self.cr.clone(), opts)
135
- .await
136
- .map_err(|e| napi::Error::from_reason(format!("toRDF failed: {e}")))
168
+ /// Convert a JSON-LD document to an RDF dataset.
169
+ ///
170
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
171
+ /// `format` (`"application/n-quads"` for string output), `processingMode`.
172
+ ///
173
+ /// Returns an array of quads or an N-Quads string (depending on `format`).
174
+ #[napi(js_name = "toRDF")]
175
+ pub async fn to_rdf(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
176
+ let opts = options.unwrap_or(json!({}));
177
+ // v2 only produces N-Quads string output. If caller expects
178
+ // a quads-array (default when format is omitted), fall back to v1.
179
+ let wants_nquads = opts
180
+ .get("format")
181
+ .and_then(|v| v.as_str())
182
+ .map(|s| s == "application/n-quads")
183
+ .unwrap_or(false);
184
+ if vc::jsonld_v2::use_v2() && wants_nquads {
185
+ let additional = self.additional_contexts();
186
+ let nquads = vc::jsonld_v2::to_nquads_v2_sync(input, &additional, opts)
187
+ .map_err(|e| napi::Error::from_reason(format!("toRDF (v2) failed: {e}")))?;
188
+ Ok(Value::String(nquads))
189
+ } else {
190
+ jsonld::to_rdf(input, self.loader.clone(), self.cr.clone(), opts)
191
+ .await
192
+ .map_err(|e| napi::Error::from_reason(format!("toRDF failed: {e}")))
137
193
  }
194
+ }
138
195
 
139
- /// Convert an RDF dataset to a JSON-LD document.
140
- ///
141
- /// `dataset` — N-Quads string or array of quads.
142
- /// `options` can include: `useRdfType`, `useNativeTypes`, `rdfDirection`.
143
- ///
144
- /// Returns an array of JSON-LD objects.
145
- #[napi(js_name = "fromRDF")]
146
- pub fn from_rdf(&self, dataset: Value, options: Option<Value>) -> napi::Result<Value> {
147
- let opts = options.unwrap_or(json!({}));
148
- jsonld::from_rdf_api(dataset, opts)
149
- .map_err(|e| napi::Error::from_reason(format!("fromRDF failed: {e}")))
150
- }
196
+ /// Convert an RDF dataset to a JSON-LD document.
197
+ ///
198
+ /// `dataset` — N-Quads string or array of quads.
199
+ /// `options` can include: `useRdfType`, `useNativeTypes`, `rdfDirection`.
200
+ ///
201
+ /// Returns an array of JSON-LD objects.
202
+ #[napi(js_name = "fromRDF")]
203
+ pub fn from_rdf(&self, dataset: Value, options: Option<Value>) -> napi::Result<Value> {
204
+ let opts = options.unwrap_or(json!({}));
205
+ jsonld::from_rdf_api(dataset, opts)
206
+ .map_err(|e| napi::Error::from_reason(format!("fromRDF failed: {e}")))
207
+ }
151
208
 
152
- /// Canonize (normalize) a JSON-LD document.
153
- ///
154
- /// `options` can include: `base`, `expandContext`, `skipExpansion`,
155
- /// `inputFormat` (`"application/n-quads"`), `format`, `algorithm`,
156
- /// `processingMode`.
157
- ///
158
- /// Returns the canonical N-Quads string.
159
- #[napi]
160
- pub async fn canonize(
161
- &self,
162
- input: Value,
163
- options: Option<Value>,
164
- ) -> napi::Result<Value> {
165
- let mut opts = options.unwrap_or(json!({}));
166
- // Defaults matching JS jsonld.js canonize()
167
- if opts.get("algorithm").is_none() {
168
- opts["algorithm"] = json!("RDFC-1.0");
169
- }
170
- if opts.get("format").is_none() {
171
- opts["format"] = json!("application/n-quads");
172
- }
173
- let result = jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
174
- .await
175
- .map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?;
176
- Ok(Value::String(result))
209
+ /// Canonize (normalize) a JSON-LD document.
210
+ ///
211
+ /// `options` can include: `base`, `expandContext`, `skipExpansion`,
212
+ /// `inputFormat` (`"application/n-quads"`), `format`, `algorithm`,
213
+ /// `processingMode`.
214
+ ///
215
+ /// Returns the canonical N-Quads string.
216
+ #[napi]
217
+ pub async fn canonize(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
218
+ let mut opts = options.unwrap_or(json!({}));
219
+ // Defaults matching JS jsonld.js canonize()
220
+ if opts.get("algorithm").is_none() {
221
+ opts["algorithm"] = json!("RDFC-1.0");
222
+ }
223
+ if opts.get("format").is_none() {
224
+ opts["format"] = json!("application/n-quads");
177
225
  }
226
+ let input_is_nquads = opts
227
+ .get("inputFormat")
228
+ .and_then(|v| v.as_str())
229
+ .map(|s| s == "application/n-quads")
230
+ .unwrap_or(false);
231
+ let result = if vc::jsonld_v2::use_v2() && !input_is_nquads {
232
+ let additional = self.additional_contexts();
233
+ vc::jsonld_v2::canonize_v2_sync(
234
+ input,
235
+ &additional,
236
+ self.loader.clone(),
237
+ self.cr.clone(),
238
+ opts,
239
+ )
240
+ .map_err(|e| napi::Error::from_reason(format!("canonize (v2) failed: {e}")))?
241
+ } else {
242
+ jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
243
+ .await
244
+ .map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?
245
+ };
246
+ Ok(Value::String(result))
247
+ }
178
248
  }
@@ -135,7 +135,10 @@ test('cross-verify: flatten(inputDocument, null) matches JS reference', async ()
135
135
  const napiResult = await proc.flatten(inputDocument, null);
136
136
  const jsResult = await jsonld.flatten(inputDocument, null, { documentLoader });
137
137
 
138
- assert.deepStrictEqual(napiResult, jsResult);
138
+ // Flatten without compaction returns an unordered set of node objects
139
+ // (each keyed by @id). Sort by @id before comparing.
140
+ const sortById = arr => [...arr].sort((a, b) => String(a['@id'] || '').localeCompare(String(b['@id'] || '')));
141
+ assert.deepStrictEqual(sortById(napiResult), sortById(jsResult));
139
142
  });
140
143
 
141
144
 
@@ -171,7 +174,10 @@ test('cross-verify: toRDF(inputDocument, n-quads) matches JS reference', async (
171
174
  const napiResult = await proc.toRDF(inputDocument, { format: 'application/n-quads' });
172
175
  const jsResult = await jsonld.toRDF(inputDocument, { documentLoader, format: 'application/n-quads' });
173
176
 
174
- assert.strictEqual(napiResult, jsResult);
177
+ // toRDF triple order is implementation-defined (RDFC-1.0 normalises it later).
178
+ // Compare as a set: split into lines, drop empties, sort.
179
+ const norm = s => s.split('\n').filter(l => l.length).sort().join('\n');
180
+ assert.strictEqual(norm(napiResult), norm(jsResult));
175
181
  });
176
182
 
177
183
 
@@ -32,6 +32,7 @@ const updateBaseline = flag('--update');
32
32
 
33
33
  const TESTS_ROOT = resolve(__dirname, 'tmp-w3c-tests');
34
34
  const JLD_TESTS = join(TESTS_ROOT, 'json-ld-api', 'tests');
35
+ const JLD_FRAMING_TESTS = join(TESTS_ROOT, 'json-ld-framing', 'tests');
35
36
  const RDFC_TESTS = join(TESTS_ROOT, 'rdf-canon', 'tests');
36
37
  const JLD_BASE = 'https://w3c.github.io/json-ld-api/tests/';
37
38
  const BASELINE_PATH = join(__dirname, 'w3c-baseline.json');
@@ -70,6 +71,7 @@ function preloadJldContexts() {
70
71
  }
71
72
  }
72
73
  walk(JLD_TESTS, '');
74
+ if (existsSync(JLD_FRAMING_TESTS)) walk(JLD_FRAMING_TESTS, '');
73
75
  return ctxs;
74
76
  }
75
77
 
@@ -84,6 +86,25 @@ const normNQuads = (s) => typeof s === 'string'
84
86
  ? s.split('\n').map(l => l.trim()).filter(Boolean).sort().join('\n')
85
87
  : s;
86
88
 
89
+ // W3C toRdf expected fixtures contain post-URDNA2015 blank-node labels
90
+ // (e.g. `_:c14n0`), but the spec itself defines toRdf as producing
91
+ // pre-canonical labels (e.g. `_:b0`). jsonld.js exhibits the same
92
+ // pre-canonical output. To compare semantically we run URDNA2015 on both
93
+ // sides before string-matching. Falls back to normNQuads if either side
94
+ // can't be canonicalized (so a bug in the processor still surfaces).
95
+ const canonProc = new JsonLd();
96
+ async function nquadsEqual(got, expected) {
97
+ if (typeof got !== 'string' || typeof expected !== 'string') return false;
98
+ try {
99
+ const opts = { algorithm: 'URDNA2015', inputFormat: 'application/n-quads', format: 'application/n-quads' };
100
+ const a = await canonProc.canonize(got, opts);
101
+ const b = await canonProc.canonize(expected, opts);
102
+ return a === b;
103
+ } catch {
104
+ return normNQuads(got) === normNQuads(expected);
105
+ }
106
+ }
107
+
87
108
  const DENYLIST_PATH = join(__dirname, 'w3c-denylist.json');
88
109
  let DENYLIST = {};
89
110
  if (existsSync(DENYLIST_PATH)) {
@@ -144,10 +165,18 @@ async function runJld(algo, methodName) {
144
165
  const opt = entry.option || {};
145
166
  const callOpts = { base: opt.base || inputUrl };
146
167
  if (opt.processingMode) callOpts.processingMode = opt.processingMode;
168
+ else if (opt.specVersion) callOpts.processingMode = opt.specVersion;
147
169
  if (opt.expandContext) {
148
170
  const ctxUrl = JLD_BASE + opt.expandContext;
149
171
  callOpts.expandContext = contexts[ctxUrl] || ctxUrl;
150
172
  }
173
+ // toRdf-specific options from W3C test entries
174
+ if (opt.rdfDirection) callOpts.rdfDirection = opt.rdfDirection;
175
+ if (opt.produceGeneralizedRdf != null) callOpts.produceGeneralizedRdf = opt.produceGeneralizedRdf;
176
+ if (opt.compactArrays != null) callOpts.compactArrays = opt.compactArrays;
177
+ // fromRDF-specific options
178
+ if (opt.useNativeTypes != null) callOpts.useNativeTypes = opt.useNativeTypes;
179
+ if (opt.useRdfType != null) callOpts.useRdfType = opt.useRdfType;
151
180
 
152
181
  try {
153
182
  const input = methodName === 'fromRDF' ? loadText(inputPath) : loadJson(inputPath);
@@ -172,7 +201,7 @@ async function runJld(algo, methodName) {
172
201
  const expected = methodName === 'toRDF' ? loadText(expectPath) : loadJson(expectPath);
173
202
 
174
203
  const ok = methodName === 'toRDF'
175
- ? normNQuads(napiResult) === normNQuads(expected)
204
+ ? await nquadsEqual(napiResult, expected)
176
205
  : jsonEqual(napiResult, expected);
177
206
  record(algo, id, ok ? 'pass' : 'fail');
178
207
  } catch (e) {
@@ -229,20 +258,64 @@ async function runOnce(algo) {
229
258
  else if (algo === 'flatten') await runJld('flatten', 'flatten');
230
259
  else if (algo === 'toRdf') await runJld('toRdf', 'toRDF');
231
260
  else if (algo === 'fromRdf') await runJld('fromRdf', 'fromRDF');
261
+ else if (algo === 'frame') await runFraming();
232
262
  }
233
263
 
264
+ async function runFraming() {
265
+ const manifestPath = join(JLD_FRAMING_TESTS, 'frame-manifest.jsonld');
266
+ if (!existsSync(manifestPath)) return;
267
+ const manifest = loadJson(manifestPath);
268
+ const contexts = preloadJldContexts();
269
+ const proc = new JsonLd({ contexts });
270
+ let seen = 0;
271
+ for (const entry of manifest.sequence) {
272
+ const types = Array.isArray(entry['@type']) ? entry['@type'] : [entry['@type']];
273
+ if (!types.includes('jld:PositiveEvaluationTest') && !types.includes('jld:NegativeEvaluationTest')) continue;
274
+ const id = entry['@id'];
275
+ if (isDenied('frame', id)) continue;
276
+ if (!isInCheckScope('frame', id)) continue;
277
+ seen += 1;
278
+ if (seen % PROGRESS_EVERY === 0) process.stderr.write(` frame: ${seen} tests processed\n`);
279
+ if (!entry.input || !entry.frame) { record('frame', id, 'err'); continue; }
280
+ const inputPath = join(JLD_FRAMING_TESTS, entry.input);
281
+ const framePath = join(JLD_FRAMING_TESTS, entry.frame);
282
+ const expectPath = entry.expect ? join(JLD_FRAMING_TESTS, entry.expect) : null;
283
+ if (!existsSync(inputPath) || !existsSync(framePath)) { record('frame', id, 'err'); continue; }
284
+ const opt = entry.option || {};
285
+ const inputUrl = 'https://w3c.github.io/json-ld-framing/tests/' + entry.input;
286
+ const callOpts = { base: opt.base || inputUrl };
287
+ if (opt.processingMode) callOpts.processingMode = opt.processingMode;
288
+ else if (opt.specVersion) callOpts.processingMode = opt.specVersion;
289
+ if (opt.omitGraph != null) callOpts.omitGraph = opt.omitGraph;
290
+ if (opt.requireAll != null) callOpts.requireAll = opt.requireAll;
291
+ try {
292
+ const input = loadJson(inputPath);
293
+ const frame = loadJson(framePath);
294
+ const napiResult = await withTimeout(proc.frame(input, frame, callOpts), id);
295
+ if (!expectPath || !existsSync(expectPath)) { record('frame', id, 'err'); continue; }
296
+ const expected = loadJson(expectPath);
297
+ record('frame', id, jsonEqual(napiResult, expected) ? 'pass' : 'fail');
298
+ } catch (e) {
299
+ const isTimeout = e && /^timeout:/.test(e.message || '');
300
+ record('frame', id, isTimeout ? 'err' : 'err');
301
+ }
302
+ }
303
+ }
304
+
305
+
234
306
  async function main() {
235
307
  if (!existsSync(JLD_TESTS) || !existsSync(RDFC_TESTS)) {
236
308
  console.error('W3C test suites not found at ' + TESTS_ROOT);
237
309
  console.error('Run: vc/js/scripts/fetch-w3c-tests.sh');
238
310
  process.exit(2);
239
311
  }
240
- // flatten and toRdf are not in the default run because clientffi has
241
- // Rust-level deadlocks in those algos. Pass --only flatten or --only toRdf
242
- // to opt in when investigating. They'll move into the default list once
243
- // their hangs are fixed.
244
- const algos = ['canonize', 'fromRdf', 'expand', 'compact'];
245
- const allAlgos = ['expand', 'compact', 'flatten', 'toRdf', 'fromRdf', 'canonize'];
312
+ // flatten and toRdf are gated by V2: the v1 path has Rust-level deadlocks
313
+ // (sync block_on bridging into tokio::sync locks in the document loader and
314
+ // node_map). The v2 path (jsonld_v2::flatten_v2 / to_nquads_v2) uses a
315
+ // pre-resolved HashMap loader with no tokio sync primitives and runs the
316
+ // suite without hangs, so it is included in the default list.
317
+ const algos = ['canonize', 'fromRdf', 'expand', 'compact', 'flatten', 'toRdf', 'frame'];
318
+ const allAlgos = ['expand', 'compact', 'flatten', 'toRdf', 'fromRdf', 'canonize', 'frame'];
246
319
  if (onlyArg && !allAlgos.includes(onlyArg)) {
247
320
  console.error(`Invalid --only value: ${onlyArg}. Expected one of: ${allAlgos.join(', ')}`);
248
321
  process.exit(2);