eyeleng 1.1.1 → 1.1.2
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 -17
- package/dist/browser/eyeleng.browser.js +20 -10
- package/eyeleng.js +20 -10
- package/package.json +1 -1
- package/src/parser.js +15 -5
- package/src/rdfSyntax.js +5 -5
- package/test/api.test.js +35 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- **SRL** — the Shape Rules Language syntax used by the SHACL 1.2 Rules draft.
|
|
9
9
|
- **RDF Rules** — a Turtle/RDF syntax for rule sets.
|
|
10
10
|
|
|
11
|
-
Eyeleng is a
|
|
11
|
+
Eyeleng is a compact reasoner over RDF-style triples. It still uses forward chaining for ordinary finite materialization, but the default execution mode now includes conservative auto-hybrid planning: selected function-like predicates can be proved just in time by a tabled backward prover when they are demanded by a query or by another rule body. It is deliberately small, dependency-free at runtime, readable as ordinary JavaScript, and usable from the CLI, Node.js, and the browser playground.
|
|
12
12
|
|
|
13
13
|
Eyeleng implements the rules/reasoning surface. It is **not** a SHACL validation engine and does not emit SHACL validation reports.
|
|
14
14
|
|
|
@@ -76,6 +76,10 @@ then the body matches with `?parent = :alice` and `?child = :bob`, and the head
|
|
|
76
76
|
|
|
77
77
|
Negation is handled by stratified evaluation: rules are grouped into dependency layers, and recursion through negation is rejected so the result stays deterministic.
|
|
78
78
|
|
|
79
|
+
By default, run mode uses **auto-hybrid reasoning**. Ordinary rules are still materialized forward into the closure. Rules that behave like internal functions can instead be proved backward with tabling when their predicates occur as demanded body goals. For example, a Fibonacci helper predicate can be computed just in time for the public `:fib` results, without printing all intermediate helper triples. Use `--no-hybrid` or `{ hybrid: false }` to force pure forward materialization, and use `--hybrid` or `{ hybrid: true }` to force more aggressive backward orientation.
|
|
80
|
+
|
|
81
|
+
For explicit `--query` / `--query-file` use, the query body is the top-level goal. Each triple pattern in that body is then a sub-goal that may be answered by the backward prover. If pure backward proving is not safe for the demanded predicates, query mode can run a hybrid plan and finally fall back to ordinary forward closure.
|
|
82
|
+
|
|
79
83
|
Recursive rules that create new terms through `SET`/`BIND` or head blank nodes run in relaxed mode by default. Relaxed mode can derive useful finite closures, but termination is not guaranteed; use `--max-iterations` as a safety valve. `--strict` rejects these recursive term-generating cycles at analysis time. Head blank nodes are deterministically skolemized per rule and solution mapping, so the same firing reuses the same witness node instead of creating a fresh one each pass.
|
|
80
84
|
|
|
81
85
|
## Language surface
|
|
@@ -153,7 +157,6 @@ npm run w3c:rules:earl
|
|
|
153
157
|
npm run w3c:rdf
|
|
154
158
|
npm run w3c:rdf:json
|
|
155
159
|
npm run w3c:rdf:earl
|
|
156
|
-
npm run w3c:all
|
|
157
160
|
```
|
|
158
161
|
|
|
159
162
|
`npm test` includes the W3C harnesses. When W3C URLs are reachable, progress is printed test by test. In offline environments, remote W3C checks are reported as unreachable unless `EYELENG_W3C_REQUIRED=1` is set. The `*:earl` scripts also print test progress, but write the EARL Turtle only to `reports/` instead of printing the report to the terminal.
|
|
@@ -238,28 +241,30 @@ RULE { ?x a :Mortal } WHERE { ?x a :Man }
|
|
|
238
241
|
console.log(formatTriples(result.inferred, result.prefixes));
|
|
239
242
|
```
|
|
240
243
|
|
|
241
|
-
|
|
244
|
+
Hybrid and query mode:
|
|
242
245
|
|
|
243
246
|
```js
|
|
244
|
-
const { runQuery, formatBindings } = require('./src/index.js');
|
|
247
|
+
const { run, runQuery, formatBindings } = require('./src/index.js');
|
|
248
|
+
|
|
249
|
+
// Normal run mode defaults to conservative auto-hybrid reasoning.
|
|
250
|
+
// Ordinary output rules still materialize forward, while selected
|
|
251
|
+
// function-like helper predicates can be proved backward on demand.
|
|
252
|
+
const resultWithAutoHybrid = run(source);
|
|
245
253
|
|
|
254
|
+
// Force pure forward closure if you want every derivable helper fact materialized.
|
|
255
|
+
const pureForward = run(source, { hybrid: false });
|
|
256
|
+
|
|
257
|
+
// For explicit queries, the whole raw body is the top-level goal.
|
|
258
|
+
// Each triple pattern in that body is a sub-goal for the planner.
|
|
246
259
|
const result = runQuery(source, '?x :ancestorOf ?y');
|
|
247
260
|
console.log(formatBindings(result.query.bindings, result.prefixes));
|
|
248
261
|
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
// hybrid plan before falling back to plain forward closure.
|
|
253
|
-
const justInTime = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
|
|
254
|
-
|
|
255
|
-
// Run mode uses conservative auto-hybrid planning by default. It keeps
|
|
256
|
-
// ordinary rules materialized, but can prove demanded function-like predicates
|
|
257
|
-
// backward with tabling. Pass { hybrid: false } to force pure forward closure,
|
|
258
|
-
// or { hybrid: true } to force aggressive hybrid orientation.
|
|
259
|
-
const resultWithAutoHybrid = run(source);
|
|
262
|
+
// queryMode defaults to auto: try tabled backward proving for demanded
|
|
263
|
+
// predicates, then hybrid execution, then ordinary forward closure.
|
|
264
|
+
const justInTime = runQuery(source, ':alice :computedValue ?value');
|
|
260
265
|
|
|
261
|
-
//
|
|
262
|
-
|
|
266
|
+
// Force the tabled backward prover for supported query shapes.
|
|
267
|
+
const backwardOnly = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
|
|
263
268
|
```
|
|
264
269
|
|
|
265
270
|
Imports:
|
|
@@ -326,6 +331,7 @@ Examples live in [examples/](./examples/):
|
|
|
326
331
|
- `property-paths.srl` — path matching in bodies
|
|
327
332
|
- `basic-ruleset.ttl` — RDF Rules syntax
|
|
328
333
|
- `rdf-messages.srl` / `rdf-messages.trig` — RDF Message Log replay
|
|
334
|
+
- `fibonacci.srl` — fast-doubling Fibonacci using just-in-time backward helper predicates
|
|
329
335
|
- `deep-taxonomy-*.srl` — generated benchmark programs
|
|
330
336
|
|
|
331
337
|
## Known boundaries
|
|
@@ -441,7 +441,7 @@
|
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
parseTripleStatement(options = {}) {
|
|
444
|
-
const subjectNode = this.parseGraphNode(options);
|
|
444
|
+
const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
|
|
445
445
|
const triples = [...subjectNode.triples];
|
|
446
446
|
triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
|
|
447
447
|
return triples;
|
|
@@ -455,7 +455,7 @@
|
|
|
455
455
|
if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
|
|
456
456
|
const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
|
|
457
457
|
do {
|
|
458
|
-
const objectNode = this.parseGraphNode(options);
|
|
458
|
+
const objectNode = this.parseGraphNode({ ...options, position: 'object' });
|
|
459
459
|
triples.push(...objectNode.triples);
|
|
460
460
|
const baseTriple = { s: subject, p: predicate, o: objectNode.term };
|
|
461
461
|
triples.push(baseTriple);
|
|
@@ -522,6 +522,7 @@
|
|
|
522
522
|
currentReifier = this.parseOptionalReifier(options);
|
|
523
523
|
triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
|
|
524
524
|
} else if (this.matchValue('{|')) {
|
|
525
|
+
if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
|
|
525
526
|
const annotationSubject = currentReifier || this.freshGraphNode(options);
|
|
526
527
|
triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
|
|
527
528
|
triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
|
|
@@ -571,7 +572,7 @@
|
|
|
571
572
|
}
|
|
572
573
|
|
|
573
574
|
parseVerbTerm(options = {}) {
|
|
574
|
-
const term = this.parseTerm(options);
|
|
575
|
+
const term = this.parseTerm({ ...options, position: 'predicate' });
|
|
575
576
|
if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
|
|
576
577
|
return term;
|
|
577
578
|
}
|
|
@@ -694,7 +695,10 @@
|
|
|
694
695
|
if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
|
|
695
696
|
if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
|
|
696
697
|
if (token.type === 'word') {
|
|
697
|
-
if (token.value === 'a')
|
|
698
|
+
if (token.value === 'a') {
|
|
699
|
+
if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
|
|
700
|
+
return iri(RDF_TYPE);
|
|
701
|
+
}
|
|
698
702
|
if (token.value === 'true') return literal(true, XSD_BOOLEAN);
|
|
699
703
|
if (token.value === 'false') return literal(false, XSD_BOOLEAN);
|
|
700
704
|
if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
|
|
@@ -728,7 +732,13 @@
|
|
|
728
732
|
return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
|
|
729
733
|
}
|
|
730
734
|
if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
|
|
731
|
-
const
|
|
735
|
+
const tagToken = this.advance();
|
|
736
|
+
const rawTag = tagToken.value.slice(1);
|
|
737
|
+
const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
|
|
738
|
+
if (direction && direction !== 'ltr' && direction !== 'rtl') {
|
|
739
|
+
throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
|
|
740
|
+
}
|
|
741
|
+
const tag = rawTag.toLowerCase();
|
|
732
742
|
const [lang, langDir = null] = tag.split('--');
|
|
733
743
|
return literal(token.value, null, lang, langDir);
|
|
734
744
|
}
|
|
@@ -2507,6 +2517,7 @@
|
|
|
2507
2517
|
const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
|
|
2508
2518
|
const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
|
|
2509
2519
|
const implicitStatementNodes = new Set();
|
|
2520
|
+
function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
|
|
2510
2521
|
|
|
2511
2522
|
function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
|
|
2512
2523
|
function peek(offset = 0) { return tokens[i + offset]; }
|
|
@@ -2657,7 +2668,7 @@
|
|
|
2657
2668
|
expect('>>');
|
|
2658
2669
|
const node = reifier || freshBlank();
|
|
2659
2670
|
out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
|
|
2660
|
-
|
|
2671
|
+
implicitStatementNodes.add(implicitStatementNodeKey(node));
|
|
2661
2672
|
return node;
|
|
2662
2673
|
}
|
|
2663
2674
|
|
|
@@ -2686,7 +2697,7 @@
|
|
|
2686
2697
|
if (accept(']')) return node;
|
|
2687
2698
|
parsePredicateObjectList(node, out, graph);
|
|
2688
2699
|
expect(']');
|
|
2689
|
-
if (node.kind === 'blank') implicitStatementNodes.add(node
|
|
2700
|
+
if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
|
|
2690
2701
|
return node;
|
|
2691
2702
|
}
|
|
2692
2703
|
|
|
@@ -2764,9 +2775,8 @@
|
|
|
2764
2775
|
if (options3.requireDot) expect('.'); else accept('.');
|
|
2765
2776
|
return;
|
|
2766
2777
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
|
|
2778
|
+
const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
|
|
2779
|
+
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
|
|
2770
2780
|
if (options3.requireDot) expect('.'); else accept('.');
|
|
2771
2781
|
return;
|
|
2772
2782
|
}
|
package/eyeleng.js
CHANGED
|
@@ -710,7 +710,7 @@
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
parseTripleStatement(options = {}) {
|
|
713
|
-
const subjectNode = this.parseGraphNode(options);
|
|
713
|
+
const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
|
|
714
714
|
const triples = [...subjectNode.triples];
|
|
715
715
|
triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
|
|
716
716
|
return triples;
|
|
@@ -724,7 +724,7 @@
|
|
|
724
724
|
if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
|
|
725
725
|
const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
|
|
726
726
|
do {
|
|
727
|
-
const objectNode = this.parseGraphNode(options);
|
|
727
|
+
const objectNode = this.parseGraphNode({ ...options, position: 'object' });
|
|
728
728
|
triples.push(...objectNode.triples);
|
|
729
729
|
const baseTriple = { s: subject, p: predicate, o: objectNode.term };
|
|
730
730
|
triples.push(baseTriple);
|
|
@@ -791,6 +791,7 @@
|
|
|
791
791
|
currentReifier = this.parseOptionalReifier(options);
|
|
792
792
|
triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
|
|
793
793
|
} else if (this.matchValue('{|')) {
|
|
794
|
+
if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
|
|
794
795
|
const annotationSubject = currentReifier || this.freshGraphNode(options);
|
|
795
796
|
triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
|
|
796
797
|
triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
|
|
@@ -840,7 +841,7 @@
|
|
|
840
841
|
}
|
|
841
842
|
|
|
842
843
|
parseVerbTerm(options = {}) {
|
|
843
|
-
const term = this.parseTerm(options);
|
|
844
|
+
const term = this.parseTerm({ ...options, position: 'predicate' });
|
|
844
845
|
if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
|
|
845
846
|
return term;
|
|
846
847
|
}
|
|
@@ -963,7 +964,10 @@
|
|
|
963
964
|
if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
|
|
964
965
|
if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
|
|
965
966
|
if (token.type === 'word') {
|
|
966
|
-
if (token.value === 'a')
|
|
967
|
+
if (token.value === 'a') {
|
|
968
|
+
if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
|
|
969
|
+
return iri(RDF_TYPE);
|
|
970
|
+
}
|
|
967
971
|
if (token.value === 'true') return literal(true, XSD_BOOLEAN);
|
|
968
972
|
if (token.value === 'false') return literal(false, XSD_BOOLEAN);
|
|
969
973
|
if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
|
|
@@ -997,7 +1001,13 @@
|
|
|
997
1001
|
return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
|
|
998
1002
|
}
|
|
999
1003
|
if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
|
|
1000
|
-
const
|
|
1004
|
+
const tagToken = this.advance();
|
|
1005
|
+
const rawTag = tagToken.value.slice(1);
|
|
1006
|
+
const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
|
|
1007
|
+
if (direction && direction !== 'ltr' && direction !== 'rtl') {
|
|
1008
|
+
throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
|
|
1009
|
+
}
|
|
1010
|
+
const tag = rawTag.toLowerCase();
|
|
1001
1011
|
const [lang, langDir = null] = tag.split('--');
|
|
1002
1012
|
return literal(token.value, null, lang, langDir);
|
|
1003
1013
|
}
|
|
@@ -2776,6 +2786,7 @@
|
|
|
2776
2786
|
const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
|
|
2777
2787
|
const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
|
|
2778
2788
|
const implicitStatementNodes = new Set();
|
|
2789
|
+
function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
|
|
2779
2790
|
|
|
2780
2791
|
function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
|
|
2781
2792
|
function peek(offset = 0) { return tokens[i + offset]; }
|
|
@@ -2926,7 +2937,7 @@
|
|
|
2926
2937
|
expect('>>');
|
|
2927
2938
|
const node = reifier || freshBlank();
|
|
2928
2939
|
out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
|
|
2929
|
-
|
|
2940
|
+
implicitStatementNodes.add(implicitStatementNodeKey(node));
|
|
2930
2941
|
return node;
|
|
2931
2942
|
}
|
|
2932
2943
|
|
|
@@ -2955,7 +2966,7 @@
|
|
|
2955
2966
|
if (accept(']')) return node;
|
|
2956
2967
|
parsePredicateObjectList(node, out, graph);
|
|
2957
2968
|
expect(']');
|
|
2958
|
-
if (node.kind === 'blank') implicitStatementNodes.add(node
|
|
2969
|
+
if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
|
|
2959
2970
|
return node;
|
|
2960
2971
|
}
|
|
2961
2972
|
|
|
@@ -3033,9 +3044,8 @@
|
|
|
3033
3044
|
if (options3.requireDot) expect('.'); else accept('.');
|
|
3034
3045
|
return;
|
|
3035
3046
|
}
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
|
|
3047
|
+
const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
|
|
3048
|
+
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
|
|
3039
3049
|
if (options3.requireDot) expect('.'); else accept('.');
|
|
3040
3050
|
return;
|
|
3041
3051
|
}
|
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -296,7 +296,7 @@ class Parser {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
parseTripleStatement(options = {}) {
|
|
299
|
-
const subjectNode = this.parseGraphNode(options);
|
|
299
|
+
const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
|
|
300
300
|
const triples = [...subjectNode.triples];
|
|
301
301
|
triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
|
|
302
302
|
return triples;
|
|
@@ -310,7 +310,7 @@ class Parser {
|
|
|
310
310
|
if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
|
|
311
311
|
const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
|
|
312
312
|
do {
|
|
313
|
-
const objectNode = this.parseGraphNode(options);
|
|
313
|
+
const objectNode = this.parseGraphNode({ ...options, position: 'object' });
|
|
314
314
|
triples.push(...objectNode.triples);
|
|
315
315
|
const baseTriple = { s: subject, p: predicate, o: objectNode.term };
|
|
316
316
|
triples.push(baseTriple);
|
|
@@ -377,6 +377,7 @@ class Parser {
|
|
|
377
377
|
currentReifier = this.parseOptionalReifier(options);
|
|
378
378
|
triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
|
|
379
379
|
} else if (this.matchValue('{|')) {
|
|
380
|
+
if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
|
|
380
381
|
const annotationSubject = currentReifier || this.freshGraphNode(options);
|
|
381
382
|
triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
|
|
382
383
|
triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
|
|
@@ -426,7 +427,7 @@ class Parser {
|
|
|
426
427
|
}
|
|
427
428
|
|
|
428
429
|
parseVerbTerm(options = {}) {
|
|
429
|
-
const term = this.parseTerm(options);
|
|
430
|
+
const term = this.parseTerm({ ...options, position: 'predicate' });
|
|
430
431
|
if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
|
|
431
432
|
return term;
|
|
432
433
|
}
|
|
@@ -549,7 +550,10 @@ class Parser {
|
|
|
549
550
|
if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
|
|
550
551
|
if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
|
|
551
552
|
if (token.type === 'word') {
|
|
552
|
-
if (token.value === 'a')
|
|
553
|
+
if (token.value === 'a') {
|
|
554
|
+
if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
|
|
555
|
+
return iri(RDF_TYPE);
|
|
556
|
+
}
|
|
553
557
|
if (token.value === 'true') return literal(true, XSD_BOOLEAN);
|
|
554
558
|
if (token.value === 'false') return literal(false, XSD_BOOLEAN);
|
|
555
559
|
if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
|
|
@@ -583,7 +587,13 @@ class Parser {
|
|
|
583
587
|
return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
|
|
584
588
|
}
|
|
585
589
|
if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
|
|
586
|
-
const
|
|
590
|
+
const tagToken = this.advance();
|
|
591
|
+
const rawTag = tagToken.value.slice(1);
|
|
592
|
+
const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
|
|
593
|
+
if (direction && direction !== 'ltr' && direction !== 'rtl') {
|
|
594
|
+
throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
|
|
595
|
+
}
|
|
596
|
+
const tag = rawTag.toLowerCase();
|
|
587
597
|
const [lang, langDir = null] = tag.split('--');
|
|
588
598
|
return literal(token.value, null, lang, langDir);
|
|
589
599
|
}
|
package/src/rdfSyntax.js
CHANGED
|
@@ -1160,6 +1160,7 @@ function parseN3(source, options = {}) {
|
|
|
1160
1160
|
const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
|
|
1161
1161
|
const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
|
|
1162
1162
|
const implicitStatementNodes = new Set();
|
|
1163
|
+
function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
|
|
1163
1164
|
|
|
1164
1165
|
function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
|
|
1165
1166
|
function peek(offset = 0) { return tokens[i + offset]; }
|
|
@@ -1310,7 +1311,7 @@ function parseN3(source, options = {}) {
|
|
|
1310
1311
|
expect('>>');
|
|
1311
1312
|
const node = reifier || freshBlank();
|
|
1312
1313
|
out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
|
|
1313
|
-
|
|
1314
|
+
implicitStatementNodes.add(implicitStatementNodeKey(node));
|
|
1314
1315
|
return node;
|
|
1315
1316
|
}
|
|
1316
1317
|
|
|
@@ -1339,7 +1340,7 @@ function parseN3(source, options = {}) {
|
|
|
1339
1340
|
if (accept(']')) return node;
|
|
1340
1341
|
parsePredicateObjectList(node, out, graph);
|
|
1341
1342
|
expect(']');
|
|
1342
|
-
if (node.kind === 'blank') implicitStatementNodes.add(node
|
|
1343
|
+
if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
|
|
1343
1344
|
return node;
|
|
1344
1345
|
}
|
|
1345
1346
|
|
|
@@ -1417,9 +1418,8 @@ function parseN3(source, options = {}) {
|
|
|
1417
1418
|
if (options3.requireDot) expect('.'); else accept('.');
|
|
1418
1419
|
return;
|
|
1419
1420
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
|
|
1421
|
+
const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
|
|
1422
|
+
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
|
|
1423
1423
|
if (options3.requireDot) expect('.'); else accept('.');
|
|
1424
1424
|
return;
|
|
1425
1425
|
}
|
package/test/api.test.js
CHANGED
|
@@ -746,6 +746,41 @@ RULE { :test :annotation ?source } WHERE {
|
|
|
746
746
|
assert.match(output, /:test :annotation :witness \./);
|
|
747
747
|
});
|
|
748
748
|
|
|
749
|
+
test('DATA blocks accept RDF 1.2 symmetric and standalone reifier syntax', () => {
|
|
750
|
+
const symmetric = parse(`
|
|
751
|
+
PREFIX : <http://example/>
|
|
752
|
+
DATA {
|
|
753
|
+
123 :q 456 .
|
|
754
|
+
<<( :s :p :o )>> :q <<( :s1 :p1 :o1 )>> .
|
|
755
|
+
}
|
|
756
|
+
`);
|
|
757
|
+
assert.equal(symmetric.data.length, 2);
|
|
758
|
+
assert.equal(symmetric.data[0].s.type, 'literal');
|
|
759
|
+
assert.equal(symmetric.data[1].s.type, 'triple');
|
|
760
|
+
|
|
761
|
+
const standalone = parse(`
|
|
762
|
+
PREFIX : <http://example/>
|
|
763
|
+
DATA { << :a :b :c ~:r >> . }
|
|
764
|
+
`);
|
|
765
|
+
assert.equal(standalone.data.length, 1);
|
|
766
|
+
assert.equal(standalone.data[0].s.value, 'http://example/r');
|
|
767
|
+
assert.equal(standalone.data[0].p.value, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies');
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test('SRL rejects invalid base directions, subject a, and empty annotations', () => {
|
|
771
|
+
const invalidPatterns = [
|
|
772
|
+
'RULE { } WHERE { :s :p "abc"@en--LTR }',
|
|
773
|
+
'RULE { :s :p "abc"@en--LTR } WHERE { ?a ?b ?c }',
|
|
774
|
+
'RULE { } WHERE { a :p "abc" }',
|
|
775
|
+
'RULE { a :p "abc" } WHERE { ?a ?b ?c }',
|
|
776
|
+
'RULE { } WHERE { :s :p :o ~:r {| |} }',
|
|
777
|
+
'RULE { :s :p :o ~:r {| |} } WHERE { ?a ?b ?c }',
|
|
778
|
+
];
|
|
779
|
+
for (const pattern of invalidPatterns) {
|
|
780
|
+
assert.throws(() => parse(`PREFIX : <http://example/>\n${pattern}`), Error, pattern);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
|
|
749
784
|
|
|
750
785
|
test('strict conformance allows recursive constant BIND aliases', () => {
|
|
751
786
|
assert.doesNotThrow(() => compile(`
|