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/README.md +23 -4
- package/dist/browser/eyeling.browser.js +164 -43
- package/eyeling.js +164 -43
- package/index.d.ts +3 -2
- package/lib/engine.js +22 -18
- package/lib/rdfjs.js +142 -25
- package/notes/rdf12-roundtrip.md +231 -0
- package/notes/rdfjs-integration.md +378 -0
- package/package.json +4 -2
- package/spec/rdf12-parser.js +432 -0
- package/test/api.test.js +75 -20
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
|
-
|
|
398
|
+
internalRdf12TermToRdfJs(triple.s, rdfFactory, 'subject'),
|
|
354
399
|
internalTermToRdfJs(triple.p, rdfFactory, 'predicate'),
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|