eyeleng 1.0.4 → 1.0.6
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 +43 -0
- package/dist/browser/eyeleng.browser.js +2462 -1085
- package/examples/cat-koko.srl +52 -0
- package/examples/graph-term-emulation.srl +83 -0
- package/examples/output/cat-koko.trig +3 -0
- package/examples/output/collection-nesting.trig +1 -1
- package/examples/output/graph-term-emulation.trig +11 -0
- package/examples/output/rdf-messages.trig +3 -0
- package/examples/rdf-messages.srl +15 -0
- package/examples/rdf-messages.trig +12 -0
- package/eyeleng.js +2466 -1083
- package/package.json +7 -2
- package/playground.html +1 -1
- package/src/api.js +4 -0
- package/src/cli.js +6 -0
- package/src/parser.js +95 -1
- package/src/rdfEntailment.js +571 -0
- package/src/rdfManifest.js +724 -0
- package/src/rdfMessages.js +321 -0
- package/src/rdfSyntax.js +955 -0
- package/test/api.test.js +63 -0
- package/test/harness.js +38 -10
- package/test/run.js +6 -3
- package/test/shacl12-rules.test.js +14 -13
- package/test/w3c-rdf.test.js +202 -0
- package/tools/browser-bundle.js +0 -0
- package/tools/bundle.js +0 -0
- package/tools/w3c-rdf.js +36 -0
package/src/rdfSyntax.js
CHANGED
|
@@ -560,3 +560,958 @@ module.exports = {
|
|
|
560
560
|
SRL_RULE,
|
|
561
561
|
},
|
|
562
562
|
};
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
// ---- Grammar-hardened RDF 1.1 / RDF 1.2 syntax helpers ----
|
|
566
|
+
// These functions are used by the W3C RDF manifest harness and are kept in
|
|
567
|
+
// rdfSyntax.js beside the existing Turtle/RDF-Rules front-end instead of in a
|
|
568
|
+
// separate monolithic test file. They intentionally keep an internal test
|
|
569
|
+
// graph representation because the W3C manifests exercise syntax, datasets,
|
|
570
|
+
// triple terms, and RDF 1.2 annotation isomorphism independently from the SRL
|
|
571
|
+
// rule engine representation.
|
|
572
|
+
const rdfW3cSyntax = (() => {
|
|
573
|
+
// Grammar-hardened RDF syntax code shared by the RDF Rules front-end and W3C manifest harness.
|
|
574
|
+
function iri(value) {
|
|
575
|
+
if (!value) throw new Error('iri(value) requires a non-empty value');
|
|
576
|
+
return Object.freeze({ kind: 'iri', value: String(value) });
|
|
577
|
+
}
|
|
578
|
+
function literal(value, datatype = null, language = null, langDir = null) {
|
|
579
|
+
return Object.freeze({ kind: 'literal', value: String(value), datatype, language, langDir });
|
|
580
|
+
}
|
|
581
|
+
function blank(value) {
|
|
582
|
+
const clean = String(value || '').replace(/^_:/, '');
|
|
583
|
+
if (!clean) throw new Error('blank(value) requires a name');
|
|
584
|
+
return Object.freeze({ kind: 'blank', value: clean });
|
|
585
|
+
}
|
|
586
|
+
function tripleTerm(s, p, o) { return Object.freeze({ kind: 'triple', s, p, o }); }
|
|
587
|
+
function variable(name) {
|
|
588
|
+
const clean = String(name || '').replace(/^\?/, '');
|
|
589
|
+
if (!clean) throw new Error('variable(name) requires a name');
|
|
590
|
+
return Object.freeze({ kind: 'var', name: clean });
|
|
591
|
+
}
|
|
592
|
+
function triple(s, p, o, graph = null) { return Object.freeze({ s, p, o, graph }); }
|
|
593
|
+
function termKey(term) {
|
|
594
|
+
if (!term) return 'default';
|
|
595
|
+
switch (term.kind) {
|
|
596
|
+
case 'iri': return `I:${term.value}`;
|
|
597
|
+
case 'literal': return `L:${JSON.stringify(term.value)}^^${term.datatype || ''}@${term.language || ''}`;
|
|
598
|
+
case 'blank': return `B:${term.value}`;
|
|
599
|
+
case 'var': return `V:${term.name}`;
|
|
600
|
+
case 'triple': return `T:${termKey(term.s)} ${termKey(term.p)} ${termKey(term.o)}`;
|
|
601
|
+
default: throw new Error(`Unsupported term kind: ${term.kind}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function tripleKey(t) { return `${termKey(t.s)} ${termKey(t.p)} ${termKey(t.o)} ${termKey(t.graph)}`; }
|
|
605
|
+
class Rule { constructor({ id, body = [], head = [], profile = 'n3-rules-subset-v0' } = {}) { this.id = id; this.body = body; this.head = head; this.profile = profile; } }
|
|
606
|
+
|
|
607
|
+
const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
608
|
+
const RDF_TYPE = `${RDF_NS}type`;
|
|
609
|
+
const RDF_FIRST = `${RDF_NS}first`;
|
|
610
|
+
const RDF_REST = `${RDF_NS}rest`;
|
|
611
|
+
const RDF_NIL = `${RDF_NS}nil`;
|
|
612
|
+
const RDF_REIFIES = `${RDF_NS}reifies`;
|
|
613
|
+
const RDF_LANG_STRING = `${RDF_NS}langString`;
|
|
614
|
+
const RDF_DIR_LANG_STRING = `${RDF_NS}dirLangString`;
|
|
615
|
+
const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
|
|
616
|
+
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
|
|
617
|
+
const XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';
|
|
618
|
+
const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
|
|
619
|
+
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
|
|
620
|
+
|
|
621
|
+
// ---- N-Triples / N-Quads parser ----
|
|
622
|
+
const { parseNQuads, termToNQuads, tripleToNQuads, triplesToNQuads } = (() => {
|
|
623
|
+
|
|
624
|
+
function isWs(ch) { return ch === ' ' || ch === '\t'; }
|
|
625
|
+
function isLineEnd(ch) { return ch === '\n' || ch === '\r'; }
|
|
626
|
+
function isHex(text) { return /^[0-9A-Fa-f]+$/.test(text); }
|
|
627
|
+
|
|
628
|
+
function decodeCodePoint(hex, token) {
|
|
629
|
+
const code = Number.parseInt(hex, 16);
|
|
630
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
631
|
+
throw new Error(`Invalid Unicode escape in ${token}`);
|
|
632
|
+
}
|
|
633
|
+
return String.fromCodePoint(code);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function decodeIriEscapes(value, token = 'IRI') {
|
|
637
|
+
let out = '';
|
|
638
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
639
|
+
const ch = value[i];
|
|
640
|
+
if (ch !== '\\') {
|
|
641
|
+
if (/[<>"{}|^`\u0000-\u0020]/.test(ch)) throw new Error(`Invalid character in ${token}`);
|
|
642
|
+
out += ch;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const esc = value[++i];
|
|
646
|
+
if (esc === 'u') {
|
|
647
|
+
const hex = value.slice(i + 1, i + 5);
|
|
648
|
+
if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
649
|
+
out += decodeCodePoint(hex, token);
|
|
650
|
+
i += 4;
|
|
651
|
+
} else if (esc === 'U') {
|
|
652
|
+
const hex = value.slice(i + 1, i + 9);
|
|
653
|
+
if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
654
|
+
out += decodeCodePoint(hex, token);
|
|
655
|
+
i += 8;
|
|
656
|
+
} else {
|
|
657
|
+
throw new Error(`Invalid IRI escape \\${esc} in ${token}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function decodeLiteralEscapes(value, token = 'literal') {
|
|
664
|
+
let out = '';
|
|
665
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
666
|
+
const ch = value[i];
|
|
667
|
+
if (ch !== '\\') {
|
|
668
|
+
if (ch === '\n' || ch === '\r') throw new Error(`Raw line break in ${token}`);
|
|
669
|
+
out += ch;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const esc = value[++i];
|
|
673
|
+
if (!esc) throw new Error(`Trailing escape in ${token}`);
|
|
674
|
+
if (esc === 't') out += '\t';
|
|
675
|
+
else if (esc === 'b') out += '\b';
|
|
676
|
+
else if (esc === 'n') out += '\n';
|
|
677
|
+
else if (esc === 'r') out += '\r';
|
|
678
|
+
else if (esc === 'f') out += '\f';
|
|
679
|
+
else if (esc === '"') out += '"';
|
|
680
|
+
else if (esc === "'") out += "'";
|
|
681
|
+
else if (esc === '\\') out += '\\';
|
|
682
|
+
else if (esc === 'u') {
|
|
683
|
+
const hex = value.slice(i + 1, i + 5);
|
|
684
|
+
if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
685
|
+
out += decodeCodePoint(hex, token);
|
|
686
|
+
i += 4;
|
|
687
|
+
} else if (esc === 'U') {
|
|
688
|
+
const hex = value.slice(i + 1, i + 9);
|
|
689
|
+
if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
690
|
+
out += decodeCodePoint(hex, token);
|
|
691
|
+
i += 8;
|
|
692
|
+
} else {
|
|
693
|
+
throw new Error(`Invalid escape \\${esc} in ${token}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function stripNqComment(line) {
|
|
700
|
+
let inString = false;
|
|
701
|
+
let inIri = false;
|
|
702
|
+
let escaped = false;
|
|
703
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
704
|
+
const ch = line[i];
|
|
705
|
+
if (escaped) { escaped = false; continue; }
|
|
706
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
707
|
+
if (!inIri && ch === '"') { inString = !inString; continue; }
|
|
708
|
+
if (!inString && ch === '<' && line[i + 1] !== '<') { inIri = true; continue; }
|
|
709
|
+
if (!inString && inIri && ch === '>') { inIri = false; continue; }
|
|
710
|
+
if (!inString && !inIri && ch === '#') return line.slice(0, i);
|
|
711
|
+
}
|
|
712
|
+
return line;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function validateAbsoluteIri(value, position) {
|
|
716
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) throw new Error(`${position} must be absolute`);
|
|
717
|
+
return value;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function validateBlankLabel(value) {
|
|
721
|
+
// RDF blank node labels follow PN_CHARS-style rules. This deliberately accepts
|
|
722
|
+
// Unicode letters and leading underscores; it still rejects empty labels,
|
|
723
|
+
// labels ending in '.', and doubled dots because those are common false
|
|
724
|
+
// positives when a compact statement terminator is adjacent to a blank node.
|
|
725
|
+
if (!value || value.endsWith('.') || value.includes('..')) throw new Error(`Invalid blank node label _: ${value}`);
|
|
726
|
+
if (!/^[\p{L}\p{N}_](?:[\p{L}\p{N}._\-\u00B7\u0300-\u036F\u203F-\u2040]*[\p{L}\p{N}_\-\u00B7\u0300-\u036F\u203F-\u2040])?$/u.test(value)) {
|
|
727
|
+
throw new Error(`Invalid blank node label _: ${value}`);
|
|
728
|
+
}
|
|
729
|
+
return value;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function validateLang(value) {
|
|
733
|
+
// LANG_DIR uses BCP47-style language tags. Keep this intentionally strict enough
|
|
734
|
+
// for the W3C syntax tests: each subtag is 1..8 alphanumeric chars, starting alpha.
|
|
735
|
+
if (!value || value.includes('--') || !/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value)) throw new Error(`Invalid language tag @${value}`);
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
class LineReader {
|
|
740
|
+
constructor(line, lineNumber) {
|
|
741
|
+
this.line = line;
|
|
742
|
+
this.lineNumber = lineNumber;
|
|
743
|
+
this.i = 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
eof() { return this.i >= this.line.length; }
|
|
747
|
+
peek(offset = 0) { return this.line[this.i + offset]; }
|
|
748
|
+
startsWith(value) { return this.line.startsWith(value, this.i); }
|
|
749
|
+
skipWs() { while (isWs(this.peek())) this.i += 1; }
|
|
750
|
+
|
|
751
|
+
expect(value) {
|
|
752
|
+
if (!this.startsWith(value)) throw new Error(`Expected ${value} on line ${this.lineNumber}, got ${this.line.slice(this.i, this.i + 20) || 'end of line'}`);
|
|
753
|
+
this.i += value.length;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
readIri(position = 'IRI') {
|
|
757
|
+
this.expect('<');
|
|
758
|
+
let raw = '';
|
|
759
|
+
while (!this.eof()) {
|
|
760
|
+
const ch = this.peek();
|
|
761
|
+
if (ch === '>') { this.i += 1; return validateAbsoluteIri(decodeIriEscapes(raw, position), position); }
|
|
762
|
+
raw += ch;
|
|
763
|
+
this.i += 1;
|
|
764
|
+
}
|
|
765
|
+
throw new Error(`Unterminated ${position} on line ${this.lineNumber}`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
readBlank() {
|
|
769
|
+
this.expect('_:');
|
|
770
|
+
const start = this.i;
|
|
771
|
+
while (!this.eof()) {
|
|
772
|
+
const ch = this.peek();
|
|
773
|
+
if (!/[\p{L}\p{N}._\-\u00B7\u0300-\u036F\u203F-\u2040]/u.test(ch)) break;
|
|
774
|
+
if (ch === '.') {
|
|
775
|
+
const next = this.peek(1);
|
|
776
|
+
if (!next || isWs(next) || next === '<' || next === '_' || next === '"' || next === '#') break;
|
|
777
|
+
}
|
|
778
|
+
this.i += 1;
|
|
779
|
+
}
|
|
780
|
+
return blank(validateBlankLabel(this.line.slice(start, this.i)));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
readLiteral() {
|
|
784
|
+
this.expect('"');
|
|
785
|
+
let raw = '';
|
|
786
|
+
let escaped = false;
|
|
787
|
+
while (!this.eof()) {
|
|
788
|
+
const ch = this.peek();
|
|
789
|
+
this.i += 1;
|
|
790
|
+
if (escaped) { raw += `\\${ch}`; escaped = false; continue; }
|
|
791
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
792
|
+
if (ch === '"') {
|
|
793
|
+
const value = decodeLiteralEscapes(raw, 'literal');
|
|
794
|
+
let language = null;
|
|
795
|
+
let datatype = XSD_STRING;
|
|
796
|
+
if (this.peek() === '@') {
|
|
797
|
+
this.i += 1;
|
|
798
|
+
const start = this.i;
|
|
799
|
+
while (!this.eof() && /[A-Za-z0-9-]/.test(this.peek())) this.i += 1;
|
|
800
|
+
let rawLang = this.line.slice(start, this.i);
|
|
801
|
+
if (!rawLang) throw new Error('Invalid language tag: missing');
|
|
802
|
+
if (rawLang.endsWith('--ltr') || rawLang.endsWith('--rtl')) rawLang = rawLang.slice(0, -5);
|
|
803
|
+
language = validateLang(rawLang);
|
|
804
|
+
datatype = null;
|
|
805
|
+
} else if (this.startsWith('^^')) {
|
|
806
|
+
this.i += 2;
|
|
807
|
+
this.skipWs();
|
|
808
|
+
datatype = this.readIri('datatype IRI');
|
|
809
|
+
if (datatype === RDF_LANG_STRING || datatype === RDF_DIR_LANG_STRING) {
|
|
810
|
+
throw new Error(`Datatype ${datatype} requires LANG_DIR syntax, not ^^`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// RDF 1.2 base direction suffix, e.g. --ltr / --rtl. The core term model does not preserve it yet;
|
|
814
|
+
// accepting it is enough for syntax tests and keeps eval comparison conservative for now.
|
|
815
|
+
if (this.startsWith('--ltr') || this.startsWith('--rtl')) {
|
|
816
|
+
if (!language) throw new Error('Base direction requires a language tag');
|
|
817
|
+
this.i += 5;
|
|
818
|
+
}
|
|
819
|
+
return literal(value, datatype, language);
|
|
820
|
+
}
|
|
821
|
+
raw += ch;
|
|
822
|
+
}
|
|
823
|
+
throw new Error(`Unterminated literal on line ${this.lineNumber}`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
readTerm(position = 'term') {
|
|
827
|
+
this.skipWs();
|
|
828
|
+
if (this.startsWith('<<')) return this.readTripleTerm();
|
|
829
|
+
if (this.peek() === '<') return iri(this.readIri(position));
|
|
830
|
+
if (this.startsWith('_:')) return this.readBlank();
|
|
831
|
+
if (this.peek() === '"') return this.readLiteral();
|
|
832
|
+
throw new Error(`Expected RDF term for ${position}, got ${this.line.slice(this.i, this.i + 20) || 'end of line'}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
readSubjectOrGraph(position) {
|
|
836
|
+
const term = this.readTerm(position);
|
|
837
|
+
if (term.kind === 'literal') throw new Error(`N-Quads ${position} cannot be a literal`);
|
|
838
|
+
if (term.kind === 'triple' && (position === 'subject' || position === 'graph')) throw new Error(`N-Quads ${position} cannot be a triple term`);
|
|
839
|
+
return term;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
readPredicate() {
|
|
843
|
+
this.skipWs();
|
|
844
|
+
if (this.peek() !== '<') throw new Error(`N-Quads predicate must be an IRI, got ${this.line.slice(this.i, this.i + 20) || 'end of line'}`);
|
|
845
|
+
return iri(this.readIri('predicate'));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
readTripleTerm() {
|
|
849
|
+
this.expect('<<');
|
|
850
|
+
this.skipWs();
|
|
851
|
+
// RDF 1.2 N-Triples/N-Quads triple terms use parenthesized triples: <<( s p o )>>.
|
|
852
|
+
// The older unparenthesized RDF-star form is a reified-triple syntax form and is not
|
|
853
|
+
// accepted as a plain subject/object term by the RDF 1.2 syntax manifests.
|
|
854
|
+
this.expect('(');
|
|
855
|
+
this.skipWs();
|
|
856
|
+
const s = this.readSubjectOrGraph('triple-term subject');
|
|
857
|
+
this.skipWs();
|
|
858
|
+
const p = this.readPredicate();
|
|
859
|
+
this.skipWs();
|
|
860
|
+
const o = this.readTerm('triple-term object');
|
|
861
|
+
this.skipWs();
|
|
862
|
+
this.expect(')');
|
|
863
|
+
this.skipWs();
|
|
864
|
+
this.expect('>>');
|
|
865
|
+
return tripleTerm(s, p, o);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function parseLine(line, lineNumber, format) {
|
|
870
|
+
const clean = stripNqComment(line).trim();
|
|
871
|
+
if (!clean) return null;
|
|
872
|
+
const r = new LineReader(clean, lineNumber);
|
|
873
|
+
const s = r.readSubjectOrGraph('subject');
|
|
874
|
+
r.skipWs();
|
|
875
|
+
const p = r.readPredicate();
|
|
876
|
+
r.skipWs();
|
|
877
|
+
const o = r.readTerm('object');
|
|
878
|
+
r.skipWs();
|
|
879
|
+
let g = null;
|
|
880
|
+
if (r.peek() !== '.') {
|
|
881
|
+
if (format === 'ntriples') throw new Error(`N-Triples line ${lineNumber} has too many terms before .`);
|
|
882
|
+
g = r.readSubjectOrGraph('graph');
|
|
883
|
+
r.skipWs();
|
|
884
|
+
}
|
|
885
|
+
if (r.peek() !== '.') throw new Error(`N-Quads line ${lineNumber} must end with .`);
|
|
886
|
+
r.i += 1;
|
|
887
|
+
r.skipWs();
|
|
888
|
+
if (!r.eof()) throw new Error(`Unexpected trailing content on N-Quads line ${lineNumber}: ${clean.slice(r.i)}`);
|
|
889
|
+
return triple(s, p, o, g);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function parseNQuads(source, options = {}) {
|
|
893
|
+
const facts = [];
|
|
894
|
+
const prefixes = { ...(options.prefixes || {}) };
|
|
895
|
+
const format = options.format || (options.profileId === 'ntriples-graph-v0' ? 'ntriples' : 'nquads');
|
|
896
|
+
const lines = String(source || '').split(/\r\n|\n|\r/);
|
|
897
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
898
|
+
const fact = parseLine(lines[lineIndex], lineIndex + 1, format);
|
|
899
|
+
if (fact) facts.push(fact);
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
profile: options.profileId || (format === 'ntriples' ? 'ntriples-graph-v0' : 'nquads-dataset-v0'),
|
|
903
|
+
prefixes,
|
|
904
|
+
base: options.base || '',
|
|
905
|
+
imports: [],
|
|
906
|
+
facts,
|
|
907
|
+
rules: [],
|
|
908
|
+
queries: [],
|
|
909
|
+
expectations: [],
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function escapeIri(value) {
|
|
914
|
+
return String(value).replace(/[\\>\u0000-\u0020]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function escapeLiteral(value) {
|
|
918
|
+
return String(value)
|
|
919
|
+
.replace(/\\/g, '\\\\')
|
|
920
|
+
.replace(/"/g, '\\"')
|
|
921
|
+
.replace(/\n/g, '\\n')
|
|
922
|
+
.replace(/\r/g, '\\r')
|
|
923
|
+
.replace(/\t/g, '\\t')
|
|
924
|
+
.replace(/\u0008/g, '\\b')
|
|
925
|
+
.replace(/\u000c/g, '\\f');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function termToNQuads(term) {
|
|
929
|
+
if (!term) return '';
|
|
930
|
+
switch (term.kind) {
|
|
931
|
+
case 'iri':
|
|
932
|
+
return `<${escapeIri(term.value)}>`;
|
|
933
|
+
case 'blank':
|
|
934
|
+
return `_:${term.value}`;
|
|
935
|
+
case 'literal': {
|
|
936
|
+
let out = `"${escapeLiteral(term.value)}"`;
|
|
937
|
+
if (term.language) out += `@${term.language}`;
|
|
938
|
+
else if (term.datatype && term.datatype !== XSD_STRING) out += `^^<${escapeIri(term.datatype)}>`;
|
|
939
|
+
return out;
|
|
940
|
+
}
|
|
941
|
+
case 'triple':
|
|
942
|
+
return `<< ${termToNQuads(term.s)} ${termToNQuads(term.p)} ${termToNQuads(term.o)} >>`;
|
|
943
|
+
default:
|
|
944
|
+
throw new Error(`Cannot serialize ${term.kind} as N-Quads`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function tripleToNQuads(value) {
|
|
949
|
+
const terms = [termToNQuads(value.s), termToNQuads(value.p), termToNQuads(value.o)];
|
|
950
|
+
if (value.graph) terms.push(termToNQuads(value.graph));
|
|
951
|
+
return `${terms.join(' ')} .`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function triplesToNQuads(triples) {
|
|
955
|
+
return Array.from(new Set(Array.from(triples || []).map(tripleToNQuads))).sort().join('\n');
|
|
956
|
+
}
|
|
957
|
+
return { parseNQuads, termToNQuads, tripleToNQuads, triplesToNQuads };
|
|
958
|
+
})();
|
|
959
|
+
|
|
960
|
+
// ---- Turtle / TriG parser ----
|
|
961
|
+
const { parseN3 } = (() => {
|
|
962
|
+
|
|
963
|
+
const DEFAULT_PREFIXES = Object.freeze({
|
|
964
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
965
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
966
|
+
log: 'http://www.w3.org/2000/10/swap/log#',
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
function isWs(ch) { return /\s/.test(ch || ''); }
|
|
971
|
+
function isPunct(ch) { return '{}.;,()[]|'.includes(ch || ''); }
|
|
972
|
+
function isHex(text) { return /^[0-9A-Fa-f]+$/.test(text); }
|
|
973
|
+
function isAbsoluteIri(value) { return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value || ''); }
|
|
974
|
+
function resolveIriReference(value, base) {
|
|
975
|
+
if (isAbsoluteIri(value)) return value;
|
|
976
|
+
if (!base) return value;
|
|
977
|
+
try {
|
|
978
|
+
const url = new URL(value, base);
|
|
979
|
+
let href = url.href;
|
|
980
|
+
// The RDF IRI-resolution tests expect bare authority references such as //g
|
|
981
|
+
// to remain http://g, not to gain the URL API's cosmetic trailing slash.
|
|
982
|
+
if (/^\/\/[^/?#]+$/.test(value) && href.endsWith('/')) href = href.slice(0, -1);
|
|
983
|
+
if (/^file:\/\/[^/?#]+$/.test(value) && href.endsWith('/')) href = href.slice(0, -1);
|
|
984
|
+
return href;
|
|
985
|
+
} catch { return `${base}${value}`; }
|
|
986
|
+
}
|
|
987
|
+
function validateBlankLabel(value) {
|
|
988
|
+
const clean = String(value || '').replace(/^_:/, '');
|
|
989
|
+
if (!clean || clean.endsWith('.') || clean.includes('..')) throw new Error(`Invalid blank node label _: ${clean}`);
|
|
990
|
+
// BLANK_NODE_LABEL follows the PN_CHARS family; ':' is only for prefixed names, not blank labels.
|
|
991
|
+
if (/[\s<>"{}|^`\\:]/u.test(clean)) throw new Error(`Invalid blank node label _: ${clean}`);
|
|
992
|
+
if (/^[\-.]/u.test(clean)) throw new Error(`Invalid blank node label _: ${clean}`);
|
|
993
|
+
return clean;
|
|
994
|
+
}
|
|
995
|
+
function validateIriReference(value) {
|
|
996
|
+
if (/[<>\"{}|^`\u0000-\u0020]/.test(value)) throw new Error('Invalid character in IRIREF');
|
|
997
|
+
return value;
|
|
998
|
+
}
|
|
999
|
+
function validatePrefixedLocal(raw, decoded) {
|
|
1000
|
+
if (!raw) return decoded;
|
|
1001
|
+
if (raw.startsWith('-') || raw.startsWith('\\-') || raw.startsWith('.')) throw new Error(`Invalid prefixed name local ${raw}`);
|
|
1002
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
1003
|
+
const ch = raw[i];
|
|
1004
|
+
if (ch === '\\') {
|
|
1005
|
+
const esc = raw[i + 1];
|
|
1006
|
+
if (!esc || !'_~.-!$&\'()*+,;=/?#@%'.includes(esc)) throw new Error(`Invalid prefixed name local escape ${raw}`);
|
|
1007
|
+
i += 1;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (ch === '%') {
|
|
1011
|
+
const hex = raw.slice(i + 1, i + 3);
|
|
1012
|
+
if (hex.length !== 2 || !isHex(hex)) throw new Error(`Invalid percent escape in prefixed name local ${raw}`);
|
|
1013
|
+
i += 2;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (ch === '~' || ch === '^') throw new Error(`Invalid prefixed name local ${raw}`);
|
|
1017
|
+
}
|
|
1018
|
+
return decoded;
|
|
1019
|
+
}
|
|
1020
|
+
function decodePrefixedLocal(raw) {
|
|
1021
|
+
let out = '';
|
|
1022
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
1023
|
+
const ch = raw[i];
|
|
1024
|
+
if (ch === '\\') { out += raw[i + 1] || ''; i += 1; }
|
|
1025
|
+
else out += ch;
|
|
1026
|
+
}
|
|
1027
|
+
return out;
|
|
1028
|
+
}
|
|
1029
|
+
function codePoint(hex, label) {
|
|
1030
|
+
const n = Number.parseInt(hex, 16);
|
|
1031
|
+
if (!Number.isFinite(n) || n < 0 || n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff)) throw new Error(`Invalid Unicode escape in ${label}`);
|
|
1032
|
+
return String.fromCodePoint(n);
|
|
1033
|
+
}
|
|
1034
|
+
function decodeEscapes(text, label, iriMode = false) {
|
|
1035
|
+
let out = '';
|
|
1036
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1037
|
+
const ch = text[i];
|
|
1038
|
+
if (ch !== '\\') { out += ch; continue; }
|
|
1039
|
+
const esc = text[++i];
|
|
1040
|
+
if (!esc) throw new Error(`Trailing escape in ${label}`);
|
|
1041
|
+
if (esc === 'u') {
|
|
1042
|
+
const hex = text.slice(i + 1, i + 5); if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${label}`);
|
|
1043
|
+
out += codePoint(hex, label); i += 4;
|
|
1044
|
+
} else if (esc === 'U') {
|
|
1045
|
+
const hex = text.slice(i + 1, i + 9); if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${label}`);
|
|
1046
|
+
out += codePoint(hex, label); i += 8;
|
|
1047
|
+
} else if (!iriMode && 'tbnrf"\''.includes(esc)) {
|
|
1048
|
+
out += { t: '\t', b: '\b', n: '\n', r: '\r', f: '\f', '"': '"', "'": "'" }[esc] ?? esc;
|
|
1049
|
+
} else if (!iriMode && esc === '\\') out += '\\';
|
|
1050
|
+
else if (iriMode) throw new Error(`Invalid escape \\${esc} in ${label}`);
|
|
1051
|
+
else throw new Error(`Invalid escape \\${esc} in ${label}`);
|
|
1052
|
+
}
|
|
1053
|
+
return out;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
class Tokenizer {
|
|
1057
|
+
constructor(source) { this.source = String(source || ''); this.i = 0; this.tokens = []; }
|
|
1058
|
+
eof() { return this.i >= this.source.length; }
|
|
1059
|
+
peek(offset = 0) { return this.source[this.i + offset]; }
|
|
1060
|
+
startsWith(value) { return this.source.startsWith(value, this.i); }
|
|
1061
|
+
push(type, value, extra = {}) { this.tokens.push({ type, value, ...extra }); }
|
|
1062
|
+
skipComment() { while (!this.eof() && this.peek() !== '\n' && this.peek() !== '\r') this.i += 1; }
|
|
1063
|
+
readIri() {
|
|
1064
|
+
this.i += 1;
|
|
1065
|
+
let raw = '';
|
|
1066
|
+
while (!this.eof()) {
|
|
1067
|
+
const ch = this.peek();
|
|
1068
|
+
if (ch === '>') { this.i += 1; this.push('iri', validateIriReference(decodeEscapes(raw, 'IRI', true))); return; }
|
|
1069
|
+
raw += ch; this.i += 1;
|
|
1070
|
+
}
|
|
1071
|
+
throw new Error('Unterminated IRIREF');
|
|
1072
|
+
}
|
|
1073
|
+
readString() {
|
|
1074
|
+
const quote = this.peek();
|
|
1075
|
+
const long = this.source.startsWith(quote.repeat(3), this.i);
|
|
1076
|
+
this.i += long ? 3 : 1;
|
|
1077
|
+
let raw = '';
|
|
1078
|
+
let escaped = false;
|
|
1079
|
+
while (!this.eof()) {
|
|
1080
|
+
const ch = this.peek();
|
|
1081
|
+
if (!escaped && long && this.source.startsWith(quote.repeat(3), this.i)) { this.i += 3; this.push(long ? 'longString' : 'string', decodeEscapes(raw, 'string'), { long }); return; }
|
|
1082
|
+
if (!escaped && !long && ch === quote) { this.i += 1; this.push('string', decodeEscapes(raw, 'string'), { long: false }); return; }
|
|
1083
|
+
if (!long && (ch === '\n' || ch === '\r')) throw new Error('Raw line break in short string');
|
|
1084
|
+
raw += ch;
|
|
1085
|
+
this.i += 1;
|
|
1086
|
+
escaped = !escaped && ch === '\\';
|
|
1087
|
+
if (ch !== '\\') escaped = false;
|
|
1088
|
+
}
|
|
1089
|
+
throw new Error('Unterminated string');
|
|
1090
|
+
}
|
|
1091
|
+
readBare() {
|
|
1092
|
+
const start = this.i;
|
|
1093
|
+
if (this.startsWith('_:')) {
|
|
1094
|
+
this.i += 2;
|
|
1095
|
+
while (!this.eof()) {
|
|
1096
|
+
const ch = this.peek();
|
|
1097
|
+
// BLANK_NODE_LABEL uses PN_CHARS, including broad Unicode ranges that are
|
|
1098
|
+
// not all JavaScript \p{L}/\p{N}. Tokenize generously up to a real
|
|
1099
|
+
// Turtle delimiter, then validate the label separately. Keep ':' as a
|
|
1100
|
+
// boundary so compact forms such as _:s:p tokenize as blank-node _:s
|
|
1101
|
+
// followed by predicate :p.
|
|
1102
|
+
if (isWs(ch) || '<>\"{}|^`\\;,)[]'.includes(ch) || ch === ':' || ch === '#') break;
|
|
1103
|
+
if (ch === '.') {
|
|
1104
|
+
const next = this.source[this.i + 1];
|
|
1105
|
+
if (!next || isWs(next) || '{};,)[]'.includes(next)) break;
|
|
1106
|
+
}
|
|
1107
|
+
this.i += 1;
|
|
1108
|
+
}
|
|
1109
|
+
this.push('bare', this.source.slice(start, this.i));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
while (!this.eof()) {
|
|
1113
|
+
const ch = this.peek();
|
|
1114
|
+
if (isWs(ch)) break;
|
|
1115
|
+
if (ch === '\\' && this.source[this.i + 1]) { this.i += 2; continue; }
|
|
1116
|
+
if (ch === '<' || ch === '>' || ch === '"' || ch === "'") break;
|
|
1117
|
+
if (ch === '.') {
|
|
1118
|
+
const next = this.source[this.i + 1];
|
|
1119
|
+
if (!next || isWs(next) || '{};,)[]'.includes(next)) break;
|
|
1120
|
+
} else if (isPunct(ch)) break;
|
|
1121
|
+
if (ch === '#') break;
|
|
1122
|
+
if (ch === '^' && this.peek(1) === '^') break;
|
|
1123
|
+
if (ch === '=' && this.peek(1) === '>') break;
|
|
1124
|
+
this.i += 1;
|
|
1125
|
+
}
|
|
1126
|
+
this.push('bare', this.source.slice(start, this.i));
|
|
1127
|
+
}
|
|
1128
|
+
tokenize() {
|
|
1129
|
+
while (!this.eof()) {
|
|
1130
|
+
const ch = this.peek();
|
|
1131
|
+
if (isWs(ch)) { this.i += 1; continue; }
|
|
1132
|
+
if (ch === '#') { this.skipComment(); continue; }
|
|
1133
|
+
if (this.startsWith('@prefix') && (isWs(this.source[this.i + 7]) || this.source[this.i + 7] === ':')) { this.push('bare', '@prefix'); this.i += 7; continue; }
|
|
1134
|
+
if (this.startsWith('@base') && isWs(this.source[this.i + 5])) { this.push('bare', '@base'); this.i += 5; continue; }
|
|
1135
|
+
if (this.startsWith('@version') && isWs(this.source[this.i + 8])) { this.push('bare', '@version'); this.i += 8; continue; }
|
|
1136
|
+
if (this.startsWith('=>')) { this.push('=>', '=>'); this.i += 2; continue; }
|
|
1137
|
+
if (this.startsWith('^^')) { this.push('^^', '^^'); this.i += 2; continue; }
|
|
1138
|
+
if (this.startsWith('<<')) { this.push('<<', '<<'); this.i += 2; continue; }
|
|
1139
|
+
if (this.startsWith('>>')) { this.push('>>', '>>'); this.i += 2; continue; }
|
|
1140
|
+
if (ch === '<') { this.readIri(); continue; }
|
|
1141
|
+
if (ch === '"' || ch === "'") { this.readString(); continue; }
|
|
1142
|
+
if (ch === '.' && /[0-9]/.test(this.peek(1) || '')) { this.readBare(); continue; }
|
|
1143
|
+
if (ch === '~') { this.readBare(); continue; }
|
|
1144
|
+
if (isPunct(ch)) { this.push(ch, ch); this.i += 1; continue; }
|
|
1145
|
+
this.readBare();
|
|
1146
|
+
}
|
|
1147
|
+
return this.tokens;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function parseN3(source, options = {}) {
|
|
1152
|
+
const tokens = new Tokenizer(source).tokenize();
|
|
1153
|
+
let i = 0;
|
|
1154
|
+
let base = options.base || '';
|
|
1155
|
+
const prefixes = { ...DEFAULT_PREFIXES, ...(options.prefixes || {}) };
|
|
1156
|
+
const facts = [];
|
|
1157
|
+
const rules = [];
|
|
1158
|
+
let bnodeCounter = 0;
|
|
1159
|
+
const bnodes = new Map();
|
|
1160
|
+
const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
|
|
1161
|
+
const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
|
|
1162
|
+
const implicitStatementNodes = new Set();
|
|
1163
|
+
|
|
1164
|
+
function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
|
|
1165
|
+
function peek(offset = 0) { return tokens[i + offset]; }
|
|
1166
|
+
function next() { return tokens[i++]; }
|
|
1167
|
+
function eof() { return i >= tokens.length; }
|
|
1168
|
+
function accept(value) { if (peek()?.value === value || peek()?.type === value) { i += 1; return true; } return false; }
|
|
1169
|
+
function expect(value) { const t = next(); if (!t || (t.value !== value && t.type !== value)) throw new Error(`Expected ${value}, got ${t?.value || 'end of input'}`); return t; }
|
|
1170
|
+
function error(msg) { throw new Error(msg); }
|
|
1171
|
+
|
|
1172
|
+
function parseIriValueFromBare(token) {
|
|
1173
|
+
if (token === 'a') return RDF_TYPE;
|
|
1174
|
+
if (token.startsWith('_:')) error(`Blank node label ${token} cannot be used as IRI`);
|
|
1175
|
+
const split = token.indexOf(':');
|
|
1176
|
+
if (split >= 0) {
|
|
1177
|
+
const prefix = token.slice(0, split);
|
|
1178
|
+
let local = token.slice(split + 1);
|
|
1179
|
+
if (!(prefix in prefixes)) throw new Error(`Unknown prefix ${prefix}:`);
|
|
1180
|
+
// Turtle permits reserved escaped characters in local names.
|
|
1181
|
+
local = validatePrefixedLocal(local, decodePrefixedLocal(local));
|
|
1182
|
+
return prefixes[prefix] + local;
|
|
1183
|
+
}
|
|
1184
|
+
throw new Error(`Expected IRI or prefixed name, got ${token}`);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function parseIriLike() {
|
|
1188
|
+
const t = next();
|
|
1189
|
+
if (!t) error('Unexpected end of input while reading IRI');
|
|
1190
|
+
if (t.type === 'iri') return resolveIriReference(t.value, base);
|
|
1191
|
+
if (t.type === 'bare') return parseIriValueFromBare(t.value);
|
|
1192
|
+
throw new Error(`Expected IRI or prefixed name, got ${t.value}`);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function parseBlankLabel(label) {
|
|
1196
|
+
const key = validateBlankLabel(label);
|
|
1197
|
+
if (!bnodes.has(key)) bnodes.set(key, blank(key));
|
|
1198
|
+
return bnodes.get(key);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function parseNumber(token) {
|
|
1202
|
+
if (/^[+-]?\d+$/.test(token)) return literal(token, XSD_INTEGER);
|
|
1203
|
+
if (/^[+-]?(?:\d+\.\d*|\.\d+)$/.test(token)) return literal(token, XSD_DECIMAL);
|
|
1204
|
+
if (/^[+-]?(?:(?:\d+\.\d*)|(?:\.\d+)|\d+)[eE][+-]?\d+$/.test(token)) return literal(token, XSD_DOUBLE);
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function parseTerm(out = facts, graph = null, options2 = {}) {
|
|
1209
|
+
const t = peek();
|
|
1210
|
+
if (!t) error('Unexpected end of input while reading Turtle term');
|
|
1211
|
+
if (t.type === '<<') return parseTripleTerm(out, graph, options2);
|
|
1212
|
+
if (t.type === 'iri') { next(); return iri(resolveIriReference(t.value, base)); }
|
|
1213
|
+
if (t.type === 'string' || t.type === 'longString') {
|
|
1214
|
+
if (options2.noLiteral) throw new Error('Literal is not allowed here');
|
|
1215
|
+
return parseLiteral();
|
|
1216
|
+
}
|
|
1217
|
+
if (t.type === '[') {
|
|
1218
|
+
// ANON is a BlankNode and is permitted in rtSubject/ttSubject, even where
|
|
1219
|
+
// blankNodePropertyList is not. Keep [ ... ] rejected in those positions.
|
|
1220
|
+
if (options2.noCompound) {
|
|
1221
|
+
if (peek(1)?.type === ']') { expect('['); expect(']'); return freshBlank(); }
|
|
1222
|
+
throw new Error('Compound blank node expression is not allowed here');
|
|
1223
|
+
}
|
|
1224
|
+
return parseBlankNodePropertyList(out, graph);
|
|
1225
|
+
}
|
|
1226
|
+
if (t.type === '(') {
|
|
1227
|
+
if (options2.noCompound) throw new Error('Collection is not allowed here');
|
|
1228
|
+
return parseCollection(out, graph);
|
|
1229
|
+
}
|
|
1230
|
+
if (t.type === 'bare') {
|
|
1231
|
+
next();
|
|
1232
|
+
if (t.value.startsWith('?')) {
|
|
1233
|
+
if (rdf12Surface) throw new Error(`Variables are not allowed in Turtle/TriG: ${t.value}`);
|
|
1234
|
+
return variable(t.value.slice(1));
|
|
1235
|
+
}
|
|
1236
|
+
if (t.value.startsWith('_:')) return parseBlankLabel(t.value);
|
|
1237
|
+
if (t.value === 'a' && options2.noA) throw new Error('a is only allowed as a predicate');
|
|
1238
|
+
if (t.value === 'true' || t.value === 'false') {
|
|
1239
|
+
if (options2.noLiteral) throw new Error('Literal is not allowed here');
|
|
1240
|
+
return literal(t.value, XSD_BOOLEAN);
|
|
1241
|
+
}
|
|
1242
|
+
const num = parseNumber(t.value);
|
|
1243
|
+
if (num) {
|
|
1244
|
+
if (options2.noLiteral) throw new Error('Literal is not allowed here');
|
|
1245
|
+
return num;
|
|
1246
|
+
}
|
|
1247
|
+
return iri(parseIriValueFromBare(t.value));
|
|
1248
|
+
}
|
|
1249
|
+
throw new Error(`Expected IRI or prefixed name, got ${t.value}`);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function parseLiteral() {
|
|
1253
|
+
const t = next(); if (!t || (t.type !== 'string' && t.type !== 'longString')) throw new Error(`Expected string, got ${t?.value || 'end of input'}`);
|
|
1254
|
+
if (peek()?.type === 'bare' && peek().value.startsWith('@')) {
|
|
1255
|
+
let lang = next().value.slice(1);
|
|
1256
|
+
let langDir = null;
|
|
1257
|
+
if (lang.endsWith('--ltr') || lang.endsWith('--rtl')) {
|
|
1258
|
+
langDir = lang.slice(-3);
|
|
1259
|
+
lang = lang.slice(0, -5);
|
|
1260
|
+
}
|
|
1261
|
+
if (!lang || lang.includes('--') || !/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) throw new Error(`Invalid language tag @${lang}`);
|
|
1262
|
+
if (peek()?.type === 'bare' && ['--ltr', '--rtl'].includes(peek().value)) langDir = next().value.slice(2);
|
|
1263
|
+
return literal(t.value, null, lang, langDir);
|
|
1264
|
+
}
|
|
1265
|
+
if (accept('^^')) return literal(t.value, parseIriLike());
|
|
1266
|
+
if (peek()?.type === 'bare' && ['--ltr', '--rtl'].includes(peek().value)) throw new Error('Base direction requires a language tag');
|
|
1267
|
+
return literal(t.value, XSD_STRING);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function parseReifierToken(out, graph) {
|
|
1271
|
+
const t = peek();
|
|
1272
|
+
if (!t || t.type !== 'bare' || !t.value.startsWith('~')) return null;
|
|
1273
|
+
next();
|
|
1274
|
+
const suffix = t.value.slice(1);
|
|
1275
|
+
if (suffix) {
|
|
1276
|
+
if (suffix.startsWith('_:')) return parseBlankLabel(suffix);
|
|
1277
|
+
return iri(parseIriValueFromBare(suffix));
|
|
1278
|
+
}
|
|
1279
|
+
const n = peek();
|
|
1280
|
+
if (n && (n.type === 'iri' || (n.type === 'bare' && (n.value.startsWith('_:') || n.value.includes(':') || n.value === 'a')))) {
|
|
1281
|
+
const term = parseTerm(out, graph, { noLiteral: true, noCompound: true, noTripleTerm: true });
|
|
1282
|
+
if (term.kind !== 'iri' && term.kind !== 'blank') throw new Error('Reifier must be an IRI or blank node');
|
|
1283
|
+
return term;
|
|
1284
|
+
}
|
|
1285
|
+
return freshBlank();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function parseTripleTerm(out, graph, options2 = {}) {
|
|
1289
|
+
expect('<<');
|
|
1290
|
+
const parenthesized = accept('(');
|
|
1291
|
+
if (parenthesized) {
|
|
1292
|
+
if (options2.noTripleTerm) throw new Error('Triple term is not allowed here');
|
|
1293
|
+
const s = parseTerm(out, graph, { noLiteral: true, noCompound: true, noReifiedTriple: true });
|
|
1294
|
+
if (s.kind === 'triple') throw new Error('Triple term subject cannot be a triple term');
|
|
1295
|
+
const p = iri(parseIriLike());
|
|
1296
|
+
const o = parseTerm(out, graph, { noCompound: true, noReifiedTriple: true });
|
|
1297
|
+
if (o.kind !== 'iri' && o.kind !== 'blank' && o.kind !== 'literal' && o.kind !== 'triple') throw new Error('Invalid triple term object');
|
|
1298
|
+
expect(')');
|
|
1299
|
+
expect('>>');
|
|
1300
|
+
return tripleTerm(s, p, o);
|
|
1301
|
+
}
|
|
1302
|
+
if (options2.noReifiedTriple) throw new Error('Reified triple is not allowed here');
|
|
1303
|
+
const s = parseTerm(out, graph, { noLiteral: true, noCompound: true, noTripleTerm: true });
|
|
1304
|
+
if (s.kind !== 'iri' && s.kind !== 'blank') throw new Error('Invalid reified triple subject');
|
|
1305
|
+
const p = iri(parseIriLike());
|
|
1306
|
+
const o = parseTerm(out, graph, { noCompound: true });
|
|
1307
|
+
if (o.kind !== 'iri' && o.kind !== 'blank' && o.kind !== 'literal' && o.kind !== 'triple') throw new Error('Invalid reified triple object');
|
|
1308
|
+
let reifier = null;
|
|
1309
|
+
if (peek()?.type === 'bare' && peek().value.startsWith('~')) reifier = parseReifierToken(out, graph);
|
|
1310
|
+
expect('>>');
|
|
1311
|
+
const node = reifier || freshBlank();
|
|
1312
|
+
out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
|
|
1313
|
+
if (node.kind === 'blank') implicitStatementNodes.add(node.value);
|
|
1314
|
+
return node;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function parseCollection(out, graph) {
|
|
1318
|
+
expect('(');
|
|
1319
|
+
if (accept(')')) return iri(RDF_NIL);
|
|
1320
|
+
const head = freshBlank();
|
|
1321
|
+
let current = head;
|
|
1322
|
+
while (true) {
|
|
1323
|
+
const item = parseTerm(out, graph);
|
|
1324
|
+
out.push(triple(current, iri(RDF_FIRST), item, graph));
|
|
1325
|
+
if (accept(')')) {
|
|
1326
|
+
out.push(triple(current, iri(RDF_REST), iri(RDF_NIL), graph));
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
const rest = freshBlank();
|
|
1330
|
+
out.push(triple(current, iri(RDF_REST), rest, graph));
|
|
1331
|
+
current = rest;
|
|
1332
|
+
}
|
|
1333
|
+
return head;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function parseBlankNodePropertyList(out, graph) {
|
|
1337
|
+
expect('[');
|
|
1338
|
+
const node = freshBlank();
|
|
1339
|
+
if (accept(']')) return node;
|
|
1340
|
+
parsePredicateObjectList(node, out, graph);
|
|
1341
|
+
expect(']');
|
|
1342
|
+
if (node.kind === 'blank') implicitStatementNodes.add(node.value);
|
|
1343
|
+
return node;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function parseAnnotationBlock(reifier, out, graph = null) {
|
|
1347
|
+
expect('{');
|
|
1348
|
+
expect('|');
|
|
1349
|
+
if (peek()?.type === '|') {
|
|
1350
|
+
// Empty annotation blocks are rejected by the RDF 1.2 syntax tests.
|
|
1351
|
+
throw new Error('Empty annotation block');
|
|
1352
|
+
}
|
|
1353
|
+
parsePredicateObjectList(reifier, out, graph);
|
|
1354
|
+
expect('|');
|
|
1355
|
+
expect('}');
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function ensureReifierForTriple(assertedTriple, out, graph = null) {
|
|
1359
|
+
const reifier = freshBlank();
|
|
1360
|
+
out.push(triple(reifier, iri(RDF_REIFIES), tripleTerm(assertedTriple.s, assertedTriple.p, assertedTriple.o), graph));
|
|
1361
|
+
return reifier;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function parseObjectList(subject, predicate, out, graph = null) {
|
|
1365
|
+
while (true) {
|
|
1366
|
+
const object = parseTerm(out, graph, { noA: true });
|
|
1367
|
+
const asserted = triple(subject, predicate, object, graph);
|
|
1368
|
+
out.push(asserted);
|
|
1369
|
+
let pendingReifier = null;
|
|
1370
|
+
while (true) {
|
|
1371
|
+
if (peek()?.type === 'bare' && peek().value.startsWith('~')) {
|
|
1372
|
+
pendingReifier = parseReifierToken(out, graph);
|
|
1373
|
+
out.push(triple(pendingReifier, iri(RDF_REIFIES), tripleTerm(asserted.s, asserted.p, asserted.o), graph));
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (peek()?.type === '{' && peek(1)?.type === '|') {
|
|
1377
|
+
const blockReifier = pendingReifier || ensureReifierForTriple(asserted, out, graph);
|
|
1378
|
+
parseAnnotationBlock(blockReifier, out, graph);
|
|
1379
|
+
pendingReifier = null;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
if (!accept(',')) break;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function parsePredicateObjectList(subject, out, graph = null) {
|
|
1389
|
+
while (true) {
|
|
1390
|
+
const predicate = iri(parseIriLike());
|
|
1391
|
+
parseObjectList(subject, predicate, out, graph);
|
|
1392
|
+
if (!accept(';')) break;
|
|
1393
|
+
while (accept(';')) {}
|
|
1394
|
+
if ([']', '.', '}', '|'].includes(peek()?.type)) break;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function parseGraphLabel(out, inheritedGraph = null) {
|
|
1399
|
+
if (peek()?.type === '[' && peek(1)?.type === ']') { expect('['); expect(']'); return freshBlank(); }
|
|
1400
|
+
if (peek()?.type === '(') throw new Error('GRAPH name must be an IRI or blank node');
|
|
1401
|
+
const graph = parseTerm(out, inheritedGraph, { noLiteral: true, noCompound: true, noTripleTerm: true, noReifiedTriple: true, noA: true });
|
|
1402
|
+
if (graph.kind !== 'iri' && graph.kind !== 'blank') throw new Error('GRAPH name must be an IRI or blank node');
|
|
1403
|
+
return graph;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function parseGraphBlock(out, inheritedGraph = null) {
|
|
1407
|
+
expect('GRAPH');
|
|
1408
|
+
const graph = parseGraphLabel(out, inheritedGraph);
|
|
1409
|
+
parseFormula(graph, out);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function parseTripleStatement(out, graph = null, options3 = {}) {
|
|
1413
|
+
if (String(peek()?.value || '').toUpperCase() === 'GRAPH') {
|
|
1414
|
+
if (syntaxProfile === 'turtle') throw new Error('GRAPH blocks are not Turtle');
|
|
1415
|
+
if (graph) throw new Error('GRAPH blocks cannot be nested inside a graph block');
|
|
1416
|
+
parseGraphBlock(out, graph);
|
|
1417
|
+
if (options3.requireDot) expect('.'); else accept('.');
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
|
|
1421
|
+
const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
|
|
1422
|
+
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
|
|
1423
|
+
if (options3.requireDot) expect('.'); else accept('.');
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
parsePredicateObjectList(subject, out, graph);
|
|
1427
|
+
if (options3.requireDot) expect('.'); else accept('.');
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function parseFormula(graph = null, target = null) {
|
|
1431
|
+
expect('{');
|
|
1432
|
+
const triples = target || [];
|
|
1433
|
+
while (peek()?.type !== '}') parseTripleStatement(triples, graph);
|
|
1434
|
+
expect('}');
|
|
1435
|
+
return triples;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function parseBase() {
|
|
1439
|
+
const directive = next();
|
|
1440
|
+
const iriToken = next();
|
|
1441
|
+
if (iriToken?.type !== 'iri') throw new Error(`Expected base IRI, got ${iriToken?.value}`);
|
|
1442
|
+
base = resolveIriReference(iriToken.value, base);
|
|
1443
|
+
if (String(directive.value || '').startsWith('@')) expect('.');
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function parsePrefix() {
|
|
1447
|
+
const directive = next();
|
|
1448
|
+
const label = next();
|
|
1449
|
+
if (label?.type !== 'bare' || !label.value.endsWith(':')) throw new Error(`Expected prefix label ending with :, got ${label?.value}`);
|
|
1450
|
+
const prefixLabel = label.value.slice(0, -1);
|
|
1451
|
+
if (prefixLabel.endsWith('.') || prefixLabel.includes('..')) throw new Error(`Invalid prefix label ${prefixLabel}`);
|
|
1452
|
+
const iriToken = next();
|
|
1453
|
+
if (iriToken?.type !== 'iri') throw new Error(`Expected prefix IRI, got ${iriToken?.value}`);
|
|
1454
|
+
prefixes[prefixLabel] = resolveIriReference(iriToken.value, base);
|
|
1455
|
+
if (String(directive.value || '').startsWith('@')) expect('.');
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function isSimpleGraphLabelStart(t) {
|
|
1459
|
+
return t && (t.type === 'iri' || (t.type === 'bare' && (t.value.startsWith('_:') || t.value.includes(':'))));
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
while (!eof()) {
|
|
1463
|
+
const token = peek();
|
|
1464
|
+
const lowerValue = String(token.value || '').toLowerCase();
|
|
1465
|
+
if (token.value === '@base' || (!String(token.value || '').startsWith('@') && lowerValue === 'base')) parseBase();
|
|
1466
|
+
else if (token.value === '@prefix' || (!String(token.value || '').startsWith('@') && lowerValue === 'prefix')) parsePrefix();
|
|
1467
|
+
else if ((!String(token.value || '').startsWith('@') && String(token.value || '').toUpperCase() === 'VERSION') || token.value === '@version') {
|
|
1468
|
+
const directive = next();
|
|
1469
|
+
const v = next();
|
|
1470
|
+
if (!v || v.type !== 'string') throw new Error('VERSION requires a short quoted string');
|
|
1471
|
+
if (String(directive.value || '').startsWith('@')) expect('.');
|
|
1472
|
+
}
|
|
1473
|
+
else if (token.type === '{') {
|
|
1474
|
+
if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow top-level graph/formula blocks');
|
|
1475
|
+
const body = parseFormula();
|
|
1476
|
+
if (accept('=>')) {
|
|
1477
|
+
const head = parseFormula();
|
|
1478
|
+
accept('.');
|
|
1479
|
+
rules.push(new Rule({ id: `n3${rules.length + 1}`, body, head, profile: 'n3-rules-subset-v0' }));
|
|
1480
|
+
} else {
|
|
1481
|
+
facts.push(...body);
|
|
1482
|
+
accept('.');
|
|
1483
|
+
}
|
|
1484
|
+
} else if (String(token.value || '').toUpperCase() === 'GRAPH') {
|
|
1485
|
+
parseGraphBlock(facts);
|
|
1486
|
+
if (accept('.')) throw new Error('GRAPH block must not be followed by .');
|
|
1487
|
+
} else if (token.type === '[' && peek(1)?.type === ']' && peek(2)?.type === '{') {
|
|
1488
|
+
if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow graph labels');
|
|
1489
|
+
const graph = parseGraphLabel(facts, null);
|
|
1490
|
+
parseFormula(graph, facts);
|
|
1491
|
+
accept('.');
|
|
1492
|
+
} else if (isSimpleGraphLabelStart(token) && peek(1)?.type === '{') {
|
|
1493
|
+
if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow graph labels');
|
|
1494
|
+
const graph = parseGraphLabel(facts, null);
|
|
1495
|
+
parseFormula(graph, facts);
|
|
1496
|
+
accept('.');
|
|
1497
|
+
} else {
|
|
1498
|
+
parseTripleStatement(facts, null, { requireDot: syntaxProfile === 'turtle' });
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
return { profile: 'n3-rules-subset-v0', prefixes, base, facts, rules };
|
|
1503
|
+
}
|
|
1504
|
+
return { parseN3 };
|
|
1505
|
+
})();
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
return {
|
|
1509
|
+
parseNQuads,
|
|
1510
|
+
termToNQuads,
|
|
1511
|
+
tripleToNQuads,
|
|
1512
|
+
triplesToNQuads,
|
|
1513
|
+
parseN3,
|
|
1514
|
+
};
|
|
1515
|
+
})();
|
|
1516
|
+
|
|
1517
|
+
Object.assign(module.exports, rdfW3cSyntax);
|