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.
Files changed (3) hide show
  1. package/eyeling.js +180 -254
  2. package/lib/parser.js +180 -254
  3. 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.peek().typ === 'AtPrefix') {
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.parsePrefixDirective();
8460
- } else if (this.peek().typ === 'AtBase') {
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.parseBaseDirective();
8463
- } else if (
8464
- // SPARQL-style/Turtle-style directives (case-insensitive, no trailing '.')
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
- const first = this.parseTerm();
8490
- if (this.peek().typ === 'OpImplies') {
8491
- this.next();
8492
- const second = this.parseTerm();
8493
- this.expectDot();
8494
- forwardRules.push(this.makeRule(first, second, true));
8495
- } else if (this.peek().typ === 'OpImpliedBy') {
8496
- this.next();
8497
- const second = this.parseTerm();
8498
- this.expectDot();
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
- let more;
8502
-
8503
- if (this.peek().typ === 'Dot') {
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
- // normalize explicit log:implies / log:impliedBy at top-level
8524
- for (const tr of more) {
8525
- if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
8526
- forwardRules.push(this.makeRule(tr.s, tr.o, true));
8527
- } else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
8528
- backwardRules.push(this.makeRule(tr.s, tr.o, false));
8529
- } else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
8530
- // Output-selection directive: { premise } log:query { conclusion }.
8531
- // When present at top-level, eyeling prints only the instantiated conclusion
8532
- // triples (unique) instead of all newly derived facts.
8533
- logQueries.push(this.makeRule(tr.s, tr.o, true));
8534
- } else {
8535
- triples.push(tr);
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 subj = iriTerm;
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
- // Verb (can also be 'a')
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
- if (this.pendingTriplesAfter.length > 0) {
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 o = this.parseTerm();
8864
- const post = this.pendingTriplesAfter;
8845
+ const term = this.parseTerm();
8846
+ const postTriples = this.pendingTriplesAfter;
8865
8847
  this.pendingTriplesAfter = [];
8866
- objs.push({ term: o, postTriples: post });
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
- if (this.pendingTriples.length > 0) {
8877
- localTriples.push(...this.pendingTriples);
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 === 'RBracket') break;
8866
+ if (this.peek().typ === closingTyp) break;
8887
8867
  continue;
8888
8868
  }
8889
8869
  break;
8890
8870
  }
8891
8871
 
8892
- if (this.peek().typ === 'RBracket') {
8893
- this.next();
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
- // Defer the blank-node description until after the triple that references it.
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.peek().typ === 'AtPrefix') {
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
- if (this.pendingTriples.length > 0) {
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
- if (this.pendingTriples.length > 0) {
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
- let verb;
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
- if (this.pendingTriples.length > 0) {
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.peek().typ === 'AtPrefix') {
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.parsePrefixDirective();
89
- } else if (this.peek().typ === 'AtBase') {
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.parseBaseDirective();
92
- } else if (
93
- // SPARQL-style/Turtle-style directives (case-insensitive, no trailing '.')
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
- const first = this.parseTerm();
119
- if (this.peek().typ === 'OpImplies') {
120
- this.next();
121
- const second = this.parseTerm();
122
- this.expectDot();
123
- forwardRules.push(this.makeRule(first, second, true));
124
- } else if (this.peek().typ === 'OpImpliedBy') {
125
- this.next();
126
- const second = this.parseTerm();
127
- this.expectDot();
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
- let more;
131
-
132
- if (this.peek().typ === 'Dot') {
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
- // normalize explicit log:implies / log:impliedBy at top-level
153
- for (const tr of more) {
154
- if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
155
- forwardRules.push(this.makeRule(tr.s, tr.o, true));
156
- } else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
157
- backwardRules.push(this.makeRule(tr.s, tr.o, false));
158
- } else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
159
- // Output-selection directive: { premise } log:query { conclusion }.
160
- // When present at top-level, eyeling prints only the instantiated conclusion
161
- // triples (unique) instead of all newly derived facts.
162
- logQueries.push(this.makeRule(tr.s, tr.o, true));
163
- } else {
164
- triples.push(tr);
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 subj = iriTerm;
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
- // Verb (can also be 'a')
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
- if (this.pendingTriplesAfter.length > 0) {
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 o = this.parseTerm();
493
- const post = this.pendingTriplesAfter;
474
+ const term = this.parseTerm();
475
+ const postTriples = this.pendingTriplesAfter;
494
476
  this.pendingTriplesAfter = [];
495
- objs.push({ term: o, postTriples: post });
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
- if (this.pendingTriples.length > 0) {
506
- localTriples.push(...this.pendingTriples);
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 === 'RBracket') break;
495
+ if (this.peek().typ === closingTyp) break;
516
496
  continue;
517
497
  }
518
498
  break;
519
499
  }
520
500
 
521
- if (this.peek().typ === 'RBracket') {
522
- this.next();
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
- // Defer the blank-node description until after the triple that references it.
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.peek().typ === 'AtPrefix') {
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
- if (this.pendingTriples.length > 0) {
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
- if (this.pendingTriples.length > 0) {
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
- let verb;
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
- if (this.pendingTriples.length > 0) {
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.14.12",
3
+ "version": "1.14.13",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [