@kgdiff/formats 0.0.0-stage → 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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/dist/detect.d.ts +47 -0
  4. package/dist/detect.d.ts.map +1 -0
  5. package/dist/detect.js +425 -0
  6. package/dist/detect.js.map +1 -0
  7. package/dist/index.d.ts +203 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +114 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/iri.d.ts +13 -0
  12. package/dist/iri.d.ts.map +1 -0
  13. package/dist/iri.js +91 -0
  14. package/dist/iri.js.map +1 -0
  15. package/dist/json.d.ts +30 -0
  16. package/dist/json.d.ts.map +1 -0
  17. package/dist/json.js +749 -0
  18. package/dist/json.js.map +1 -0
  19. package/dist/jsonld.d.ts +16 -0
  20. package/dist/jsonld.d.ts.map +1 -0
  21. package/dist/jsonld.js +1249 -0
  22. package/dist/jsonld.js.map +1 -0
  23. package/dist/obo.d.ts +25 -0
  24. package/dist/obo.d.ts.map +1 -0
  25. package/dist/obo.js +478 -0
  26. package/dist/obo.js.map +1 -0
  27. package/dist/prefixes.d.ts +49 -0
  28. package/dist/prefixes.d.ts.map +1 -0
  29. package/dist/prefixes.js +177 -0
  30. package/dist/prefixes.js.map +1 -0
  31. package/dist/rdf.d.ts +46 -0
  32. package/dist/rdf.d.ts.map +1 -0
  33. package/dist/rdf.js +1047 -0
  34. package/dist/rdf.js.map +1 -0
  35. package/dist/rdfxml.d.ts +8 -0
  36. package/dist/rdfxml.d.ts.map +1 -0
  37. package/dist/rdfxml.js +529 -0
  38. package/dist/rdfxml.js.map +1 -0
  39. package/dist/shared.d.ts +117 -0
  40. package/dist/shared.d.ts.map +1 -0
  41. package/dist/shared.js +355 -0
  42. package/dist/shared.js.map +1 -0
  43. package/dist/xml.d.ts +50 -0
  44. package/dist/xml.d.ts.map +1 -0
  45. package/dist/xml.js +508 -0
  46. package/dist/xml.js.map +1 -0
  47. package/package.json +55 -3
  48. package/src/detect.ts +435 -0
  49. package/src/index.ts +296 -0
  50. package/src/iri.ts +86 -0
  51. package/src/json.ts +835 -0
  52. package/src/jsonld.ts +1376 -0
  53. package/src/obo.ts +557 -0
  54. package/src/prefixes.ts +195 -0
  55. package/src/rdf.ts +1181 -0
  56. package/src/rdfxml.ts +633 -0
  57. package/src/shared.ts +408 -0
  58. package/src/xml.ts +550 -0
package/src/rdfxml.ts ADDED
@@ -0,0 +1,633 @@
1
+ /**
2
+ * RDF/XML → RDF quads (W3C RDF 1.1 XML Syntax), on top of the XML parser in `xml.ts`. The
3
+ * quads go through the same mapping as Turtle (`rdf.ts`), so a graph diffs identically in
4
+ * either syntax.
5
+ *
6
+ * The whole grammar: `rdf:RDF` or a single node element as the root; typed node elements;
7
+ * `rdf:about`, `rdf:ID` and `rdf:nodeID`; property attributes; `rdf:li`; `rdf:datatype`;
8
+ * `xml:lang` and `xml:base` scoping; empty property elements; `rdf:parseType` `Resource`,
9
+ * `Collection` and `Literal` (exclusive XML canonicalization, with comments); reification by
10
+ * `rdf:ID` on property elements; the legacy unqualified `about`, `ID`, `resource`,
11
+ * `parseType` and `type` attributes. Grammar errors carry the line of the offending element.
12
+ */
13
+ import { BlankNode, DataFactory, type Literal, NamedNode, type Quad } from 'n3';
14
+ import { classifyXmlRoot, unsupportedFormat } from './detect.js';
15
+ import { resolveIri } from './iri.js';
16
+ import { RDF_NS } from './prefixes.js';
17
+ import type { RdfDocument } from './rdf.js';
18
+ import { compareStrings, invalidInput, setOwn, stripBom } from './shared.js';
19
+ import {
20
+ isXmlSpace,
21
+ NC_NAME,
22
+ parseXml,
23
+ XML_NS,
24
+ type XmlAttribute,
25
+ type XmlElement,
26
+ type XmlHandler,
27
+ type XmlName,
28
+ XmlSyntaxError,
29
+ } from './xml.js';
30
+
31
+ const RDF_TYPE = `${RDF_NS}type`;
32
+ const RDF_DESCRIPTION = `${RDF_NS}Description`;
33
+ const RDF_LI = `${RDF_NS}li`;
34
+ const RDF_FIRST = `${RDF_NS}first`;
35
+ const RDF_REST = `${RDF_NS}rest`;
36
+ const RDF_NIL = `${RDF_NS}nil`;
37
+ const RDF_STATEMENT = `${RDF_NS}Statement`;
38
+ const RDF_SUBJECT = `${RDF_NS}subject`;
39
+ const RDF_PREDICATE = `${RDF_NS}predicate`;
40
+ const RDF_OBJECT = `${RDF_NS}object`;
41
+ const RDF_XML_LITERAL = `${RDF_NS}XMLLiteral`;
42
+
43
+ /** `rdf:` names that are syntax: never node elements, property elements or properties. */
44
+ const SYNTAX_NAMES: ReadonlySet<string> = new Set([
45
+ 'RDF',
46
+ 'ID',
47
+ 'about',
48
+ 'parseType',
49
+ 'resource',
50
+ 'nodeID',
51
+ 'datatype',
52
+ // Removed from RDF/XML in 2004.
53
+ 'aboutEach',
54
+ 'aboutEachPrefix',
55
+ 'bagID',
56
+ ]);
57
+
58
+ /** Syntax attributes of property elements (RDF 1.1 XML Syntax §7.2.14–7.2.21). */
59
+ const PROPERTY_SYNTAX: ReadonlySet<string> = new Set([
60
+ 'ID',
61
+ 'parseType',
62
+ 'resource',
63
+ 'nodeID',
64
+ 'datatype',
65
+ ]);
66
+
67
+ /** Unqualified attributes RDF/XML still reads as `rdf:` names (RDF 1.1 XML Syntax §6.1.4). */
68
+ const LEGACY_UNQUALIFIED: ReadonlySet<string> = new Set([
69
+ 'ID',
70
+ 'about',
71
+ 'resource',
72
+ 'parseType',
73
+ 'type',
74
+ ]);
75
+
76
+ type Resource = NamedNode | BlankNode;
77
+ type Value = Resource | Literal;
78
+
79
+ /** In-scope `xml:base` and `xml:lang`. */
80
+ interface Scope {
81
+ readonly base: string | undefined;
82
+ readonly language: string;
83
+ }
84
+
85
+ /** The triple a property element asserts, and the IRI that reifies it (`rdf:ID`). */
86
+ interface Statement {
87
+ readonly subject: Resource;
88
+ readonly predicate: NamedNode;
89
+ readonly reifier: NamedNode | undefined;
90
+ }
91
+
92
+ interface PropertyFrame {
93
+ readonly kind: 'property';
94
+ readonly scope: Scope;
95
+ readonly statement: Statement;
96
+ readonly resource: string | undefined;
97
+ readonly nodeId: string | undefined;
98
+ readonly datatype: string | undefined;
99
+ readonly properties: readonly (readonly [NamedNode, string])[];
100
+ readonly offset: number;
101
+ text: string;
102
+ object: Resource | undefined;
103
+ }
104
+
105
+ type Frame =
106
+ | { readonly kind: 'document' | 'nodes'; readonly scope: Scope }
107
+ | { readonly kind: 'node'; readonly scope: Scope; readonly subject: Resource; li: number }
108
+ | {
109
+ readonly kind: 'collection';
110
+ readonly scope: Scope;
111
+ readonly statement: Statement;
112
+ readonly items: Resource[];
113
+ }
114
+ | PropertyFrame
115
+ | {
116
+ readonly kind: 'literal';
117
+ /** Set on the property element itself; nested literal elements only write markup. */
118
+ readonly statement: Statement | undefined;
119
+ readonly writer: XmlLiteralWriter;
120
+ };
121
+
122
+ /**
123
+ * Reads RDF/XML (`.rdf`, `.owl` as written by Protégé, the OWL API and ROBOT) into quads.
124
+ * Whitespace-only input is an empty graph. Throws `KgdiffError('invalid-input')` with the line
125
+ * for XML that is not well-formed or not RDF/XML (OWL/XML and GraphML get a conversion hint).
126
+ */
127
+ export function readRdfXml(text: string, baseIRI: string | undefined): RdfDocument {
128
+ const source = stripBom(text);
129
+ if (isXmlSpace(source)) return { quads: [], prefixes: {} };
130
+ const reader = new RdfXmlReader(baseIRI);
131
+ try {
132
+ parseXml(source, reader);
133
+ } catch (error) {
134
+ if (!(error instanceof XmlSyntaxError)) throw error;
135
+ throw invalidInput(`Invalid RDF/XML: ${error.message} on line ${error.line}.`, {
136
+ format: 'rdfxml',
137
+ line: error.line,
138
+ });
139
+ }
140
+ return { quads: reader.quads, prefixes: reader.prefixes };
141
+ }
142
+
143
+ class RdfXmlReader implements XmlHandler {
144
+ readonly quads: Quad[] = [];
145
+ readonly prefixes: Record<string, string> = {};
146
+ private readonly stack: Frame[];
147
+ /** IRIs minted by `rdf:ID`, which must be unique in a document. */
148
+ private readonly ids = new Set<string>();
149
+ private readonly iris = new Map<string, NamedNode>();
150
+ private blanks = 0;
151
+
152
+ constructor(baseIRI: string | undefined) {
153
+ this.stack = [{ kind: 'document', scope: { base: baseIRI, language: '' } }];
154
+ }
155
+
156
+ // ─── XML events ─────────────────────────────────────────────────────────
157
+
158
+ startElement(element: XmlElement): void {
159
+ for (const [prefix, uri] of element.namespaces) {
160
+ if (prefix !== 'xml' && !Object.hasOwn(this.prefixes, prefix))
161
+ setOwn(this.prefixes, prefix, uri);
162
+ }
163
+ const parent = this.top();
164
+ if (parent.kind === 'literal') {
165
+ parent.writer.start(element);
166
+ this.stack.push({ kind: 'literal', statement: undefined, writer: parent.writer });
167
+ return;
168
+ }
169
+ const scope = inScope(element, parent.scope);
170
+ switch (parent.kind) {
171
+ case 'document':
172
+ this.root(element, scope);
173
+ return;
174
+ case 'nodes':
175
+ this.nodeElement(element, scope);
176
+ return;
177
+ case 'collection':
178
+ parent.items.push(this.nodeElement(element, scope));
179
+ return;
180
+ case 'node':
181
+ this.propertyElement(element, scope, parent);
182
+ return;
183
+ case 'property': {
184
+ if (parent.object !== undefined) {
185
+ throw this.error('A property element can contain only one node element', element.offset);
186
+ }
187
+ if (!isXmlSpace(parent.text)) {
188
+ throw this.error('A property element cannot mix text and a node element', element.offset);
189
+ }
190
+ if (
191
+ parent.resource !== undefined ||
192
+ parent.nodeId !== undefined ||
193
+ parent.datatype !== undefined ||
194
+ parent.properties.length > 0
195
+ ) {
196
+ throw this.error(
197
+ 'A property element with rdf:resource, rdf:nodeID, rdf:datatype or property attributes cannot contain a node element',
198
+ element.offset,
199
+ );
200
+ }
201
+ const object = this.nodeElement(element, scope);
202
+ parent.object = object;
203
+ this.assert(parent.statement, object);
204
+ return;
205
+ }
206
+ default:
207
+ assertExhausted(parent);
208
+ }
209
+ }
210
+
211
+ endElement(): void {
212
+ const frame = this.stack.pop();
213
+ if (frame === undefined) return;
214
+ switch (frame.kind) {
215
+ case 'property':
216
+ this.endProperty(frame);
217
+ return;
218
+ case 'collection': {
219
+ let head: Resource = this.iri(RDF_NIL);
220
+ for (let i = frame.items.length - 1; i >= 0; i--) {
221
+ const cell = this.blank();
222
+ this.emit(cell, this.iri(RDF_FIRST), frame.items[i] as Resource);
223
+ this.emit(cell, this.iri(RDF_REST), head);
224
+ head = cell;
225
+ }
226
+ this.assert(frame.statement, head);
227
+ return;
228
+ }
229
+ case 'literal':
230
+ if (frame.statement) {
231
+ const value = DataFactory.literal(frame.writer.result(), this.iri(RDF_XML_LITERAL));
232
+ this.assert(frame.statement, value);
233
+ } else frame.writer.end();
234
+ return;
235
+ default:
236
+ return;
237
+ }
238
+ }
239
+
240
+ text(value: string, offset: number): void {
241
+ const frame = this.top();
242
+ if (frame.kind === 'literal') frame.writer.text(value);
243
+ else if (frame.kind === 'property' && frame.object === undefined) frame.text += value;
244
+ else if (!isXmlSpace(value)) {
245
+ throw this.error(
246
+ frame.kind === 'property'
247
+ ? 'A property element cannot mix text and a node element'
248
+ : `Unexpected text "${value.trim().slice(0, 40)}"`,
249
+ offset,
250
+ );
251
+ }
252
+ }
253
+
254
+ processingInstruction(target: string, data: string): void {
255
+ const frame = this.top();
256
+ if (frame.kind === 'literal') frame.writer.processingInstruction(target, data);
257
+ }
258
+
259
+ comment(value: string): void {
260
+ const frame = this.top();
261
+ if (frame.kind === 'literal') frame.writer.comment(value);
262
+ }
263
+
264
+ // ─── Grammar ────────────────────────────────────────────────────────────
265
+
266
+ private root(element: XmlElement, scope: Scope): void {
267
+ if (element.uri === RDF_NS && element.local === 'RDF') {
268
+ this.stack.push({ kind: 'nodes', scope });
269
+ return;
270
+ }
271
+ // A lone node element is RDF/XML too (whatever its vocabulary), but OWL/XML, GraphML and
272
+ // GEXF roots are recognisably something else: say so instead of reading them as nodes.
273
+ const kind = classifyXmlRoot({
274
+ local: element.local,
275
+ uri: element.uri,
276
+ rdfAttributes: element.attributes.some((attribute) => attribute.uri === RDF_NS),
277
+ rdfNamespace: element.namespaces.some(([, uri]) => uri === RDF_NS),
278
+ });
279
+ if (kind !== 'rdfxml' && kind !== 'XML') throw unsupportedFormat(kind);
280
+ this.nodeElement(element, scope);
281
+ }
282
+
283
+ private nodeElement(element: XmlElement, scope: Scope): Resource {
284
+ const type = this.elementIri(element, 'node');
285
+ let subject: Resource | undefined;
286
+ let identifiers = 0;
287
+ const properties: [NamedNode, string][] = [];
288
+ for (const attribute of element.attributes) {
289
+ const meaning = this.attribute(attribute, element.offset);
290
+ if (meaning === undefined) continue;
291
+ if (meaning instanceof NamedNode) {
292
+ properties.push([meaning, attribute.value]);
293
+ continue;
294
+ }
295
+ identifiers++;
296
+ if (meaning === 'about') subject = this.iri(resolveIri(attribute.value, scope.base));
297
+ else if (meaning === 'ID') subject = this.idIri(attribute.value, scope, element.offset);
298
+ else if (meaning === 'nodeID') subject = this.labelled(attribute.value, element.offset);
299
+ else throw this.error(`rdf:${meaning} is not allowed on a node element`, element.offset);
300
+ }
301
+ if (identifiers > 1) {
302
+ throw this.error(
303
+ 'A node element can have only one of rdf:about, rdf:ID and rdf:nodeID',
304
+ element.offset,
305
+ );
306
+ }
307
+ subject ??= this.blank();
308
+ if (type !== RDF_DESCRIPTION) this.emit(subject, this.iri(RDF_TYPE), this.iri(type));
309
+ this.propertyAttributes(subject, properties, scope);
310
+ this.stack.push({ kind: 'node', scope, subject, li: 0 });
311
+ return subject;
312
+ }
313
+
314
+ private propertyElement(
315
+ element: XmlElement,
316
+ scope: Scope,
317
+ parent: Extract<Frame, { kind: 'node' }>,
318
+ ): void {
319
+ let iri = this.elementIri(element, 'property');
320
+ if (iri === RDF_LI) iri = `${RDF_NS}_${++parent.li}`;
321
+ const syntax = new Map<string, string>();
322
+ const properties: [NamedNode, string][] = [];
323
+ for (const attribute of element.attributes) {
324
+ const meaning = this.attribute(attribute, element.offset);
325
+ if (meaning === undefined) continue;
326
+ if (meaning instanceof NamedNode) properties.push([meaning, attribute.value]);
327
+ else if (!PROPERTY_SYNTAX.has(meaning)) {
328
+ throw this.error(`rdf:${meaning} is not allowed on a property element`, element.offset);
329
+ } else if (syntax.has(meaning)) {
330
+ throw this.error(`rdf:${meaning} is given twice`, element.offset);
331
+ } else syntax.set(meaning, attribute.value);
332
+ }
333
+ const id = syntax.get('ID');
334
+ const statement: Statement = {
335
+ subject: parent.subject,
336
+ predicate: this.iri(iri),
337
+ reifier: id === undefined ? undefined : this.idIri(id, scope, element.offset),
338
+ };
339
+ const parseType = syntax.get('parseType');
340
+ const resource = syntax.get('resource');
341
+ const nodeId = syntax.get('nodeID');
342
+ const datatype = syntax.get('datatype');
343
+ if (parseType !== undefined) {
344
+ if (
345
+ resource !== undefined ||
346
+ nodeId !== undefined ||
347
+ datatype !== undefined ||
348
+ properties.length > 0
349
+ ) {
350
+ throw this.error(
351
+ 'rdf:parseType cannot be combined with rdf:resource, rdf:nodeID, rdf:datatype or property attributes',
352
+ element.offset,
353
+ );
354
+ }
355
+ if (parseType === 'Resource') {
356
+ const object = this.blank();
357
+ this.assert(statement, object);
358
+ this.stack.push({ kind: 'node', scope, subject: object, li: 0 });
359
+ } else if (parseType === 'Collection') {
360
+ this.stack.push({ kind: 'collection', scope, statement, items: [] });
361
+ } else {
362
+ // "Literal", and any other value (RDF/XML §7.2.20).
363
+ this.stack.push({ kind: 'literal', statement, writer: new XmlLiteralWriter() });
364
+ }
365
+ return;
366
+ }
367
+ if ([resource, nodeId, datatype].filter((value) => value !== undefined).length > 1) {
368
+ throw this.error(
369
+ 'A property element can have only one of rdf:resource, rdf:nodeID and rdf:datatype',
370
+ element.offset,
371
+ );
372
+ }
373
+ if (datatype !== undefined && properties.length > 0) {
374
+ throw this.error('rdf:datatype cannot be combined with property attributes', element.offset);
375
+ }
376
+ this.stack.push({
377
+ kind: 'property',
378
+ scope,
379
+ statement,
380
+ resource,
381
+ nodeId,
382
+ datatype,
383
+ properties,
384
+ offset: element.offset,
385
+ text: '',
386
+ object: undefined,
387
+ });
388
+ }
389
+
390
+ /** Literal, empty and resource-valued property elements, decided by what they contained. */
391
+ private endProperty(frame: PropertyFrame): void {
392
+ if (frame.object !== undefined) return; // A node element: asserted when it started.
393
+ const { scope, resource, nodeId, datatype, properties, text } = frame;
394
+ const resourceLike = resource !== undefined || nodeId !== undefined || properties.length > 0;
395
+ if (text !== '' && !resourceLike) {
396
+ this.assert(frame.statement, this.literal(text, scope, datatype));
397
+ return;
398
+ }
399
+ if (!isXmlSpace(text)) {
400
+ throw this.error(
401
+ 'A property element with rdf:resource, rdf:nodeID or property attributes cannot contain text',
402
+ frame.offset,
403
+ );
404
+ }
405
+ if (!resourceLike) {
406
+ this.assert(frame.statement, this.literal('', scope, datatype));
407
+ return;
408
+ }
409
+ const object =
410
+ resource !== undefined
411
+ ? this.iri(resolveIri(resource, scope.base))
412
+ : nodeId !== undefined
413
+ ? this.labelled(nodeId, frame.offset)
414
+ : this.blank();
415
+ this.propertyAttributes(object, properties, scope);
416
+ this.assert(frame.statement, object);
417
+ }
418
+
419
+ private propertyAttributes(
420
+ subject: Resource,
421
+ properties: readonly (readonly [NamedNode, string])[],
422
+ scope: Scope,
423
+ ): void {
424
+ for (const [predicate, value] of properties) {
425
+ const object =
426
+ predicate.value === RDF_TYPE
427
+ ? this.iri(resolveIri(value, scope.base))
428
+ : this.literal(value, scope, undefined);
429
+ this.emit(subject, predicate, object);
430
+ }
431
+ }
432
+
433
+ /** The IRI an element names, rejecting syntax names in the wrong place. */
434
+ private elementIri(element: XmlElement, role: 'node' | 'property'): string {
435
+ const { uri, local } = element;
436
+ if (uri === '') throw this.error(`Element <${local}> is in no namespace`, element.offset);
437
+ if (
438
+ uri === RDF_NS &&
439
+ (SYNTAX_NAMES.has(local) || local === (role === 'node' ? 'li' : 'Description'))
440
+ ) {
441
+ throw this.error(`rdf:${local} cannot be used as a ${role} element`, element.offset);
442
+ }
443
+ return uri + local;
444
+ }
445
+
446
+ /**
447
+ * What an attribute means: an `rdf:` syntax name, a property, or nothing (reserved `xml…`
448
+ * names, which RDF/XML ignores; `xml:lang` and `xml:base` are read by `inScope`).
449
+ */
450
+ private attribute(attribute: XmlAttribute, offset: number): NamedNode | string | undefined {
451
+ const { prefix, local, uri } = attribute;
452
+ if (/^xml/i.test(prefix) || (prefix === '' && /^xml/i.test(local))) return undefined;
453
+ if (uri === '') {
454
+ if (!LEGACY_UNQUALIFIED.has(local)) {
455
+ throw this.error(`Attribute "${local}" is in no namespace`, offset);
456
+ }
457
+ return local === 'type' ? this.iri(RDF_TYPE) : local;
458
+ }
459
+ if (uri === RDF_NS) {
460
+ if (SYNTAX_NAMES.has(local)) return local;
461
+ if (local === 'li' || local === 'Description') {
462
+ throw this.error(`rdf:${local} cannot be used as an attribute`, offset);
463
+ }
464
+ }
465
+ return this.iri(uri + local);
466
+ }
467
+
468
+ // ─── Terms ──────────────────────────────────────────────────────────────
469
+
470
+ /** `rdf:ID` → `base#id`, unique per document. */
471
+ private idIri(id: string, scope: Scope, offset: number): NamedNode {
472
+ if (!NC_NAME.test(id)) throw this.error(`rdf:ID "${id}" is not an XML name`, offset);
473
+ const iri = resolveIri(`#${id}`, scope.base);
474
+ if (this.ids.has(iri)) throw this.error(`rdf:ID "${id}" is used twice`, offset);
475
+ this.ids.add(iri);
476
+ return this.iri(iri);
477
+ }
478
+
479
+ /** `rdf:nodeID` blank nodes; their labels cannot clash with generated ones (`g…`). */
480
+ private labelled(label: string, offset: number): BlankNode {
481
+ if (!NC_NAME.test(label)) throw this.error(`rdf:nodeID "${label}" is not an XML name`, offset);
482
+ return new BlankNode(`n${label}`);
483
+ }
484
+
485
+ private blank(): BlankNode {
486
+ return new BlankNode(`g${this.blanks++}`);
487
+ }
488
+
489
+ private iri(value: string): NamedNode {
490
+ let node = this.iris.get(value);
491
+ if (!node) {
492
+ node = new NamedNode(value);
493
+ this.iris.set(value, node);
494
+ }
495
+ return node;
496
+ }
497
+
498
+ private literal(value: string, scope: Scope, datatype: string | undefined): Literal {
499
+ if (datatype !== undefined) {
500
+ return DataFactory.literal(value, this.iri(resolveIri(datatype, scope.base)));
501
+ }
502
+ return scope.language === ''
503
+ ? DataFactory.literal(value)
504
+ : DataFactory.literal(value, scope.language);
505
+ }
506
+
507
+ private assert({ subject, predicate, reifier }: Statement, object: Value): void {
508
+ this.emit(subject, predicate, object);
509
+ if (!reifier) return;
510
+ this.emit(reifier, this.iri(RDF_TYPE), this.iri(RDF_STATEMENT));
511
+ this.emit(reifier, this.iri(RDF_SUBJECT), subject);
512
+ this.emit(reifier, this.iri(RDF_PREDICATE), predicate);
513
+ this.emit(reifier, this.iri(RDF_OBJECT), object);
514
+ }
515
+
516
+ private emit(subject: Resource, predicate: NamedNode, object: Value): void {
517
+ this.quads.push(DataFactory.quad(subject, predicate, object));
518
+ }
519
+
520
+ private top(): Frame {
521
+ return this.stack[this.stack.length - 1] as Frame;
522
+ }
523
+
524
+ private error(message: string, offset: number): XmlSyntaxError {
525
+ return new XmlSyntaxError(message, offset);
526
+ }
527
+ }
528
+
529
+ /** `xml:base` and `xml:lang` of an element, inheriting the enclosing scope. */
530
+ function inScope(element: XmlElement, outer: Scope): Scope {
531
+ let { base, language } = outer;
532
+ for (const attribute of element.attributes) {
533
+ if (attribute.uri !== XML_NS) continue;
534
+ if (attribute.local === 'base') base = resolveIri(attribute.value, base);
535
+ else if (attribute.local === 'lang') language = attribute.value;
536
+ }
537
+ return base === outer.base && language === outer.language ? outer : { base, language };
538
+ }
539
+
540
+ function assertExhausted(frame: never): never {
541
+ throw new XmlSyntaxError(`Unexpected parser state ${String(frame)}`, 0);
542
+ }
543
+
544
+ // ─── XML literals ───────────────────────────────────────────────────────────
545
+
546
+ const NO_NAMESPACES: ReadonlyMap<string, string> = new Map();
547
+
548
+ /**
549
+ * Lexical form of an `rdf:parseType="Literal"` value: Exclusive XML Canonicalization (with
550
+ * comments, empty InclusiveNamespaces list) of the element content, as RDF 1.1 XML Syntax
551
+ * requires. Namespaces are declared where they are first visibly used.
552
+ */
553
+ class XmlLiteralWriter {
554
+ private out = '';
555
+ private readonly open: {
556
+ readonly qname: string;
557
+ readonly rendered: ReadonlyMap<string, string>;
558
+ }[] = [];
559
+
560
+ start(element: XmlElement): void {
561
+ const outer = this.open[this.open.length - 1]?.rendered ?? NO_NAMESPACES;
562
+ const used = new Map<string, string>([[element.prefix, element.uri]]);
563
+ for (const attribute of element.attributes) {
564
+ if (attribute.prefix !== '' && attribute.prefix !== 'xml')
565
+ used.set(attribute.prefix, attribute.uri);
566
+ }
567
+ const declarations = [...used]
568
+ .filter(([prefix, uri]) => (outer.get(prefix) ?? '') !== uri)
569
+ .sort(([a], [b]) => compareStrings(a, b));
570
+ let rendered = outer;
571
+ if (declarations.length > 0) {
572
+ const next = new Map(outer);
573
+ for (const [prefix, uri] of declarations) next.set(prefix, uri);
574
+ rendered = next;
575
+ }
576
+ const attributes = [...element.attributes].sort(
577
+ (a, b) => compareStrings(a.uri, b.uri) || compareStrings(a.local, b.local),
578
+ );
579
+ const name = qualified(element);
580
+ let tag = `<${name}`;
581
+ for (const [prefix, uri] of declarations) {
582
+ tag += ` xmlns${prefix === '' ? '' : `:${prefix}`}="${escapeAttribute(uri)}"`;
583
+ }
584
+ for (const attribute of attributes)
585
+ tag += ` ${qualified(attribute)}="${escapeAttribute(attribute.value)}"`;
586
+ this.out += `${tag}>`;
587
+ this.open.push({ qname: name, rendered });
588
+ }
589
+
590
+ end(): void {
591
+ const element = this.open.pop();
592
+ if (element) this.out += `</${element.qname}>`;
593
+ }
594
+
595
+ text(value: string): void {
596
+ this.out += value.replace(/[&<>\r]/g, (c) => TEXT_ESCAPES[c] ?? c);
597
+ }
598
+
599
+ comment(value: string): void {
600
+ this.out += `<!--${value}-->`;
601
+ }
602
+
603
+ processingInstruction(target: string, data: string): void {
604
+ this.out += data === '' ? `<?${target}?>` : `<?${target} ${data}?>`;
605
+ }
606
+
607
+ result(): string {
608
+ return this.out;
609
+ }
610
+ }
611
+
612
+ const TEXT_ESCAPES: Readonly<Record<string, string>> = {
613
+ '&': '&amp;',
614
+ '<': '&lt;',
615
+ '>': '&gt;',
616
+ '\r': '&#xD;',
617
+ };
618
+
619
+ const ATTRIBUTE_ESCAPES: Readonly<Record<string, string>> = {
620
+ '&': '&amp;',
621
+ '<': '&lt;',
622
+ '"': '&quot;',
623
+ '\t': '&#x9;',
624
+ '\n': '&#xA;',
625
+ '\r': '&#xD;',
626
+ };
627
+
628
+ function escapeAttribute(value: string): string {
629
+ return value.replace(/[&<"\t\n\r]/g, (c) => ATTRIBUTE_ESCAPES[c] ?? c);
630
+ }
631
+
632
+ const qualified = (name: XmlName): string =>
633
+ name.prefix === '' ? name.local : `${name.prefix}:${name.local}`;