eyeling 1.14.12 → 1.14.13
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/eyeling.js +180 -254
- package/lib/parser.js +180 -254
- package/package.json +1 -1
package/eyeling.js
CHANGED
|
@@ -8447,6 +8447,72 @@ class Parser {
|
|
|
8447
8447
|
}
|
|
8448
8448
|
}
|
|
8449
8449
|
|
|
8450
|
+
peekAt(offset) {
|
|
8451
|
+
return this.toks[this.pos + offset];
|
|
8452
|
+
}
|
|
8453
|
+
|
|
8454
|
+
isIdentKeyword(tok, keyword) {
|
|
8455
|
+
return tok && tok.typ === 'Ident' && typeof tok.value === 'string' && tok.value.toLowerCase() === keyword;
|
|
8456
|
+
}
|
|
8457
|
+
|
|
8458
|
+
canStartSparqlPrefixDirective() {
|
|
8459
|
+
const prefixNameTok = this.peekAt(1);
|
|
8460
|
+
const iriTok = this.peekAt(2);
|
|
8461
|
+
return (
|
|
8462
|
+
this.isIdentKeyword(this.peek(), 'prefix') &&
|
|
8463
|
+
prefixNameTok &&
|
|
8464
|
+
prefixNameTok.typ === 'Ident' &&
|
|
8465
|
+
typeof prefixNameTok.value === 'string' &&
|
|
8466
|
+
prefixNameTok.value.endsWith(':') &&
|
|
8467
|
+
iriTok &&
|
|
8468
|
+
(iriTok.typ === 'IriRef' || iriTok.typ === 'Ident')
|
|
8469
|
+
);
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8472
|
+
canStartSparqlBaseDirective(allowIdentIri = false) {
|
|
8473
|
+
const iriTok = this.peekAt(1);
|
|
8474
|
+
return (
|
|
8475
|
+
this.isIdentKeyword(this.peek(), 'base') &&
|
|
8476
|
+
iriTok &&
|
|
8477
|
+
(iriTok.typ === 'IriRef' || (allowIdentIri && iriTok.typ === 'Ident'))
|
|
8478
|
+
);
|
|
8479
|
+
}
|
|
8480
|
+
|
|
8481
|
+
parseDirectiveIfPresent({ allowIdentBaseIri = false } = {}) {
|
|
8482
|
+
if (this.peek().typ === 'AtPrefix') {
|
|
8483
|
+
this.next();
|
|
8484
|
+
this.parsePrefixDirective();
|
|
8485
|
+
return true;
|
|
8486
|
+
}
|
|
8487
|
+
if (this.peek().typ === 'AtBase') {
|
|
8488
|
+
this.next();
|
|
8489
|
+
this.parseBaseDirective();
|
|
8490
|
+
return true;
|
|
8491
|
+
}
|
|
8492
|
+
if (this.canStartSparqlPrefixDirective()) {
|
|
8493
|
+
this.next();
|
|
8494
|
+
this.parseSparqlPrefixDirective();
|
|
8495
|
+
return true;
|
|
8496
|
+
}
|
|
8497
|
+
if (this.canStartSparqlBaseDirective(allowIdentBaseIri)) {
|
|
8498
|
+
this.next();
|
|
8499
|
+
this.parseSparqlBaseDirective();
|
|
8500
|
+
return true;
|
|
8501
|
+
}
|
|
8502
|
+
return false;
|
|
8503
|
+
}
|
|
8504
|
+
|
|
8505
|
+
flushPendingTriples(out, { includeBefore = true, includeAfter = true } = {}) {
|
|
8506
|
+
if (includeBefore && this.pendingTriples.length > 0) {
|
|
8507
|
+
out.push(...this.pendingTriples);
|
|
8508
|
+
this.pendingTriples = [];
|
|
8509
|
+
}
|
|
8510
|
+
if (includeAfter && this.pendingTriplesAfter.length > 0) {
|
|
8511
|
+
out.push(...this.pendingTriplesAfter);
|
|
8512
|
+
this.pendingTriplesAfter = [];
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
|
|
8450
8516
|
parseDocument() {
|
|
8451
8517
|
const triples = [];
|
|
8452
8518
|
const forwardRules = [];
|
|
@@ -8454,86 +8520,50 @@ class Parser {
|
|
|
8454
8520
|
const logQueries = [];
|
|
8455
8521
|
|
|
8456
8522
|
while (this.peek().typ !== 'EOF') {
|
|
8457
|
-
if (this.
|
|
8523
|
+
if (this.parseDirectiveIfPresent({ allowIdentBaseIri: true })) {
|
|
8524
|
+
continue;
|
|
8525
|
+
}
|
|
8526
|
+
|
|
8527
|
+
const first = this.parseTerm();
|
|
8528
|
+
if (this.peek().typ === 'OpImplies') {
|
|
8458
8529
|
this.next();
|
|
8459
|
-
this.
|
|
8460
|
-
|
|
8530
|
+
const second = this.parseTerm();
|
|
8531
|
+
this.expectDot();
|
|
8532
|
+
forwardRules.push(this.makeRule(first, second, true));
|
|
8533
|
+
} else if (this.peek().typ === 'OpImpliedBy') {
|
|
8461
8534
|
this.next();
|
|
8462
|
-
this.
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
this.peek().typ === 'Ident' &&
|
|
8466
|
-
typeof this.peek().value === 'string' &&
|
|
8467
|
-
this.peek().value.toLowerCase() === 'prefix' &&
|
|
8468
|
-
this.toks[this.pos + 1] &&
|
|
8469
|
-
this.toks[this.pos + 1].typ === 'Ident' &&
|
|
8470
|
-
typeof this.toks[this.pos + 1].value === 'string' &&
|
|
8471
|
-
// Require PNAME_NS form (e.g., "ex:" or ":") to avoid clashing with a normal triple starting with IRI "prefix".
|
|
8472
|
-
this.toks[this.pos + 1].value.endsWith(':') &&
|
|
8473
|
-
this.toks[this.pos + 2] &&
|
|
8474
|
-
(this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')
|
|
8475
|
-
) {
|
|
8476
|
-
this.next(); // consume PREFIX keyword
|
|
8477
|
-
this.parseSparqlPrefixDirective();
|
|
8478
|
-
} else if (
|
|
8479
|
-
this.peek().typ === 'Ident' &&
|
|
8480
|
-
typeof this.peek().value === 'string' &&
|
|
8481
|
-
this.peek().value.toLowerCase() === 'base' &&
|
|
8482
|
-
this.toks[this.pos + 1] &&
|
|
8483
|
-
// SPARQL BASE requires an IRIREF.
|
|
8484
|
-
this.toks[this.pos + 1].typ === 'IriRef'
|
|
8485
|
-
) {
|
|
8486
|
-
this.next(); // consume BASE keyword
|
|
8487
|
-
this.parseSparqlBaseDirective();
|
|
8535
|
+
const second = this.parseTerm();
|
|
8536
|
+
this.expectDot();
|
|
8537
|
+
backwardRules.push(this.makeRule(first, second, false));
|
|
8488
8538
|
} else {
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
this.
|
|
8499
|
-
backwardRules.push(this.makeRule(first, second, false));
|
|
8539
|
+
let more;
|
|
8540
|
+
|
|
8541
|
+
if (this.peek().typ === 'Dot') {
|
|
8542
|
+
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
8543
|
+
// So a bare subject followed by '.' is syntactically valid.
|
|
8544
|
+
// If the subject was a path / property-list that generated helper triples,
|
|
8545
|
+
// we emit those; otherwise this statement contributes no triples.
|
|
8546
|
+
more = [];
|
|
8547
|
+
this.flushPendingTriples(more);
|
|
8548
|
+
this.next(); // consume '.'
|
|
8500
8549
|
} else {
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
8505
|
-
// So a bare subject followed by '.' is syntactically valid.
|
|
8506
|
-
// If the subject was a path / property-list that generated helper triples,
|
|
8507
|
-
// we emit those; otherwise this statement contributes no triples.
|
|
8508
|
-
more = [];
|
|
8509
|
-
if (this.pendingTriples.length > 0) {
|
|
8510
|
-
more.push(...this.pendingTriples);
|
|
8511
|
-
this.pendingTriples = [];
|
|
8512
|
-
}
|
|
8513
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
8514
|
-
more.push(...this.pendingTriplesAfter);
|
|
8515
|
-
this.pendingTriplesAfter = [];
|
|
8516
|
-
}
|
|
8517
|
-
this.next(); // consume '.'
|
|
8518
|
-
} else {
|
|
8519
|
-
more = this.parsePredicateObjectList(first);
|
|
8520
|
-
this.expectDot();
|
|
8521
|
-
}
|
|
8550
|
+
more = this.parsePredicateObjectList(first);
|
|
8551
|
+
this.expectDot();
|
|
8552
|
+
}
|
|
8522
8553
|
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
}
|
|
8554
|
+
// normalize explicit log:implies / log:impliedBy at top-level
|
|
8555
|
+
for (const tr of more) {
|
|
8556
|
+
if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
8557
|
+
forwardRules.push(this.makeRule(tr.s, tr.o, true));
|
|
8558
|
+
} else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
8559
|
+
backwardRules.push(this.makeRule(tr.s, tr.o, false));
|
|
8560
|
+
} else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
8561
|
+
// Output-selection directive: { premise } log:query { conclusion }.
|
|
8562
|
+
// When present at top-level, eyeling prints only the instantiated conclusion
|
|
8563
|
+
// triples (unique) instead of all newly derived facts.
|
|
8564
|
+
logQueries.push(this.makeRule(tr.s, tr.o, true));
|
|
8565
|
+
} else {
|
|
8566
|
+
triples.push(tr);
|
|
8537
8567
|
}
|
|
8538
8568
|
}
|
|
8539
8569
|
}
|
|
@@ -8765,64 +8795,7 @@ class Parser {
|
|
|
8765
8795
|
return iriTerm;
|
|
8766
8796
|
}
|
|
8767
8797
|
|
|
8768
|
-
const
|
|
8769
|
-
const localTriples = [];
|
|
8770
|
-
while (true) {
|
|
8771
|
-
let pred;
|
|
8772
|
-
let invert = false;
|
|
8773
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
8774
|
-
this.next();
|
|
8775
|
-
pred = internIri(RDF_NS + 'type');
|
|
8776
|
-
} else if (this.peek().typ === 'OpPredInvert') {
|
|
8777
|
-
this.next(); // "<-"
|
|
8778
|
-
pred = this.parseTerm();
|
|
8779
|
-
invert = true;
|
|
8780
|
-
} else {
|
|
8781
|
-
pred = this.parseTerm();
|
|
8782
|
-
}
|
|
8783
|
-
|
|
8784
|
-
// If a pathological predicate term produced post-triples, don't let them leak.
|
|
8785
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
8786
|
-
localTriples.push(...this.pendingTriplesAfter);
|
|
8787
|
-
this.pendingTriplesAfter = [];
|
|
8788
|
-
}
|
|
8789
|
-
|
|
8790
|
-
// Object list: o1, o2, ... (capture post-triples per object)
|
|
8791
|
-
const objs = [];
|
|
8792
|
-
const readObj = () => {
|
|
8793
|
-
const o = this.parseTerm();
|
|
8794
|
-
const post = this.pendingTriplesAfter;
|
|
8795
|
-
this.pendingTriplesAfter = [];
|
|
8796
|
-
objs.push({ term: o, postTriples: post });
|
|
8797
|
-
};
|
|
8798
|
-
readObj();
|
|
8799
|
-
while (this.peek().typ === 'Comma') {
|
|
8800
|
-
this.next();
|
|
8801
|
-
readObj();
|
|
8802
|
-
}
|
|
8803
|
-
|
|
8804
|
-
for (const { term: o, postTriples } of objs) {
|
|
8805
|
-
// Path helper triples must come before the triple that consumes the path result.
|
|
8806
|
-
if (this.pendingTriples.length > 0) {
|
|
8807
|
-
localTriples.push(...this.pendingTriples);
|
|
8808
|
-
this.pendingTriples = [];
|
|
8809
|
-
}
|
|
8810
|
-
localTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
8811
|
-
if (postTriples && postTriples.length) localTriples.push(...postTriples);
|
|
8812
|
-
}
|
|
8813
|
-
|
|
8814
|
-
if (this.peek().typ === 'Semicolon') {
|
|
8815
|
-
this.next();
|
|
8816
|
-
if (this.peek().typ === 'RBracket') break;
|
|
8817
|
-
continue;
|
|
8818
|
-
}
|
|
8819
|
-
break;
|
|
8820
|
-
}
|
|
8821
|
-
|
|
8822
|
-
if (this.peek().typ !== 'RBracket') {
|
|
8823
|
-
this.fail(`Expected ']' at end of IRI property list, got ${this.peek().toString()}`);
|
|
8824
|
-
}
|
|
8825
|
-
this.next();
|
|
8798
|
+
const localTriples = this.parsePropertyListTriples(iriTerm, 'RBracket', 'IRI property list');
|
|
8826
8799
|
|
|
8827
8800
|
// Defer the embedded description until after the triple that references the IRI.
|
|
8828
8801
|
if (localTriples.length) this.pendingTriplesAfter.push(...localTriples);
|
|
@@ -8833,38 +8806,48 @@ class Parser {
|
|
|
8833
8806
|
this.blankCounter += 1;
|
|
8834
8807
|
const id = `_:b${this.blankCounter}`;
|
|
8835
8808
|
const subj = new Blank(id);
|
|
8809
|
+
const localTriples = this.parsePropertyListTriples(subj, 'RBracket', 'blank node property list');
|
|
8836
8810
|
|
|
8811
|
+
// Defer the blank-node description until after the triple that references it.
|
|
8812
|
+
if (localTriples.length) this.pendingTriplesAfter.push(...localTriples);
|
|
8813
|
+
return new Blank(id);
|
|
8814
|
+
}
|
|
8815
|
+
|
|
8816
|
+
parsePropertyVerb() {
|
|
8817
|
+
let pred;
|
|
8818
|
+
let invert = false;
|
|
8819
|
+
|
|
8820
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
8821
|
+
this.next();
|
|
8822
|
+
pred = internIri(RDF_NS + 'type');
|
|
8823
|
+
} else if (this.peek().typ === 'OpPredInvert') {
|
|
8824
|
+
this.next();
|
|
8825
|
+
pred = this.parseTerm();
|
|
8826
|
+
invert = true;
|
|
8827
|
+
} else {
|
|
8828
|
+
pred = this.parseTerm();
|
|
8829
|
+
}
|
|
8830
|
+
|
|
8831
|
+
return { pred, invert };
|
|
8832
|
+
}
|
|
8833
|
+
|
|
8834
|
+
parsePropertyListTriples(subject, closingTyp, contextLabel) {
|
|
8837
8835
|
const localTriples = [];
|
|
8838
8836
|
|
|
8839
8837
|
while (true) {
|
|
8840
|
-
|
|
8841
|
-
let pred;
|
|
8842
|
-
let invert = false;
|
|
8843
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
8844
|
-
this.next();
|
|
8845
|
-
pred = internIri(RDF_NS + 'type');
|
|
8846
|
-
} else if (this.peek().typ === 'OpPredInvert') {
|
|
8847
|
-
this.next(); // consume "<-"
|
|
8848
|
-
pred = this.parseTerm();
|
|
8849
|
-
invert = true;
|
|
8850
|
-
} else {
|
|
8851
|
-
pred = this.parseTerm();
|
|
8852
|
-
}
|
|
8838
|
+
const { pred, invert } = this.parsePropertyVerb();
|
|
8853
8839
|
|
|
8854
8840
|
// If a pathological predicate term produced post-triples, don't let them leak.
|
|
8855
|
-
|
|
8856
|
-
localTriples.push(...this.pendingTriplesAfter);
|
|
8857
|
-
this.pendingTriplesAfter = [];
|
|
8858
|
-
}
|
|
8841
|
+
this.flushPendingTriples(localTriples, { includeBefore: false, includeAfter: true });
|
|
8859
8842
|
|
|
8860
|
-
// Object list: o1, o2, ... (capture post-triples per object)
|
|
8861
8843
|
const objs = [];
|
|
8862
8844
|
const readObj = () => {
|
|
8863
|
-
const
|
|
8864
|
-
const
|
|
8845
|
+
const term = this.parseTerm();
|
|
8846
|
+
const postTriples = this.pendingTriplesAfter;
|
|
8865
8847
|
this.pendingTriplesAfter = [];
|
|
8866
|
-
objs.push({ term
|
|
8848
|
+
objs.push({ term, postTriples });
|
|
8867
8849
|
};
|
|
8850
|
+
|
|
8868
8851
|
readObj();
|
|
8869
8852
|
while (this.peek().typ === 'Comma') {
|
|
8870
8853
|
this.next();
|
|
@@ -8873,31 +8856,25 @@ class Parser {
|
|
|
8873
8856
|
|
|
8874
8857
|
for (const { term: o, postTriples } of objs) {
|
|
8875
8858
|
// Path helper triples must come before the triple that consumes the path result.
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
this.pendingTriples = [];
|
|
8879
|
-
}
|
|
8880
|
-
localTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
8859
|
+
this.flushPendingTriples(localTriples, { includeBefore: true, includeAfter: false });
|
|
8860
|
+
localTriples.push(invert ? new Triple(o, pred, subject) : new Triple(subject, pred, o));
|
|
8881
8861
|
if (postTriples && postTriples.length) localTriples.push(...postTriples);
|
|
8882
8862
|
}
|
|
8883
8863
|
|
|
8884
8864
|
if (this.peek().typ === 'Semicolon') {
|
|
8885
8865
|
this.next();
|
|
8886
|
-
if (this.peek().typ ===
|
|
8866
|
+
if (this.peek().typ === closingTyp) break;
|
|
8887
8867
|
continue;
|
|
8888
8868
|
}
|
|
8889
8869
|
break;
|
|
8890
8870
|
}
|
|
8891
8871
|
|
|
8892
|
-
if (this.peek().typ
|
|
8893
|
-
this.
|
|
8894
|
-
} else {
|
|
8895
|
-
this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
|
|
8872
|
+
if (this.peek().typ !== closingTyp) {
|
|
8873
|
+
this.fail(`Expected ']' at end of ${contextLabel}, got ${this.peek().toString()}`);
|
|
8896
8874
|
}
|
|
8875
|
+
this.next();
|
|
8897
8876
|
|
|
8898
|
-
|
|
8899
|
-
if (localTriples.length) this.pendingTriplesAfter.push(...localTriples);
|
|
8900
|
-
return new Blank(id);
|
|
8877
|
+
return localTriples;
|
|
8901
8878
|
}
|
|
8902
8879
|
|
|
8903
8880
|
parseGraph() {
|
|
@@ -8906,40 +8883,7 @@ class Parser {
|
|
|
8906
8883
|
// N3 allows @prefix/@base and SPARQL-style PREFIX/BASE directives anywhere
|
|
8907
8884
|
// outside of a triple. This includes inside quoted graph terms.
|
|
8908
8885
|
// These directives affect parsing (prefix/base resolution) but do not emit triples.
|
|
8909
|
-
if (this.
|
|
8910
|
-
this.next();
|
|
8911
|
-
this.parsePrefixDirective();
|
|
8912
|
-
continue;
|
|
8913
|
-
}
|
|
8914
|
-
if (this.peek().typ === 'AtBase') {
|
|
8915
|
-
this.next();
|
|
8916
|
-
this.parseBaseDirective();
|
|
8917
|
-
continue;
|
|
8918
|
-
}
|
|
8919
|
-
if (
|
|
8920
|
-
this.peek().typ === 'Ident' &&
|
|
8921
|
-
typeof this.peek().value === 'string' &&
|
|
8922
|
-
this.peek().value.toLowerCase() === 'prefix' &&
|
|
8923
|
-
this.toks[this.pos + 1] &&
|
|
8924
|
-
this.toks[this.pos + 1].typ === 'Ident' &&
|
|
8925
|
-
typeof this.toks[this.pos + 1].value === 'string' &&
|
|
8926
|
-
this.toks[this.pos + 1].value.endsWith(':') &&
|
|
8927
|
-
this.toks[this.pos + 2] &&
|
|
8928
|
-
(this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')
|
|
8929
|
-
) {
|
|
8930
|
-
this.next();
|
|
8931
|
-
this.parseSparqlPrefixDirective();
|
|
8932
|
-
continue;
|
|
8933
|
-
}
|
|
8934
|
-
if (
|
|
8935
|
-
this.peek().typ === 'Ident' &&
|
|
8936
|
-
typeof this.peek().value === 'string' &&
|
|
8937
|
-
this.peek().value.toLowerCase() === 'base' &&
|
|
8938
|
-
this.toks[this.pos + 1] &&
|
|
8939
|
-
(this.toks[this.pos + 1].typ === 'IriRef' || this.toks[this.pos + 1].typ === 'Ident')
|
|
8940
|
-
) {
|
|
8941
|
-
this.next();
|
|
8942
|
-
this.parseSparqlBaseDirective();
|
|
8886
|
+
if (this.parseDirectiveIfPresent()) {
|
|
8943
8887
|
continue;
|
|
8944
8888
|
}
|
|
8945
8889
|
|
|
@@ -8970,14 +8914,7 @@ class Parser {
|
|
|
8970
8914
|
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
8971
8915
|
// So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
|
|
8972
8916
|
if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
|
|
8973
|
-
|
|
8974
|
-
triples.push(...this.pendingTriples);
|
|
8975
|
-
this.pendingTriples = [];
|
|
8976
|
-
}
|
|
8977
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
8978
|
-
triples.push(...this.pendingTriplesAfter);
|
|
8979
|
-
this.pendingTriplesAfter = [];
|
|
8980
|
-
}
|
|
8917
|
+
this.flushPendingTriples(triples);
|
|
8981
8918
|
if (this.peek().typ === 'Dot') this.next();
|
|
8982
8919
|
continue;
|
|
8983
8920
|
}
|
|
@@ -8995,61 +8932,50 @@ class Parser {
|
|
|
8995
8932
|
return new GraphTerm(triples);
|
|
8996
8933
|
}
|
|
8997
8934
|
|
|
8935
|
+
parseStatementVerb() {
|
|
8936
|
+
let verb;
|
|
8937
|
+
let invert = false;
|
|
8938
|
+
|
|
8939
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
8940
|
+
this.next();
|
|
8941
|
+
verb = internIri(RDF_NS + 'type');
|
|
8942
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
8943
|
+
// N3 syntactic sugar: "S has P O." means "S P O."
|
|
8944
|
+
this.next();
|
|
8945
|
+
verb = this.parseTerm();
|
|
8946
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
8947
|
+
// N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
|
|
8948
|
+
this.next();
|
|
8949
|
+
verb = this.parseTerm();
|
|
8950
|
+
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
8951
|
+
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
8952
|
+
}
|
|
8953
|
+
this.next();
|
|
8954
|
+
invert = true;
|
|
8955
|
+
} else if (this.peek().typ === 'OpPredInvert') {
|
|
8956
|
+
this.next();
|
|
8957
|
+
verb = this.parseTerm();
|
|
8958
|
+
invert = true;
|
|
8959
|
+
} else {
|
|
8960
|
+
verb = this.parseTerm();
|
|
8961
|
+
}
|
|
8962
|
+
|
|
8963
|
+
return { verb, invert };
|
|
8964
|
+
}
|
|
8965
|
+
|
|
8998
8966
|
parsePredicateObjectList(subject) {
|
|
8999
8967
|
const out = [];
|
|
9000
8968
|
|
|
9001
8969
|
// If the SUBJECT was a path or property-list, emit its helper triples first.
|
|
9002
|
-
|
|
9003
|
-
out.push(...this.pendingTriples);
|
|
9004
|
-
this.pendingTriples = [];
|
|
9005
|
-
}
|
|
9006
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
9007
|
-
out.push(...this.pendingTriplesAfter);
|
|
9008
|
-
this.pendingTriplesAfter = [];
|
|
9009
|
-
}
|
|
8970
|
+
this.flushPendingTriples(out);
|
|
9010
8971
|
|
|
9011
8972
|
while (true) {
|
|
9012
|
-
|
|
9013
|
-
let invert = false;
|
|
9014
|
-
|
|
9015
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
9016
|
-
this.next();
|
|
9017
|
-
verb = internIri(RDF_NS + 'type');
|
|
9018
|
-
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
9019
|
-
// N3 syntactic sugar: "S has P O." means "S P O."
|
|
9020
|
-
this.next(); // consume "has"
|
|
9021
|
-
verb = this.parseTerm();
|
|
9022
|
-
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
9023
|
-
// N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
|
|
9024
|
-
this.next(); // consume "is"
|
|
9025
|
-
verb = this.parseTerm();
|
|
9026
|
-
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
9027
|
-
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
9028
|
-
}
|
|
9029
|
-
this.next(); // consume "of"
|
|
9030
|
-
invert = true;
|
|
9031
|
-
} else if (this.peek().typ === 'OpPredInvert') {
|
|
9032
|
-
this.next(); // "<-"
|
|
9033
|
-
verb = this.parseTerm();
|
|
9034
|
-
invert = true;
|
|
9035
|
-
} else {
|
|
9036
|
-
verb = this.parseTerm();
|
|
9037
|
-
}
|
|
9038
|
-
|
|
8973
|
+
const { verb, invert } = this.parseStatementVerb();
|
|
9039
8974
|
const objects = this.parseObjectList();
|
|
9040
8975
|
|
|
9041
8976
|
// If VERB or OBJECTS contained paths, their helper triples must come
|
|
9042
8977
|
// before the triples that consume the path results (Easter depends on this).
|
|
9043
|
-
|
|
9044
|
-
out.push(...this.pendingTriples);
|
|
9045
|
-
this.pendingTriples = [];
|
|
9046
|
-
}
|
|
9047
|
-
|
|
9048
|
-
// If VERB produced a property list (rare), don't let it leak.
|
|
9049
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
9050
|
-
out.push(...this.pendingTriplesAfter);
|
|
9051
|
-
this.pendingTriplesAfter = [];
|
|
9052
|
-
}
|
|
8978
|
+
this.flushPendingTriples(out);
|
|
9053
8979
|
|
|
9054
8980
|
for (const { term: o, postTriples } of objects) {
|
|
9055
8981
|
out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
|
package/lib/parser.js
CHANGED
|
@@ -76,6 +76,72 @@ class Parser {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
peekAt(offset) {
|
|
80
|
+
return this.toks[this.pos + offset];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isIdentKeyword(tok, keyword) {
|
|
84
|
+
return tok && tok.typ === 'Ident' && typeof tok.value === 'string' && tok.value.toLowerCase() === keyword;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
canStartSparqlPrefixDirective() {
|
|
88
|
+
const prefixNameTok = this.peekAt(1);
|
|
89
|
+
const iriTok = this.peekAt(2);
|
|
90
|
+
return (
|
|
91
|
+
this.isIdentKeyword(this.peek(), 'prefix') &&
|
|
92
|
+
prefixNameTok &&
|
|
93
|
+
prefixNameTok.typ === 'Ident' &&
|
|
94
|
+
typeof prefixNameTok.value === 'string' &&
|
|
95
|
+
prefixNameTok.value.endsWith(':') &&
|
|
96
|
+
iriTok &&
|
|
97
|
+
(iriTok.typ === 'IriRef' || iriTok.typ === 'Ident')
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
canStartSparqlBaseDirective(allowIdentIri = false) {
|
|
102
|
+
const iriTok = this.peekAt(1);
|
|
103
|
+
return (
|
|
104
|
+
this.isIdentKeyword(this.peek(), 'base') &&
|
|
105
|
+
iriTok &&
|
|
106
|
+
(iriTok.typ === 'IriRef' || (allowIdentIri && iriTok.typ === 'Ident'))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
parseDirectiveIfPresent({ allowIdentBaseIri = false } = {}) {
|
|
111
|
+
if (this.peek().typ === 'AtPrefix') {
|
|
112
|
+
this.next();
|
|
113
|
+
this.parsePrefixDirective();
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (this.peek().typ === 'AtBase') {
|
|
117
|
+
this.next();
|
|
118
|
+
this.parseBaseDirective();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (this.canStartSparqlPrefixDirective()) {
|
|
122
|
+
this.next();
|
|
123
|
+
this.parseSparqlPrefixDirective();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (this.canStartSparqlBaseDirective(allowIdentBaseIri)) {
|
|
127
|
+
this.next();
|
|
128
|
+
this.parseSparqlBaseDirective();
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
flushPendingTriples(out, { includeBefore = true, includeAfter = true } = {}) {
|
|
135
|
+
if (includeBefore && this.pendingTriples.length > 0) {
|
|
136
|
+
out.push(...this.pendingTriples);
|
|
137
|
+
this.pendingTriples = [];
|
|
138
|
+
}
|
|
139
|
+
if (includeAfter && this.pendingTriplesAfter.length > 0) {
|
|
140
|
+
out.push(...this.pendingTriplesAfter);
|
|
141
|
+
this.pendingTriplesAfter = [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
79
145
|
parseDocument() {
|
|
80
146
|
const triples = [];
|
|
81
147
|
const forwardRules = [];
|
|
@@ -83,86 +149,50 @@ class Parser {
|
|
|
83
149
|
const logQueries = [];
|
|
84
150
|
|
|
85
151
|
while (this.peek().typ !== 'EOF') {
|
|
86
|
-
if (this.
|
|
152
|
+
if (this.parseDirectiveIfPresent({ allowIdentBaseIri: true })) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const first = this.parseTerm();
|
|
157
|
+
if (this.peek().typ === 'OpImplies') {
|
|
87
158
|
this.next();
|
|
88
|
-
this.
|
|
89
|
-
|
|
159
|
+
const second = this.parseTerm();
|
|
160
|
+
this.expectDot();
|
|
161
|
+
forwardRules.push(this.makeRule(first, second, true));
|
|
162
|
+
} else if (this.peek().typ === 'OpImpliedBy') {
|
|
90
163
|
this.next();
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
this.peek().typ === 'Ident' &&
|
|
95
|
-
typeof this.peek().value === 'string' &&
|
|
96
|
-
this.peek().value.toLowerCase() === 'prefix' &&
|
|
97
|
-
this.toks[this.pos + 1] &&
|
|
98
|
-
this.toks[this.pos + 1].typ === 'Ident' &&
|
|
99
|
-
typeof this.toks[this.pos + 1].value === 'string' &&
|
|
100
|
-
// Require PNAME_NS form (e.g., "ex:" or ":") to avoid clashing with a normal triple starting with IRI "prefix".
|
|
101
|
-
this.toks[this.pos + 1].value.endsWith(':') &&
|
|
102
|
-
this.toks[this.pos + 2] &&
|
|
103
|
-
(this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')
|
|
104
|
-
) {
|
|
105
|
-
this.next(); // consume PREFIX keyword
|
|
106
|
-
this.parseSparqlPrefixDirective();
|
|
107
|
-
} else if (
|
|
108
|
-
this.peek().typ === 'Ident' &&
|
|
109
|
-
typeof this.peek().value === 'string' &&
|
|
110
|
-
this.peek().value.toLowerCase() === 'base' &&
|
|
111
|
-
this.toks[this.pos + 1] &&
|
|
112
|
-
// SPARQL BASE requires an IRIREF.
|
|
113
|
-
this.toks[this.pos + 1].typ === 'IriRef'
|
|
114
|
-
) {
|
|
115
|
-
this.next(); // consume BASE keyword
|
|
116
|
-
this.parseSparqlBaseDirective();
|
|
164
|
+
const second = this.parseTerm();
|
|
165
|
+
this.expectDot();
|
|
166
|
+
backwardRules.push(this.makeRule(first, second, false));
|
|
117
167
|
} else {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
backwardRules.push(this.makeRule(first, second, false));
|
|
168
|
+
let more;
|
|
169
|
+
|
|
170
|
+
if (this.peek().typ === 'Dot') {
|
|
171
|
+
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
172
|
+
// So a bare subject followed by '.' is syntactically valid.
|
|
173
|
+
// If the subject was a path / property-list that generated helper triples,
|
|
174
|
+
// we emit those; otherwise this statement contributes no triples.
|
|
175
|
+
more = [];
|
|
176
|
+
this.flushPendingTriples(more);
|
|
177
|
+
this.next(); // consume '.'
|
|
129
178
|
} else {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
134
|
-
// So a bare subject followed by '.' is syntactically valid.
|
|
135
|
-
// If the subject was a path / property-list that generated helper triples,
|
|
136
|
-
// we emit those; otherwise this statement contributes no triples.
|
|
137
|
-
more = [];
|
|
138
|
-
if (this.pendingTriples.length > 0) {
|
|
139
|
-
more.push(...this.pendingTriples);
|
|
140
|
-
this.pendingTriples = [];
|
|
141
|
-
}
|
|
142
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
143
|
-
more.push(...this.pendingTriplesAfter);
|
|
144
|
-
this.pendingTriplesAfter = [];
|
|
145
|
-
}
|
|
146
|
-
this.next(); // consume '.'
|
|
147
|
-
} else {
|
|
148
|
-
more = this.parsePredicateObjectList(first);
|
|
149
|
-
this.expectDot();
|
|
150
|
-
}
|
|
179
|
+
more = this.parsePredicateObjectList(first);
|
|
180
|
+
this.expectDot();
|
|
181
|
+
}
|
|
151
182
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
183
|
+
// normalize explicit log:implies / log:impliedBy at top-level
|
|
184
|
+
for (const tr of more) {
|
|
185
|
+
if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
186
|
+
forwardRules.push(this.makeRule(tr.s, tr.o, true));
|
|
187
|
+
} else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
188
|
+
backwardRules.push(this.makeRule(tr.s, tr.o, false));
|
|
189
|
+
} else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
190
|
+
// Output-selection directive: { premise } log:query { conclusion }.
|
|
191
|
+
// When present at top-level, eyeling prints only the instantiated conclusion
|
|
192
|
+
// triples (unique) instead of all newly derived facts.
|
|
193
|
+
logQueries.push(this.makeRule(tr.s, tr.o, true));
|
|
194
|
+
} else {
|
|
195
|
+
triples.push(tr);
|
|
166
196
|
}
|
|
167
197
|
}
|
|
168
198
|
}
|
|
@@ -394,64 +424,7 @@ class Parser {
|
|
|
394
424
|
return iriTerm;
|
|
395
425
|
}
|
|
396
426
|
|
|
397
|
-
const
|
|
398
|
-
const localTriples = [];
|
|
399
|
-
while (true) {
|
|
400
|
-
let pred;
|
|
401
|
-
let invert = false;
|
|
402
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
403
|
-
this.next();
|
|
404
|
-
pred = internIri(RDF_NS + 'type');
|
|
405
|
-
} else if (this.peek().typ === 'OpPredInvert') {
|
|
406
|
-
this.next(); // "<-"
|
|
407
|
-
pred = this.parseTerm();
|
|
408
|
-
invert = true;
|
|
409
|
-
} else {
|
|
410
|
-
pred = this.parseTerm();
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// If a pathological predicate term produced post-triples, don't let them leak.
|
|
414
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
415
|
-
localTriples.push(...this.pendingTriplesAfter);
|
|
416
|
-
this.pendingTriplesAfter = [];
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Object list: o1, o2, ... (capture post-triples per object)
|
|
420
|
-
const objs = [];
|
|
421
|
-
const readObj = () => {
|
|
422
|
-
const o = this.parseTerm();
|
|
423
|
-
const post = this.pendingTriplesAfter;
|
|
424
|
-
this.pendingTriplesAfter = [];
|
|
425
|
-
objs.push({ term: o, postTriples: post });
|
|
426
|
-
};
|
|
427
|
-
readObj();
|
|
428
|
-
while (this.peek().typ === 'Comma') {
|
|
429
|
-
this.next();
|
|
430
|
-
readObj();
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
for (const { term: o, postTriples } of objs) {
|
|
434
|
-
// Path helper triples must come before the triple that consumes the path result.
|
|
435
|
-
if (this.pendingTriples.length > 0) {
|
|
436
|
-
localTriples.push(...this.pendingTriples);
|
|
437
|
-
this.pendingTriples = [];
|
|
438
|
-
}
|
|
439
|
-
localTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
440
|
-
if (postTriples && postTriples.length) localTriples.push(...postTriples);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (this.peek().typ === 'Semicolon') {
|
|
444
|
-
this.next();
|
|
445
|
-
if (this.peek().typ === 'RBracket') break;
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (this.peek().typ !== 'RBracket') {
|
|
452
|
-
this.fail(`Expected ']' at end of IRI property list, got ${this.peek().toString()}`);
|
|
453
|
-
}
|
|
454
|
-
this.next();
|
|
427
|
+
const localTriples = this.parsePropertyListTriples(iriTerm, 'RBracket', 'IRI property list');
|
|
455
428
|
|
|
456
429
|
// Defer the embedded description until after the triple that references the IRI.
|
|
457
430
|
if (localTriples.length) this.pendingTriplesAfter.push(...localTriples);
|
|
@@ -462,38 +435,48 @@ class Parser {
|
|
|
462
435
|
this.blankCounter += 1;
|
|
463
436
|
const id = `_:b${this.blankCounter}`;
|
|
464
437
|
const subj = new Blank(id);
|
|
438
|
+
const localTriples = this.parsePropertyListTriples(subj, 'RBracket', 'blank node property list');
|
|
439
|
+
|
|
440
|
+
// Defer the blank-node description until after the triple that references it.
|
|
441
|
+
if (localTriples.length) this.pendingTriplesAfter.push(...localTriples);
|
|
442
|
+
return new Blank(id);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
parsePropertyVerb() {
|
|
446
|
+
let pred;
|
|
447
|
+
let invert = false;
|
|
465
448
|
|
|
449
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
450
|
+
this.next();
|
|
451
|
+
pred = internIri(RDF_NS + 'type');
|
|
452
|
+
} else if (this.peek().typ === 'OpPredInvert') {
|
|
453
|
+
this.next();
|
|
454
|
+
pred = this.parseTerm();
|
|
455
|
+
invert = true;
|
|
456
|
+
} else {
|
|
457
|
+
pred = this.parseTerm();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { pred, invert };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
parsePropertyListTriples(subject, closingTyp, contextLabel) {
|
|
466
464
|
const localTriples = [];
|
|
467
465
|
|
|
468
466
|
while (true) {
|
|
469
|
-
|
|
470
|
-
let pred;
|
|
471
|
-
let invert = false;
|
|
472
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
473
|
-
this.next();
|
|
474
|
-
pred = internIri(RDF_NS + 'type');
|
|
475
|
-
} else if (this.peek().typ === 'OpPredInvert') {
|
|
476
|
-
this.next(); // consume "<-"
|
|
477
|
-
pred = this.parseTerm();
|
|
478
|
-
invert = true;
|
|
479
|
-
} else {
|
|
480
|
-
pred = this.parseTerm();
|
|
481
|
-
}
|
|
467
|
+
const { pred, invert } = this.parsePropertyVerb();
|
|
482
468
|
|
|
483
469
|
// If a pathological predicate term produced post-triples, don't let them leak.
|
|
484
|
-
|
|
485
|
-
localTriples.push(...this.pendingTriplesAfter);
|
|
486
|
-
this.pendingTriplesAfter = [];
|
|
487
|
-
}
|
|
470
|
+
this.flushPendingTriples(localTriples, { includeBefore: false, includeAfter: true });
|
|
488
471
|
|
|
489
|
-
// Object list: o1, o2, ... (capture post-triples per object)
|
|
490
472
|
const objs = [];
|
|
491
473
|
const readObj = () => {
|
|
492
|
-
const
|
|
493
|
-
const
|
|
474
|
+
const term = this.parseTerm();
|
|
475
|
+
const postTriples = this.pendingTriplesAfter;
|
|
494
476
|
this.pendingTriplesAfter = [];
|
|
495
|
-
objs.push({ term
|
|
477
|
+
objs.push({ term, postTriples });
|
|
496
478
|
};
|
|
479
|
+
|
|
497
480
|
readObj();
|
|
498
481
|
while (this.peek().typ === 'Comma') {
|
|
499
482
|
this.next();
|
|
@@ -502,31 +485,25 @@ class Parser {
|
|
|
502
485
|
|
|
503
486
|
for (const { term: o, postTriples } of objs) {
|
|
504
487
|
// Path helper triples must come before the triple that consumes the path result.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this.pendingTriples = [];
|
|
508
|
-
}
|
|
509
|
-
localTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
488
|
+
this.flushPendingTriples(localTriples, { includeBefore: true, includeAfter: false });
|
|
489
|
+
localTriples.push(invert ? new Triple(o, pred, subject) : new Triple(subject, pred, o));
|
|
510
490
|
if (postTriples && postTriples.length) localTriples.push(...postTriples);
|
|
511
491
|
}
|
|
512
492
|
|
|
513
493
|
if (this.peek().typ === 'Semicolon') {
|
|
514
494
|
this.next();
|
|
515
|
-
if (this.peek().typ ===
|
|
495
|
+
if (this.peek().typ === closingTyp) break;
|
|
516
496
|
continue;
|
|
517
497
|
}
|
|
518
498
|
break;
|
|
519
499
|
}
|
|
520
500
|
|
|
521
|
-
if (this.peek().typ
|
|
522
|
-
this.
|
|
523
|
-
} else {
|
|
524
|
-
this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
|
|
501
|
+
if (this.peek().typ !== closingTyp) {
|
|
502
|
+
this.fail(`Expected ']' at end of ${contextLabel}, got ${this.peek().toString()}`);
|
|
525
503
|
}
|
|
504
|
+
this.next();
|
|
526
505
|
|
|
527
|
-
|
|
528
|
-
if (localTriples.length) this.pendingTriplesAfter.push(...localTriples);
|
|
529
|
-
return new Blank(id);
|
|
506
|
+
return localTriples;
|
|
530
507
|
}
|
|
531
508
|
|
|
532
509
|
parseGraph() {
|
|
@@ -535,40 +512,7 @@ class Parser {
|
|
|
535
512
|
// N3 allows @prefix/@base and SPARQL-style PREFIX/BASE directives anywhere
|
|
536
513
|
// outside of a triple. This includes inside quoted graph terms.
|
|
537
514
|
// These directives affect parsing (prefix/base resolution) but do not emit triples.
|
|
538
|
-
if (this.
|
|
539
|
-
this.next();
|
|
540
|
-
this.parsePrefixDirective();
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
if (this.peek().typ === 'AtBase') {
|
|
544
|
-
this.next();
|
|
545
|
-
this.parseBaseDirective();
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
if (
|
|
549
|
-
this.peek().typ === 'Ident' &&
|
|
550
|
-
typeof this.peek().value === 'string' &&
|
|
551
|
-
this.peek().value.toLowerCase() === 'prefix' &&
|
|
552
|
-
this.toks[this.pos + 1] &&
|
|
553
|
-
this.toks[this.pos + 1].typ === 'Ident' &&
|
|
554
|
-
typeof this.toks[this.pos + 1].value === 'string' &&
|
|
555
|
-
this.toks[this.pos + 1].value.endsWith(':') &&
|
|
556
|
-
this.toks[this.pos + 2] &&
|
|
557
|
-
(this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')
|
|
558
|
-
) {
|
|
559
|
-
this.next();
|
|
560
|
-
this.parseSparqlPrefixDirective();
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
if (
|
|
564
|
-
this.peek().typ === 'Ident' &&
|
|
565
|
-
typeof this.peek().value === 'string' &&
|
|
566
|
-
this.peek().value.toLowerCase() === 'base' &&
|
|
567
|
-
this.toks[this.pos + 1] &&
|
|
568
|
-
(this.toks[this.pos + 1].typ === 'IriRef' || this.toks[this.pos + 1].typ === 'Ident')
|
|
569
|
-
) {
|
|
570
|
-
this.next();
|
|
571
|
-
this.parseSparqlBaseDirective();
|
|
515
|
+
if (this.parseDirectiveIfPresent()) {
|
|
572
516
|
continue;
|
|
573
517
|
}
|
|
574
518
|
|
|
@@ -599,14 +543,7 @@ class Parser {
|
|
|
599
543
|
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
600
544
|
// So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
|
|
601
545
|
if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
|
|
602
|
-
|
|
603
|
-
triples.push(...this.pendingTriples);
|
|
604
|
-
this.pendingTriples = [];
|
|
605
|
-
}
|
|
606
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
607
|
-
triples.push(...this.pendingTriplesAfter);
|
|
608
|
-
this.pendingTriplesAfter = [];
|
|
609
|
-
}
|
|
546
|
+
this.flushPendingTriples(triples);
|
|
610
547
|
if (this.peek().typ === 'Dot') this.next();
|
|
611
548
|
continue;
|
|
612
549
|
}
|
|
@@ -624,61 +561,50 @@ class Parser {
|
|
|
624
561
|
return new GraphTerm(triples);
|
|
625
562
|
}
|
|
626
563
|
|
|
564
|
+
parseStatementVerb() {
|
|
565
|
+
let verb;
|
|
566
|
+
let invert = false;
|
|
567
|
+
|
|
568
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
569
|
+
this.next();
|
|
570
|
+
verb = internIri(RDF_NS + 'type');
|
|
571
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
572
|
+
// N3 syntactic sugar: "S has P O." means "S P O."
|
|
573
|
+
this.next();
|
|
574
|
+
verb = this.parseTerm();
|
|
575
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
576
|
+
// N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
|
|
577
|
+
this.next();
|
|
578
|
+
verb = this.parseTerm();
|
|
579
|
+
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
580
|
+
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
581
|
+
}
|
|
582
|
+
this.next();
|
|
583
|
+
invert = true;
|
|
584
|
+
} else if (this.peek().typ === 'OpPredInvert') {
|
|
585
|
+
this.next();
|
|
586
|
+
verb = this.parseTerm();
|
|
587
|
+
invert = true;
|
|
588
|
+
} else {
|
|
589
|
+
verb = this.parseTerm();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return { verb, invert };
|
|
593
|
+
}
|
|
594
|
+
|
|
627
595
|
parsePredicateObjectList(subject) {
|
|
628
596
|
const out = [];
|
|
629
597
|
|
|
630
598
|
// If the SUBJECT was a path or property-list, emit its helper triples first.
|
|
631
|
-
|
|
632
|
-
out.push(...this.pendingTriples);
|
|
633
|
-
this.pendingTriples = [];
|
|
634
|
-
}
|
|
635
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
636
|
-
out.push(...this.pendingTriplesAfter);
|
|
637
|
-
this.pendingTriplesAfter = [];
|
|
638
|
-
}
|
|
599
|
+
this.flushPendingTriples(out);
|
|
639
600
|
|
|
640
601
|
while (true) {
|
|
641
|
-
|
|
642
|
-
let invert = false;
|
|
643
|
-
|
|
644
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
645
|
-
this.next();
|
|
646
|
-
verb = internIri(RDF_NS + 'type');
|
|
647
|
-
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
648
|
-
// N3 syntactic sugar: "S has P O." means "S P O."
|
|
649
|
-
this.next(); // consume "has"
|
|
650
|
-
verb = this.parseTerm();
|
|
651
|
-
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
652
|
-
// N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
|
|
653
|
-
this.next(); // consume "is"
|
|
654
|
-
verb = this.parseTerm();
|
|
655
|
-
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
656
|
-
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
657
|
-
}
|
|
658
|
-
this.next(); // consume "of"
|
|
659
|
-
invert = true;
|
|
660
|
-
} else if (this.peek().typ === 'OpPredInvert') {
|
|
661
|
-
this.next(); // "<-"
|
|
662
|
-
verb = this.parseTerm();
|
|
663
|
-
invert = true;
|
|
664
|
-
} else {
|
|
665
|
-
verb = this.parseTerm();
|
|
666
|
-
}
|
|
667
|
-
|
|
602
|
+
const { verb, invert } = this.parseStatementVerb();
|
|
668
603
|
const objects = this.parseObjectList();
|
|
669
604
|
|
|
670
605
|
// If VERB or OBJECTS contained paths, their helper triples must come
|
|
671
606
|
// before the triples that consume the path results (Easter depends on this).
|
|
672
|
-
|
|
673
|
-
out.push(...this.pendingTriples);
|
|
674
|
-
this.pendingTriples = [];
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// If VERB produced a property list (rare), don't let it leak.
|
|
678
|
-
if (this.pendingTriplesAfter.length > 0) {
|
|
679
|
-
out.push(...this.pendingTriplesAfter);
|
|
680
|
-
this.pendingTriplesAfter = [];
|
|
681
|
-
}
|
|
607
|
+
this.flushPendingTriples(out);
|
|
682
608
|
|
|
683
609
|
for (const { term: o, postTriples } of objects) {
|
|
684
610
|
out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
|