@factoidal/core 0.10.0 → 0.12.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,23 @@
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
+ import { FactoidalError } from './lib/errors.mjs';
19
+
20
+ // RDF/JS Data Model defaults of DataFactory.literal.
21
+ const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string'; // purity-lint: allow RDF/JS DataFactory default
23
22
  const RDF_LANGSTRING =
24
- 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString';
23
+ 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString'; // purity-lint: allow RDF/JS DataFactory default
24
+ const RDF_DIRLANGSTRING =
25
+ 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString'; // purity-lint: allow RDF/JS DataFactory default
25
26
 
26
27
  // ---------------------------------------------------------------------
27
28
  // Terms
@@ -52,18 +53,23 @@ class BlankNode {
52
53
  }
53
54
 
54
55
  class Literal {
55
- constructor(value, language, datatype) {
56
+ constructor(value, language, datatype, direction) {
56
57
  this.termType = 'Literal';
57
58
  this.value = value;
58
59
  this.language = language || '';
60
+ // RDF/JS Data Model DataFactory.literal: no datatype argument means
61
+ // xsd:string, a language means rdf:langString, a language and a
62
+ // direction mean rdf:dirLangString.
59
63
  this.datatype = datatype ||
60
- new NamedNode(language ? RDF_LANGSTRING : XSD_STRING);
64
+ new NamedNode(language ? (direction ? RDF_DIRLANGSTRING : RDF_LANGSTRING) : XSD_STRING); // purity-lint: allow RDF/JS DataFactory default
65
+ this.direction = direction || '';
61
66
  Object.freeze(this);
62
67
  }
63
68
  equals(other) {
64
69
  return !!other && other.termType === 'Literal' &&
65
70
  other.value === this.value &&
66
71
  other.language === this.language &&
72
+ (other.direction || '') === this.direction &&
67
73
  !!other.datatype && other.datatype.value === this.datatype.value;
68
74
  }
69
75
  }
@@ -139,6 +145,11 @@ const dataFactory = {
139
145
  if (typeof languageOrDatatype === 'string') {
140
146
  return new Literal(v, languageOrDatatype, null);
141
147
  }
148
+ // RDF/JS Data Model 1.1: {language, direction} for a directional
149
+ // language-tagged string.
150
+ if (typeof languageOrDatatype.language === 'string' && languageOrDatatype.termType === undefined) {
151
+ return new Literal(v, languageOrDatatype.language, null, languageOrDatatype.direction || '');
152
+ }
142
153
  // A NamedNode datatype.
143
154
  if (languageOrDatatype.termType === 'NamedNode') {
144
155
  if (languageOrDatatype.value === RDF_LANGSTRING) {
@@ -174,10 +185,8 @@ const dataFactory = {
174
185
  return new Literal(
175
186
  original.value,
176
187
  original.language || '',
177
- original.language
178
- ? null
179
- : new NamedNode(
180
- (original.datatype && original.datatype.value) || XSD_STRING)
188
+ original.datatype ? new NamedNode(original.datatype.value) : null,
189
+ original.direction || ''
181
190
  );
182
191
  case 'Quad': return dataFactory.fromQuad(original);
183
192
  default:
@@ -207,31 +216,38 @@ const dataFactory = {
207
216
  * datatype}) into an RDF/JS term.
208
217
  */
209
218
  function termFromSrj(t) {
219
+ return termFromSrjWith(t, dataFactory);
220
+ }
221
+
222
+ function termFromSrjWith(t, f) {
210
223
  if (!t || typeof t.type !== 'string') {
211
224
  throw new TypeError('termFromSrj: not a results-JSON term');
212
225
  }
213
226
  switch (t.type) {
214
227
  case 'uri':
215
- return dataFactory.namedNode(t.value);
228
+ return f.namedNode(t.value);
216
229
  case 'bnode':
217
- return dataFactory.blankNode(t.value);
230
+ return f.blankNode(t.value);
218
231
  case 'literal':
219
232
  case 'typed-literal': {
220
233
  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));
234
+ if (lang) {
235
+ if (t['its:dir'] && f === dataFactory) {
236
+ return new Literal(t.value, lang,
237
+ t.datatype ? new NamedNode(t.datatype) : null, t['its:dir']);
238
+ }
239
+ return f.literal(t.value, lang);
224
240
  }
225
- return dataFactory.literal(t.value);
241
+ if (t.datatype) return f.literal(t.value, f.namedNode(t.datatype));
242
+ return f.literal(t.value);
226
243
  }
227
244
  case 'triple': {
228
- // RDF 1.2 triple term in SPARQL Results JSON:
245
+ // RDF 1.2 triple term:
229
246
  // {"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.
247
+ // -> an RDF/JS Quad term. The object may itself be a triple term.
232
248
  const v = t.value || {};
233
- return dataFactory.quad(
234
- termFromSrj(v.subject), termFromSrj(v.predicate), termFromSrj(v.object));
249
+ return f.quad(termFromSrjWith(v.subject, f), termFromSrjWith(v.predicate, f),
250
+ termFromSrjWith(v.object, f));
235
251
  }
236
252
  default:
237
253
  throw new TypeError(`termFromSrj: unknown term type '${t.type}'`);
@@ -239,231 +255,132 @@ function termFromSrj(t) {
239
255
  }
240
256
 
241
257
  // ---------------------------------------------------------------------
242
- // N-Quads token output (mirrors RDF.NQuads.Serialize.escape_char)
258
+ // RDF/JS term <-> engine term object (Lean `L4Factoidal.RDF.TermJson`).
259
+ //
260
+ // These functions copy fields; they read and write no RDF syntax. Every
261
+ // N-Quads parse and write below is an engine op
262
+ // (https://github.com/danbri/factoidal/issues/894 items 1 and 2).
243
263
  // ---------------------------------------------------------------------
244
264
 
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) {
265
+ /** The engine term object of an RDF/JS term (`TermJson.encodeTerm`'s shape). */
266
+ function termToJson(term) {
267
+ if (term === null || term === undefined) return null;
268
+ // A term given as text is one RDF 1.2 N-Triples term (`<IRI>`, `_:label`,
269
+ // a literal, `<<( s p o )>>`). The engine reads it
270
+ // (L4Factoidal.RDF.TermJson.readTermString); this file copies the text.
271
+ if (typeof term === 'string') return { type: 'nt', value: term };
262
272
  switch (term.termType) {
263
- case 'NamedNode':
264
- return '<' + term.value + '>';
265
- case 'BlankNode':
266
- return '_:' + term.value;
273
+ case 'NamedNode': return { type: 'uri', value: term.value };
274
+ case 'BlankNode': return { type: 'bnode', value: term.value };
267
275
  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;
276
+ const o = { type: 'literal', value: term.value };
277
+ if (term.datatype) o.datatype = term.datatype.value;
278
+ if (term.language) o['xml:lang'] = term.language;
279
+ if (term.direction) o['its:dir'] = term.direction;
280
+ return o;
274
281
  }
275
- case 'DefaultGraph':
276
- return '';
277
282
  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) + ' )>>';
283
+ return { type: 'triple', value: {
284
+ subject: termToJson(term.subject), predicate: termToJson(term.predicate),
285
+ object: termToJson(term.object) } };
286
+ case 'DefaultGraph': return { type: 'default' };
284
287
  default:
285
- throw new TypeError(
286
- `termToNQuads: cannot serialize termType '${term.termType}'`);
288
+ throw new TypeError(`termToJson: cannot send termType '${term.termType}' to the engine`);
287
289
  }
288
290
  }
289
291
 
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';
292
+ /**
293
+ * The engine term object of a term an application supplies, for example an
294
+ * extension function's answer: an RDF/JS term (duck-typed on termType), an
295
+ * engine term object ({type, value, ...}), or a plain object
296
+ * {value, datatype} or {value, language, direction?}. The components are
297
+ * copied as given; the engine checks them (TermJson.decodeCheckedTerm) and
298
+ * keeps the lexical form and the datatype IRI unchanged.
299
+ */
300
+ function appTermToJson(out) {
301
+ if (typeof out.termType === 'string') return termToJson(out);
302
+ if (typeof out.type === 'string') return out;
303
+ const o = { type: 'literal', value: out.value };
304
+ if (out.datatype !== undefined && out.datatype !== null) {
305
+ o.datatype = typeof out.datatype === 'string' ? out.datatype : out.datatype.value;
306
+ }
307
+ if (out.language !== undefined && out.language !== null && out.language !== '') o['xml:lang'] = out.language;
308
+ if (out.direction !== undefined && out.direction !== null && out.direction !== '') o['its:dir'] = out.direction;
309
+ return o;
298
310
  }
299
311
 
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);
312
+ /** The engine quad array of an RDF/JS quad: [s, p, o] or [s, p, o, g]. */
313
+ function quadToJson(quad) {
314
+ const out = [termToJson(quad.subject), termToJson(quad.predicate), termToJson(quad.object)];
315
+ if (quad.graph && quad.graph.termType !== 'DefaultGraph') out.push(termToJson(quad.graph));
304
316
  return out;
305
317
  }
306
318
 
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
- // ---------------------------------------------------------------------
319
+ /** The RDF/JS quad of an engine quad array. */
320
+ function quadFromJson(arr, factory) {
321
+ const f = factory || dataFactory;
322
+ return f.quad(termFromSrjWith(arr[0], f), termFromSrjWith(arr[1], f),
323
+ termFromSrjWith(arr[2], f), arr.length > 3 ? termFromSrjWith(arr[3], f) : f.defaultGraph());
324
+ }
316
325
 
317
- const UNESCAPE_MAP = {
318
- 't': '\t', 'b': '\b', 'n': '\n', 'r': '\r', 'f': '\f',
319
- '"': '"', "'": "'", '\\': '\\',
320
- };
326
+ // The engine the text functions below call. lib/api.mjs installs it when it
327
+ // loads the engine; until then they throw.
328
+ let textEngine = null;
321
329
 
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;
330
+ /** @private lib/api.mjs: install the engine ops the text functions call. */
331
+ function _setTextEngine(engine) { textEngine = engine; }
332
+
333
+ function needEngine(who) {
334
+ if (!textEngine) {
335
+ throw new FactoidalError(
336
+ `${who}: N-Quads text is read and written by the engine, which is not ` +
337
+ 'loaded yet. Await any @factoidal/core call first (for example ' +
338
+ '`await ready()`), or use parse() and Dataset.toString().', { code: 'op.unknown' });
339
+ }
340
+ return textEngine;
344
341
  }
345
342
 
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}`);
343
+ /** One RDF/JS term as its canonical N-Quads token (the engine writes it). */
344
+ function termToNQuads(term) {
345
+ if (term && term.termType === 'DefaultGraph') return '';
346
+ return needEngine('termToNQuads').termTokens([termToJson(term)])[0];
408
347
  }
409
348
 
410
- function skipWs(s, i) {
411
- while (i < s.length && (s[i] === ' ' || s[i] === '\t')) i++;
412
- return i;
349
+ /** One RDF/JS quad as its canonical N-Quads line (with trailing newline). */
350
+ function quadToNQuads(quad) {
351
+ return needEngine('quadToNQuads').writeQuads([quadToJson(quad)]);
413
352
  }
414
353
 
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());
354
+ /** An iterable of RDF/JS quads as an N-Quads document, one line per quad. */
355
+ function quadsToNQuads(quads) {
356
+ const arr = [];
357
+ for (const q of quads) arr.push(quadToJson(q));
358
+ return needEngine('quadsToNQuads').writeQuads(arr);
437
359
  }
438
360
 
439
361
  /**
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).
362
+ * Parse an N-Quads document (RDF 1.2 grammar) into RDF/JS quads; the engine
363
+ * parses it and rejects any line the grammar rejects.
364
+ * The quads come in the engine's order: the default graph, then each named
365
+ * graph in the order of its first quad.
366
+ * options.blankNodePrefix: prepended to every blank-node label (keeps the
367
+ * labels of separate documents apart); options.factory: an RDF/JS
368
+ * DataFactory.
447
369
  */
448
370
  function nquadsToQuads(text, options) {
449
371
  const opts = options || {};
450
372
  const factory = opts.factory || dataFactory;
451
373
  const f = opts.blankNodePrefix
452
- ? {
453
- ...factory,
454
- blankNode: (label) => factory.blankNode(
455
- label === undefined || label === null
456
- ? undefined
457
- : opts.blankNodePrefix + label),
458
- }
374
+ ? { ...factory, blankNode: (label) => factory.blankNode(opts.blankNodePrefix + label) }
459
375
  : 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;
376
+ const quads = needEngine('nquadsToQuads').parseQuads(String(text));
377
+ return quads.map((q) => quadFromJson(q, f));
378
+ }
379
+
380
+ /** One N-Quads statement as an RDF/JS quad; null for a blank or comment line. */
381
+ function nquadsLineToQuad(line, _lineNo, factory) {
382
+ const qs = nquadsToQuads(line, { factory });
383
+ return qs.length ? qs[0] : null;
467
384
  }
468
385
 
469
386
  // ---------------------------------------------------------------------
@@ -481,7 +398,8 @@ function termKey(t) {
481
398
  if (!t) return 'null';
482
399
  switch (t.termType) {
483
400
  case 'Literal':
484
- return JSON.stringify(['Literal', t.value, t.language, t.datatype ? t.datatype.value : null]);
401
+ return JSON.stringify(['Literal', t.value, t.language, t.datatype ? t.datatype.value : null,
402
+ t.direction || '']);
485
403
  case 'DefaultGraph':
486
404
  return '["DefaultGraph"]';
487
405
  case 'Quad':
@@ -562,7 +480,7 @@ class DatasetCore {
562
480
  toArray() {
563
481
  return this._quads.slice();
564
482
  }
565
- /** N-Quads text — the engine interchange handle. */
483
+ /** N-Quads text, written by the engine. */
566
484
  toNQuads() {
567
485
  return quadsToNQuads(this._quads);
568
486
  }
@@ -584,6 +502,13 @@ export {
584
502
  Quad,
585
503
  DatasetCore,
586
504
  termFromSrj,
505
+ termToJson,
506
+ appTermToJson,
507
+ quadToJson,
508
+ quadFromJson,
509
+ quadKey,
510
+ termKey,
511
+ _setTextEngine,
587
512
  nquadsLineToQuad,
588
513
  termToNQuads,
589
514
  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