eyeling 1.27.8 → 1.28.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/lib/rdfjs.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  const {
11
11
  XSD_NS,
12
+ LOG_NS,
12
13
  Literal: InternalLiteral,
13
14
  Iri,
14
15
  Blank,
@@ -19,6 +20,7 @@ const {
19
20
  Triple,
20
21
  Rule,
21
22
  PrefixEnv,
23
+ annotateQuotedGraphTerm,
22
24
  literalParts,
23
25
  } = require('./prelude');
24
26
  const { termToN3, tripleToN3 } = require('./printing');
@@ -322,6 +324,49 @@ function internalRdf12TermToRdfJs(term, factory, position) {
322
324
  return internalTermToRdfJs(term, rdfFactory, position);
323
325
  }
324
326
 
327
+ function isDefaultGraphTerm(term) {
328
+ return isRdfJsTerm(term) && term.termType === 'DefaultGraph';
329
+ }
330
+
331
+ function assertDefaultGraphForRdfJsQuadTerm(term, position) {
332
+ if (!isDefaultGraphTerm(term.graph)) {
333
+ throw new TypeError(`RDF/JS Quad terms with named graphs are not supported in ${position}`);
334
+ }
335
+ }
336
+
337
+ function rdfJsGraphNameToInternal(term, position = 'quad.graph') {
338
+ assertSupportedRdfJsTerm(term, position);
339
+ switch (term.termType) {
340
+ case 'NamedNode':
341
+ return new Iri(term.value);
342
+ case 'BlankNode':
343
+ return new Blank(`_:${term.value}`);
344
+ case 'DefaultGraph':
345
+ return null;
346
+ default:
347
+ throw new TypeError(`Invalid RDF graph termType ${term.termType}`);
348
+ }
349
+ }
350
+
351
+ function internalGraphNameToRdfJs(term, factory, position = 'graph') {
352
+ if (term instanceof Iri || term instanceof Blank) return internalTermToRdfJs(term, factory, position);
353
+ return unsupportedRdfJsTerm(term, position);
354
+ }
355
+
356
+ function graphKey(term) {
357
+ return `${term.termType}:${term.value}`;
358
+ }
359
+
360
+ function isInternalLogNameOfTriple(triple) {
361
+ return (
362
+ triple instanceof Triple &&
363
+ (triple.s instanceof Iri || triple.s instanceof Blank) &&
364
+ triple.p instanceof Iri &&
365
+ triple.p.value === LOG_NS + 'nameOf' &&
366
+ triple.o instanceof GraphTerm
367
+ );
368
+ }
369
+
325
370
  function assertRdfJsQuadShape(subject, predicate, object, graph) {
326
371
  if (!['NamedNode', 'BlankNode', 'Quad'].includes(subject.termType)) {
327
372
  throw new TypeError(`Invalid RDF subject termType ${subject.termType}`);
@@ -350,13 +395,22 @@ function internalTripleToRdfJsQuadInGraph(triple, graph, factory) {
350
395
  function internalTripleToRdfJsQuad(triple, factory) {
351
396
  const rdfFactory = getDataFactory(factory);
352
397
  return rdfFactory.quad(
353
- internalTermToRdfJs(triple.s, rdfFactory, 'subject'),
398
+ internalRdf12TermToRdfJs(triple.s, rdfFactory, 'subject'),
354
399
  internalTermToRdfJs(triple.p, rdfFactory, 'predicate'),
355
- internalTermToRdfJs(triple.o, rdfFactory, 'object'),
400
+ internalRdf12TermToRdfJs(triple.o, rdfFactory, 'object'),
356
401
  rdfFactory.defaultGraph(),
357
402
  );
358
403
  }
359
404
 
405
+ function internalTripleToRdfJsQuads(triple, factory) {
406
+ const rdfFactory = getDataFactory(factory);
407
+ if (isInternalLogNameOfTriple(triple)) {
408
+ const graph = internalGraphNameToRdfJs(triple.s, rdfFactory, 'graph');
409
+ return (triple.o.triples || []).map((inner) => internalTripleToRdfJsQuadInGraph(inner, graph, rdfFactory));
410
+ }
411
+ return [internalTripleToRdfJsQuad(triple, rdfFactory)];
412
+ }
413
+
360
414
  function escapeStringForN3(value) {
361
415
  return JSON.stringify(String(value));
362
416
  }
@@ -388,7 +442,14 @@ function rdfJsTermToN3(term, position = 'term') {
388
442
  return `${lexical}^^<${datatype}>`;
389
443
  }
390
444
  case 'Quad':
391
- throw new TypeError(`Quoted triple terms are not supported in ${position}`);
445
+ if (String(position).endsWith('.predicate') || position === 'quad.predicate' || position === 'predicate') {
446
+ throw new TypeError(`Quoted triple terms are not valid RDF predicates in ${position}`);
447
+ }
448
+ assertDefaultGraphForRdfJsQuadTerm(term, position);
449
+ return `{ ${rdfJsTermToN3(term.subject, `${position}.subject`)} ${rdfJsTermToN3(
450
+ term.predicate,
451
+ `${position}.predicate`,
452
+ )} ${rdfJsTermToN3(term.object, `${position}.object`)} . }`;
392
453
  default:
393
454
  throw new TypeError(`Unsupported RDF/JS termType ${JSON.stringify(term.termType)} in ${position}`);
394
455
  }
@@ -408,8 +469,21 @@ function rdfJsTermToInternal(term, position = 'term') {
408
469
  return new InternalLiteral(rdfJsTermToN3(term, position));
409
470
  case 'DefaultGraph':
410
471
  throw new TypeError(`DefaultGraph is not a valid standalone N3 term in ${position}`);
411
- case 'Quad':
412
- throw new TypeError(`Quoted triple terms are not supported in ${position}`);
472
+ case 'Quad': {
473
+ if (String(position).endsWith('.predicate') || position === 'quad.predicate' || position === 'predicate') {
474
+ throw new TypeError(`Quoted triple terms are not valid RDF predicates in ${position}`);
475
+ }
476
+ assertDefaultGraphForRdfJsQuadTerm(term, position);
477
+ return annotateQuotedGraphTerm(
478
+ new GraphTerm([
479
+ new Triple(
480
+ rdfJsTermToInternal(term.subject, `${position}.subject`),
481
+ rdfJsTermToInternal(term.predicate, `${position}.predicate`),
482
+ rdfJsTermToInternal(term.object, `${position}.object`),
483
+ ),
484
+ ]),
485
+ );
486
+ }
413
487
  default:
414
488
  throw new TypeError(`Unsupported RDF/JS termType ${JSON.stringify(term.termType)} in ${position}`);
415
489
  }
@@ -417,22 +491,58 @@ function rdfJsTermToInternal(term, position = 'term') {
417
491
 
418
492
  function rdfJsQuadToInternalTriple(quad) {
419
493
  if (!isRdfJsQuad(quad)) throw new TypeError('Expected an RDF/JS Quad');
420
- if (quad.graph.termType !== 'DefaultGraph') {
421
- throw new TypeError('Named graph quads are not supported by Eyeling input; use the default graph only');
422
- }
423
- return new Triple(
494
+ const inner = new Triple(
424
495
  rdfJsTermToInternal(quad.subject, 'quad.subject'),
425
496
  rdfJsTermToInternal(quad.predicate, 'quad.predicate'),
426
497
  rdfJsTermToInternal(quad.object, 'quad.object'),
427
498
  );
499
+ const graph = rdfJsGraphNameToInternal(quad.graph, 'quad.graph');
500
+ if (!graph) return inner;
501
+ return new Triple(graph, new Iri(LOG_NS + 'nameOf'), annotateQuotedGraphTerm(new GraphTerm([inner])));
502
+ }
503
+
504
+ function rdfJsQuadsToInternalTriples(quads) {
505
+ const out = [];
506
+ const namedGraphs = new Map();
507
+
508
+ for (const quad of quads) {
509
+ if (!isRdfJsQuad(quad)) throw new TypeError('Expected an RDF/JS Quad');
510
+ const inner = new Triple(
511
+ rdfJsTermToInternal(quad.subject, 'quad.subject'),
512
+ rdfJsTermToInternal(quad.predicate, 'quad.predicate'),
513
+ rdfJsTermToInternal(quad.object, 'quad.object'),
514
+ );
515
+ const graph = rdfJsGraphNameToInternal(quad.graph, 'quad.graph');
516
+ if (!graph) {
517
+ out.push(inner);
518
+ continue;
519
+ }
520
+
521
+ const key = graphKey(quad.graph);
522
+ let group = namedGraphs.get(key);
523
+ if (!group) {
524
+ group = { graph, triples: [] };
525
+ namedGraphs.set(key, group);
526
+ }
527
+ group.triples.push(inner);
528
+ }
529
+
530
+ for (const { graph, triples } of namedGraphs.values()) {
531
+ out.push(new Triple(graph, new Iri(LOG_NS + 'nameOf'), annotateQuotedGraphTerm(new GraphTerm(triples))));
532
+ }
533
+
534
+ return out;
428
535
  }
429
536
 
430
537
  function rdfJsQuadToN3(quad) {
431
538
  if (!isRdfJsQuad(quad)) throw new TypeError('Expected an RDF/JS Quad');
432
- if (quad.graph.termType !== 'DefaultGraph') {
433
- throw new TypeError('Named graph quads are not supported by Eyeling input; use the default graph only');
434
- }
435
- return `${rdfJsTermToN3(quad.subject, 'quad.subject')} ${rdfJsTermToN3(quad.predicate, 'quad.predicate')} ${rdfJsTermToN3(quad.object, 'quad.object')}.`;
539
+ return tripleToN3(rdfJsQuadToInternalTriple(quad), PrefixEnv.newDefault());
540
+ }
541
+
542
+ function rdfJsQuadsToN3(quads) {
543
+ return rdfJsQuadsToInternalTriples(quads)
544
+ .map((triple) => tripleToN3(triple, PrefixEnv.newDefault()))
545
+ .join('\n');
436
546
  }
437
547
 
438
548
  function collectIterableToArray(iterable, label) {
@@ -477,6 +587,11 @@ function getPrefixesText(input) {
477
587
  return '';
478
588
  }
479
589
 
590
+ function getN3Text(input) {
591
+ if (!isObject(input)) return '';
592
+ return typeof input.n3 === 'string' ? input.n3 : '';
593
+ }
594
+
480
595
  function joinN3Sections(parts) {
481
596
  return parts
482
597
  .filter((part) => typeof part === 'string' && part.length > 0)
@@ -745,7 +860,7 @@ function appendSyncQuadFacts(doc, input) {
745
860
  const quads = collectIterableToArray(quadsInfo.value, quadsInfo.label);
746
861
  return {
747
862
  ...doc,
748
- triples: doc.triples.concat(quads.map((quad) => rdfJsQuadToInternalTriple(quad))),
863
+ triples: doc.triples.concat(rdfJsQuadsToInternalTriples(quads)),
749
864
  };
750
865
  }
751
866
 
@@ -755,7 +870,7 @@ async function appendAsyncQuadFacts(doc, input) {
755
870
  const quads = await collectAsyncIterableToArray(quadsInfo.value, quadsInfo.label);
756
871
  return {
757
872
  ...doc,
758
- triples: doc.triples.concat(quads.map((quad) => rdfJsQuadToInternalTriple(quad))),
873
+ triples: doc.triples.concat(rdfJsQuadsToInternalTriples(quads)),
759
874
  };
760
875
  }
761
876
 
@@ -774,13 +889,13 @@ async function normalizeParsedReasonerInputAsync(input) {
774
889
  function normalizeReasonerInputSync(input) {
775
890
  if (typeof input === 'string') return input;
776
891
  const parsed = normalizeParsedReasonerInputSync(input);
777
- if (parsed) return serializeEyelingDocument(parsed);
892
+ const n3Text = getN3Text(input);
893
+ if (parsed) return joinN3Sections([n3Text, serializeEyelingDocument(parsed)]);
778
894
  if (!isObject(input)) {
779
895
  throw new TypeError(
780
896
  'Reasoner input must be an N3 string, an Eyeling AST/rule object, or an object containing RDF/JS quads plus optional rules',
781
897
  );
782
898
  }
783
- if (typeof input.n3 === 'string') return input.n3;
784
899
 
785
900
  const quadsInfo = pickInputQuadIterable(input);
786
901
  const rulesText = getRulesText(input);
@@ -788,25 +903,25 @@ function normalizeReasonerInputSync(input) {
788
903
  const prefixesText = getPrefixesText(input);
789
904
 
790
905
  if (!quadsInfo) {
791
- if (rulesText || factsText || prefixesText) return joinN3Sections([prefixesText, factsText, rulesText]);
906
+ if (n3Text || rulesText || factsText || prefixesText) return joinN3Sections([prefixesText, n3Text, factsText, rulesText]);
792
907
  throw new TypeError('Input object must provide n3 text, Eyeling AST/rule objects, or RDF/JS quads/facts/dataset');
793
908
  }
794
909
 
795
910
  const quads = collectIterableToArray(quadsInfo.value, quadsInfo.label);
796
- const quadText = quads.map((quad) => rdfJsQuadToN3(quad)).join('\n');
797
- return joinN3Sections([prefixesText, factsText, quadText, rulesText]);
911
+ const quadText = rdfJsQuadsToN3(quads);
912
+ return joinN3Sections([prefixesText, n3Text, factsText, quadText, rulesText]);
798
913
  }
799
914
 
800
915
  async function normalizeReasonerInputAsync(input) {
801
916
  if (typeof input === 'string') return input;
802
917
  const parsed = await normalizeParsedReasonerInputAsync(input);
803
- if (parsed) return serializeEyelingDocument(parsed);
918
+ const n3Text = getN3Text(input);
919
+ if (parsed) return joinN3Sections([n3Text, serializeEyelingDocument(parsed)]);
804
920
  if (!isObject(input)) {
805
921
  throw new TypeError(
806
922
  'Reasoner input must be an N3 string, an Eyeling AST/rule object, or an object containing RDF/JS quads plus optional rules',
807
923
  );
808
924
  }
809
- if (typeof input.n3 === 'string') return input.n3;
810
925
 
811
926
  const quadsInfo = pickInputQuadIterable(input);
812
927
  const rulesText = getRulesText(input);
@@ -814,13 +929,13 @@ async function normalizeReasonerInputAsync(input) {
814
929
  const prefixesText = getPrefixesText(input);
815
930
 
816
931
  if (!quadsInfo) {
817
- if (rulesText || factsText || prefixesText) return joinN3Sections([prefixesText, factsText, rulesText]);
932
+ if (n3Text || rulesText || factsText || prefixesText) return joinN3Sections([prefixesText, n3Text, factsText, rulesText]);
818
933
  throw new TypeError('Input object must provide n3 text, Eyeling AST/rule objects, or RDF/JS quads/facts/dataset');
819
934
  }
820
935
 
821
936
  const quads = await collectAsyncIterableToArray(quadsInfo.value, quadsInfo.label);
822
- const quadText = quads.map((quad) => rdfJsQuadToN3(quad)).join('\n');
823
- return joinN3Sections([prefixesText, factsText, quadText, rulesText]);
937
+ const quadText = rdfJsQuadsToN3(quads);
938
+ return joinN3Sections([prefixesText, n3Text, factsText, quadText, rulesText]);
824
939
  }
825
940
 
826
941
  module.exports = {
@@ -831,8 +946,10 @@ module.exports = {
831
946
  rdfJsTermToN3,
832
947
  rdfJsQuadToN3,
833
948
  rdfJsQuadToInternalTriple,
949
+ rdfJsQuadsToInternalTriples,
834
950
  internalTermToRdfJs,
835
951
  internalTripleToRdfJsQuad,
952
+ internalTripleToRdfJsQuads,
836
953
  internalTripleToRdfJsQuadInGraph,
837
954
  normalizeParsedReasonerInputSync,
838
955
  normalizeReasonerInputSync,
@@ -0,0 +1,231 @@
1
+ # Eyeling RDF 1.2 Compatibility Mode and Roundtripping
2
+
3
+ ## Summary
4
+
5
+ Eyeling does **not** implement RDF 1.2 as a separate internal data model. RDF/TriG compatibility is an opt-in **syntax-normalization layer** in `lib/lexer.js`.
6
+
7
+ When parsing with RDF mode enabled, Eyeling rewrites RDF 1.2/TriG surface syntax into ordinary N3 syntax. The normal parser and reasoner then operate on Eyeling's existing N3 AST. On output, `lib/printing.js` can print some N3 graph terms back as RDF 1.2 triple terms.
8
+
9
+ The flow is:
10
+
11
+ ```text
12
+ RDF 1.2 / TriG input
13
+ -- lex(input, { rdf: true }) / normalizeRdfCompatibility() -->
14
+ normalized N3 text
15
+ -- Parser -->
16
+ Eyeling N3 AST
17
+ -- reasoner -->
18
+ derived N3 facts
19
+ -- tripleToRdfCompatible() -->
20
+ RDF-compatible output
21
+ ```
22
+
23
+ ## Main internal representation
24
+
25
+ RDF 1.2 triple terms are represented as **singleton N3 graph terms**.
26
+
27
+ Input:
28
+
29
+ ```turtle
30
+ :obs rdf:reifies <<( :s :p :o )>> .
31
+ ```
32
+
33
+ Normalized N3:
34
+
35
+ ```n3
36
+ :obs rdf:reifies { :s :p :o } .
37
+ ```
38
+
39
+ Conceptual AST shape:
40
+
41
+ ```js
42
+ Triple(
43
+ Iri(':obs'),
44
+ Iri('rdf:reifies'),
45
+ GraphTerm([
46
+ Triple(Iri(':s'), Iri(':p'), Iri(':o'))
47
+ ])
48
+ )
49
+ ```
50
+
51
+ So the core mapping is:
52
+
53
+ ```text
54
+ <<( S P O )>> <=> { S P O }
55
+ ```
56
+
57
+ But internally, Eyeling keeps only the N3 side: a `GraphTerm` containing one `Triple`.
58
+
59
+ ## Reifier sugar
60
+
61
+ RDF 1.2 reifier syntax is desugared by `convertTripleTerms()` in `lib/lexer.js`.
62
+
63
+ Input:
64
+
65
+ ```turtle
66
+ << :s :p :o ~ :r >> :source :doc .
67
+ ```
68
+
69
+ Normalized shape:
70
+
71
+ ```n3
72
+ { :s :p :o } :source :doc .
73
+ :r <http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies> { :s :p :o } .
74
+ ```
75
+
76
+ So the reifier is preserved structurally, but not as a special AST node. It becomes an ordinary `rdf:reifies` triple whose object is the singleton graph term.
77
+
78
+ ## Annotation syntax
79
+
80
+ Annotation syntax is handled by `convertAnnotations()` in `lib/lexer.js`.
81
+
82
+ Input:
83
+
84
+ ```turtle
85
+ :s :p :o ~ :r {| :source :doc |} .
86
+ ```
87
+
88
+ Normalized N3:
89
+
90
+ ```n3
91
+ :s :p :o .
92
+ :r <http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies> { :s :p :o } .
93
+ :r :source :doc .
94
+ ```
95
+
96
+ If an annotation block exists without an explicit reifier, the lexer generates a blank node such as `_:rdfAnnotation1`.
97
+
98
+ ## TriG named graphs
99
+
100
+ TriG-style named graph blocks are normalized by `normalizeNamedGraphs()`.
101
+
102
+ Input:
103
+
104
+ ```trig
105
+ :g {
106
+ :s :p :o .
107
+ }
108
+ ```
109
+
110
+ Normalized N3:
111
+
112
+ ```n3
113
+ :g <http://www.w3.org/2000/10/swap/log#nameOf> {
114
+ :s :p :o .
115
+ } .
116
+ ```
117
+
118
+ A top-level default graph block is unwrapped:
119
+
120
+ ```trig
121
+ {
122
+ :s :p :o .
123
+ }
124
+ ```
125
+
126
+ becomes:
127
+
128
+ ```n3
129
+ :s :p :o .
130
+ ```
131
+
132
+ ## Reasoning model
133
+
134
+ After normalization, RDF 1.2 constructs are ordinary N3 terms. For example, this rule can match reified RDF 1.2 triples:
135
+
136
+ ```n3
137
+ { ?r rdf:reifies { ?s ?p ?o } }
138
+ =>
139
+ { ?r :mentionsSubject ?s } .
140
+ ```
141
+
142
+ That works because the RDF triple term has already become an N3 graph term.
143
+
144
+ ## Output and roundtrip
145
+
146
+ RDF-compatible output is implemented in `lib/printing.js` by:
147
+
148
+ - `termToRdfCompatible()`
149
+ - `tripleToRdfCompatible()`
150
+ - `rdfCompatibleGraphBlock()`
151
+
152
+ A `GraphTerm` is printed back as an RDF 1.2 triple term only if it is a singleton graph containing one RDF-compatible triple:
153
+
154
+ ```js
155
+ GraphTerm([
156
+ Triple(subject, predicate, object)
157
+ ])
158
+ ```
159
+
160
+ with:
161
+
162
+ ```text
163
+ subject = IRI or blank node
164
+ predicate = IRI
165
+ object = IRI, blank node, or literal
166
+ ```
167
+
168
+ Then:
169
+
170
+ ```n3
171
+ :obs rdf:reifies { :s :p :o } .
172
+ ```
173
+
174
+ can print as:
175
+
176
+ ```turtle
177
+ :obs rdf:reifies <<( :s :p :o )>> .
178
+ ```
179
+
180
+ And:
181
+
182
+ ```n3
183
+ :g log:nameOf {
184
+ :s :p :o .
185
+ } .
186
+ ```
187
+
188
+ can print as:
189
+
190
+ ```trig
191
+ :g {
192
+ :s :p :o .
193
+ }
194
+ ```
195
+
196
+ ## What roundtrip means
197
+
198
+ The roundtrip is **structural**, not lexical.
199
+
200
+ Eyeling can roundtrip the meaning of RDF 1.2 triple terms through its N3 representation:
201
+
202
+ ```text
203
+ RDF 1.2 triple term
204
+ <<( :s :p :o )>>
205
+
206
+ N3 internal representation
207
+ { :s :p :o }
208
+
209
+ RDF-compatible output
210
+ <<( :s :p :o )>>
211
+ ```
212
+
213
+ But it does not preserve exact source spelling, whitespace, prefix layout, or whether the user originally wrote explicit `rdf:reifies`, triple-term sugar, or annotation syntax.
214
+
215
+ ## Limitations
216
+
217
+ Only singleton graph terms that are RDF-compatible are printed back as `<<( ... )>>`.
218
+
219
+ These remain ordinary N3 graph terms on output:
220
+
221
+ ```n3
222
+ { :s :p :o . :x :y :z . }
223
+ ```
224
+
225
+ because the graph has more than one triple.
226
+
227
+ Also, graph terms containing N3-only constructs, variables, or nested formula objects are not valid RDF 1.2 triple terms and therefore remain N3.
228
+
229
+ ## One-line takeaway
230
+
231
+ Eyeling maps RDF 1.2 triple terms to singleton N3 `GraphTerm`s, maps TriG named graphs to `log:nameOf` triples, reasons over the normal N3 AST, and prints singleton RDF-compatible graph terms back as `<<( ... )>>` when possible.