@factoidal/core 0.10.0 → 0.11.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/rdfjs.mjs CHANGED
@@ -6,22 +6,21 @@
6
6
  // terms with spec-compliant `termType` / `value` / `language` /
7
7
  // `datatype` and `.equals()`. Self-contained — no runtime deps.
8
8
  //
9
- // Also provides converters between RDF/JS quads and the engine's
10
- // N-Quads token strings, which are the package's dataset interchange
11
- // handle (the Lean engine parses and serializes N-Quads; the
12
- // converters below only tokenize/untokenize the engine's own output —
13
- // full RDF parsing of user input always goes through the engine).
9
+ // Also: the field copy between RDF/JS terms and the engine's term objects
10
+ // (Lean L4Factoidal.RDF.TermJson), and N-Quads text in and out through
11
+ // engine ops. This file reads and writes no RDF syntax itself: no escaping,
12
+ // no tokenising (https://github.com/danbri/factoidal/issues/894). The text
13
+ // functions need the engine, which lib/api.mjs installs when it loads.
14
14
  //
15
- // ES module (0.9.0): the same file serves Node (index.mjs, and
16
- // rdfjs.js for require()) and the browser (browser.js), with no build
17
- // step. On output, \\ \" \n \r \t (and nothing else) are escaped;
18
- // unescaping additionally accepts the full N-Triples ECHAR + UCHAR set
19
- // (\t \b \n \r \f \" \' \\ \uXXXX \UXXXXXXXX) since engine input may
20
- // legally contain them.
21
-
22
- const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
15
+ // ES module (0.9.0): the same file serves Node (index.mjs, and rdfjs.js for
16
+ // require()) and the browser (browser.js), with no build step.
17
+
18
+ // RDF/JS Data Model defaults of DataFactory.literal.
19
+ const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string'; // purity-lint: allow RDF/JS DataFactory default
23
20
  const RDF_LANGSTRING =
24
- 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString';
21
+ 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString'; // purity-lint: allow RDF/JS DataFactory default
22
+ const RDF_DIRLANGSTRING =
23
+ 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString'; // purity-lint: allow RDF/JS DataFactory default
25
24
 
26
25
  // ---------------------------------------------------------------------
27
26
  // Terms
@@ -52,18 +51,23 @@ class BlankNode {
52
51
  }
53
52
 
54
53
  class Literal {
55
- constructor(value, language, datatype) {
54
+ constructor(value, language, datatype, direction) {
56
55
  this.termType = 'Literal';
57
56
  this.value = value;
58
57
  this.language = language || '';
58
+ // RDF/JS Data Model DataFactory.literal: no datatype argument means
59
+ // xsd:string, a language means rdf:langString, a language and a
60
+ // direction mean rdf:dirLangString.
59
61
  this.datatype = datatype ||
60
- new NamedNode(language ? RDF_LANGSTRING : XSD_STRING);
62
+ new NamedNode(language ? (direction ? RDF_DIRLANGSTRING : RDF_LANGSTRING) : XSD_STRING); // purity-lint: allow RDF/JS DataFactory default
63
+ this.direction = direction || '';
61
64
  Object.freeze(this);
62
65
  }
63
66
  equals(other) {
64
67
  return !!other && other.termType === 'Literal' &&
65
68
  other.value === this.value &&
66
69
  other.language === this.language &&
70
+ (other.direction || '') === this.direction &&
67
71
  !!other.datatype && other.datatype.value === this.datatype.value;
68
72
  }
69
73
  }
@@ -139,6 +143,11 @@ const dataFactory = {
139
143
  if (typeof languageOrDatatype === 'string') {
140
144
  return new Literal(v, languageOrDatatype, null);
141
145
  }
146
+ // RDF/JS Data Model 1.1: {language, direction} for a directional
147
+ // language-tagged string.
148
+ if (typeof languageOrDatatype.language === 'string' && languageOrDatatype.termType === undefined) {
149
+ return new Literal(v, languageOrDatatype.language, null, languageOrDatatype.direction || '');
150
+ }
142
151
  // A NamedNode datatype.
143
152
  if (languageOrDatatype.termType === 'NamedNode') {
144
153
  if (languageOrDatatype.value === RDF_LANGSTRING) {
@@ -174,10 +183,8 @@ const dataFactory = {
174
183
  return new Literal(
175
184
  original.value,
176
185
  original.language || '',
177
- original.language
178
- ? null
179
- : new NamedNode(
180
- (original.datatype && original.datatype.value) || XSD_STRING)
186
+ original.datatype ? new NamedNode(original.datatype.value) : null,
187
+ original.direction || ''
181
188
  );
182
189
  case 'Quad': return dataFactory.fromQuad(original);
183
190
  default:
@@ -207,31 +214,38 @@ const dataFactory = {
207
214
  * datatype}) into an RDF/JS term.
208
215
  */
209
216
  function termFromSrj(t) {
217
+ return termFromSrjWith(t, dataFactory);
218
+ }
219
+
220
+ function termFromSrjWith(t, f) {
210
221
  if (!t || typeof t.type !== 'string') {
211
222
  throw new TypeError('termFromSrj: not a results-JSON term');
212
223
  }
213
224
  switch (t.type) {
214
225
  case 'uri':
215
- return dataFactory.namedNode(t.value);
226
+ return f.namedNode(t.value);
216
227
  case 'bnode':
217
- return dataFactory.blankNode(t.value);
228
+ return f.blankNode(t.value);
218
229
  case 'literal':
219
230
  case 'typed-literal': {
220
231
  const lang = t['xml:lang'];
221
- if (lang) return dataFactory.literal(t.value, lang);
222
- if (t.datatype) {
223
- return dataFactory.literal(t.value, dataFactory.namedNode(t.datatype));
232
+ if (lang) {
233
+ if (t['its:dir'] && f === dataFactory) {
234
+ return new Literal(t.value, lang,
235
+ t.datatype ? new NamedNode(t.datatype) : null, t['its:dir']);
236
+ }
237
+ return f.literal(t.value, lang);
224
238
  }
225
- return dataFactory.literal(t.value);
239
+ if (t.datatype) return f.literal(t.value, f.namedNode(t.datatype));
240
+ return f.literal(t.value);
226
241
  }
227
242
  case 'triple': {
228
- // RDF 1.2 triple term in SPARQL Results JSON:
243
+ // RDF 1.2 triple term:
229
244
  // {"type":"triple","value":{"subject":T,"predicate":T,"object":T}}
230
- // (SPARQL_Protocol.json_term) -> an RDF/JS Quad term. Recursive:
231
- // the object may itself be a triple term.
245
+ // -> an RDF/JS Quad term. The object may itself be a triple term.
232
246
  const v = t.value || {};
233
- return dataFactory.quad(
234
- termFromSrj(v.subject), termFromSrj(v.predicate), termFromSrj(v.object));
247
+ return f.quad(termFromSrjWith(v.subject, f), termFromSrjWith(v.predicate, f),
248
+ termFromSrjWith(v.object, f));
235
249
  }
236
250
  default:
237
251
  throw new TypeError(`termFromSrj: unknown term type '${t.type}'`);
@@ -239,231 +253,132 @@ function termFromSrj(t) {
239
253
  }
240
254
 
241
255
  // ---------------------------------------------------------------------
242
- // N-Quads token output (mirrors RDF.NQuads.Serialize.escape_char)
256
+ // RDF/JS term <-> engine term object (Lean `L4Factoidal.RDF.TermJson`).
257
+ //
258
+ // These functions copy fields; they read and write no RDF syntax. Every
259
+ // N-Quads parse and write below is an engine op
260
+ // (https://github.com/danbri/factoidal/issues/894 items 1 and 2).
243
261
  // ---------------------------------------------------------------------
244
262
 
245
- function escapeLiteral(s) {
246
- let out = '';
247
- for (const ch of s) {
248
- switch (ch) {
249
- case '\\': out += '\\\\'; break;
250
- case '"': out += '\\"'; break;
251
- case '\n': out += '\\n'; break;
252
- case '\r': out += '\\r'; break;
253
- case '\t': out += '\\t'; break;
254
- default: out += ch;
255
- }
256
- }
257
- return out;
258
- }
259
-
260
- /** Serialize one RDF/JS term to its N-Quads token. */
261
- function termToNQuads(term) {
263
+ /** The engine term object of an RDF/JS term (`TermJson.encodeTerm`'s shape). */
264
+ function termToJson(term) {
265
+ if (term === null || term === undefined) return null;
266
+ // A term given as text is one RDF 1.2 N-Triples term (`<IRI>`, `_:label`,
267
+ // a literal, `<<( s p o )>>`). The engine reads it
268
+ // (L4Factoidal.RDF.TermJson.readTermString); this file copies the text.
269
+ if (typeof term === 'string') return { type: 'nt', value: term };
262
270
  switch (term.termType) {
263
- case 'NamedNode':
264
- return '<' + term.value + '>';
265
- case 'BlankNode':
266
- return '_:' + term.value;
271
+ case 'NamedNode': return { type: 'uri', value: term.value };
272
+ case 'BlankNode': return { type: 'bnode', value: term.value };
267
273
  case 'Literal': {
268
- const body = '"' + escapeLiteral(term.value) + '"';
269
- if (term.language) return body + '@' + term.language;
270
- if (term.datatype && term.datatype.value !== XSD_STRING) {
271
- return body + '^^<' + term.datatype.value + '>';
272
- }
273
- return body;
274
+ const o = { type: 'literal', value: term.value };
275
+ if (term.datatype) o.datatype = term.datatype.value;
276
+ if (term.language) o['xml:lang'] = term.language;
277
+ if (term.direction) o['its:dir'] = term.direction;
278
+ return o;
274
279
  }
275
- case 'DefaultGraph':
276
- return '';
277
280
  case 'Quad':
278
- // RDF 1.2 triple term -> <<( s p o )>> (matches the engine's
279
- // Mode_12 N-Quads serializer, so it round-trips back through the
280
- // *12 parse path).
281
- return '<<( ' + termToNQuads(term.subject) + ' ' +
282
- termToNQuads(term.predicate) + ' ' +
283
- termToNQuads(term.object) + ' )>>';
281
+ return { type: 'triple', value: {
282
+ subject: termToJson(term.subject), predicate: termToJson(term.predicate),
283
+ object: termToJson(term.object) } };
284
+ case 'DefaultGraph': return { type: 'default' };
284
285
  default:
285
- throw new TypeError(
286
- `termToNQuads: cannot serialize termType '${term.termType}'`);
286
+ throw new TypeError(`termToJson: cannot send termType '${term.termType}' to the engine`);
287
287
  }
288
288
  }
289
289
 
290
- /** Serialize one RDF/JS quad to an N-Quads line (with trailing " .\n"). */
291
- function quadToNQuads(quad) {
292
- const g = quad.graph && quad.graph.termType !== 'DefaultGraph'
293
- ? ' ' + termToNQuads(quad.graph)
294
- : '';
295
- return termToNQuads(quad.subject) + ' ' +
296
- termToNQuads(quad.predicate) + ' ' +
297
- termToNQuads(quad.object) + g + ' .\n';
290
+ /**
291
+ * The engine term object of a term an application supplies, for example an
292
+ * extension function's answer: an RDF/JS term (duck-typed on termType), an
293
+ * engine term object ({type, value, ...}), or a plain object
294
+ * {value, datatype} or {value, language, direction?}. The components are
295
+ * copied as given; the engine checks them (TermJson.decodeCheckedTerm) and
296
+ * keeps the lexical form and the datatype IRI unchanged.
297
+ */
298
+ function appTermToJson(out) {
299
+ if (typeof out.termType === 'string') return termToJson(out);
300
+ if (typeof out.type === 'string') return out;
301
+ const o = { type: 'literal', value: out.value };
302
+ if (out.datatype !== undefined && out.datatype !== null) {
303
+ o.datatype = typeof out.datatype === 'string' ? out.datatype : out.datatype.value;
304
+ }
305
+ if (out.language !== undefined && out.language !== null && out.language !== '') o['xml:lang'] = out.language;
306
+ if (out.direction !== undefined && out.direction !== null && out.direction !== '') o['its:dir'] = out.direction;
307
+ return o;
298
308
  }
299
309
 
300
- /** Serialize an iterable of quads to an N-Quads document. */
301
- function quadsToNQuads(quads) {
302
- let out = '';
303
- for (const q of quads) out += quadToNQuads(q);
310
+ /** The engine quad array of an RDF/JS quad: [s, p, o] or [s, p, o, g]. */
311
+ function quadToJson(quad) {
312
+ const out = [termToJson(quad.subject), termToJson(quad.predicate), termToJson(quad.object)];
313
+ if (quad.graph && quad.graph.termType !== 'DefaultGraph') out.push(termToJson(quad.graph));
304
314
  return out;
305
315
  }
306
316
 
307
- // ---------------------------------------------------------------------
308
- // N-Quads token input.
309
- //
310
- // A line tokenizer for the engine's own N-Quads output (and canonical
311
- // N-Quads generally): one statement per line, terms separated by
312
- // whitespace, ECHAR/UCHAR escapes inside literals. This is a token
313
- // reader for a machine-generated regular syntax, not a general RDF
314
- // parser — user documents in any syntax go through the engine.
315
- // ---------------------------------------------------------------------
317
+ /** The RDF/JS quad of an engine quad array. */
318
+ function quadFromJson(arr, factory) {
319
+ const f = factory || dataFactory;
320
+ return f.quad(termFromSrjWith(arr[0], f), termFromSrjWith(arr[1], f),
321
+ termFromSrjWith(arr[2], f), arr.length > 3 ? termFromSrjWith(arr[3], f) : f.defaultGraph());
322
+ }
316
323
 
317
- const UNESCAPE_MAP = {
318
- 't': '\t', 'b': '\b', 'n': '\n', 'r': '\r', 'f': '\f',
319
- '"': '"', "'": "'", '\\': '\\',
320
- };
324
+ // The engine the text functions below call. lib/api.mjs installs it when it
325
+ // loads the engine; until then they throw.
326
+ let textEngine = null;
321
327
 
322
- function unescapeLiteral(s, lineNo) {
323
- if (s.indexOf('\\') < 0) return s;
324
- let out = '';
325
- for (let i = 0; i < s.length; i++) {
326
- const c = s[i];
327
- if (c !== '\\') { out += c; continue; }
328
- const e = s[++i];
329
- if (e in UNESCAPE_MAP) { out += UNESCAPE_MAP[e]; continue; }
330
- if (e === 'u') {
331
- out += String.fromCodePoint(parseInt(s.slice(i + 1, i + 5), 16));
332
- i += 4;
333
- continue;
334
- }
335
- if (e === 'U') {
336
- out += String.fromCodePoint(parseInt(s.slice(i + 1, i + 9), 16));
337
- i += 8;
338
- continue;
339
- }
340
- throw new SyntaxError(
341
- `N-Quads line ${lineNo}: bad escape '\\${e}'`);
342
- }
343
- return out;
328
+ /** @private lib/api.mjs: install the engine ops the text functions call. */
329
+ function _setTextEngine(engine) { textEngine = engine; }
330
+
331
+ function needEngine(who) {
332
+ if (!textEngine) {
333
+ throw new Error(
334
+ `${who}: N-Quads text is read and written by the engine, which is not ` +
335
+ 'loaded yet. Await any @factoidal/core call first (for example ' +
336
+ '`await ready()`), or use parse() and Dataset.toString().');
337
+ }
338
+ return textEngine;
344
339
  }
345
340
 
346
- // Read one term starting at s[i]; returns [term, nextIndex].
347
- function readTerm(s, i, lineNo, factory) {
348
- const c = s[i];
349
- // RDF 1.2 triple term: <<( s p o )>> -> an RDF/JS Quad term
350
- // (termType 'Quad'). Checked BEFORE the '<' IRI case since a triple
351
- // term also opens with '<'. This reads the engine's own Mode_12
352
- // N-Quads serializer output (RDF.NQuads.Serialize) into RDF/JS
353
- // objects; it is not a conformance parser (that is the F* --rdf12
354
- // path) — same interop role the rest of this reader plays.
355
- if (c === '<' && s[i + 1] === '<' && s[i + 2] === '(') {
356
- let j = skipWs(s, i + 3);
357
- const [sub, a] = readTerm(s, j, lineNo, factory); j = skipWs(s, a);
358
- const [pred, b] = readTerm(s, j, lineNo, factory); j = skipWs(s, b);
359
- const [obj, d] = readTerm(s, j, lineNo, factory); j = skipWs(s, d);
360
- if (s[j] === ')' && s[j + 1] === '>' && s[j + 2] === '>') {
361
- return [factory.quad(sub, pred, obj), j + 3];
362
- }
363
- throw new SyntaxError(
364
- `N-Quads line ${lineNo}: unterminated triple term (expected ')>>')`);
365
- }
366
- if (c === '<') {
367
- const end = s.indexOf('>', i + 1);
368
- if (end < 0) throw new SyntaxError(`N-Quads line ${lineNo}: unclosed IRI`);
369
- return [factory.namedNode(s.slice(i + 1, end)), end + 1];
370
- }
371
- if (c === '_' && s[i + 1] === ':') {
372
- let j = i + 2;
373
- while (j < s.length && !/\s/.test(s[j])) j++;
374
- return [factory.blankNode(s.slice(i + 2, j)), j];
375
- }
376
- if (c === '"') {
377
- // Scan to the closing unescaped quote.
378
- let j = i + 1;
379
- for (;;) {
380
- if (j >= s.length) {
381
- throw new SyntaxError(`N-Quads line ${lineNo}: unclosed literal`);
382
- }
383
- if (s[j] === '\\') { j += 2; continue; }
384
- if (s[j] === '"') break;
385
- j++;
386
- }
387
- const lex = unescapeLiteral(s.slice(i + 1, j), lineNo);
388
- j++; // past closing quote
389
- if (s[j] === '@') {
390
- let k = j + 1;
391
- while (k < s.length && !/\s/.test(s[k])) k++;
392
- return [factory.literal(lex, s.slice(j + 1, k)), k];
393
- }
394
- if (s[j] === '^' && s[j + 1] === '^' && s[j + 2] === '<') {
395
- const end = s.indexOf('>', j + 3);
396
- if (end < 0) {
397
- throw new SyntaxError(`N-Quads line ${lineNo}: unclosed datatype IRI`);
398
- }
399
- return [
400
- factory.literal(lex, factory.namedNode(s.slice(j + 3, end))),
401
- end + 1,
402
- ];
403
- }
404
- return [factory.literal(lex), j];
405
- }
406
- throw new SyntaxError(
407
- `N-Quads line ${lineNo}: unexpected character '${c}' at column ${i + 1}`);
341
+ /** One RDF/JS term as its canonical N-Quads token (the engine writes it). */
342
+ function termToNQuads(term) {
343
+ if (term && term.termType === 'DefaultGraph') return '';
344
+ return needEngine('termToNQuads').termTokens([termToJson(term)])[0];
408
345
  }
409
346
 
410
- function skipWs(s, i) {
411
- while (i < s.length && (s[i] === ' ' || s[i] === '\t')) i++;
412
- return i;
347
+ /** One RDF/JS quad as its canonical N-Quads line (with trailing newline). */
348
+ function quadToNQuads(quad) {
349
+ return needEngine('quadToNQuads').writeQuads([quadToJson(quad)]);
413
350
  }
414
351
 
415
- /**
416
- * Tokenize one N-Quads statement line into an RDF/JS quad.
417
- * Returns null for blank/comment lines.
418
- */
419
- function nquadsLineToQuad(line, lineNo, factory) {
420
- const f = factory || dataFactory;
421
- let i = skipWs(line, 0);
422
- if (i >= line.length || line[i] === '#') return null;
423
- const terms = [];
424
- while (terms.length < 4) {
425
- const [term, next] = readTerm(line, i, lineNo, f);
426
- terms.push(term);
427
- i = skipWs(line, next);
428
- if (line[i] === '.') break;
429
- }
430
- if (line[i] !== '.') {
431
- throw new SyntaxError(`N-Quads line ${lineNo}: missing terminating '.'`);
432
- }
433
- if (terms.length < 3) {
434
- throw new SyntaxError(`N-Quads line ${lineNo}: fewer than 3 terms`);
435
- }
436
- return f.quad(terms[0], terms[1], terms[2], terms[3] || f.defaultGraph());
352
+ /** An iterable of RDF/JS quads as an N-Quads document, one line per quad. */
353
+ function quadsToNQuads(quads) {
354
+ const arr = [];
355
+ for (const q of quads) arr.push(quadToJson(q));
356
+ return needEngine('quadsToNQuads').writeQuads(arr);
437
357
  }
438
358
 
439
359
  /**
440
- * Tokenize an engine-emitted N-Quads document into RDF/JS quads.
441
- *
442
- * options.blankNodePrefix — prepended to every blank-node label, used
443
- * by parse() to keep labels from separate parse calls distinct (blank
444
- * node identity is document-scoped; the prefix is bookkeeping, the
445
- * semantic per-document renaming lives in F*
446
- * RDF.Dataset.Merge.rename_dataset_bnodes).
360
+ * Parse an N-Quads document (RDF 1.2 grammar) into RDF/JS quads; the engine
361
+ * parses it and rejects any line the grammar rejects.
362
+ * The quads come in the engine's order: the default graph, then each named
363
+ * graph in the order of its first quad.
364
+ * options.blankNodePrefix: prepended to every blank-node label (keeps the
365
+ * labels of separate documents apart); options.factory: an RDF/JS
366
+ * DataFactory.
447
367
  */
448
368
  function nquadsToQuads(text, options) {
449
369
  const opts = options || {};
450
370
  const factory = opts.factory || dataFactory;
451
371
  const f = opts.blankNodePrefix
452
- ? {
453
- ...factory,
454
- blankNode: (label) => factory.blankNode(
455
- label === undefined || label === null
456
- ? undefined
457
- : opts.blankNodePrefix + label),
458
- }
372
+ ? { ...factory, blankNode: (label) => factory.blankNode(opts.blankNodePrefix + label) }
459
373
  : factory;
460
- const quads = [];
461
- const lines = String(text).split('\n');
462
- for (let n = 0; n < lines.length; n++) {
463
- const q = nquadsLineToQuad(lines[n].replace(/\r$/, ''), n + 1, f);
464
- if (q) quads.push(q);
465
- }
466
- return quads;
374
+ const quads = needEngine('nquadsToQuads').parseQuads(String(text));
375
+ return quads.map((q) => quadFromJson(q, f));
376
+ }
377
+
378
+ /** One N-Quads statement as an RDF/JS quad; null for a blank or comment line. */
379
+ function nquadsLineToQuad(line, _lineNo, factory) {
380
+ const qs = nquadsToQuads(line, { factory });
381
+ return qs.length ? qs[0] : null;
467
382
  }
468
383
 
469
384
  // ---------------------------------------------------------------------
@@ -481,7 +396,8 @@ function termKey(t) {
481
396
  if (!t) return 'null';
482
397
  switch (t.termType) {
483
398
  case 'Literal':
484
- return JSON.stringify(['Literal', t.value, t.language, t.datatype ? t.datatype.value : null]);
399
+ return JSON.stringify(['Literal', t.value, t.language, t.datatype ? t.datatype.value : null,
400
+ t.direction || '']);
485
401
  case 'DefaultGraph':
486
402
  return '["DefaultGraph"]';
487
403
  case 'Quad':
@@ -562,7 +478,7 @@ class DatasetCore {
562
478
  toArray() {
563
479
  return this._quads.slice();
564
480
  }
565
- /** N-Quads text — the engine interchange handle. */
481
+ /** N-Quads text, written by the engine. */
566
482
  toNQuads() {
567
483
  return quadsToNQuads(this._quads);
568
484
  }
@@ -584,6 +500,13 @@ export {
584
500
  Quad,
585
501
  DatasetCore,
586
502
  termFromSrj,
503
+ termToJson,
504
+ appTermToJson,
505
+ quadToJson,
506
+ quadFromJson,
507
+ quadKey,
508
+ termKey,
509
+ _setTextEngine,
587
510
  nquadsLineToQuad,
588
511
  termToNQuads,
589
512
  quadToNQuads,
@@ -158,17 +158,29 @@ export async function createSolidClient (options = {}) {
158
158
  const maxSteps = typeof options.maxSteps === 'number' ? options.maxSteps : 16
159
159
  const baseIri = typeof options.baseIri === 'string' ? options.baseIri : null
160
160
 
161
- /** Where to send a request whose target the engine wrote as a path.
162
- * This is address resolution, not IRI construction: the engine's
163
- * target is used verbatim when it is already absolute. */
161
+ /** Where to send a request whose target the engine wrote. The engine
162
+ * resolves the target against baseIri (RFC 3986 section 5.2, op
163
+ * resolveIri) and says whether it was already absolute
164
+ * (https://github.com/danbri/factoidal/issues/894 item 5). */
164
165
  function endpoint (target) {
165
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target)) return target
166
+ let r
167
+ try {
168
+ r = engine.call('resolveIri', [baseIri === null ? '' : baseIri, String(target)])
169
+ } catch (error) {
170
+ if (isUnknownOp(error)) {
171
+ throw new SolidClientHostError(
172
+ 'the loaded l4factoidal WebAssembly module does not carry the ' +
173
+ 'resolveIri operation; rebuild it with formal/lean4/Wasm/build-wasm.sh')
174
+ }
175
+ throw error
176
+ }
177
+ if (r.referenceAbsolute) return String(target)
166
178
  if (baseIri === null) {
167
179
  throw new SolidClientHostError(
168
- `the engine answered the path "${target}" and this client has no ` +
169
- 'baseIri to send it to; pass baseIri to createSolidClient')
180
+ `the engine answered the relative reference "${target}" and this client has no ` +
181
+ 'baseIri to resolve it against; pass baseIri to createSolidClient')
170
182
  }
171
- return baseIri.replace(/\/$/, '') + target
183
+ return r.iri
172
184
  }
173
185
 
174
186
  function callRequest (kind, args) {
@@ -244,9 +256,13 @@ export async function createSolidClient (options = {}) {
244
256
  if (interpretKind === null) return answer
245
257
  // The record says what came back; `target` says what was asked. The
246
258
  // storage walk needs both, and this is the engine's own target
247
- // echoed back, not a URL this file composed.
259
+ // echoed back, not a URL this file composed. `requestUri` is the URL
260
+ // the request went to (the `endpoint` call above): the base IRI of
261
+ // the representation (RFC 3986 §5.1.3), which the engine resolves
262
+ // relative references against.
248
263
  const interpretation = interpretationOf(
249
- callResponse(interpretKind, { ...record, target: request.target }))
264
+ callResponse(interpretKind, { ...record, target: request.target,
265
+ requestUri: endpoint(request.target) }))
250
266
  answer.interpretation = interpretation
251
267
  if (interpretation.continue !== true) return answer
252
268
  state = interpretation.state === undefined ? null : interpretation.state