eyeleng 1.0.5 → 1.0.7
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 +311 -11
- package/dist/browser/eyeleng.browser.js +2649 -1162
- package/examples/output/collection-nesting.trig +1 -1
- 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 +2631 -1138
- package/package.json +9 -2
- package/playground.html +1 -1
- package/reports/w3c-rdf-earl.ttl +11707 -0
- package/reports/w3c-shacl12-rules-earl.ttl +1055 -1336
- package/src/api.js +4 -0
- package/src/cli.js +6 -0
- package/src/parser.js +171 -3
- package/src/rdfEntailment.js +571 -0
- package/src/rdfManifest.js +737 -0
- package/src/rdfMessages.js +321 -0
- package/src/rdfSyntax.js +955 -0
- package/src/shacl12RulesManifest.js +386 -0
- package/src/tokenizer.js +47 -11
- package/test/api.test.js +95 -0
- package/test/harness.js +38 -10
- package/test/run.js +6 -3
- package/test/shacl12-rules.test.js +40 -217
- package/test/w3c-rdf.test.js +205 -0
- package/tools/browser-bundle.js +0 -0
- package/tools/bundle.js +0 -0
- package/tools/w3c-rdf.js +55 -0
- package/tools/w3c-shacl12-rules.js +55 -0
- package/HANDBOOK.md +0 -1070
package/eyeleng.js
CHANGED
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
imports: true,
|
|
59
59
|
syntax: 'auto',
|
|
60
60
|
ruleSet: null,
|
|
61
|
+
rdfMessages: false,
|
|
62
|
+
includeMessageFacts: false,
|
|
61
63
|
};
|
|
62
64
|
const files = [];
|
|
63
65
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -70,6 +72,8 @@
|
|
|
70
72
|
else if (arg === '--strict') options.strict = true;
|
|
71
73
|
else if (arg === '--deps') options.deps = true;
|
|
72
74
|
else if (arg === '--no-imports') options.imports = false;
|
|
75
|
+
else if (arg === '--rdf-messages' || arg === '--stream-messages') options.rdfMessages = true;
|
|
76
|
+
else if (arg === '--include-message-facts') options.includeMessageFacts = true;
|
|
73
77
|
else if (arg === '--syntax') {
|
|
74
78
|
i += 1;
|
|
75
79
|
if (i >= argv.length) throw new Error('--syntax requires srl, rdf, or auto');
|
|
@@ -179,6 +183,8 @@
|
|
|
179
183
|
importResolver: options.imports ? createFileImportResolver() : null,
|
|
180
184
|
syntax: options.syntax === 'auto' ? undefined : options.syntax,
|
|
181
185
|
ruleSet: options.ruleSet,
|
|
186
|
+
rdfMessages: options.rdfMessages,
|
|
187
|
+
includeMessageFacts: options.includeMessageFacts,
|
|
182
188
|
});
|
|
183
189
|
const fatal = hasFatalDiagnostics(compiled.analysis, options.strict);
|
|
184
190
|
|
|
@@ -236,6 +242,7 @@
|
|
|
236
242
|
|
|
237
243
|
const { parse, parseQuery } = require('./parser.js');
|
|
238
244
|
const { parseRdfSyntax, parseRdfDocument, rdfDocumentToProgram, looksLikeRdfRules } = require('./rdfSyntax.js');
|
|
245
|
+
const { parseRdfMessageLog, looksLikeRdfMessageLog } = require('./rdfMessages.js');
|
|
239
246
|
const { evaluate } = require('./engine.js');
|
|
240
247
|
const { analyze } = require('./analyze.js');
|
|
241
248
|
const { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');
|
|
@@ -244,6 +251,7 @@
|
|
|
244
251
|
|
|
245
252
|
function parseInput(source, options = {}) {
|
|
246
253
|
if (typeof source !== 'string') return source;
|
|
254
|
+
if (looksLikeRdfMessageLog(source, options)) return parseRdfMessageLog(source, options);
|
|
247
255
|
return looksLikeRdfRules(source, options) ? parseRdfSyntax(source, options) : parse(source, options);
|
|
248
256
|
}
|
|
249
257
|
|
|
@@ -340,6 +348,8 @@
|
|
|
340
348
|
parseInput,
|
|
341
349
|
parseRdfSyntax,
|
|
342
350
|
parseRdfDocument,
|
|
351
|
+
parseRdfMessageLog,
|
|
352
|
+
looksLikeRdfMessageLog,
|
|
343
353
|
rdfDocumentToProgram,
|
|
344
354
|
compile,
|
|
345
355
|
resolveImports,
|
|
@@ -363,6 +373,7 @@
|
|
|
363
373
|
'use strict';
|
|
364
374
|
|
|
365
375
|
const { tokenize, SyntaxErrorWithLocation } = require('./tokenizer.js');
|
|
376
|
+
const { parseN3 } = require('./rdfSyntax.js');
|
|
366
377
|
const { isBuiltinName } = require('./builtins.js');
|
|
367
378
|
const { ruleNeedsRunOnce } = require('./assignments.js');
|
|
368
379
|
const {
|
|
@@ -376,6 +387,7 @@
|
|
|
376
387
|
RDF_REST,
|
|
377
388
|
RDF_NIL,
|
|
378
389
|
RDF_REIFIES,
|
|
390
|
+
XSD_STRING,
|
|
379
391
|
XSD_BOOLEAN,
|
|
380
392
|
XSD_INTEGER,
|
|
381
393
|
XSD_DECIMAL,
|
|
@@ -384,7 +396,7 @@
|
|
|
384
396
|
|
|
385
397
|
class Parser {
|
|
386
398
|
constructor(source, options = {}) {
|
|
387
|
-
this.tokens = Array.isArray(source) ? source : tokenize(source, options
|
|
399
|
+
this.tokens = Array.isArray(source) ? source : tokenize(source, options);
|
|
388
400
|
this.pos = 0;
|
|
389
401
|
this.options = options;
|
|
390
402
|
this.baseIRI = options.baseIRI || null;
|
|
@@ -414,7 +426,7 @@
|
|
|
414
426
|
this.parseImports();
|
|
415
427
|
} else if (this.matchWord('DATA')) {
|
|
416
428
|
this.expectValue('{');
|
|
417
|
-
data.push(...this.
|
|
429
|
+
data.push(...this.parseDataBlockWithRdfSyntax());
|
|
418
430
|
} else if (this.matchWord('RULE')) {
|
|
419
431
|
rules.push(this.parseRule());
|
|
420
432
|
} else if (this.matchWord('IF')) {
|
|
@@ -447,6 +459,7 @@
|
|
|
447
459
|
let name = nameToken.value;
|
|
448
460
|
if (!name.endsWith(':')) throw this.error('Prefix name must end with :', nameToken);
|
|
449
461
|
name = name.slice(0, -1);
|
|
462
|
+
if (this.strictGrammar() && !isValidPNPrefix(name)) throw this.error(`Invalid prefix name ${nameToken.value}`, nameToken);
|
|
450
463
|
const iriToken = this.expectType('iri');
|
|
451
464
|
this.prefixes[name] = this.resolveIRI(iriToken.value, iriToken);
|
|
452
465
|
if (wasAtPrefix) this.consumeOptionalDot();
|
|
@@ -454,6 +467,10 @@
|
|
|
454
467
|
|
|
455
468
|
parseVersion() {
|
|
456
469
|
const token = this.expectType('string');
|
|
470
|
+
if (this.strictGrammar()) {
|
|
471
|
+
if (token.long) throw this.error('VERSION must use a short string literal', token);
|
|
472
|
+
if (token.value !== '1.2') throw this.error('VERSION must be the SHACL Rules version label \"1.2\"', token);
|
|
473
|
+
}
|
|
457
474
|
this.version = token.value;
|
|
458
475
|
}
|
|
459
476
|
|
|
@@ -558,6 +575,98 @@
|
|
|
558
575
|
return triples;
|
|
559
576
|
}
|
|
560
577
|
|
|
578
|
+
parseDataBlockWithRdfSyntax() {
|
|
579
|
+
const blockSource = this.collectBalancedDataBlockSource();
|
|
580
|
+
const program = parseN3(blockSource, {
|
|
581
|
+
profile: 'trig',
|
|
582
|
+
base: this.baseIRI || '',
|
|
583
|
+
prefixes: this.prefixes,
|
|
584
|
+
});
|
|
585
|
+
return (program.facts || []).map((triple) => this.convertRdfSyntaxTriple(triple));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
collectBalancedDataBlockSource() {
|
|
589
|
+
const tokens = [];
|
|
590
|
+
let depth = 1;
|
|
591
|
+
while (!this.is('eof')) {
|
|
592
|
+
const token = this.advance();
|
|
593
|
+
if (token.value === '{') {
|
|
594
|
+
depth += 1;
|
|
595
|
+
tokens.push(token);
|
|
596
|
+
} else if (token.value === '}') {
|
|
597
|
+
depth -= 1;
|
|
598
|
+
if (depth === 0) return this.tokensToRdfSource(tokens);
|
|
599
|
+
tokens.push(token);
|
|
600
|
+
} else {
|
|
601
|
+
tokens.push(token);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
throw this.error('Unterminated DATA block');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
tokensToRdfSource(tokens) {
|
|
608
|
+
const parts = [];
|
|
609
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
610
|
+
const token = tokens[i];
|
|
611
|
+
if (token.type === 'eof') continue;
|
|
612
|
+
if ((token.value === '+' || token.value === '-') && tokens[i + 1] && tokens[i + 1].type === 'number') {
|
|
613
|
+
parts.push(`${token.value}${this.tokenToRdfSource(tokens[i + 1])}`);
|
|
614
|
+
i += 1;
|
|
615
|
+
} else {
|
|
616
|
+
parts.push(this.tokenToRdfSource(token));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return parts.join(' ');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
tokenToRdfSource(token) {
|
|
623
|
+
if (token.type === 'iri') return `<${String(token.value).replace(/>/g, '\\>')}>`;
|
|
624
|
+
if (token.type === 'string') return JSON.stringify(token.value);
|
|
625
|
+
if (token.type === 'variable') return `?${token.value}`;
|
|
626
|
+
if (token.type === 'number') return String(token.value);
|
|
627
|
+
return String(token.value);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
convertRdfSyntaxTriple(triple) {
|
|
631
|
+
const out = {
|
|
632
|
+
s: this.convertRdfSyntaxTerm(triple.s),
|
|
633
|
+
p: this.convertRdfSyntaxTerm(triple.p),
|
|
634
|
+
o: this.convertRdfSyntaxTerm(triple.o),
|
|
635
|
+
};
|
|
636
|
+
if (out.s.type === 'var' || out.p.type === 'var' || out.o.type === 'var') {
|
|
637
|
+
throw this.error('DATA blocks may not contain variables');
|
|
638
|
+
}
|
|
639
|
+
if (out.p.type !== 'iri') {
|
|
640
|
+
throw this.error('DATA predicates must be IRIs');
|
|
641
|
+
}
|
|
642
|
+
if (triple.graph) out.graph = this.convertRdfSyntaxTerm(triple.graph);
|
|
643
|
+
return out;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
convertRdfSyntaxTerm(term) {
|
|
647
|
+
if (!term) return null;
|
|
648
|
+
if (term.type) return term;
|
|
649
|
+
if (term.kind === 'iri') return iri(term.value);
|
|
650
|
+
if (term.kind === 'blank') return blankNode(term.value);
|
|
651
|
+
if (term.kind === 'var') return variable(term.name || term.value);
|
|
652
|
+
if (term.kind === 'literal') {
|
|
653
|
+
return literal(
|
|
654
|
+
coerceLexicalLiteral(term.value, term.datatype),
|
|
655
|
+
term.datatype === XSD_STRING ? null : (term.datatype || null),
|
|
656
|
+
term.language || null,
|
|
657
|
+
term.langDir || null,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
if (term.kind === 'triple') {
|
|
661
|
+
return tripleTerm(
|
|
662
|
+
this.convertRdfSyntaxTerm(term.s),
|
|
663
|
+
this.convertRdfSyntaxTerm(term.p),
|
|
664
|
+
this.convertRdfSyntaxTerm(term.o),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
throw this.error(`Unsupported RDF term kind ${term.kind || typeof term}`);
|
|
668
|
+
}
|
|
669
|
+
|
|
561
670
|
parseTripleStatement(options = {}) {
|
|
562
671
|
const subjectNode = this.parseGraphNode(options);
|
|
563
672
|
const triples = [...subjectNode.triples];
|
|
@@ -757,6 +866,7 @@
|
|
|
757
866
|
} else if (this.matchWord('SET')) {
|
|
758
867
|
clauses.push(this.parseSetClause());
|
|
759
868
|
} else if (this.matchWord('BIND')) {
|
|
869
|
+
if (this.strictGrammar()) throw this.error('BIND is not part of the SHACL 1.2 Rules grammar; use SET');
|
|
760
870
|
clauses.push(this.parseBindClause());
|
|
761
871
|
} else if (this.matchWord('NOT')) {
|
|
762
872
|
this.expectValue('{');
|
|
@@ -864,8 +974,9 @@
|
|
|
864
974
|
if (colon < 0) throw this.error(`Expected IRI, prefixed name, literal, blank node, or variable; got ${value}`, token);
|
|
865
975
|
const prefix = value.slice(0, colon);
|
|
866
976
|
const local = value.slice(colon + 1);
|
|
977
|
+
if (this.strictGrammar()) validatePrefixedName(prefix, local, value, token, (message, errToken) => this.error(message, errToken));
|
|
867
978
|
if (!(prefix in this.prefixes)) throw this.error(`Unknown prefix ${prefix}:`, token);
|
|
868
|
-
return this.prefixes[prefix] + local;
|
|
979
|
+
return this.prefixes[prefix] + decodePNLocalEscapes(local);
|
|
869
980
|
}
|
|
870
981
|
|
|
871
982
|
resolveIRI(value, token = null) {
|
|
@@ -1024,9 +1135,76 @@
|
|
|
1024
1135
|
peek() { return this.tokens[this.pos]; }
|
|
1025
1136
|
peekN(n) { return this.tokens[this.pos + n] || this.tokens[this.tokens.length - 1]; }
|
|
1026
1137
|
previous() { return this.tokens[this.pos - 1]; }
|
|
1138
|
+
strictGrammar() { return !!this.options.strictGrammar; }
|
|
1027
1139
|
error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token); }
|
|
1028
1140
|
}
|
|
1029
1141
|
|
|
1142
|
+
|
|
1143
|
+
function isPnCharsBase(ch) {
|
|
1144
|
+
if (!ch) return false;
|
|
1145
|
+
return /[A-Za-z]/.test(ch) || ch.codePointAt(0) >= 0x00C0;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function isPnCharsU(ch) {
|
|
1149
|
+
return isPnCharsBase(ch) || ch === '_';
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function isPnChars(ch) {
|
|
1153
|
+
return isPnCharsU(ch) || /[0-9-]/.test(ch) || ch === '\u00B7' || /[\u0300-\u036F\u203F-\u2040]/u.test(ch);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function isValidPNPrefix(prefix) {
|
|
1157
|
+
if (prefix === '') return true;
|
|
1158
|
+
const chars = Array.from(prefix);
|
|
1159
|
+
if (!isPnCharsBase(chars[0])) return false;
|
|
1160
|
+
if (chars.length > 1 && chars.at(-1) === '.') return false;
|
|
1161
|
+
return chars.slice(1).every((ch) => isPnChars(ch) || ch === '.');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function plxLength(text, index) {
|
|
1165
|
+
const ch = text[index];
|
|
1166
|
+
if (ch === '%' && /[0-9A-Fa-f]/.test(text[index + 1] || '') && /[0-9A-Fa-f]/.test(text[index + 2] || '')) return 3;
|
|
1167
|
+
if (ch === '\\' && /[_~.!$&'()*+,;=/?#@%-]/.test(text[index + 1] || '')) return 2;
|
|
1168
|
+
return 0;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function isPNLocalStartAt(text, index) {
|
|
1172
|
+
const ch = text[index];
|
|
1173
|
+
return isPnCharsU(ch) || /[0-9:]/.test(ch || '') || plxLength(text, index) > 0;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function isPNLocalBodyAt(text, index) {
|
|
1177
|
+
const ch = text[index];
|
|
1178
|
+
return isPnChars(ch) || ch === '.' || ch === ':' || plxLength(text, index) > 0;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function isPNLocalEndAt(text, index) {
|
|
1182
|
+
const ch = text[index];
|
|
1183
|
+
return isPnChars(ch) || ch === ':' || plxLength(text, index) > 0;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function validatePNLocal(local) {
|
|
1187
|
+
if (local === '') return true;
|
|
1188
|
+
if (!isPNLocalStartAt(local, 0)) return false;
|
|
1189
|
+
let lastStart = 0;
|
|
1190
|
+
for (let i = 0; i < local.length;) {
|
|
1191
|
+
const len = plxLength(local, i) || 1;
|
|
1192
|
+
if (i > 0 && !isPNLocalBodyAt(local, i)) return false;
|
|
1193
|
+
lastStart = i;
|
|
1194
|
+
i += len;
|
|
1195
|
+
}
|
|
1196
|
+
return isPNLocalEndAt(local, lastStart);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function validatePrefixedName(prefix, local, value, token, makeError) {
|
|
1200
|
+
if (!isValidPNPrefix(prefix)) throw makeError(`Invalid prefixed name ${value}: invalid prefix`, token);
|
|
1201
|
+
if (!validatePNLocal(local)) throw makeError(`Invalid prefixed name ${value}: invalid local name`, token);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function decodePNLocalEscapes(local) {
|
|
1205
|
+
return String(local).replace(/\\([_~.!$&'()*+,;=/?#@%-])/g, '$1');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1030
1208
|
function numericLiteral(value) {
|
|
1031
1209
|
if (Number.isInteger(value)) return literal(value, XSD_INTEGER);
|
|
1032
1210
|
return literal(value, XSD_DECIMAL);
|
|
@@ -1096,7 +1274,10 @@
|
|
|
1096
1274
|
}
|
|
1097
1275
|
}
|
|
1098
1276
|
|
|
1099
|
-
function tokenize(source,
|
|
1277
|
+
function tokenize(source, filenameOrOptions = '<input>') {
|
|
1278
|
+
const options = typeof filenameOrOptions === 'object' && filenameOrOptions !== null ? filenameOrOptions : { filename: filenameOrOptions };
|
|
1279
|
+
const filename = options.filename || '<input>';
|
|
1280
|
+
const strictGrammar = !!options.strictGrammar;
|
|
1100
1281
|
const tokens = [];
|
|
1101
1282
|
let i = 0;
|
|
1102
1283
|
let line = 1;
|
|
@@ -1111,8 +1292,8 @@
|
|
|
1111
1292
|
else column += 1;
|
|
1112
1293
|
return ch;
|
|
1113
1294
|
}
|
|
1114
|
-
function token(type, value, startLine, startColumn) {
|
|
1115
|
-
tokens.push({ type, value, line: startLine, column: startColumn, filename });
|
|
1295
|
+
function token(type, value, startLine, startColumn, extra = {}) {
|
|
1296
|
+
tokens.push({ type, value, line: startLine, column: startColumn, filename, ...extra });
|
|
1116
1297
|
}
|
|
1117
1298
|
function syntax(message, startLine, startColumn) {
|
|
1118
1299
|
throw new SyntaxErrorWithLocation(message, { line: startLine, column: startColumn, filename });
|
|
@@ -1150,14 +1331,37 @@
|
|
|
1150
1331
|
const length = esc === 'u' ? 4 : 8;
|
|
1151
1332
|
let hex = '';
|
|
1152
1333
|
for (let j = 0; j < length; j += 1) {
|
|
1153
|
-
if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid
|
|
1334
|
+
if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
|
|
1154
1335
|
hex += advance();
|
|
1155
1336
|
}
|
|
1156
|
-
|
|
1337
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
1338
|
+
try { return String.fromCodePoint(codePoint); }
|
|
1339
|
+
catch { syntax(`Invalid \\${esc} escape`, startLine, startColumn); }
|
|
1157
1340
|
}
|
|
1341
|
+
if (strictGrammar && !Object.hasOwn(escapeMap, esc)) syntax(`Invalid escape \\${esc}`, startLine, startColumn);
|
|
1158
1342
|
return escapeValue(esc);
|
|
1159
1343
|
}
|
|
1160
1344
|
|
|
1345
|
+
function readIriChar(startLine, startColumn) {
|
|
1346
|
+
if (current() === '\\') {
|
|
1347
|
+
advance();
|
|
1348
|
+
const esc = advance();
|
|
1349
|
+
if (esc !== 'u' && esc !== 'U') syntax(`Invalid IRI escape \\${esc}`, startLine, startColumn);
|
|
1350
|
+
const length = esc === 'u' ? 4 : 8;
|
|
1351
|
+
let hex = '';
|
|
1352
|
+
for (let j = 0; j < length; j += 1) {
|
|
1353
|
+
if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
|
|
1354
|
+
hex += advance();
|
|
1355
|
+
}
|
|
1356
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
1357
|
+
try { return String.fromCodePoint(codePoint); }
|
|
1358
|
+
catch { syntax(`Invalid \\${esc} escape`, startLine, startColumn); }
|
|
1359
|
+
}
|
|
1360
|
+
const c = current();
|
|
1361
|
+
if (strictGrammar && (/[\u0000-\u0020]/.test(c) || /[<>"{}|^`]/.test(c))) syntax(`Invalid character in IRI reference ${JSON.stringify(c)}`, startLine, startColumn);
|
|
1362
|
+
return advance();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1161
1365
|
while (i < source.length) {
|
|
1162
1366
|
const ch = current();
|
|
1163
1367
|
if (/\s/.test(ch)) { advance(); continue; }
|
|
@@ -1203,7 +1407,7 @@
|
|
|
1203
1407
|
if (ch === '<' && looksLikeIRI(source, i)) {
|
|
1204
1408
|
let value = '';
|
|
1205
1409
|
advance();
|
|
1206
|
-
while (i < source.length && current() !== '>') value +=
|
|
1410
|
+
while (i < source.length && current() !== '>') value += readIriChar(startLine, startColumn);
|
|
1207
1411
|
if (current() !== '>') syntax('Unterminated IRI', startLine, startColumn);
|
|
1208
1412
|
advance();
|
|
1209
1413
|
token('iri', value, startLine, startColumn);
|
|
@@ -1223,7 +1427,7 @@
|
|
|
1223
1427
|
}
|
|
1224
1428
|
if (!startsWith(quote.repeat(3))) syntax('Unterminated long string literal', startLine, startColumn);
|
|
1225
1429
|
advance(); advance(); advance();
|
|
1226
|
-
token('string', value, startLine, startColumn);
|
|
1430
|
+
token('string', value, startLine, startColumn, { long: true, quote });
|
|
1227
1431
|
continue;
|
|
1228
1432
|
}
|
|
1229
1433
|
|
|
@@ -1241,7 +1445,7 @@
|
|
|
1241
1445
|
}
|
|
1242
1446
|
if (current() !== quote) syntax('Unterminated string literal', startLine, startColumn);
|
|
1243
1447
|
advance();
|
|
1244
|
-
token('string', value, startLine, startColumn);
|
|
1448
|
+
token('string', value, startLine, startColumn, { long: false, quote });
|
|
1245
1449
|
continue;
|
|
1246
1450
|
}
|
|
1247
1451
|
|
|
@@ -1287,7 +1491,16 @@
|
|
|
1287
1491
|
let value = '';
|
|
1288
1492
|
while (i < source.length) {
|
|
1289
1493
|
const c = current();
|
|
1290
|
-
if (
|
|
1494
|
+
if (c === '\\' && peek() !== undefined) {
|
|
1495
|
+
value += advance();
|
|
1496
|
+
value += advance();
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (/\s/.test(c) || '{}()[],;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;
|
|
1500
|
+
if (c === '.') {
|
|
1501
|
+
const n = peek();
|
|
1502
|
+
if (n === undefined || /\s/.test(n) || '{}()[],;|'.includes(n) || '=<>+-*/!^~'.includes(n)) break;
|
|
1503
|
+
}
|
|
1291
1504
|
if (c === '#') break;
|
|
1292
1505
|
value += advance();
|
|
1293
1506
|
}
|
|
@@ -1320,558 +1533,621 @@
|
|
|
1320
1533
|
return false;
|
|
1321
1534
|
}
|
|
1322
1535
|
|
|
1536
|
+
const escapeMap = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', '"': '"', "'": "'", '\\': '\\' };
|
|
1537
|
+
|
|
1323
1538
|
function escapeValue(esc) {
|
|
1324
|
-
|
|
1325
|
-
return map[esc] ?? esc;
|
|
1539
|
+
return escapeMap[esc] ?? esc;
|
|
1326
1540
|
}
|
|
1327
1541
|
|
|
1328
1542
|
module.exports = { tokenize, SyntaxErrorWithLocation };
|
|
1329
1543
|
|
|
1330
1544
|
},
|
|
1331
|
-
"src/
|
|
1545
|
+
"src/rdfSyntax.js": function (require, module, exports) {
|
|
1332
1546
|
'use strict';
|
|
1333
1547
|
|
|
1548
|
+
const { tokenize, SyntaxErrorWithLocation } = require('./tokenizer.js');
|
|
1549
|
+
const { ruleNeedsRunOnce } = require('./assignments.js');
|
|
1334
1550
|
const {
|
|
1335
1551
|
iri,
|
|
1552
|
+
variable,
|
|
1336
1553
|
blankNode,
|
|
1337
1554
|
literal,
|
|
1338
1555
|
tripleTerm,
|
|
1556
|
+
termKey,
|
|
1339
1557
|
termEquals,
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
isLiteral,
|
|
1347
|
-
isTripleTerm,
|
|
1348
|
-
valueToTerm,
|
|
1349
|
-
inferDatatype,
|
|
1350
|
-
XSD_STRING,
|
|
1351
|
-
RDF_NS,
|
|
1558
|
+
formatTerm,
|
|
1559
|
+
RDF_TYPE,
|
|
1560
|
+
RDF_FIRST,
|
|
1561
|
+
RDF_REST,
|
|
1562
|
+
RDF_NIL,
|
|
1563
|
+
XSD_BOOLEAN,
|
|
1352
1564
|
XSD_INTEGER,
|
|
1353
1565
|
XSD_DECIMAL,
|
|
1354
1566
|
XSD_DOUBLE,
|
|
1355
1567
|
} = require('./term.js');
|
|
1356
1568
|
|
|
1357
|
-
const
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1569
|
+
const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
1570
|
+
const SRL_NS = 'http://www.w3.org/ns/shacl-rules#';
|
|
1571
|
+
const SHNEX_NS = 'http://www.w3.org/ns/shacl-node-expr#';
|
|
1572
|
+
const SPARQL_NS = 'http://www.w3.org/ns/sparql#';
|
|
1573
|
+
const OWL_IMPORTS = 'http://www.w3.org/2002/07/owl#imports';
|
|
1574
|
+
const SRL_RULE_SET = `${SRL_NS}RuleSet`;
|
|
1575
|
+
const SRL_RULE = `${SRL_NS}Rule`;
|
|
1576
|
+
const SRL_DATA = `${SRL_NS}data`;
|
|
1577
|
+
const SRL_RULES = `${SRL_NS}rules`;
|
|
1578
|
+
const SRL_BODY = `${SRL_NS}body`;
|
|
1579
|
+
const SRL_HEAD = `${SRL_NS}head`;
|
|
1580
|
+
const SRL_SUBJECT = `${SRL_NS}subject`;
|
|
1581
|
+
const SRL_PREDICATE = `${SRL_NS}predicate`;
|
|
1582
|
+
const SRL_OBJECT = `${SRL_NS}object`;
|
|
1583
|
+
const SRL_FILTER = `${SRL_NS}filter`;
|
|
1584
|
+
const SRL_EXPR = `${SRL_NS}expr`;
|
|
1585
|
+
const SRL_ASSIGN = `${SRL_NS}assign`;
|
|
1586
|
+
const SRL_ASSIGN_VAR = `${SRL_NS}assignVar`;
|
|
1587
|
+
const SRL_ASSIGN_VALUE = `${SRL_NS}assignValue`;
|
|
1588
|
+
const SRL_NOT = `${SRL_NS}not`;
|
|
1589
|
+
const SRL_VAR_NAME = `${SRL_NS}varName`;
|
|
1590
|
+
const SHNEX_VAR = `${SHNEX_NS}var`;
|
|
1362
1591
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
STRLEN: { min: 1, max: 1 },
|
|
1383
|
-
REPLACE: { min: 3, max: 4 },
|
|
1384
|
-
UCASE: { min: 1, max: 1 },
|
|
1385
|
-
LCASE: { min: 1, max: 1 },
|
|
1386
|
-
ENCODE_FOR_URI: { min: 1, max: 1 },
|
|
1387
|
-
CONTAINS: { min: 2, max: 2 },
|
|
1388
|
-
STRSTARTS: { min: 2, max: 2 },
|
|
1389
|
-
STRENDS: { min: 2, max: 2 },
|
|
1390
|
-
STRBEFORE: { min: 2, max: 2 },
|
|
1391
|
-
STRAFTER: { min: 2, max: 2 },
|
|
1392
|
-
YEAR: { min: 1, max: 1 },
|
|
1393
|
-
MONTH: { min: 1, max: 1 },
|
|
1394
|
-
DAY: { min: 1, max: 1 },
|
|
1395
|
-
HOURS: { min: 1, max: 1 },
|
|
1396
|
-
MINUTES: { min: 1, max: 1 },
|
|
1397
|
-
SECONDS: { min: 1, max: 1 },
|
|
1398
|
-
TIMEZONE: { min: 1, max: 1 },
|
|
1399
|
-
TZ: { min: 1, max: 1 },
|
|
1400
|
-
NOW: { min: 0, max: 0 },
|
|
1401
|
-
UUID: { min: 0, max: 0 },
|
|
1402
|
-
STRUUID: { min: 0, max: 0 },
|
|
1403
|
-
IF: { min: 3, max: 3, lazy: true },
|
|
1404
|
-
STRLANG: { min: 2, max: 2 },
|
|
1405
|
-
STRLANGDIR: { min: 3, max: 3 },
|
|
1406
|
-
STRDT: { min: 2, max: 2 },
|
|
1407
|
-
sameTerm: { min: 2, max: 2 },
|
|
1408
|
-
isIRI: { min: 1, max: 1 },
|
|
1409
|
-
isURI: { min: 1, max: 1 },
|
|
1410
|
-
isBLANK: { min: 1, max: 1 },
|
|
1411
|
-
isLITERAL: { min: 1, max: 1 },
|
|
1412
|
-
isNUMERIC: { min: 1, max: 1 },
|
|
1413
|
-
hasLANG: { min: 1, max: 1 },
|
|
1414
|
-
hasLANGDIR: { min: 1, max: 1 },
|
|
1415
|
-
REGEX: { min: 2, max: 3 },
|
|
1416
|
-
isTRIPLE: { min: 1, max: 1 },
|
|
1417
|
-
TRIPLE: { min: 3, max: 3 },
|
|
1418
|
-
SUBJECT: { min: 1, max: 1 },
|
|
1419
|
-
PREDICATE: { min: 1, max: 1 },
|
|
1420
|
-
OBJECT: { min: 1, max: 1 },
|
|
1421
|
-
});
|
|
1592
|
+
class TurtleParser {
|
|
1593
|
+
constructor(source, options = {}) {
|
|
1594
|
+
this.tokens = Array.isArray(source) ? source : tokenize(source, options.filename || '<rdf>');
|
|
1595
|
+
this.pos = 0;
|
|
1596
|
+
this.baseIRI = options.baseIRI || null;
|
|
1597
|
+
this.bnodeCounter = 0;
|
|
1598
|
+
this.prefixes = {
|
|
1599
|
+
'': 'http://example/',
|
|
1600
|
+
rdf: RDF_NS,
|
|
1601
|
+
srl: SRL_NS,
|
|
1602
|
+
shnex: SHNEX_NS,
|
|
1603
|
+
sparql: SPARQL_NS,
|
|
1604
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
1605
|
+
owl: 'http://www.w3.org/2002/07/owl#',
|
|
1606
|
+
...options.prefixes,
|
|
1607
|
+
};
|
|
1608
|
+
this.triples = [];
|
|
1609
|
+
this.imports = [];
|
|
1610
|
+
}
|
|
1422
1611
|
|
|
1423
|
-
|
|
1612
|
+
parseDocument() {
|
|
1613
|
+
while (!this.is('eof')) {
|
|
1614
|
+
if (this.matchDirective('PREFIX', '@prefix')) this.parsePrefix(this.previous().value.startsWith('@'));
|
|
1615
|
+
else if (this.matchDirective('BASE', '@base')) this.parseBase(this.previous().value.startsWith('@'));
|
|
1616
|
+
else this.parseTriplesStatement();
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
baseIRI: this.baseIRI,
|
|
1620
|
+
prefixes: { ...this.prefixes },
|
|
1621
|
+
triples: this.triples,
|
|
1622
|
+
imports: this.imports.slice(),
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1424
1625
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1626
|
+
parsePrefix(atStyle = false) {
|
|
1627
|
+
const nameToken = this.advance();
|
|
1628
|
+
if (nameToken.type !== 'word' || !nameToken.value.endsWith(':')) throw this.error('Expected prefix label ending in :', nameToken);
|
|
1629
|
+
const iriToken = this.expectType('iri');
|
|
1630
|
+
this.prefixes[nameToken.value.slice(0, -1)] = this.resolveIRI(iriToken.value, iriToken);
|
|
1631
|
+
if (atStyle) this.expectValue('.');
|
|
1632
|
+
}
|
|
1428
1633
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1634
|
+
parseBase(atStyle = false) {
|
|
1635
|
+
const iriToken = this.expectType('iri');
|
|
1636
|
+
this.baseIRI = this.resolveIRI(iriToken.value, iriToken);
|
|
1637
|
+
if (atStyle) this.expectValue('.');
|
|
1638
|
+
}
|
|
1432
1639
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1640
|
+
parseTriplesStatement() {
|
|
1641
|
+
const subjectNode = this.parseNode();
|
|
1642
|
+
this.triples.push(...subjectNode.triples);
|
|
1643
|
+
this.triples.push(...this.parsePredicateObjectList(subjectNode.term, ['.']));
|
|
1644
|
+
this.expectValue('.');
|
|
1645
|
+
}
|
|
1436
1646
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (expr.op === '+') return unaryPlusNumeric(termToPrimitive(valueToTermIfNeeded(value)));
|
|
1452
|
-
throw new Error(`Unsupported unary operator ${expr.op}`);
|
|
1453
|
-
}
|
|
1454
|
-
case 'binary': {
|
|
1455
|
-
const left = evalExpression(expr.left, binding, options);
|
|
1456
|
-
if (expr.op === '&&') return booleanValue(left) && booleanValue(evalExpression(expr.right, binding, options));
|
|
1457
|
-
if (expr.op === '||') return booleanValue(left) || booleanValue(evalExpression(expr.right, binding, options));
|
|
1458
|
-
const right = evalExpression(expr.right, binding, options);
|
|
1459
|
-
return evalBinary(expr.op, left, right);
|
|
1647
|
+
parsePredicateObjectList(subject, terminators = [']']) {
|
|
1648
|
+
const triples = [];
|
|
1649
|
+
while (!terminators.some((value) => this.checkValue(value))) {
|
|
1650
|
+
const predicate = this.parseVerb();
|
|
1651
|
+
do {
|
|
1652
|
+
const objectNode = this.parseNode();
|
|
1653
|
+
triples.push(...objectNode.triples);
|
|
1654
|
+
triples.push({ s: subject, p: predicate, o: objectNode.term });
|
|
1655
|
+
if (predicate.type === 'iri' && predicate.value === OWL_IMPORTS && objectNode.term.type === 'iri') this.imports.push(objectNode.term.value);
|
|
1656
|
+
} while (this.matchValue(','));
|
|
1657
|
+
if (this.matchValue(';')) {
|
|
1658
|
+
while (this.matchValue(';')) { /* tolerate repeated semicolons */ }
|
|
1659
|
+
if (terminators.some((value) => this.checkValue(value))) break;
|
|
1660
|
+
} else break;
|
|
1460
1661
|
}
|
|
1461
|
-
|
|
1462
|
-
return evalCallExpression(expr, binding, options);
|
|
1463
|
-
default:
|
|
1464
|
-
throw new Error(`Unsupported expression type ${expr.type}`);
|
|
1662
|
+
return triples;
|
|
1465
1663
|
}
|
|
1466
|
-
}
|
|
1467
1664
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
const condition = evalExpression(expr.args[0], binding, options);
|
|
1473
|
-
return evalExpression(booleanValue(condition) ? expr.args[1] : expr.args[2], binding, options);
|
|
1665
|
+
parseNode() {
|
|
1666
|
+
if (this.checkValue('[')) return this.parseBlankNodePropertyList();
|
|
1667
|
+
if (this.checkValue('(')) return this.parseCollection();
|
|
1668
|
+
return { term: this.parseTerm(), triples: [] };
|
|
1474
1669
|
}
|
|
1475
|
-
return callBuiltin(expr.name, expr.args.map((arg) => evalExpression(arg, binding, options)), binding, options);
|
|
1476
|
-
}
|
|
1477
1670
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
return
|
|
1671
|
+
parseBlankNodePropertyList() {
|
|
1672
|
+
this.expectValue('[');
|
|
1673
|
+
const node = this.freshBlankNode();
|
|
1674
|
+
if (this.matchValue(']')) return { term: node, triples: [] };
|
|
1675
|
+
const triples = this.parsePredicateObjectList(node, [']']);
|
|
1676
|
+
this.expectValue(']');
|
|
1677
|
+
return { term: node, triples };
|
|
1485
1678
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
|
|
1679
|
+
|
|
1680
|
+
parseCollection() {
|
|
1681
|
+
this.expectValue('(');
|
|
1682
|
+
if (this.matchValue(')')) return { term: iri(RDF_NIL), triples: [] };
|
|
1683
|
+
const items = [];
|
|
1684
|
+
while (!this.checkValue(')')) items.push(this.parseNode());
|
|
1685
|
+
this.expectValue(')');
|
|
1686
|
+
const triples = [];
|
|
1687
|
+
for (const item of items) triples.push(...item.triples);
|
|
1688
|
+
const cells = items.map(() => this.freshBlankNode());
|
|
1689
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
1690
|
+
triples.push({ s: cells[i], p: iri(RDF_FIRST), o: items[i].term });
|
|
1691
|
+
triples.push({ s: cells[i], p: iri(RDF_REST), o: i + 1 < cells.length ? cells[i + 1] : iri(RDF_NIL) });
|
|
1692
|
+
}
|
|
1693
|
+
return { term: cells[0], triples };
|
|
1492
1694
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1695
|
+
|
|
1696
|
+
parseVerb() {
|
|
1697
|
+
if (this.checkType('word') && this.peek().value === 'a') { this.advance(); return iri(RDF_TYPE); }
|
|
1698
|
+
const term = this.parseTerm();
|
|
1699
|
+
if (term.type !== 'iri') throw this.error('Expected IRI as Turtle predicate');
|
|
1700
|
+
return term;
|
|
1498
1701
|
}
|
|
1499
|
-
if (op === '-') return subtractNumeric(lp, rp);
|
|
1500
|
-
if (op === '*') return multiplyNumeric(lp, rp);
|
|
1501
|
-
if (op === '/') return Number(lp) / Number(rp);
|
|
1502
|
-
throw new Error(`Unsupported binary operator ${op}`);
|
|
1503
|
-
}
|
|
1504
1702
|
|
|
1703
|
+
parseTerm() {
|
|
1704
|
+
const token = this.advance();
|
|
1705
|
+
if (token.type === 'operator' && (token.value === '+' || token.value === '-') && this.peek().type === 'number') {
|
|
1706
|
+
const numberToken = this.advance();
|
|
1707
|
+
return numericLiteral(token.value === '-' ? -numberToken.value : numberToken.value);
|
|
1708
|
+
}
|
|
1709
|
+
if (token.type === 'iri') return iri(this.resolveIRI(token.value, token));
|
|
1710
|
+
if (token.type === 'string') return this.parseLiteralAfterToken(token);
|
|
1711
|
+
if (token.type === 'number') return numericLiteral(token.value);
|
|
1712
|
+
if (token.value === '<<(') return this.parseTripleTermAfterOpen();
|
|
1713
|
+
if (token.type === 'word') {
|
|
1714
|
+
const word = token.value.includes(':') || token.value.startsWith('_:') ? this.consumeHyphenatedWord(token.value) : token.value;
|
|
1715
|
+
if (word === 'a') return iri(RDF_TYPE);
|
|
1716
|
+
if (word === 'true') return literal(true, XSD_BOOLEAN);
|
|
1717
|
+
if (word === 'false') return literal(false, XSD_BOOLEAN);
|
|
1718
|
+
if (word.startsWith('_:')) return blankNode(word.slice(2));
|
|
1719
|
+
if (word.includes(':')) return iri(this.expandPrefixedName(word, token));
|
|
1720
|
+
}
|
|
1721
|
+
throw this.error(`Expected RDF term, got ${token.value}`, token);
|
|
1722
|
+
}
|
|
1505
1723
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1724
|
+
parseTripleTermAfterOpen() {
|
|
1725
|
+
const s = this.parseTerm();
|
|
1726
|
+
const p = this.parseVerb();
|
|
1727
|
+
const o = this.parseTerm();
|
|
1728
|
+
this.expectValue(')>>');
|
|
1729
|
+
return tripleTerm(s, p, o);
|
|
1730
|
+
}
|
|
1509
1731
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1732
|
+
parseLiteralAfterToken(token) {
|
|
1733
|
+
if (this.matchValue('^^')) {
|
|
1734
|
+
const datatype = this.parseDatatypeIRI();
|
|
1735
|
+
return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
|
|
1736
|
+
}
|
|
1737
|
+
if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
|
|
1738
|
+
const tag = this.advance().value.slice(1).toLowerCase();
|
|
1739
|
+
const [lang, langDir = null] = tag.split('--');
|
|
1740
|
+
return literal(token.value, null, lang, langDir);
|
|
1741
|
+
}
|
|
1742
|
+
return literal(token.value);
|
|
1743
|
+
}
|
|
1513
1744
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1745
|
+
parseDatatypeIRI() {
|
|
1746
|
+
const token = this.advance();
|
|
1747
|
+
if (token.type === 'iri') return this.resolveIRI(token.value, token);
|
|
1748
|
+
if (token.type === 'word' && token.value.includes(':')) return this.expandPrefixedName(token.value, token);
|
|
1749
|
+
throw this.error(`Expected datatype IRI, got ${token.value}`, token);
|
|
1750
|
+
}
|
|
1519
1751
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1752
|
+
freshBlankNode() {
|
|
1753
|
+
this.bnodeCounter += 1;
|
|
1754
|
+
return blankNode(`rdf${this.bnodeCounter}`);
|
|
1755
|
+
}
|
|
1524
1756
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1757
|
+
consumeHyphenatedWord(value) {
|
|
1758
|
+
let out = value;
|
|
1759
|
+
while (this.checkValue('-') && (this.peekN(1).type === 'word' || this.peekN(1).type === 'number')) {
|
|
1760
|
+
this.advance();
|
|
1761
|
+
out += `-${this.advance().value}`;
|
|
1762
|
+
}
|
|
1763
|
+
return out;
|
|
1531
1764
|
}
|
|
1532
|
-
return Number(left) + Number(right);
|
|
1533
|
-
}
|
|
1534
1765
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
if (
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1766
|
+
expandPrefixedName(value, token) {
|
|
1767
|
+
const colon = value.indexOf(':');
|
|
1768
|
+
if (colon < 0) throw this.error(`Expected prefixed name, got ${value}`, token);
|
|
1769
|
+
const prefix = value.slice(0, colon);
|
|
1770
|
+
const local = value.slice(colon + 1);
|
|
1771
|
+
if (!Object.hasOwn(this.prefixes, prefix)) throw this.error(`Unknown prefix ${prefix}:`, token);
|
|
1772
|
+
return this.prefixes[prefix] + local;
|
|
1541
1773
|
}
|
|
1542
|
-
return Number(left) - Number(right);
|
|
1543
|
-
}
|
|
1544
1774
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
const result = left * right;
|
|
1549
|
-
if (Number.isSafeInteger(result)) return result;
|
|
1550
|
-
return toBigIntInteger(left) * toBigIntInteger(right);
|
|
1775
|
+
resolveIRI(value) {
|
|
1776
|
+
if (!this.baseIRI || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return value;
|
|
1777
|
+
try { return new URL(value, this.baseIRI).href; } catch (_) { return value; }
|
|
1551
1778
|
}
|
|
1552
|
-
return Number(left) * Number(right);
|
|
1553
|
-
}
|
|
1554
1779
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1780
|
+
matchDirective(...names) {
|
|
1781
|
+
if (this.checkType('word')) {
|
|
1782
|
+
const value = this.peek().value;
|
|
1783
|
+
if (names.some((name) => value.toUpperCase() === name.toUpperCase())) { this.advance(); return true; }
|
|
1784
|
+
}
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1559
1787
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
return
|
|
1788
|
+
previous() { return this.tokens[this.pos - 1]; }
|
|
1789
|
+
peek() { return this.tokens[this.pos]; }
|
|
1790
|
+
peekN(n) { return this.tokens[this.pos + n]; }
|
|
1791
|
+
is(type) { return this.peek().type === type; }
|
|
1792
|
+
checkType(type) { return this.peek().type === type; }
|
|
1793
|
+
checkValue(value) { return this.peek().value === value; }
|
|
1794
|
+
matchValue(value) { if (this.checkValue(value)) { this.advance(); return true; } return false; }
|
|
1795
|
+
advance() { return this.tokens[this.pos++]; }
|
|
1796
|
+
expectType(type) { const token = this.advance(); if (token.type !== type) throw this.error(`Expected ${type}, got ${token.value}`, token); return token; }
|
|
1797
|
+
expectValue(value) { const token = this.advance(); if (token.value !== value) throw this.error(`Expected ${value}, got ${token.value}`, token); return token; }
|
|
1798
|
+
error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token); }
|
|
1563
1799
|
}
|
|
1564
1800
|
|
|
1565
|
-
function
|
|
1566
|
-
return
|
|
1801
|
+
function parseRdfDocument(source, options = {}) {
|
|
1802
|
+
return new TurtleParser(source, options).parseDocument();
|
|
1567
1803
|
}
|
|
1568
1804
|
|
|
1569
|
-
function
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
const rp = right && right.type ? termToPrimitive(right) : right;
|
|
1573
|
-
return lp === rp;
|
|
1805
|
+
function parseRdfSyntax(source, options = {}) {
|
|
1806
|
+
const document = parseRdfDocument(source, options);
|
|
1807
|
+
return rdfDocumentToProgram(document, options);
|
|
1574
1808
|
}
|
|
1575
1809
|
|
|
1576
|
-
function
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
if (localName(name).toLowerCase() === 'sudoku') {
|
|
1581
|
-
if (args.length !== 1) throw new Error(`SUDOKU expects 1 argument, got ${args.length}`);
|
|
1582
|
-
return solveSudoku(termToString(args[0]));
|
|
1583
|
-
}
|
|
1810
|
+
function rdfDocumentToProgram(document, options = {}) {
|
|
1811
|
+
const graph = new RdfGraph(document.triples, document.prefixes);
|
|
1812
|
+
const ruleSetNodes = chooseRuleSets(graph, options.ruleSet);
|
|
1813
|
+
if (ruleSetNodes.length === 0) throw new Error('No srl:RuleSet found in RDF Rules syntax input');
|
|
1584
1814
|
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1815
|
+
const program = {
|
|
1816
|
+
baseIRI: document.baseIRI || null,
|
|
1817
|
+
version: null,
|
|
1818
|
+
imports: options.rdfImportsAsImports ? document.imports.slice() : [],
|
|
1819
|
+
prefixes: { ...document.prefixes },
|
|
1820
|
+
data: [],
|
|
1821
|
+
rules: [],
|
|
1822
|
+
rdfSyntax: true,
|
|
1823
|
+
options: { shacl12Conformance: !!options.shacl12Conformance },
|
|
1824
|
+
ruleSets: ruleSetNodes.map((term) => formatTerm(term, document.prefixes)),
|
|
1825
|
+
};
|
|
1589
1826
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
if (key === 'concat') return args.map(termToString).join('');
|
|
1594
|
-
if (key === 'lcase') return termToString(args[0]).toLowerCase();
|
|
1595
|
-
if (key === 'ucase') return termToString(args[0]).toUpperCase();
|
|
1596
|
-
if (key === 'contains') return termToString(args[0]).includes(termToString(args[1]));
|
|
1597
|
-
if (key === 'strstarts') return termToString(args[0]).startsWith(termToString(args[1]));
|
|
1598
|
-
if (key === 'strends') return termToString(args[0]).endsWith(termToString(args[1]));
|
|
1599
|
-
if (key === 'strbefore') {
|
|
1600
|
-
const s = termToString(args[0]);
|
|
1601
|
-
const needle = termToString(args[1]);
|
|
1602
|
-
const index = s.indexOf(needle);
|
|
1603
|
-
return index < 0 ? '' : s.slice(0, index);
|
|
1604
|
-
}
|
|
1605
|
-
if (key === 'strafter') {
|
|
1606
|
-
const s = termToString(args[0]);
|
|
1607
|
-
const needle = termToString(args[1]);
|
|
1608
|
-
const index = s.indexOf(needle);
|
|
1609
|
-
return index < 0 ? '' : s.slice(index + needle.length);
|
|
1610
|
-
}
|
|
1611
|
-
if (key === 'encode_for_uri') return encodeURIComponent(termToString(args[0]));
|
|
1612
|
-
if (key === 'regex') return regex(args);
|
|
1613
|
-
if (key === 'replace') return replace(args);
|
|
1614
|
-
if (key === 'substr') return substr(args);
|
|
1615
|
-
if (key === 'sameterm') return termishEquals(args[0], args[1]);
|
|
1616
|
-
if (key === 'isiri' || key === 'isuri') return isIRI(args[0]);
|
|
1617
|
-
if (key === 'isblank') return isBlank(args[0]);
|
|
1618
|
-
if (key === 'isliteral') return isLiteral(args[0]);
|
|
1619
|
-
if (key === 'istriple') return isTripleTerm(args[0]);
|
|
1620
|
-
if (key === 'isnumeric') return isNumericValue(args[0]);
|
|
1621
|
-
if (key === 'datatype') return datatypeOf(args[0]);
|
|
1622
|
-
if (key === 'lang') return args[0] && args[0].type === 'literal' ? (args[0].lang || '') : '';
|
|
1623
|
-
if (key === 'langmatches') return langMatches(termToString(args[0]), termToString(args[1]));
|
|
1624
|
-
if (key === 'haslang') return !!(args[0] && args[0].type === 'literal' && args[0].lang);
|
|
1625
|
-
if (key === 'langdir') return args[0] && args[0].type === 'literal' ? (args[0].langDir || '') : '';
|
|
1626
|
-
if (key === 'haslangdir') return !!(args[0] && args[0].type === 'literal' && args[0].langDir);
|
|
1627
|
-
if (key === 'strlen') return termToString(args[0]).length;
|
|
1628
|
-
if (key === 'abs') return Math.abs(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
1629
|
-
if (key === 'floor') return Math.floor(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
1630
|
-
if (key === 'ceil') return Math.ceil(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
1631
|
-
if (key === 'round') return Math.round(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
1632
|
-
if (key === 'if') return booleanValue(args[0]) ? args[1] : args[2];
|
|
1633
|
-
if (key === 'strdt') return literal(termToString(args[0]), termToString(args[1]));
|
|
1634
|
-
if (key === 'strlang') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase());
|
|
1635
|
-
if (key === 'strlangdir') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase(), termToString(args[2]).toLowerCase());
|
|
1636
|
-
if (key === 'triple') return tripleTerm(valueToTermIfNeeded(args[0]), valueToTermIfNeeded(args[1]), valueToTermIfNeeded(args[2]));
|
|
1637
|
-
if (key === 'subject') return isTripleTerm(args[0]) ? args[0].s : null;
|
|
1638
|
-
if (key === 'predicate') return isTripleTerm(args[0]) ? args[0].p : null;
|
|
1639
|
-
if (key === 'object') return isTripleTerm(args[0]) ? args[0].o : null;
|
|
1640
|
-
if (key === 'year') return datePart(args[0], 'year');
|
|
1641
|
-
if (key === 'month') return datePart(args[0], 'month');
|
|
1642
|
-
if (key === 'day') return datePart(args[0], 'day');
|
|
1643
|
-
if (key === 'hours') return datePart(args[0], 'hours');
|
|
1644
|
-
if (key === 'minutes') return datePart(args[0], 'minutes');
|
|
1645
|
-
if (key === 'seconds') return datePart(args[0], 'seconds');
|
|
1646
|
-
if (key === 'timezone') return timezoneDuration(args[0]);
|
|
1647
|
-
if (key === 'tz') return timezoneLexical(args[0]);
|
|
1648
|
-
if (key === 'now') return literal((options.now || new Date()).toISOString(), XSD_DATETIME);
|
|
1649
|
-
if (key === 'uuid') return iri(`urn:uuid:${freshUuid(options)}`);
|
|
1650
|
-
if (key === 'struuid') return freshUuid(options);
|
|
1651
|
-
throw new Error(`Unimplemented builtin ${name}`);
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
function localName(name) {
|
|
1656
|
-
const text = String(name || '');
|
|
1657
|
-
const hash = text.lastIndexOf('#');
|
|
1658
|
-
const slash = text.lastIndexOf('/');
|
|
1659
|
-
const colon = text.lastIndexOf(':');
|
|
1660
|
-
const index = Math.max(hash, slash, colon);
|
|
1661
|
-
return index >= 0 ? text.slice(index + 1) : text;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
function solveSudoku(puzzle) {
|
|
1665
|
-
const text = String(puzzle || '').trim();
|
|
1666
|
-
if (!/^[0-9.]{81}$/.test(text)) throw new Error('SUDOKU expects an 81-character puzzle string containing digits or dots');
|
|
1667
|
-
const cells = Array.from(text, (ch) => (ch === '.' ? 0 : Number(ch)));
|
|
1668
|
-
const peers = sudokuPeers();
|
|
1669
|
-
|
|
1670
|
-
for (let i = 0; i < 81; i += 1) {
|
|
1671
|
-
const value = cells[i];
|
|
1672
|
-
if (value === 0) continue;
|
|
1673
|
-
for (const peer of peers[i]) {
|
|
1674
|
-
if (cells[peer] === value) throw new Error('SUDOKU puzzle has conflicting givens');
|
|
1827
|
+
for (const ruleSet of ruleSetNodes) {
|
|
1828
|
+
for (const dataList of graph.objects(ruleSet, SRL_DATA)) {
|
|
1829
|
+
for (const item of graph.list(dataList)) program.data.push(toDataTriple(item, graph));
|
|
1675
1830
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const solved = solveSudokuCells(cells, peers);
|
|
1679
|
-
if (!solved) return '';
|
|
1680
|
-
return solved.join('');
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
function solveSudokuCells(cells, peers) {
|
|
1684
|
-
let bestIndex = -1;
|
|
1685
|
-
let bestCandidates = null;
|
|
1686
|
-
|
|
1687
|
-
for (let i = 0; i < 81; i += 1) {
|
|
1688
|
-
if (cells[i] !== 0) continue;
|
|
1689
|
-
const candidates = sudokuCandidates(cells, peers[i]);
|
|
1690
|
-
if (candidates.length === 0) return null;
|
|
1691
|
-
if (!bestCandidates || candidates.length < bestCandidates.length) {
|
|
1692
|
-
bestIndex = i;
|
|
1693
|
-
bestCandidates = candidates;
|
|
1694
|
-
if (candidates.length === 1) break;
|
|
1831
|
+
for (const rulesList of graph.objects(ruleSet, SRL_RULES)) {
|
|
1832
|
+
for (const ruleNode of graph.list(rulesList)) program.rules.push(toRule(ruleNode, graph, options));
|
|
1695
1833
|
}
|
|
1696
1834
|
}
|
|
1835
|
+
return program;
|
|
1836
|
+
}
|
|
1697
1837
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
next[bestIndex] = value;
|
|
1703
|
-
const solved = solveSudokuCells(next, peers);
|
|
1704
|
-
if (solved) return solved;
|
|
1838
|
+
function chooseRuleSets(graph, selected) {
|
|
1839
|
+
if (selected) {
|
|
1840
|
+
const term = graph.parseReference(selected);
|
|
1841
|
+
return [term];
|
|
1705
1842
|
}
|
|
1706
|
-
|
|
1843
|
+
const typed = graph.subjects(RDF_TYPE, iri(SRL_RULE_SET));
|
|
1844
|
+
if (typed.length > 0) return uniqueTerms(typed);
|
|
1845
|
+
const byData = graph.subjectsWithPredicate(SRL_DATA);
|
|
1846
|
+
const byRules = graph.subjectsWithPredicate(SRL_RULES);
|
|
1847
|
+
return uniqueTerms([...byData, ...byRules]).filter((term) => graph.objects(term, SRL_RULES).length > 0 || graph.objects(term, SRL_DATA).length > 0);
|
|
1707
1848
|
}
|
|
1708
1849
|
|
|
1709
|
-
function
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
return
|
|
1850
|
+
function toDataTriple(item, graph) {
|
|
1851
|
+
if (item.type === 'triple') return { s: item.s, p: item.p, o: item.o };
|
|
1852
|
+
const triple = toTripleLike(item, graph);
|
|
1853
|
+
if ([triple.s, triple.p, triple.o].some((term) => term.type === 'var')) throw new Error('RDF Rules srl:data may not contain variables');
|
|
1854
|
+
if (triple.p.type !== 'iri') throw new Error('RDF Rules data triple predicate must be an IRI');
|
|
1855
|
+
return triple;
|
|
1715
1856
|
}
|
|
1716
1857
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const boxCol = Math.floor(col / 3) * 3;
|
|
1725
|
-
const peers = new Set();
|
|
1726
|
-
for (let c = 0; c < 9; c += 1) peers.add(row * 9 + c);
|
|
1727
|
-
for (let r = 0; r < 9; r += 1) peers.add(r * 9 + col);
|
|
1728
|
-
for (let r = boxRow; r < boxRow + 3; r += 1) {
|
|
1729
|
-
for (let c = boxCol; c < boxCol + 3; c += 1) peers.add(r * 9 + c);
|
|
1730
|
-
}
|
|
1731
|
-
peers.delete(index);
|
|
1732
|
-
return Array.from(peers);
|
|
1733
|
-
});
|
|
1734
|
-
return SUDOKU_PEERS;
|
|
1858
|
+
function toRule(ruleNode, graph, options = {}) {
|
|
1859
|
+
const bodyLists = graph.objects(ruleNode, SRL_BODY);
|
|
1860
|
+
const headLists = graph.objects(ruleNode, SRL_HEAD);
|
|
1861
|
+
if (bodyLists.length !== 1 || headLists.length !== 1) throw new Error(`RDF Rule ${graph.label(ruleNode)} must have exactly one srl:body and one srl:head`);
|
|
1862
|
+
const body = graph.list(bodyLists[0]).map((item) => toBodyElement(item, graph));
|
|
1863
|
+
const head = graph.list(headLists[0]).map((item) => toTripleLike(item, graph));
|
|
1864
|
+
return { name: graph.label(ruleNode), head, body, runOnce: ruleNeedsRunOnce(head, body, options) };
|
|
1735
1865
|
}
|
|
1736
1866
|
|
|
1737
|
-
function
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1867
|
+
function toBodyElement(node, graph) {
|
|
1868
|
+
if (hasTripleShape(node, graph)) return { type: 'triple', triple: toTripleLike(node, graph) };
|
|
1869
|
+
const filters = graph.objects(node, SRL_FILTER).concat(graph.objects(node, SRL_EXPR));
|
|
1870
|
+
if (filters.length > 0) {
|
|
1871
|
+
if (filters.length !== 1) throw new Error(`Filter element ${graph.label(node)} must have exactly one srl:filter`);
|
|
1872
|
+
return { type: 'filter', expr: toExpression(filters[0], graph) };
|
|
1873
|
+
}
|
|
1874
|
+
const assigns = graph.objects(node, SRL_ASSIGN);
|
|
1875
|
+
if (assigns.length > 0) {
|
|
1876
|
+
if (assigns.length !== 1) throw new Error(`Assignment element ${graph.label(node)} must have exactly one srl:assign`);
|
|
1877
|
+
const assign = assigns[0];
|
|
1878
|
+
const vars = graph.objects(assign, SRL_ASSIGN_VAR);
|
|
1879
|
+
const values = graph.objects(assign, SRL_ASSIGN_VALUE);
|
|
1880
|
+
if (vars.length !== 1 || values.length !== 1) throw new Error(`Assignment ${graph.label(assign)} must have exactly one srl:assignVar and srl:assignValue`);
|
|
1881
|
+
const variableTerm = toVarOrTerm(vars[0], graph);
|
|
1882
|
+
if (variableTerm.type !== 'var') throw new Error('srl:assignVar must point to a variable node');
|
|
1883
|
+
return { type: 'set', variable: variableTerm.value, expr: toExpression(values[0], graph) };
|
|
1884
|
+
}
|
|
1885
|
+
const negations = graph.objects(node, SRL_NOT);
|
|
1886
|
+
if (negations.length > 0) {
|
|
1887
|
+
if (negations.length !== 1) throw new Error(`Negation element ${graph.label(node)} must have exactly one srl:not`);
|
|
1888
|
+
const body = graph.list(negations[0]).map((item) => {
|
|
1889
|
+
const clause = toBodyElement(item, graph);
|
|
1890
|
+
if (clause.type === 'set' || clause.type === 'not') throw new Error('RDF Rules srl:not may contain only triple patterns and filters');
|
|
1891
|
+
return clause;
|
|
1892
|
+
});
|
|
1893
|
+
return { type: 'not', body };
|
|
1745
1894
|
}
|
|
1895
|
+
throw new Error(`Unsupported RDF Rules body element ${graph.label(node)}`);
|
|
1746
1896
|
}
|
|
1747
1897
|
|
|
1748
|
-
function
|
|
1749
|
-
if (
|
|
1750
|
-
|
|
1898
|
+
function toTripleLike(node, graph) {
|
|
1899
|
+
if (node.type === 'triple') return { s: node.s, p: node.p, o: node.o };
|
|
1900
|
+
const subjects = graph.objects(node, SRL_SUBJECT);
|
|
1901
|
+
const predicates = graph.objects(node, SRL_PREDICATE);
|
|
1902
|
+
const objects = graph.objects(node, SRL_OBJECT);
|
|
1903
|
+
if (subjects.length !== 1 || predicates.length !== 1 || objects.length !== 1) {
|
|
1904
|
+
throw new Error(`Triple node ${graph.label(node)} must have exactly one srl:subject, srl:predicate and srl:object`);
|
|
1751
1905
|
}
|
|
1752
|
-
return
|
|
1906
|
+
return {
|
|
1907
|
+
s: toVarOrTerm(subjects[0], graph),
|
|
1908
|
+
p: toVarOrTerm(predicates[0], graph),
|
|
1909
|
+
o: toVarOrTerm(objects[0], graph),
|
|
1910
|
+
};
|
|
1753
1911
|
}
|
|
1754
1912
|
|
|
1755
|
-
function
|
|
1756
|
-
|
|
1757
|
-
const label = termToString(args[0]);
|
|
1758
|
-
if (!options.__bnodeLabels) options.__bnodeLabels = new Map();
|
|
1759
|
-
if (!options.__bnodeLabels.has(label)) options.__bnodeLabels.set(label, label || freshId(options));
|
|
1760
|
-
return blankNode(options.__bnodeLabels.get(label));
|
|
1913
|
+
function hasTripleShape(node, graph) {
|
|
1914
|
+
return graph.objects(node, SRL_SUBJECT).length > 0 || graph.objects(node, SRL_PREDICATE).length > 0 || graph.objects(node, SRL_OBJECT).length > 0;
|
|
1761
1915
|
}
|
|
1762
1916
|
|
|
1763
|
-
function
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1917
|
+
function toVarOrTerm(node, graph) {
|
|
1918
|
+
const varNames = graph.objects(node, SRL_VAR_NAME);
|
|
1919
|
+
if (varNames.length > 0) {
|
|
1920
|
+
if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Variable node ${graph.label(node)} must have exactly one string srl:varName`);
|
|
1921
|
+
return variable(String(varNames[0].value));
|
|
1922
|
+
}
|
|
1923
|
+
return node;
|
|
1766
1924
|
}
|
|
1767
1925
|
|
|
1768
|
-
function
|
|
1769
|
-
const
|
|
1770
|
-
|
|
1771
|
-
|
|
1926
|
+
function toExpression(node, graph) {
|
|
1927
|
+
const varNames = graph.objects(node, SHNEX_VAR).concat(graph.objects(node, SRL_VAR_NAME));
|
|
1928
|
+
if (varNames.length > 0) {
|
|
1929
|
+
if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Expression variable ${graph.label(node)} must name one variable`);
|
|
1930
|
+
return { type: 'var', name: String(varNames[0].value) };
|
|
1931
|
+
}
|
|
1932
|
+
if (node.type === 'literal') {
|
|
1933
|
+
if (node.datatype || node.lang) return { type: 'term', value: node };
|
|
1934
|
+
return { type: 'literal', value: node.value };
|
|
1935
|
+
}
|
|
1936
|
+
if (node.type === 'iri' || node.type === 'blank' || node.type === 'triple') {
|
|
1937
|
+
const call = graph.functionCall(node);
|
|
1938
|
+
if (call) return toFunctionExpression(call.name, call.args.map((arg) => toExpression(arg, graph)));
|
|
1939
|
+
if (node.type === 'blank' && graph.hasOutgoing(node)) return { type: 'term', value: node };
|
|
1940
|
+
return { type: 'term', value: toVarOrTerm(node, graph) };
|
|
1941
|
+
}
|
|
1942
|
+
return { type: 'term', value: node };
|
|
1772
1943
|
}
|
|
1773
1944
|
|
|
1774
|
-
function
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
if (
|
|
1779
|
-
if ('
|
|
1945
|
+
function toFunctionExpression(name, args) {
|
|
1946
|
+
if (name.startsWith(SPARQL_NS)) {
|
|
1947
|
+
const local = name.slice(SPARQL_NS.length);
|
|
1948
|
+
if (local === 'less-than' || local === 'lessThan') return binary('<', args);
|
|
1949
|
+
if (local === 'less-than-or-equal' || local === 'lessThanOrEqual') return binary('<=', args);
|
|
1950
|
+
if (local === 'greater-than' || local === 'greaterThan') return binary('>', args);
|
|
1951
|
+
if (local === 'greater-than-or-equal' || local === 'greaterThanOrEqual') return binary('>=', args);
|
|
1952
|
+
if (local === 'equal' || local === 'equals') return binary('=', args);
|
|
1953
|
+
if (local === 'not-equal' || local === 'notEqual') return binary('!=', args);
|
|
1954
|
+
if (local === 'add') return foldBinary('+', args);
|
|
1955
|
+
if (local === 'subtract') return binary('-', args);
|
|
1956
|
+
if (local === 'multiply') return foldBinary('*', args);
|
|
1957
|
+
if (local === 'divide') return binary('/', args);
|
|
1958
|
+
if (local === 'and' || local === 'function-and') return foldBinary('&&', args);
|
|
1959
|
+
if (local === 'or' || local === 'function-or') return foldBinary('||', args);
|
|
1960
|
+
if (local === 'not') return { type: 'unary', op: '!', expr: args[0] };
|
|
1961
|
+
const builtin = sparqlLocalToBuiltin(local);
|
|
1962
|
+
return { type: 'call', name: builtin, args };
|
|
1780
1963
|
}
|
|
1781
|
-
return
|
|
1964
|
+
return { type: 'call', name, args };
|
|
1782
1965
|
}
|
|
1783
1966
|
|
|
1784
|
-
function
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
if (args.length >= 3) return value.substring(start, start + Number(termToPrimitive(valueToTermIfNeeded(args[2]))));
|
|
1788
|
-
return value.substring(start);
|
|
1967
|
+
function binary(op, args) {
|
|
1968
|
+
if (args.length !== 2) throw new Error(`sparql operator ${op} expects 2 arguments`);
|
|
1969
|
+
return { type: 'binary', op, left: args[0], right: args[1] };
|
|
1789
1970
|
}
|
|
1790
1971
|
|
|
1791
|
-
function
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
if (term.langDir) return iri(RDF_DIRLANGSTRING);
|
|
1795
|
-
if (term.lang) return iri(RDF_LANGSTRING);
|
|
1796
|
-
return iri(term.datatype || inferDatatype(term.value) || XSD_STRING);
|
|
1972
|
+
function foldBinary(op, args) {
|
|
1973
|
+
if (args.length < 2) throw new Error(`sparql operator ${op} expects at least 2 arguments`);
|
|
1974
|
+
return args.slice(1).reduce((left, right) => ({ type: 'binary', op, left, right }), args[0]);
|
|
1797
1975
|
}
|
|
1798
1976
|
|
|
1799
|
-
function
|
|
1800
|
-
|
|
1801
|
-
if (typeof termToPrimitive(term) === 'bigint') return true;
|
|
1802
|
-
if (typeof termToPrimitive(term) === 'number') return true;
|
|
1803
|
-
return term.type === 'literal' && NUMERIC_DATATYPES.has(term.datatype);
|
|
1977
|
+
function sparqlLocalToBuiltin(local) {
|
|
1978
|
+
return local.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase()).replace(/^./, (ch) => ch.toUpperCase());
|
|
1804
1979
|
}
|
|
1805
1980
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1981
|
+
class RdfGraph {
|
|
1982
|
+
constructor(triples, prefixes = {}) {
|
|
1983
|
+
this.triples = triples;
|
|
1984
|
+
this.prefixes = prefixes;
|
|
1985
|
+
this.bySubject = new Map();
|
|
1986
|
+
for (const triple of triples) {
|
|
1987
|
+
const key = termKey(triple.s);
|
|
1988
|
+
if (!this.bySubject.has(key)) this.bySubject.set(key, []);
|
|
1989
|
+
this.bySubject.get(key).push(triple);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
objects(subject, predicateIRI) {
|
|
1994
|
+
const rows = this.bySubject.get(termKey(subject)) || [];
|
|
1995
|
+
return rows.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI).map((triple) => triple.o);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
subjects(predicateIRI, object) {
|
|
1999
|
+
return this.triples.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI && termEquals(triple.o, object)).map((triple) => triple.s);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
subjectsWithPredicate(predicateIRI) {
|
|
2003
|
+
return this.triples.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI).map((triple) => triple.s);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
hasOutgoing(subject) {
|
|
2007
|
+
return (this.bySubject.get(termKey(subject)) || []).length > 0;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
list(head) {
|
|
2011
|
+
const out = [];
|
|
2012
|
+
let node = head;
|
|
2013
|
+
const seen = new Set();
|
|
2014
|
+
while (!(node.type === 'iri' && node.value === RDF_NIL)) {
|
|
2015
|
+
const key = termKey(node);
|
|
2016
|
+
if (seen.has(key)) throw new Error(`Cycle in RDF list at ${this.label(node)}`);
|
|
2017
|
+
seen.add(key);
|
|
2018
|
+
const first = this.objects(node, RDF_FIRST);
|
|
2019
|
+
const rest = this.objects(node, RDF_REST);
|
|
2020
|
+
if (first.length !== 1 || rest.length !== 1) throw new Error(`Expected RDF list node at ${this.label(node)}`);
|
|
2021
|
+
out.push(first[0]);
|
|
2022
|
+
node = rest[0];
|
|
2023
|
+
}
|
|
2024
|
+
return out;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
functionCall(node) {
|
|
2028
|
+
if (node.type !== 'blank') return null;
|
|
2029
|
+
const rows = (this.bySubject.get(termKey(node)) || []).filter((triple) => triple.p.type === 'iri');
|
|
2030
|
+
const calls = rows.filter((triple) => triple.p.value.startsWith(SPARQL_NS) || triple.p.value.includes('#') || triple.p.value.includes('/'));
|
|
2031
|
+
const viable = calls.filter((triple) => isRdfListHead(triple.o, this));
|
|
2032
|
+
if (viable.length !== 1) return null;
|
|
2033
|
+
return { name: viable[0].p.value, args: this.list(viable[0].o) };
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
parseReference(text) {
|
|
2037
|
+
if (typeof text !== 'string') return text;
|
|
2038
|
+
if (text.startsWith('<') && text.endsWith('>')) return iri(text.slice(1, -1));
|
|
2039
|
+
if (text.startsWith('_:')) return blankNode(text.slice(2));
|
|
2040
|
+
const colon = text.indexOf(':');
|
|
2041
|
+
if (colon >= 0) {
|
|
2042
|
+
const prefix = text.slice(0, colon);
|
|
2043
|
+
const local = text.slice(colon + 1);
|
|
2044
|
+
const ns = this.prefixes[prefix] || (prefix === 'srl' ? SRL_NS : null);
|
|
2045
|
+
if (ns) return iri(ns + local);
|
|
2046
|
+
}
|
|
2047
|
+
return iri(text);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
label(term) { return formatTerm(term, this.prefixes); }
|
|
1818
2051
|
}
|
|
1819
2052
|
|
|
1820
|
-
function
|
|
1821
|
-
|
|
1822
|
-
const match = lexical.match(/(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:?\d{2})$/);
|
|
1823
|
-
return match ? match[1] : '';
|
|
2053
|
+
function isRdfListHead(term, graph) {
|
|
2054
|
+
return (term.type === 'iri' && term.value === RDF_NIL) || graph.objects(term, RDF_FIRST).length === 1;
|
|
1824
2055
|
}
|
|
1825
2056
|
|
|
1826
|
-
function
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
const minutes = Number(mm);
|
|
1835
|
-
const body = `${hours ? `${hours}H` : ''}${minutes ? `${minutes}M` : ''}` || '0S';
|
|
1836
|
-
return literal(`${sign === '-' ? '-' : ''}PT${body}`, XSD_DAYTIME_DURATION);
|
|
2057
|
+
function uniqueTerms(terms) {
|
|
2058
|
+
const seen = new Set();
|
|
2059
|
+
const out = [];
|
|
2060
|
+
for (const term of terms) {
|
|
2061
|
+
const key = termKey(term);
|
|
2062
|
+
if (!seen.has(key)) { seen.add(key); out.push(term); }
|
|
2063
|
+
}
|
|
2064
|
+
return out;
|
|
1837
2065
|
}
|
|
1838
2066
|
|
|
1839
|
-
function
|
|
1840
|
-
if (
|
|
1841
|
-
|
|
2067
|
+
function numericLiteral(value) {
|
|
2068
|
+
if (Number.isInteger(value)) return literal(value, XSD_INTEGER);
|
|
2069
|
+
if (String(value).includes('e') || String(value).includes('E')) return literal(value, XSD_DOUBLE);
|
|
2070
|
+
return literal(value, XSD_DECIMAL);
|
|
1842
2071
|
}
|
|
1843
2072
|
|
|
1844
|
-
function
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
return
|
|
2073
|
+
function parseIntegerLiteral(value) {
|
|
2074
|
+
const text = String(value);
|
|
2075
|
+
const asNumber = Number.parseInt(text, 10);
|
|
2076
|
+
return Number.isSafeInteger(asNumber) && String(asNumber) === text.replace(/^\+/, '') ? asNumber : BigInt(text);
|
|
1848
2077
|
}
|
|
1849
2078
|
|
|
1850
|
-
function
|
|
1851
|
-
|
|
1852
|
-
return
|
|
2079
|
+
function coerceLexicalLiteral(value, datatype) {
|
|
2080
|
+
if (datatype === XSD_INTEGER) return parseIntegerLiteral(value);
|
|
2081
|
+
if (datatype === XSD_DECIMAL || datatype === XSD_DOUBLE) return Number(value);
|
|
2082
|
+
if (datatype === XSD_BOOLEAN) return value === true || value === 'true' || value === '1';
|
|
2083
|
+
return value;
|
|
1853
2084
|
}
|
|
1854
2085
|
|
|
1855
|
-
function
|
|
1856
|
-
|
|
2086
|
+
function looksLikeRdfRules(source, options = {}) {
|
|
2087
|
+
if (options.syntax === 'rdf') return true;
|
|
2088
|
+
if (options.syntax === 'srl') return false;
|
|
2089
|
+
if (options.filename && /\.(ttl|trig|nt|n3)$/i.test(options.filename)) return true;
|
|
2090
|
+
return /\bsrl:RuleSet\b|\bsrl:rules\b|http:\/\/www\.w3\.org\/ns\/shacl-rules#RuleSet/.test(source);
|
|
1857
2091
|
}
|
|
1858
2092
|
|
|
1859
2093
|
module.exports = {
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2094
|
+
parseRdfDocument,
|
|
2095
|
+
parseRdfSyntax,
|
|
2096
|
+
rdfDocumentToProgram,
|
|
2097
|
+
looksLikeRdfRules,
|
|
2098
|
+
TurtleParser,
|
|
2099
|
+
RdfGraph,
|
|
2100
|
+
constants: {
|
|
2101
|
+
SRL_NS,
|
|
2102
|
+
SHNEX_NS,
|
|
2103
|
+
SPARQL_NS,
|
|
2104
|
+
SRL_RULE_SET,
|
|
2105
|
+
SRL_RULE,
|
|
2106
|
+
},
|
|
1870
2107
|
};
|
|
1871
2108
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2109
|
+
|
|
2110
|
+
// ---- Grammar-hardened RDF 1.1 / RDF 1.2 syntax helpers ----
|
|
2111
|
+
// These functions are used by the W3C RDF manifest harness and are kept in
|
|
2112
|
+
// rdfSyntax.js beside the existing Turtle/RDF-Rules front-end instead of in a
|
|
2113
|
+
// separate monolithic test file. They intentionally keep an internal test
|
|
2114
|
+
// graph representation because the W3C manifests exercise syntax, datasets,
|
|
2115
|
+
// triple terms, and RDF 1.2 annotation isomorphism independently from the SRL
|
|
2116
|
+
// rule engine representation.
|
|
2117
|
+
const rdfW3cSyntax = (() => {
|
|
2118
|
+
// Grammar-hardened RDF syntax code shared by the RDF Rules front-end and W3C manifest harness.
|
|
2119
|
+
function iri(value) {
|
|
2120
|
+
if (!value) throw new Error('iri(value) requires a non-empty value');
|
|
2121
|
+
return Object.freeze({ kind: 'iri', value: String(value) });
|
|
2122
|
+
}
|
|
2123
|
+
function literal(value, datatype = null, language = null, langDir = null) {
|
|
2124
|
+
return Object.freeze({ kind: 'literal', value: String(value), datatype, language, langDir });
|
|
2125
|
+
}
|
|
2126
|
+
function blank(value) {
|
|
2127
|
+
const clean = String(value || '').replace(/^_:/, '');
|
|
2128
|
+
if (!clean) throw new Error('blank(value) requires a name');
|
|
2129
|
+
return Object.freeze({ kind: 'blank', value: clean });
|
|
2130
|
+
}
|
|
2131
|
+
function tripleTerm(s, p, o) { return Object.freeze({ kind: 'triple', s, p, o }); }
|
|
2132
|
+
function variable(name) {
|
|
2133
|
+
const clean = String(name || '').replace(/^\?/, '');
|
|
2134
|
+
if (!clean) throw new Error('variable(name) requires a name');
|
|
2135
|
+
return Object.freeze({ kind: 'var', name: clean });
|
|
2136
|
+
}
|
|
2137
|
+
function triple(s, p, o, graph = null) { return Object.freeze({ s, p, o, graph }); }
|
|
2138
|
+
function termKey(term) {
|
|
2139
|
+
if (!term) return 'default';
|
|
2140
|
+
switch (term.kind) {
|
|
2141
|
+
case 'iri': return `I:${term.value}`;
|
|
2142
|
+
case 'literal': return `L:${JSON.stringify(term.value)}^^${term.datatype || ''}@${term.language || ''}`;
|
|
2143
|
+
case 'blank': return `B:${term.value}`;
|
|
2144
|
+
case 'var': return `V:${term.name}`;
|
|
2145
|
+
case 'triple': return `T:${termKey(term.s)} ${termKey(term.p)} ${termKey(term.o)}`;
|
|
2146
|
+
default: throw new Error(`Unsupported term kind: ${term.kind}`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function tripleKey(t) { return `${termKey(t.s)} ${termKey(t.p)} ${termKey(t.o)} ${termKey(t.graph)}`; }
|
|
2150
|
+
class Rule { constructor({ id, body = [], head = [], profile = 'n3-rules-subset-v0' } = {}) { this.id = id; this.body = body; this.head = head; this.profile = profile; } }
|
|
1875
2151
|
|
|
1876
2152
|
const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
1877
2153
|
const RDF_TYPE = `${RDF_NS}type`;
|
|
@@ -1879,273 +2155,941 @@
|
|
|
1879
2155
|
const RDF_REST = `${RDF_NS}rest`;
|
|
1880
2156
|
const RDF_NIL = `${RDF_NS}nil`;
|
|
1881
2157
|
const RDF_REIFIES = `${RDF_NS}reifies`;
|
|
1882
|
-
const
|
|
2158
|
+
const RDF_LANG_STRING = `${RDF_NS}langString`;
|
|
2159
|
+
const RDF_DIR_LANG_STRING = `${RDF_NS}dirLangString`;
|
|
1883
2160
|
const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
|
|
1884
2161
|
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
|
|
1885
2162
|
const XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';
|
|
1886
2163
|
const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
|
|
2164
|
+
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
|
|
1887
2165
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
}
|
|
2166
|
+
// ---- N-Triples / N-Quads parser ----
|
|
2167
|
+
const { parseNQuads, termToNQuads, tripleToNQuads, triplesToNQuads } = (() => {
|
|
1891
2168
|
|
|
1892
|
-
function
|
|
1893
|
-
|
|
2169
|
+
function isWs(ch) { return ch === ' ' || ch === '\t'; }
|
|
2170
|
+
function isLineEnd(ch) { return ch === '\n' || ch === '\r'; }
|
|
2171
|
+
function isHex(text) { return /^[0-9A-Fa-f]+$/.test(text); }
|
|
2172
|
+
|
|
2173
|
+
function decodeCodePoint(hex, token) {
|
|
2174
|
+
const code = Number.parseInt(hex, 16);
|
|
2175
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
2176
|
+
throw new Error(`Invalid Unicode escape in ${token}`);
|
|
2177
|
+
}
|
|
2178
|
+
return String.fromCodePoint(code);
|
|
1894
2179
|
}
|
|
1895
2180
|
|
|
1896
|
-
function
|
|
1897
|
-
|
|
2181
|
+
function decodeIriEscapes(value, token = 'IRI') {
|
|
2182
|
+
let out = '';
|
|
2183
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
2184
|
+
const ch = value[i];
|
|
2185
|
+
if (ch !== '\\') {
|
|
2186
|
+
if (/[<>"{}|^`\u0000-\u0020]/.test(ch)) throw new Error(`Invalid character in ${token}`);
|
|
2187
|
+
out += ch;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
const esc = value[++i];
|
|
2191
|
+
if (esc === 'u') {
|
|
2192
|
+
const hex = value.slice(i + 1, i + 5);
|
|
2193
|
+
if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
2194
|
+
out += decodeCodePoint(hex, token);
|
|
2195
|
+
i += 4;
|
|
2196
|
+
} else if (esc === 'U') {
|
|
2197
|
+
const hex = value.slice(i + 1, i + 9);
|
|
2198
|
+
if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
2199
|
+
out += decodeCodePoint(hex, token);
|
|
2200
|
+
i += 8;
|
|
2201
|
+
} else {
|
|
2202
|
+
throw new Error(`Invalid IRI escape \\${esc} in ${token}`);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
return out;
|
|
1898
2206
|
}
|
|
1899
2207
|
|
|
1900
|
-
function
|
|
1901
|
-
|
|
2208
|
+
function decodeLiteralEscapes(value, token = 'literal') {
|
|
2209
|
+
let out = '';
|
|
2210
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
2211
|
+
const ch = value[i];
|
|
2212
|
+
if (ch !== '\\') {
|
|
2213
|
+
if (ch === '\n' || ch === '\r') throw new Error(`Raw line break in ${token}`);
|
|
2214
|
+
out += ch;
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
const esc = value[++i];
|
|
2218
|
+
if (!esc) throw new Error(`Trailing escape in ${token}`);
|
|
2219
|
+
if (esc === 't') out += '\t';
|
|
2220
|
+
else if (esc === 'b') out += '\b';
|
|
2221
|
+
else if (esc === 'n') out += '\n';
|
|
2222
|
+
else if (esc === 'r') out += '\r';
|
|
2223
|
+
else if (esc === 'f') out += '\f';
|
|
2224
|
+
else if (esc === '"') out += '"';
|
|
2225
|
+
else if (esc === "'") out += "'";
|
|
2226
|
+
else if (esc === '\\') out += '\\';
|
|
2227
|
+
else if (esc === 'u') {
|
|
2228
|
+
const hex = value.slice(i + 1, i + 5);
|
|
2229
|
+
if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
2230
|
+
out += decodeCodePoint(hex, token);
|
|
2231
|
+
i += 4;
|
|
2232
|
+
} else if (esc === 'U') {
|
|
2233
|
+
const hex = value.slice(i + 1, i + 9);
|
|
2234
|
+
if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);
|
|
2235
|
+
out += decodeCodePoint(hex, token);
|
|
2236
|
+
i += 8;
|
|
2237
|
+
} else {
|
|
2238
|
+
throw new Error(`Invalid escape \\${esc} in ${token}`);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
return out;
|
|
1902
2242
|
}
|
|
1903
2243
|
|
|
1904
|
-
function
|
|
1905
|
-
|
|
2244
|
+
function stripNqComment(line) {
|
|
2245
|
+
let inString = false;
|
|
2246
|
+
let inIri = false;
|
|
2247
|
+
let escaped = false;
|
|
2248
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
2249
|
+
const ch = line[i];
|
|
2250
|
+
if (escaped) { escaped = false; continue; }
|
|
2251
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
2252
|
+
if (!inIri && ch === '"') { inString = !inString; continue; }
|
|
2253
|
+
if (!inString && ch === '<' && line[i + 1] !== '<') { inIri = true; continue; }
|
|
2254
|
+
if (!inString && inIri && ch === '>') { inIri = false; continue; }
|
|
2255
|
+
if (!inString && !inIri && ch === '#') return line.slice(0, i);
|
|
2256
|
+
}
|
|
2257
|
+
return line;
|
|
1906
2258
|
}
|
|
1907
2259
|
|
|
1908
|
-
function
|
|
1909
|
-
|
|
2260
|
+
function validateAbsoluteIri(value, position) {
|
|
2261
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) throw new Error(`${position} must be absolute`);
|
|
2262
|
+
return value;
|
|
1910
2263
|
}
|
|
1911
2264
|
|
|
1912
|
-
function
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
return
|
|
2265
|
+
function validateBlankLabel(value) {
|
|
2266
|
+
// RDF blank node labels follow PN_CHARS-style rules. This deliberately accepts
|
|
2267
|
+
// Unicode letters and leading underscores; it still rejects empty labels,
|
|
2268
|
+
// labels ending in '.', and doubled dots because those are common false
|
|
2269
|
+
// positives when a compact statement terminator is adjacent to a blank node.
|
|
2270
|
+
if (!value || value.endsWith('.') || value.includes('..')) throw new Error(`Invalid blank node label _: ${value}`);
|
|
2271
|
+
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)) {
|
|
2272
|
+
throw new Error(`Invalid blank node label _: ${value}`);
|
|
2273
|
+
}
|
|
2274
|
+
return value;
|
|
1922
2275
|
}
|
|
1923
2276
|
|
|
1924
|
-
function
|
|
1925
|
-
|
|
2277
|
+
function validateLang(value) {
|
|
2278
|
+
// LANG_DIR uses BCP47-style language tags. Keep this intentionally strict enough
|
|
2279
|
+
// for the W3C syntax tests: each subtag is 1..8 alphanumeric chars, starting alpha.
|
|
2280
|
+
if (!value || value.includes('--') || !/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value)) throw new Error(`Invalid language tag @${value}`);
|
|
2281
|
+
return value;
|
|
1926
2282
|
}
|
|
1927
2283
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2284
|
+
class LineReader {
|
|
2285
|
+
constructor(line, lineNumber) {
|
|
2286
|
+
this.line = line;
|
|
2287
|
+
this.lineNumber = lineNumber;
|
|
2288
|
+
this.i = 0;
|
|
2289
|
+
}
|
|
1931
2290
|
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
return
|
|
1935
|
-
|
|
2291
|
+
eof() { return this.i >= this.line.length; }
|
|
2292
|
+
peek(offset = 0) { return this.line[this.i + offset]; }
|
|
2293
|
+
startsWith(value) { return this.line.startsWith(value, this.i); }
|
|
2294
|
+
skipWs() { while (isWs(this.peek())) this.i += 1; }
|
|
1936
2295
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2296
|
+
expect(value) {
|
|
2297
|
+
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'}`);
|
|
2298
|
+
this.i += value.length;
|
|
2299
|
+
}
|
|
1940
2300
|
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2301
|
+
readIri(position = 'IRI') {
|
|
2302
|
+
this.expect('<');
|
|
2303
|
+
let raw = '';
|
|
2304
|
+
while (!this.eof()) {
|
|
2305
|
+
const ch = this.peek();
|
|
2306
|
+
if (ch === '>') { this.i += 1; return validateAbsoluteIri(decodeIriEscapes(raw, position), position); }
|
|
2307
|
+
raw += ch;
|
|
2308
|
+
this.i += 1;
|
|
2309
|
+
}
|
|
2310
|
+
throw new Error(`Unterminated ${position} on line ${this.lineNumber}`);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
readBlank() {
|
|
2314
|
+
this.expect('_:');
|
|
2315
|
+
const start = this.i;
|
|
2316
|
+
while (!this.eof()) {
|
|
2317
|
+
const ch = this.peek();
|
|
2318
|
+
if (!/[\p{L}\p{N}._\-\u00B7\u0300-\u036F\u203F-\u2040]/u.test(ch)) break;
|
|
2319
|
+
if (ch === '.') {
|
|
2320
|
+
const next = this.peek(1);
|
|
2321
|
+
if (!next || isWs(next) || next === '<' || next === '_' || next === '"' || next === '#') break;
|
|
2322
|
+
}
|
|
2323
|
+
this.i += 1;
|
|
2324
|
+
}
|
|
2325
|
+
return blank(validateBlankLabel(this.line.slice(start, this.i)));
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
readLiteral() {
|
|
2329
|
+
this.expect('"');
|
|
2330
|
+
let raw = '';
|
|
2331
|
+
let escaped = false;
|
|
2332
|
+
while (!this.eof()) {
|
|
2333
|
+
const ch = this.peek();
|
|
2334
|
+
this.i += 1;
|
|
2335
|
+
if (escaped) { raw += `\\${ch}`; escaped = false; continue; }
|
|
2336
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
2337
|
+
if (ch === '"') {
|
|
2338
|
+
const value = decodeLiteralEscapes(raw, 'literal');
|
|
2339
|
+
let language = null;
|
|
2340
|
+
let datatype = XSD_STRING;
|
|
2341
|
+
if (this.peek() === '@') {
|
|
2342
|
+
this.i += 1;
|
|
2343
|
+
const start = this.i;
|
|
2344
|
+
while (!this.eof() && /[A-Za-z0-9-]/.test(this.peek())) this.i += 1;
|
|
2345
|
+
let rawLang = this.line.slice(start, this.i);
|
|
2346
|
+
if (!rawLang) throw new Error('Invalid language tag: missing');
|
|
2347
|
+
if (rawLang.endsWith('--ltr') || rawLang.endsWith('--rtl')) rawLang = rawLang.slice(0, -5);
|
|
2348
|
+
language = validateLang(rawLang);
|
|
2349
|
+
datatype = null;
|
|
2350
|
+
} else if (this.startsWith('^^')) {
|
|
2351
|
+
this.i += 2;
|
|
2352
|
+
this.skipWs();
|
|
2353
|
+
datatype = this.readIri('datatype IRI');
|
|
2354
|
+
if (datatype === RDF_LANG_STRING || datatype === RDF_DIR_LANG_STRING) {
|
|
2355
|
+
throw new Error(`Datatype ${datatype} requires LANG_DIR syntax, not ^^`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
// RDF 1.2 base direction suffix, e.g. --ltr / --rtl. The core term model does not preserve it yet;
|
|
2359
|
+
// accepting it is enough for syntax tests and keeps eval comparison conservative for now.
|
|
2360
|
+
if (this.startsWith('--ltr') || this.startsWith('--rtl')) {
|
|
2361
|
+
if (!language) throw new Error('Base direction requires a language tag');
|
|
2362
|
+
this.i += 5;
|
|
2363
|
+
}
|
|
2364
|
+
return literal(value, datatype, language);
|
|
2365
|
+
}
|
|
2366
|
+
raw += ch;
|
|
2367
|
+
}
|
|
2368
|
+
throw new Error(`Unterminated literal on line ${this.lineNumber}`);
|
|
1946
2369
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
if (
|
|
1951
|
-
return
|
|
2370
|
+
|
|
2371
|
+
readTerm(position = 'term') {
|
|
2372
|
+
this.skipWs();
|
|
2373
|
+
if (this.startsWith('<<')) return this.readTripleTerm();
|
|
2374
|
+
if (this.peek() === '<') return iri(this.readIri(position));
|
|
2375
|
+
if (this.startsWith('_:')) return this.readBlank();
|
|
2376
|
+
if (this.peek() === '"') return this.readLiteral();
|
|
2377
|
+
throw new Error(`Expected RDF term for ${position}, got ${this.line.slice(this.i, this.i + 20) || 'end of line'}`);
|
|
1952
2378
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
if (
|
|
1957
|
-
|
|
2379
|
+
|
|
2380
|
+
readSubjectOrGraph(position) {
|
|
2381
|
+
const term = this.readTerm(position);
|
|
2382
|
+
if (term.kind === 'literal') throw new Error(`N-Quads ${position} cannot be a literal`);
|
|
2383
|
+
if (term.kind === 'triple' && (position === 'subject' || position === 'graph')) throw new Error(`N-Quads ${position} cannot be a triple term`);
|
|
2384
|
+
return term;
|
|
1958
2385
|
}
|
|
1959
|
-
const diff = Number(a) - Number(b);
|
|
1960
|
-
if (diff < 0) return -1;
|
|
1961
|
-
if (diff > 0) return 1;
|
|
1962
|
-
return 0;
|
|
1963
|
-
}
|
|
1964
2386
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2387
|
+
readPredicate() {
|
|
2388
|
+
this.skipWs();
|
|
2389
|
+
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'}`);
|
|
2390
|
+
return iri(this.readIri('predicate'));
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
readTripleTerm() {
|
|
2394
|
+
this.expect('<<');
|
|
2395
|
+
this.skipWs();
|
|
2396
|
+
// RDF 1.2 N-Triples/N-Quads triple terms use parenthesized triples: <<( s p o )>>.
|
|
2397
|
+
// The older unparenthesized RDF-star form is a reified-triple syntax form and is not
|
|
2398
|
+
// accepted as a plain subject/object term by the RDF 1.2 syntax manifests.
|
|
2399
|
+
this.expect('(');
|
|
2400
|
+
this.skipWs();
|
|
2401
|
+
const s = this.readSubjectOrGraph('triple-term subject');
|
|
2402
|
+
this.skipWs();
|
|
2403
|
+
const p = this.readPredicate();
|
|
2404
|
+
this.skipWs();
|
|
2405
|
+
const o = this.readTerm('triple-term object');
|
|
2406
|
+
this.skipWs();
|
|
2407
|
+
this.expect(')');
|
|
2408
|
+
this.skipWs();
|
|
2409
|
+
this.expect('>>');
|
|
2410
|
+
return tripleTerm(s, p, o);
|
|
2411
|
+
}
|
|
1973
2412
|
}
|
|
1974
2413
|
|
|
1975
|
-
function
|
|
1976
|
-
|
|
2414
|
+
function parseLine(line, lineNumber, format) {
|
|
2415
|
+
const clean = stripNqComment(line).trim();
|
|
2416
|
+
if (!clean) return null;
|
|
2417
|
+
const r = new LineReader(clean, lineNumber);
|
|
2418
|
+
const s = r.readSubjectOrGraph('subject');
|
|
2419
|
+
r.skipWs();
|
|
2420
|
+
const p = r.readPredicate();
|
|
2421
|
+
r.skipWs();
|
|
2422
|
+
const o = r.readTerm('object');
|
|
2423
|
+
r.skipWs();
|
|
2424
|
+
let g = null;
|
|
2425
|
+
if (r.peek() !== '.') {
|
|
2426
|
+
if (format === 'ntriples') throw new Error(`N-Triples line ${lineNumber} has too many terms before .`);
|
|
2427
|
+
g = r.readSubjectOrGraph('graph');
|
|
2428
|
+
r.skipWs();
|
|
2429
|
+
}
|
|
2430
|
+
if (r.peek() !== '.') throw new Error(`N-Quads line ${lineNumber} must end with .`);
|
|
2431
|
+
r.i += 1;
|
|
2432
|
+
r.skipWs();
|
|
2433
|
+
if (!r.eof()) throw new Error(`Unexpected trailing content on N-Quads line ${lineNumber}: ${clean.slice(r.i)}`);
|
|
2434
|
+
return triple(s, p, o, g);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
function parseNQuads(source, options = {}) {
|
|
2438
|
+
const facts = [];
|
|
2439
|
+
const prefixes = { ...(options.prefixes || {}) };
|
|
2440
|
+
const format = options.format || (options.profileId === 'ntriples-graph-v0' ? 'ntriples' : 'nquads');
|
|
2441
|
+
const lines = String(source || '').split(/\r\n|\n|\r/);
|
|
2442
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
2443
|
+
const fact = parseLine(lines[lineIndex], lineIndex + 1, format);
|
|
2444
|
+
if (fact) facts.push(fact);
|
|
2445
|
+
}
|
|
2446
|
+
return {
|
|
2447
|
+
profile: options.profileId || (format === 'ntriples' ? 'ntriples-graph-v0' : 'nquads-dataset-v0'),
|
|
2448
|
+
prefixes,
|
|
2449
|
+
base: options.base || '',
|
|
2450
|
+
imports: [],
|
|
2451
|
+
facts,
|
|
2452
|
+
rules: [],
|
|
2453
|
+
queries: [],
|
|
2454
|
+
expectations: [],
|
|
2455
|
+
};
|
|
1977
2456
|
}
|
|
1978
2457
|
|
|
1979
|
-
function
|
|
1980
|
-
|
|
1981
|
-
if (term.type === 'triple') return tripleTerm(cloneTerm(term.s), cloneTerm(term.p), cloneTerm(term.o));
|
|
1982
|
-
return { ...term };
|
|
2458
|
+
function escapeIri(value) {
|
|
2459
|
+
return String(value).replace(/[\\>\u0000-\u0020]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
1983
2460
|
}
|
|
1984
2461
|
|
|
1985
|
-
function
|
|
1986
|
-
|
|
1987
|
-
|
|
2462
|
+
function escapeLiteral(value) {
|
|
2463
|
+
return String(value)
|
|
2464
|
+
.replace(/\\/g, '\\\\')
|
|
2465
|
+
.replace(/"/g, '\\"')
|
|
2466
|
+
.replace(/\n/g, '\\n')
|
|
2467
|
+
.replace(/\r/g, '\\r')
|
|
2468
|
+
.replace(/\t/g, '\\t')
|
|
2469
|
+
.replace(/\u0008/g, '\\b')
|
|
2470
|
+
.replace(/\u000c/g, '\\f');
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function termToNQuads(term) {
|
|
2474
|
+
if (!term) return '';
|
|
2475
|
+
switch (term.kind) {
|
|
2476
|
+
case 'iri':
|
|
2477
|
+
return `<${escapeIri(term.value)}>`;
|
|
2478
|
+
case 'blank':
|
|
2479
|
+
return `_:${term.value}`;
|
|
2480
|
+
case 'literal': {
|
|
2481
|
+
let out = `"${escapeLiteral(term.value)}"`;
|
|
2482
|
+
if (term.language) out += `@${term.language}`;
|
|
2483
|
+
else if (term.datatype && term.datatype !== XSD_STRING) out += `^^<${escapeIri(term.datatype)}>`;
|
|
2484
|
+
return out;
|
|
2485
|
+
}
|
|
2486
|
+
case 'triple':
|
|
2487
|
+
return `<< ${termToNQuads(term.s)} ${termToNQuads(term.p)} ${termToNQuads(term.o)} >>`;
|
|
2488
|
+
default:
|
|
2489
|
+
throw new Error(`Cannot serialize ${term.kind} as N-Quads`);
|
|
2490
|
+
}
|
|
1988
2491
|
}
|
|
1989
2492
|
|
|
1990
|
-
function
|
|
1991
|
-
|
|
1992
|
-
if (
|
|
1993
|
-
|
|
1994
|
-
if (typeof value === 'number') return XSD_DECIMAL;
|
|
1995
|
-
if (typeof value === 'string') return XSD_STRING;
|
|
1996
|
-
return null;
|
|
2493
|
+
function tripleToNQuads(value) {
|
|
2494
|
+
const terms = [termToNQuads(value.s), termToNQuads(value.p), termToNQuads(value.o)];
|
|
2495
|
+
if (value.graph) terms.push(termToNQuads(value.graph));
|
|
2496
|
+
return `${terms.join(' ')} .`;
|
|
1997
2497
|
}
|
|
1998
2498
|
|
|
1999
|
-
function
|
|
2000
|
-
|
|
2001
|
-
if (term.type === 'literal') return term.value;
|
|
2002
|
-
if (term.type === 'iri') return term.value;
|
|
2003
|
-
if (term.type === 'blank') return `_:${term.value}`;
|
|
2004
|
-
if (term.type === 'var') return undefined;
|
|
2005
|
-
if (term.type === 'triple') return term;
|
|
2006
|
-
return term;
|
|
2499
|
+
function triplesToNQuads(triples) {
|
|
2500
|
+
return Array.from(new Set(Array.from(triples || []).map(tripleToNQuads))).sort().join('\n');
|
|
2007
2501
|
}
|
|
2502
|
+
return { parseNQuads, termToNQuads, tripleToNQuads, triplesToNQuads };
|
|
2503
|
+
})();
|
|
2008
2504
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
if (value === undefined || value === null) return '';
|
|
2012
|
-
if (value && value.type === 'triple') return formatTerm(value);
|
|
2013
|
-
return String(value);
|
|
2014
|
-
}
|
|
2505
|
+
// ---- Turtle / TriG parser ----
|
|
2506
|
+
const { parseN3 } = (() => {
|
|
2015
2507
|
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (typeof primitive === 'number') return primitive !== 0 && !Number.isNaN(primitive);
|
|
2022
|
-
if (typeof primitive === 'string') return primitive.length > 0 && primitive !== 'false';
|
|
2023
|
-
return Boolean(primitive);
|
|
2024
|
-
}
|
|
2508
|
+
const DEFAULT_PREFIXES = Object.freeze({
|
|
2509
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
2510
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
2511
|
+
log: 'http://www.w3.org/2000/10/swap/log#',
|
|
2512
|
+
});
|
|
2025
2513
|
|
|
2026
|
-
function comparePrimitives(a, b) {
|
|
2027
|
-
const av = a && a.type ? termToPrimitive(a) : a;
|
|
2028
|
-
const bv = b && b.type ? termToPrimitive(b) : b;
|
|
2029
|
-
if (isNumericPrimitive(av) && isNumericPrimitive(bv)) return compareNumericPrimitives(av, bv);
|
|
2030
|
-
const as = String(av);
|
|
2031
|
-
const bs = String(bv);
|
|
2032
|
-
if (as < bs) return -1;
|
|
2033
|
-
if (as > bs) return 1;
|
|
2034
|
-
return 0;
|
|
2035
|
-
}
|
|
2036
2514
|
|
|
2037
|
-
function
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2515
|
+
function isWs(ch) { return /\s/.test(ch || ''); }
|
|
2516
|
+
function isPunct(ch) { return '{}.;,()[]|'.includes(ch || ''); }
|
|
2517
|
+
function isHex(text) { return /^[0-9A-Fa-f]+$/.test(text); }
|
|
2518
|
+
function isAbsoluteIri(value) { return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value || ''); }
|
|
2519
|
+
function resolveIriReference(value, base) {
|
|
2520
|
+
if (isAbsoluteIri(value)) return value;
|
|
2521
|
+
if (!base) return value;
|
|
2522
|
+
try {
|
|
2523
|
+
const url = new URL(value, base);
|
|
2524
|
+
let href = url.href;
|
|
2525
|
+
// The RDF IRI-resolution tests expect bare authority references such as //g
|
|
2526
|
+
// to remain http://g, not to gain the URL API's cosmetic trailing slash.
|
|
2527
|
+
if (/^\/\/[^/?#]+$/.test(value) && href.endsWith('/')) href = href.slice(0, -1);
|
|
2528
|
+
if (/^file:\/\/[^/?#]+$/.test(value) && href.endsWith('/')) href = href.slice(0, -1);
|
|
2529
|
+
return href;
|
|
2530
|
+
} catch { return `${base}${value}`; }
|
|
2531
|
+
}
|
|
2532
|
+
function validateBlankLabel(value) {
|
|
2533
|
+
const clean = String(value || '').replace(/^_:/, '');
|
|
2534
|
+
if (!clean || clean.endsWith('.') || clean.includes('..')) throw new Error(`Invalid blank node label _: ${clean}`);
|
|
2535
|
+
// BLANK_NODE_LABEL follows the PN_CHARS family; ':' is only for prefixed names, not blank labels.
|
|
2536
|
+
if (/[\s<>"{}|^`\\:]/u.test(clean)) throw new Error(`Invalid blank node label _: ${clean}`);
|
|
2537
|
+
if (/^[\-.]/u.test(clean)) throw new Error(`Invalid blank node label _: ${clean}`);
|
|
2538
|
+
return clean;
|
|
2539
|
+
}
|
|
2540
|
+
function validateIriReference(value) {
|
|
2541
|
+
if (/[<>\"{}|^`\u0000-\u0020]/.test(value)) throw new Error('Invalid character in IRIREF');
|
|
2542
|
+
return value;
|
|
2044
2543
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
if (
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
return `${prefix}:${local}`;
|
|
2544
|
+
function validatePrefixedLocal(raw, decoded) {
|
|
2545
|
+
if (!raw) return decoded;
|
|
2546
|
+
if (raw.startsWith('-') || raw.startsWith('\\-') || raw.startsWith('.')) throw new Error(`Invalid prefixed name local ${raw}`);
|
|
2547
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
2548
|
+
const ch = raw[i];
|
|
2549
|
+
if (ch === '\\') {
|
|
2550
|
+
const esc = raw[i + 1];
|
|
2551
|
+
if (!esc || !'_~.-!$&\'()*+,;=/?#@%'.includes(esc)) throw new Error(`Invalid prefixed name local escape ${raw}`);
|
|
2552
|
+
i += 1;
|
|
2553
|
+
continue;
|
|
2056
2554
|
}
|
|
2555
|
+
if (ch === '%') {
|
|
2556
|
+
const hex = raw.slice(i + 1, i + 3);
|
|
2557
|
+
if (hex.length !== 2 || !isHex(hex)) throw new Error(`Invalid percent escape in prefixed name local ${raw}`);
|
|
2558
|
+
i += 2;
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2561
|
+
if (ch === '~' || ch === '^') throw new Error(`Invalid prefixed name local ${raw}`);
|
|
2057
2562
|
}
|
|
2058
|
-
return
|
|
2563
|
+
return decoded;
|
|
2059
2564
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
if (term.type === 'literal') {
|
|
2067
|
-
const v = term.value;
|
|
2068
|
-
if (typeof v === 'bigint' && !term.lang && (!term.datatype || term.datatype === XSD_INTEGER)) return String(v);
|
|
2069
|
-
if (typeof v === 'number' && Number.isFinite(v) && !term.lang && (!term.datatype || term.datatype === XSD_INTEGER || term.datatype === XSD_DECIMAL || term.datatype === XSD_DOUBLE)) return String(v);
|
|
2070
|
-
if (typeof v === 'boolean' && !term.lang && (!term.datatype || term.datatype === XSD_BOOLEAN)) return v ? 'true' : 'false';
|
|
2071
|
-
const lexical = `"${escapeString(v)}"`;
|
|
2072
|
-
if (term.lang) return `${lexical}@${term.lang}${term.langDir ? `--${term.langDir}` : ''}`;
|
|
2073
|
-
if (term.datatype && term.datatype !== XSD_STRING) return `${lexical}^^${compactIRI(term.datatype, prefixes)}`;
|
|
2074
|
-
return lexical;
|
|
2565
|
+
function decodePrefixedLocal(raw) {
|
|
2566
|
+
let out = '';
|
|
2567
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
2568
|
+
const ch = raw[i];
|
|
2569
|
+
if (ch === '\\') { out += raw[i + 1] || ''; i += 1; }
|
|
2570
|
+
else out += ch;
|
|
2075
2571
|
}
|
|
2076
|
-
return
|
|
2572
|
+
return out;
|
|
2077
2573
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2574
|
+
function codePoint(hex, label) {
|
|
2575
|
+
const n = Number.parseInt(hex, 16);
|
|
2576
|
+
if (!Number.isFinite(n) || n < 0 || n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff)) throw new Error(`Invalid Unicode escape in ${label}`);
|
|
2577
|
+
return String.fromCodePoint(n);
|
|
2578
|
+
}
|
|
2579
|
+
function decodeEscapes(text, label, iriMode = false) {
|
|
2580
|
+
let out = '';
|
|
2581
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
2582
|
+
const ch = text[i];
|
|
2583
|
+
if (ch !== '\\') { out += ch; continue; }
|
|
2584
|
+
const esc = text[++i];
|
|
2585
|
+
if (!esc) throw new Error(`Trailing escape in ${label}`);
|
|
2586
|
+
if (esc === 'u') {
|
|
2587
|
+
const hex = text.slice(i + 1, i + 5); if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${label}`);
|
|
2588
|
+
out += codePoint(hex, label); i += 4;
|
|
2589
|
+
} else if (esc === 'U') {
|
|
2590
|
+
const hex = text.slice(i + 1, i + 9); if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${label}`);
|
|
2591
|
+
out += codePoint(hex, label); i += 8;
|
|
2592
|
+
} else if (!iriMode && 'tbnrf"\''.includes(esc)) {
|
|
2593
|
+
out += { t: '\t', b: '\b', n: '\n', r: '\r', f: '\f', '"': '"', "'": "'" }[esc] ?? esc;
|
|
2594
|
+
} else if (!iriMode && esc === '\\') out += '\\';
|
|
2595
|
+
else if (iriMode) throw new Error(`Invalid escape \\${esc} in ${label}`);
|
|
2596
|
+
else throw new Error(`Invalid escape \\${esc} in ${label}`);
|
|
2597
|
+
}
|
|
2598
|
+
return out;
|
|
2081
2599
|
}
|
|
2082
2600
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2601
|
+
class Tokenizer {
|
|
2602
|
+
constructor(source) { this.source = String(source || ''); this.i = 0; this.tokens = []; }
|
|
2603
|
+
eof() { return this.i >= this.source.length; }
|
|
2604
|
+
peek(offset = 0) { return this.source[this.i + offset]; }
|
|
2605
|
+
startsWith(value) { return this.source.startsWith(value, this.i); }
|
|
2606
|
+
push(type, value, extra = {}) { this.tokens.push({ type, value, ...extra }); }
|
|
2607
|
+
skipComment() { while (!this.eof() && this.peek() !== '\n' && this.peek() !== '\r') this.i += 1; }
|
|
2608
|
+
readIri() {
|
|
2609
|
+
this.i += 1;
|
|
2610
|
+
let raw = '';
|
|
2611
|
+
while (!this.eof()) {
|
|
2612
|
+
const ch = this.peek();
|
|
2613
|
+
if (ch === '>') { this.i += 1; this.push('iri', validateIriReference(decodeEscapes(raw, 'IRI', true))); return; }
|
|
2614
|
+
raw += ch; this.i += 1;
|
|
2615
|
+
}
|
|
2616
|
+
throw new Error('Unterminated IRIREF');
|
|
2617
|
+
}
|
|
2618
|
+
readString() {
|
|
2619
|
+
const quote = this.peek();
|
|
2620
|
+
const long = this.source.startsWith(quote.repeat(3), this.i);
|
|
2621
|
+
this.i += long ? 3 : 1;
|
|
2622
|
+
let raw = '';
|
|
2623
|
+
let escaped = false;
|
|
2624
|
+
while (!this.eof()) {
|
|
2625
|
+
const ch = this.peek();
|
|
2626
|
+
if (!escaped && long && this.source.startsWith(quote.repeat(3), this.i)) { this.i += 3; this.push(long ? 'longString' : 'string', decodeEscapes(raw, 'string'), { long }); return; }
|
|
2627
|
+
if (!escaped && !long && ch === quote) { this.i += 1; this.push('string', decodeEscapes(raw, 'string'), { long: false }); return; }
|
|
2628
|
+
if (!long && (ch === '\n' || ch === '\r')) throw new Error('Raw line break in short string');
|
|
2629
|
+
raw += ch;
|
|
2630
|
+
this.i += 1;
|
|
2631
|
+
escaped = !escaped && ch === '\\';
|
|
2632
|
+
if (ch !== '\\') escaped = false;
|
|
2633
|
+
}
|
|
2634
|
+
throw new Error('Unterminated string');
|
|
2635
|
+
}
|
|
2636
|
+
readBare() {
|
|
2637
|
+
const start = this.i;
|
|
2638
|
+
if (this.startsWith('_:')) {
|
|
2639
|
+
this.i += 2;
|
|
2640
|
+
while (!this.eof()) {
|
|
2641
|
+
const ch = this.peek();
|
|
2642
|
+
// BLANK_NODE_LABEL uses PN_CHARS, including broad Unicode ranges that are
|
|
2643
|
+
// not all JavaScript \p{L}/\p{N}. Tokenize generously up to a real
|
|
2644
|
+
// Turtle delimiter, then validate the label separately. Keep ':' as a
|
|
2645
|
+
// boundary so compact forms such as _:s:p tokenize as blank-node _:s
|
|
2646
|
+
// followed by predicate :p.
|
|
2647
|
+
if (isWs(ch) || '<>\"{}|^`\\;,)[]'.includes(ch) || ch === ':' || ch === '#') break;
|
|
2648
|
+
if (ch === '.') {
|
|
2649
|
+
const next = this.source[this.i + 1];
|
|
2650
|
+
if (!next || isWs(next) || '{};,)[]'.includes(next)) break;
|
|
2651
|
+
}
|
|
2652
|
+
this.i += 1;
|
|
2653
|
+
}
|
|
2654
|
+
this.push('bare', this.source.slice(start, this.i));
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
while (!this.eof()) {
|
|
2658
|
+
const ch = this.peek();
|
|
2659
|
+
if (isWs(ch)) break;
|
|
2660
|
+
if (ch === '\\' && this.source[this.i + 1]) { this.i += 2; continue; }
|
|
2661
|
+
if (ch === '<' || ch === '>' || ch === '"' || ch === "'") break;
|
|
2662
|
+
if (ch === '.') {
|
|
2663
|
+
const next = this.source[this.i + 1];
|
|
2664
|
+
if (!next || isWs(next) || '{};,)[]'.includes(next)) break;
|
|
2665
|
+
} else if (isPunct(ch)) break;
|
|
2666
|
+
if (ch === '#') break;
|
|
2667
|
+
if (ch === '^' && this.peek(1) === '^') break;
|
|
2668
|
+
if (ch === '=' && this.peek(1) === '>') break;
|
|
2669
|
+
this.i += 1;
|
|
2670
|
+
}
|
|
2671
|
+
this.push('bare', this.source.slice(start, this.i));
|
|
2672
|
+
}
|
|
2673
|
+
tokenize() {
|
|
2674
|
+
while (!this.eof()) {
|
|
2675
|
+
const ch = this.peek();
|
|
2676
|
+
if (isWs(ch)) { this.i += 1; continue; }
|
|
2677
|
+
if (ch === '#') { this.skipComment(); continue; }
|
|
2678
|
+
if (this.startsWith('@prefix') && (isWs(this.source[this.i + 7]) || this.source[this.i + 7] === ':')) { this.push('bare', '@prefix'); this.i += 7; continue; }
|
|
2679
|
+
if (this.startsWith('@base') && isWs(this.source[this.i + 5])) { this.push('bare', '@base'); this.i += 5; continue; }
|
|
2680
|
+
if (this.startsWith('@version') && isWs(this.source[this.i + 8])) { this.push('bare', '@version'); this.i += 8; continue; }
|
|
2681
|
+
if (this.startsWith('=>')) { this.push('=>', '=>'); this.i += 2; continue; }
|
|
2682
|
+
if (this.startsWith('^^')) { this.push('^^', '^^'); this.i += 2; continue; }
|
|
2683
|
+
if (this.startsWith('<<')) { this.push('<<', '<<'); this.i += 2; continue; }
|
|
2684
|
+
if (this.startsWith('>>')) { this.push('>>', '>>'); this.i += 2; continue; }
|
|
2685
|
+
if (ch === '<') { this.readIri(); continue; }
|
|
2686
|
+
if (ch === '"' || ch === "'") { this.readString(); continue; }
|
|
2687
|
+
if (ch === '.' && /[0-9]/.test(this.peek(1) || '')) { this.readBare(); continue; }
|
|
2688
|
+
if (ch === '~') { this.readBare(); continue; }
|
|
2689
|
+
if (isPunct(ch)) { this.push(ch, ch); this.i += 1; continue; }
|
|
2690
|
+
this.readBare();
|
|
2691
|
+
}
|
|
2692
|
+
return this.tokens;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2119
2695
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2696
|
+
function parseN3(source, options = {}) {
|
|
2697
|
+
const tokens = new Tokenizer(source).tokenize();
|
|
2698
|
+
let i = 0;
|
|
2699
|
+
let base = options.base || '';
|
|
2700
|
+
const prefixes = { ...DEFAULT_PREFIXES, ...(options.prefixes || {}) };
|
|
2701
|
+
const facts = [];
|
|
2702
|
+
const rules = [];
|
|
2703
|
+
let bnodeCounter = 0;
|
|
2704
|
+
const bnodes = new Map();
|
|
2705
|
+
const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
|
|
2706
|
+
const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
|
|
2707
|
+
const implicitStatementNodes = new Set();
|
|
2708
|
+
|
|
2709
|
+
function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
|
|
2710
|
+
function peek(offset = 0) { return tokens[i + offset]; }
|
|
2711
|
+
function next() { return tokens[i++]; }
|
|
2712
|
+
function eof() { return i >= tokens.length; }
|
|
2713
|
+
function accept(value) { if (peek()?.value === value || peek()?.type === value) { i += 1; return true; } return false; }
|
|
2714
|
+
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; }
|
|
2715
|
+
function error(msg) { throw new Error(msg); }
|
|
2716
|
+
|
|
2717
|
+
function parseIriValueFromBare(token) {
|
|
2718
|
+
if (token === 'a') return RDF_TYPE;
|
|
2719
|
+
if (token.startsWith('_:')) error(`Blank node label ${token} cannot be used as IRI`);
|
|
2720
|
+
const split = token.indexOf(':');
|
|
2721
|
+
if (split >= 0) {
|
|
2722
|
+
const prefix = token.slice(0, split);
|
|
2723
|
+
let local = token.slice(split + 1);
|
|
2724
|
+
if (!(prefix in prefixes)) throw new Error(`Unknown prefix ${prefix}:`);
|
|
2725
|
+
// Turtle permits reserved escaped characters in local names.
|
|
2726
|
+
local = validatePrefixedLocal(local, decodePrefixedLocal(local));
|
|
2727
|
+
return prefixes[prefix] + local;
|
|
2728
|
+
}
|
|
2729
|
+
throw new Error(`Expected IRI or prefixed name, got ${token}`);
|
|
2730
|
+
}
|
|
2123
2731
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
return clauses.some((clause) => clause.type === 'set' || clause.type === 'bind');
|
|
2732
|
+
function parseIriLike() {
|
|
2733
|
+
const t = next();
|
|
2734
|
+
if (!t) error('Unexpected end of input while reading IRI');
|
|
2735
|
+
if (t.type === 'iri') return resolveIriReference(t.value, base);
|
|
2736
|
+
if (t.type === 'bare') return parseIriValueFromBare(t.value);
|
|
2737
|
+
throw new Error(`Expected IRI or prefixed name, got ${t.value}`);
|
|
2131
2738
|
}
|
|
2132
|
-
const hasSet = clauses.some((clause) => clause.type === 'set');
|
|
2133
|
-
const hasNegation = clauses.some((clause) => clause.type === 'not');
|
|
2134
|
-
return (hasSet && hasNegation)
|
|
2135
|
-
|| clauses.some((clause) => (clause.type === 'set' || clause.type === 'bind') && expressionIsVolatile(clause.expr));
|
|
2136
|
-
}
|
|
2137
2739
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2740
|
+
function parseBlankLabel(label) {
|
|
2741
|
+
const key = validateBlankLabel(label);
|
|
2742
|
+
if (!bnodes.has(key)) bnodes.set(key, blank(key));
|
|
2743
|
+
return bnodes.get(key);
|
|
2744
|
+
}
|
|
2141
2745
|
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2746
|
+
function parseNumber(token) {
|
|
2747
|
+
if (/^[+-]?\d+$/.test(token)) return literal(token, XSD_INTEGER);
|
|
2748
|
+
if (/^[+-]?(?:\d+\.\d*|\.\d+)$/.test(token)) return literal(token, XSD_DECIMAL);
|
|
2749
|
+
if (/^[+-]?(?:(?:\d+\.\d*)|(?:\.\d+)|\d+)[eE][+-]?\d+$/.test(token)) return literal(token, XSD_DOUBLE);
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2147
2752
|
|
|
2148
|
-
|
|
2753
|
+
function parseTerm(out = facts, graph = null, options2 = {}) {
|
|
2754
|
+
const t = peek();
|
|
2755
|
+
if (!t) error('Unexpected end of input while reading Turtle term');
|
|
2756
|
+
if (t.type === '<<') return parseTripleTerm(out, graph, options2);
|
|
2757
|
+
if (t.type === 'iri') { next(); return iri(resolveIriReference(t.value, base)); }
|
|
2758
|
+
if (t.type === 'string' || t.type === 'longString') {
|
|
2759
|
+
if (options2.noLiteral) throw new Error('Literal is not allowed here');
|
|
2760
|
+
return parseLiteral();
|
|
2761
|
+
}
|
|
2762
|
+
if (t.type === '[') {
|
|
2763
|
+
// ANON is a BlankNode and is permitted in rtSubject/ttSubject, even where
|
|
2764
|
+
// blankNodePropertyList is not. Keep [ ... ] rejected in those positions.
|
|
2765
|
+
if (options2.noCompound) {
|
|
2766
|
+
if (peek(1)?.type === ']') { expect('['); expect(']'); return freshBlank(); }
|
|
2767
|
+
throw new Error('Compound blank node expression is not allowed here');
|
|
2768
|
+
}
|
|
2769
|
+
return parseBlankNodePropertyList(out, graph);
|
|
2770
|
+
}
|
|
2771
|
+
if (t.type === '(') {
|
|
2772
|
+
if (options2.noCompound) throw new Error('Collection is not allowed here');
|
|
2773
|
+
return parseCollection(out, graph);
|
|
2774
|
+
}
|
|
2775
|
+
if (t.type === 'bare') {
|
|
2776
|
+
next();
|
|
2777
|
+
if (t.value.startsWith('?')) {
|
|
2778
|
+
if (rdf12Surface) throw new Error(`Variables are not allowed in Turtle/TriG: ${t.value}`);
|
|
2779
|
+
return variable(t.value.slice(1));
|
|
2780
|
+
}
|
|
2781
|
+
if (t.value.startsWith('_:')) return parseBlankLabel(t.value);
|
|
2782
|
+
if (t.value === 'a' && options2.noA) throw new Error('a is only allowed as a predicate');
|
|
2783
|
+
if (t.value === 'true' || t.value === 'false') {
|
|
2784
|
+
if (options2.noLiteral) throw new Error('Literal is not allowed here');
|
|
2785
|
+
return literal(t.value, XSD_BOOLEAN);
|
|
2786
|
+
}
|
|
2787
|
+
const num = parseNumber(t.value);
|
|
2788
|
+
if (num) {
|
|
2789
|
+
if (options2.noLiteral) throw new Error('Literal is not allowed here');
|
|
2790
|
+
return num;
|
|
2791
|
+
}
|
|
2792
|
+
return iri(parseIriValueFromBare(t.value));
|
|
2793
|
+
}
|
|
2794
|
+
throw new Error(`Expected IRI or prefixed name, got ${t.value}`);
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
function parseLiteral() {
|
|
2798
|
+
const t = next(); if (!t || (t.type !== 'string' && t.type !== 'longString')) throw new Error(`Expected string, got ${t?.value || 'end of input'}`);
|
|
2799
|
+
if (peek()?.type === 'bare' && peek().value.startsWith('@')) {
|
|
2800
|
+
let lang = next().value.slice(1);
|
|
2801
|
+
let langDir = null;
|
|
2802
|
+
if (lang.endsWith('--ltr') || lang.endsWith('--rtl')) {
|
|
2803
|
+
langDir = lang.slice(-3);
|
|
2804
|
+
lang = lang.slice(0, -5);
|
|
2805
|
+
}
|
|
2806
|
+
if (!lang || lang.includes('--') || !/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) throw new Error(`Invalid language tag @${lang}`);
|
|
2807
|
+
if (peek()?.type === 'bare' && ['--ltr', '--rtl'].includes(peek().value)) langDir = next().value.slice(2);
|
|
2808
|
+
return literal(t.value, null, lang, langDir);
|
|
2809
|
+
}
|
|
2810
|
+
if (accept('^^')) return literal(t.value, parseIriLike());
|
|
2811
|
+
if (peek()?.type === 'bare' && ['--ltr', '--rtl'].includes(peek().value)) throw new Error('Base direction requires a language tag');
|
|
2812
|
+
return literal(t.value, XSD_STRING);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function parseReifierToken(out, graph) {
|
|
2816
|
+
const t = peek();
|
|
2817
|
+
if (!t || t.type !== 'bare' || !t.value.startsWith('~')) return null;
|
|
2818
|
+
next();
|
|
2819
|
+
const suffix = t.value.slice(1);
|
|
2820
|
+
if (suffix) {
|
|
2821
|
+
if (suffix.startsWith('_:')) return parseBlankLabel(suffix);
|
|
2822
|
+
return iri(parseIriValueFromBare(suffix));
|
|
2823
|
+
}
|
|
2824
|
+
const n = peek();
|
|
2825
|
+
if (n && (n.type === 'iri' || (n.type === 'bare' && (n.value.startsWith('_:') || n.value.includes(':') || n.value === 'a')))) {
|
|
2826
|
+
const term = parseTerm(out, graph, { noLiteral: true, noCompound: true, noTripleTerm: true });
|
|
2827
|
+
if (term.kind !== 'iri' && term.kind !== 'blank') throw new Error('Reifier must be an IRI or blank node');
|
|
2828
|
+
return term;
|
|
2829
|
+
}
|
|
2830
|
+
return freshBlank();
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
function parseTripleTerm(out, graph, options2 = {}) {
|
|
2834
|
+
expect('<<');
|
|
2835
|
+
const parenthesized = accept('(');
|
|
2836
|
+
if (parenthesized) {
|
|
2837
|
+
if (options2.noTripleTerm) throw new Error('Triple term is not allowed here');
|
|
2838
|
+
const s = parseTerm(out, graph, { noLiteral: true, noCompound: true, noReifiedTriple: true });
|
|
2839
|
+
if (s.kind === 'triple') throw new Error('Triple term subject cannot be a triple term');
|
|
2840
|
+
const p = iri(parseIriLike());
|
|
2841
|
+
const o = parseTerm(out, graph, { noCompound: true, noReifiedTriple: true });
|
|
2842
|
+
if (o.kind !== 'iri' && o.kind !== 'blank' && o.kind !== 'literal' && o.kind !== 'triple') throw new Error('Invalid triple term object');
|
|
2843
|
+
expect(')');
|
|
2844
|
+
expect('>>');
|
|
2845
|
+
return tripleTerm(s, p, o);
|
|
2846
|
+
}
|
|
2847
|
+
if (options2.noReifiedTriple) throw new Error('Reified triple is not allowed here');
|
|
2848
|
+
const s = parseTerm(out, graph, { noLiteral: true, noCompound: true, noTripleTerm: true });
|
|
2849
|
+
if (s.kind !== 'iri' && s.kind !== 'blank') throw new Error('Invalid reified triple subject');
|
|
2850
|
+
const p = iri(parseIriLike());
|
|
2851
|
+
const o = parseTerm(out, graph, { noCompound: true });
|
|
2852
|
+
if (o.kind !== 'iri' && o.kind !== 'blank' && o.kind !== 'literal' && o.kind !== 'triple') throw new Error('Invalid reified triple object');
|
|
2853
|
+
let reifier = null;
|
|
2854
|
+
if (peek()?.type === 'bare' && peek().value.startsWith('~')) reifier = parseReifierToken(out, graph);
|
|
2855
|
+
expect('>>');
|
|
2856
|
+
const node = reifier || freshBlank();
|
|
2857
|
+
out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
|
|
2858
|
+
if (node.kind === 'blank') implicitStatementNodes.add(node.value);
|
|
2859
|
+
return node;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
function parseCollection(out, graph) {
|
|
2863
|
+
expect('(');
|
|
2864
|
+
if (accept(')')) return iri(RDF_NIL);
|
|
2865
|
+
const head = freshBlank();
|
|
2866
|
+
let current = head;
|
|
2867
|
+
while (true) {
|
|
2868
|
+
const item = parseTerm(out, graph);
|
|
2869
|
+
out.push(triple(current, iri(RDF_FIRST), item, graph));
|
|
2870
|
+
if (accept(')')) {
|
|
2871
|
+
out.push(triple(current, iri(RDF_REST), iri(RDF_NIL), graph));
|
|
2872
|
+
break;
|
|
2873
|
+
}
|
|
2874
|
+
const rest = freshBlank();
|
|
2875
|
+
out.push(triple(current, iri(RDF_REST), rest, graph));
|
|
2876
|
+
current = rest;
|
|
2877
|
+
}
|
|
2878
|
+
return head;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
function parseBlankNodePropertyList(out, graph) {
|
|
2882
|
+
expect('[');
|
|
2883
|
+
const node = freshBlank();
|
|
2884
|
+
if (accept(']')) return node;
|
|
2885
|
+
parsePredicateObjectList(node, out, graph);
|
|
2886
|
+
expect(']');
|
|
2887
|
+
if (node.kind === 'blank') implicitStatementNodes.add(node.value);
|
|
2888
|
+
return node;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
function parseAnnotationBlock(reifier, out, graph = null) {
|
|
2892
|
+
expect('{');
|
|
2893
|
+
expect('|');
|
|
2894
|
+
if (peek()?.type === '|') {
|
|
2895
|
+
// Empty annotation blocks are rejected by the RDF 1.2 syntax tests.
|
|
2896
|
+
throw new Error('Empty annotation block');
|
|
2897
|
+
}
|
|
2898
|
+
parsePredicateObjectList(reifier, out, graph);
|
|
2899
|
+
expect('|');
|
|
2900
|
+
expect('}');
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
function ensureReifierForTriple(assertedTriple, out, graph = null) {
|
|
2904
|
+
const reifier = freshBlank();
|
|
2905
|
+
out.push(triple(reifier, iri(RDF_REIFIES), tripleTerm(assertedTriple.s, assertedTriple.p, assertedTriple.o), graph));
|
|
2906
|
+
return reifier;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
function parseObjectList(subject, predicate, out, graph = null) {
|
|
2910
|
+
while (true) {
|
|
2911
|
+
const object = parseTerm(out, graph, { noA: true });
|
|
2912
|
+
const asserted = triple(subject, predicate, object, graph);
|
|
2913
|
+
out.push(asserted);
|
|
2914
|
+
let pendingReifier = null;
|
|
2915
|
+
while (true) {
|
|
2916
|
+
if (peek()?.type === 'bare' && peek().value.startsWith('~')) {
|
|
2917
|
+
pendingReifier = parseReifierToken(out, graph);
|
|
2918
|
+
out.push(triple(pendingReifier, iri(RDF_REIFIES), tripleTerm(asserted.s, asserted.p, asserted.o), graph));
|
|
2919
|
+
continue;
|
|
2920
|
+
}
|
|
2921
|
+
if (peek()?.type === '{' && peek(1)?.type === '|') {
|
|
2922
|
+
const blockReifier = pendingReifier || ensureReifierForTriple(asserted, out, graph);
|
|
2923
|
+
parseAnnotationBlock(blockReifier, out, graph);
|
|
2924
|
+
pendingReifier = null;
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
break;
|
|
2928
|
+
}
|
|
2929
|
+
if (!accept(',')) break;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function parsePredicateObjectList(subject, out, graph = null) {
|
|
2934
|
+
while (true) {
|
|
2935
|
+
const predicate = iri(parseIriLike());
|
|
2936
|
+
parseObjectList(subject, predicate, out, graph);
|
|
2937
|
+
if (!accept(';')) break;
|
|
2938
|
+
while (accept(';')) {}
|
|
2939
|
+
if ([']', '.', '}', '|'].includes(peek()?.type)) break;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
function parseGraphLabel(out, inheritedGraph = null) {
|
|
2944
|
+
if (peek()?.type === '[' && peek(1)?.type === ']') { expect('['); expect(']'); return freshBlank(); }
|
|
2945
|
+
if (peek()?.type === '(') throw new Error('GRAPH name must be an IRI or blank node');
|
|
2946
|
+
const graph = parseTerm(out, inheritedGraph, { noLiteral: true, noCompound: true, noTripleTerm: true, noReifiedTriple: true, noA: true });
|
|
2947
|
+
if (graph.kind !== 'iri' && graph.kind !== 'blank') throw new Error('GRAPH name must be an IRI or blank node');
|
|
2948
|
+
return graph;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function parseGraphBlock(out, inheritedGraph = null) {
|
|
2952
|
+
expect('GRAPH');
|
|
2953
|
+
const graph = parseGraphLabel(out, inheritedGraph);
|
|
2954
|
+
parseFormula(graph, out);
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
function parseTripleStatement(out, graph = null, options3 = {}) {
|
|
2958
|
+
if (String(peek()?.value || '').toUpperCase() === 'GRAPH') {
|
|
2959
|
+
if (syntaxProfile === 'turtle') throw new Error('GRAPH blocks are not Turtle');
|
|
2960
|
+
if (graph) throw new Error('GRAPH blocks cannot be nested inside a graph block');
|
|
2961
|
+
parseGraphBlock(out, graph);
|
|
2962
|
+
if (options3.requireDot) expect('.'); else accept('.');
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
|
|
2966
|
+
const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
|
|
2967
|
+
if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
|
|
2968
|
+
if (options3.requireDot) expect('.'); else accept('.');
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
parsePredicateObjectList(subject, out, graph);
|
|
2972
|
+
if (options3.requireDot) expect('.'); else accept('.');
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
function parseFormula(graph = null, target = null) {
|
|
2976
|
+
expect('{');
|
|
2977
|
+
const triples = target || [];
|
|
2978
|
+
while (peek()?.type !== '}') parseTripleStatement(triples, graph);
|
|
2979
|
+
expect('}');
|
|
2980
|
+
return triples;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function parseBase() {
|
|
2984
|
+
const directive = next();
|
|
2985
|
+
const iriToken = next();
|
|
2986
|
+
if (iriToken?.type !== 'iri') throw new Error(`Expected base IRI, got ${iriToken?.value}`);
|
|
2987
|
+
base = resolveIriReference(iriToken.value, base);
|
|
2988
|
+
if (String(directive.value || '').startsWith('@')) expect('.');
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function parsePrefix() {
|
|
2992
|
+
const directive = next();
|
|
2993
|
+
const label = next();
|
|
2994
|
+
if (label?.type !== 'bare' || !label.value.endsWith(':')) throw new Error(`Expected prefix label ending with :, got ${label?.value}`);
|
|
2995
|
+
const prefixLabel = label.value.slice(0, -1);
|
|
2996
|
+
if (prefixLabel.endsWith('.') || prefixLabel.includes('..')) throw new Error(`Invalid prefix label ${prefixLabel}`);
|
|
2997
|
+
const iriToken = next();
|
|
2998
|
+
if (iriToken?.type !== 'iri') throw new Error(`Expected prefix IRI, got ${iriToken?.value}`);
|
|
2999
|
+
prefixes[prefixLabel] = resolveIriReference(iriToken.value, base);
|
|
3000
|
+
if (String(directive.value || '').startsWith('@')) expect('.');
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
function isSimpleGraphLabelStart(t) {
|
|
3004
|
+
return t && (t.type === 'iri' || (t.type === 'bare' && (t.value.startsWith('_:') || t.value.includes(':'))));
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
while (!eof()) {
|
|
3008
|
+
const token = peek();
|
|
3009
|
+
const lowerValue = String(token.value || '').toLowerCase();
|
|
3010
|
+
if (token.value === '@base' || (!String(token.value || '').startsWith('@') && lowerValue === 'base')) parseBase();
|
|
3011
|
+
else if (token.value === '@prefix' || (!String(token.value || '').startsWith('@') && lowerValue === 'prefix')) parsePrefix();
|
|
3012
|
+
else if ((!String(token.value || '').startsWith('@') && String(token.value || '').toUpperCase() === 'VERSION') || token.value === '@version') {
|
|
3013
|
+
const directive = next();
|
|
3014
|
+
const v = next();
|
|
3015
|
+
if (!v || v.type !== 'string') throw new Error('VERSION requires a short quoted string');
|
|
3016
|
+
if (String(directive.value || '').startsWith('@')) expect('.');
|
|
3017
|
+
}
|
|
3018
|
+
else if (token.type === '{') {
|
|
3019
|
+
if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow top-level graph/formula blocks');
|
|
3020
|
+
const body = parseFormula();
|
|
3021
|
+
if (accept('=>')) {
|
|
3022
|
+
const head = parseFormula();
|
|
3023
|
+
accept('.');
|
|
3024
|
+
rules.push(new Rule({ id: `n3${rules.length + 1}`, body, head, profile: 'n3-rules-subset-v0' }));
|
|
3025
|
+
} else {
|
|
3026
|
+
facts.push(...body);
|
|
3027
|
+
accept('.');
|
|
3028
|
+
}
|
|
3029
|
+
} else if (String(token.value || '').toUpperCase() === 'GRAPH') {
|
|
3030
|
+
parseGraphBlock(facts);
|
|
3031
|
+
if (accept('.')) throw new Error('GRAPH block must not be followed by .');
|
|
3032
|
+
} else if (token.type === '[' && peek(1)?.type === ']' && peek(2)?.type === '{') {
|
|
3033
|
+
if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow graph labels');
|
|
3034
|
+
const graph = parseGraphLabel(facts, null);
|
|
3035
|
+
parseFormula(graph, facts);
|
|
3036
|
+
accept('.');
|
|
3037
|
+
} else if (isSimpleGraphLabelStart(token) && peek(1)?.type === '{') {
|
|
3038
|
+
if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow graph labels');
|
|
3039
|
+
const graph = parseGraphLabel(facts, null);
|
|
3040
|
+
parseFormula(graph, facts);
|
|
3041
|
+
accept('.');
|
|
3042
|
+
} else {
|
|
3043
|
+
parseTripleStatement(facts, null, { requireDot: syntaxProfile === 'turtle' });
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
return { profile: 'n3-rules-subset-v0', prefixes, base, facts, rules };
|
|
3048
|
+
}
|
|
3049
|
+
return { parseN3 };
|
|
3050
|
+
})();
|
|
3051
|
+
|
|
3052
|
+
|
|
3053
|
+
return {
|
|
3054
|
+
parseNQuads,
|
|
3055
|
+
termToNQuads,
|
|
3056
|
+
tripleToNQuads,
|
|
3057
|
+
triplesToNQuads,
|
|
3058
|
+
parseN3,
|
|
3059
|
+
};
|
|
3060
|
+
})();
|
|
3061
|
+
|
|
3062
|
+
Object.assign(module.exports, rdfW3cSyntax);
|
|
3063
|
+
|
|
3064
|
+
},
|
|
3065
|
+
"src/assignments.js": function (require, module, exports) {
|
|
3066
|
+
'use strict';
|
|
3067
|
+
|
|
3068
|
+
// Most SET expressions are deterministic and can safely participate in the
|
|
3069
|
+
// ordinary fixpoint loop. Only genuinely fresh generators need run-once
|
|
3070
|
+
// evaluation, otherwise a recursive rule such as SET(?x := UUID()) would keep
|
|
3071
|
+
// creating new terms forever.
|
|
3072
|
+
function assignmentsNeedRunOnce(clauses = [], options = {}) {
|
|
3073
|
+
if (options.shacl12Conformance) {
|
|
3074
|
+
return clauses.some((clause) => clause.type === 'set' || clause.type === 'bind');
|
|
3075
|
+
}
|
|
3076
|
+
const hasSet = clauses.some((clause) => clause.type === 'set');
|
|
3077
|
+
const hasNegation = clauses.some((clause) => clause.type === 'not');
|
|
3078
|
+
return (hasSet && hasNegation)
|
|
3079
|
+
|| clauses.some((clause) => (clause.type === 'set' || clause.type === 'bind') && expressionIsVolatile(clause.expr));
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
function ruleNeedsRunOnce(head = [], body = [], options = {}) {
|
|
3083
|
+
return assignmentsNeedRunOnce(body, options) || head.some(tripleHasBlankNode);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function tripleHasBlankNode(triple) {
|
|
3087
|
+
return termHasBlankNode(triple && triple.s)
|
|
3088
|
+
|| termHasBlankNode(triple && triple.p)
|
|
3089
|
+
|| termHasBlankNode(triple && triple.o);
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
function termHasBlankNode(term) {
|
|
2149
3093
|
if (!term) return false;
|
|
2150
3094
|
if (term.type === 'blank') return true;
|
|
2151
3095
|
if (term.type === 'triple') return termHasBlankNode(term.s) || termHasBlankNode(term.p) || termHasBlankNode(term.o);
|
|
@@ -2184,568 +3128,1117 @@
|
|
|
2184
3128
|
module.exports = { assignmentsNeedRunOnce, ruleNeedsRunOnce, expressionIsVolatile, tripleHasBlankNode, termHasBlankNode };
|
|
2185
3129
|
|
|
2186
3130
|
},
|
|
2187
|
-
"src/
|
|
3131
|
+
"src/term.js": function (require, module, exports) {
|
|
2188
3132
|
'use strict';
|
|
2189
3133
|
|
|
2190
|
-
const { tokenize, SyntaxErrorWithLocation } = require('./tokenizer.js');
|
|
2191
|
-
const { ruleNeedsRunOnce } = require('./assignments.js');
|
|
2192
|
-
const {
|
|
2193
|
-
iri,
|
|
2194
|
-
variable,
|
|
2195
|
-
blankNode,
|
|
2196
|
-
literal,
|
|
2197
|
-
tripleTerm,
|
|
2198
|
-
termKey,
|
|
2199
|
-
termEquals,
|
|
2200
|
-
formatTerm,
|
|
2201
|
-
RDF_TYPE,
|
|
2202
|
-
RDF_FIRST,
|
|
2203
|
-
RDF_REST,
|
|
2204
|
-
RDF_NIL,
|
|
2205
|
-
XSD_BOOLEAN,
|
|
2206
|
-
XSD_INTEGER,
|
|
2207
|
-
XSD_DECIMAL,
|
|
2208
|
-
XSD_DOUBLE,
|
|
2209
|
-
} = require('./term.js');
|
|
2210
|
-
|
|
2211
3134
|
const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
2212
|
-
const
|
|
2213
|
-
const
|
|
2214
|
-
const
|
|
2215
|
-
const
|
|
2216
|
-
const
|
|
2217
|
-
const
|
|
2218
|
-
const
|
|
2219
|
-
const
|
|
2220
|
-
const
|
|
2221
|
-
const
|
|
2222
|
-
const SRL_SUBJECT = `${SRL_NS}subject`;
|
|
2223
|
-
const SRL_PREDICATE = `${SRL_NS}predicate`;
|
|
2224
|
-
const SRL_OBJECT = `${SRL_NS}object`;
|
|
2225
|
-
const SRL_FILTER = `${SRL_NS}filter`;
|
|
2226
|
-
const SRL_EXPR = `${SRL_NS}expr`;
|
|
2227
|
-
const SRL_ASSIGN = `${SRL_NS}assign`;
|
|
2228
|
-
const SRL_ASSIGN_VAR = `${SRL_NS}assignVar`;
|
|
2229
|
-
const SRL_ASSIGN_VALUE = `${SRL_NS}assignValue`;
|
|
2230
|
-
const SRL_NOT = `${SRL_NS}not`;
|
|
2231
|
-
const SRL_VAR_NAME = `${SRL_NS}varName`;
|
|
2232
|
-
const SHNEX_VAR = `${SHNEX_NS}var`;
|
|
3135
|
+
const RDF_TYPE = `${RDF_NS}type`;
|
|
3136
|
+
const RDF_FIRST = `${RDF_NS}first`;
|
|
3137
|
+
const RDF_REST = `${RDF_NS}rest`;
|
|
3138
|
+
const RDF_NIL = `${RDF_NS}nil`;
|
|
3139
|
+
const RDF_REIFIES = `${RDF_NS}reifies`;
|
|
3140
|
+
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
|
|
3141
|
+
const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
|
|
3142
|
+
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
|
|
3143
|
+
const XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';
|
|
3144
|
+
const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
|
|
2233
3145
|
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
this.pos = 0;
|
|
2238
|
-
this.baseIRI = options.baseIRI || null;
|
|
2239
|
-
this.bnodeCounter = 0;
|
|
2240
|
-
this.prefixes = {
|
|
2241
|
-
'': 'http://example/',
|
|
2242
|
-
rdf: RDF_NS,
|
|
2243
|
-
srl: SRL_NS,
|
|
2244
|
-
shnex: SHNEX_NS,
|
|
2245
|
-
sparql: SPARQL_NS,
|
|
2246
|
-
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
2247
|
-
owl: 'http://www.w3.org/2002/07/owl#',
|
|
2248
|
-
...options.prefixes,
|
|
2249
|
-
};
|
|
2250
|
-
this.triples = [];
|
|
2251
|
-
this.imports = [];
|
|
2252
|
-
}
|
|
3146
|
+
function iri(value) {
|
|
3147
|
+
return { type: 'iri', value: String(value) };
|
|
3148
|
+
}
|
|
2253
3149
|
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
else if (this.matchDirective('BASE', '@base')) this.parseBase(this.previous().value.startsWith('@'));
|
|
2258
|
-
else this.parseTriplesStatement();
|
|
2259
|
-
}
|
|
2260
|
-
return {
|
|
2261
|
-
baseIRI: this.baseIRI,
|
|
2262
|
-
prefixes: { ...this.prefixes },
|
|
2263
|
-
triples: this.triples,
|
|
2264
|
-
imports: this.imports.slice(),
|
|
2265
|
-
};
|
|
2266
|
-
}
|
|
3150
|
+
function variable(name) {
|
|
3151
|
+
return { type: 'var', value: String(name).replace(/^[?$]/, '') };
|
|
3152
|
+
}
|
|
2267
3153
|
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
const iriToken = this.expectType('iri');
|
|
2272
|
-
this.prefixes[nameToken.value.slice(0, -1)] = this.resolveIRI(iriToken.value, iriToken);
|
|
2273
|
-
if (atStyle) this.expectValue('.');
|
|
2274
|
-
}
|
|
3154
|
+
function blankNode(value) {
|
|
3155
|
+
return { type: 'blank', value: String(value).replace(/^_:/, '') };
|
|
3156
|
+
}
|
|
2275
3157
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
if (atStyle) this.expectValue('.');
|
|
2280
|
-
}
|
|
3158
|
+
function literal(value, datatype = null, lang = null, langDir = null) {
|
|
3159
|
+
return { type: 'literal', value, datatype, lang, langDir };
|
|
3160
|
+
}
|
|
2281
3161
|
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
this.triples.push(...this.parsePredicateObjectList(subjectNode.term, ['.']));
|
|
2286
|
-
this.expectValue('.');
|
|
2287
|
-
}
|
|
3162
|
+
function tripleTerm(s, p, o) {
|
|
3163
|
+
return { type: 'triple', s, p, o };
|
|
3164
|
+
}
|
|
2288
3165
|
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
const predicate = this.parseVerb();
|
|
2293
|
-
do {
|
|
2294
|
-
const objectNode = this.parseNode();
|
|
2295
|
-
triples.push(...objectNode.triples);
|
|
2296
|
-
triples.push({ s: subject, p: predicate, o: objectNode.term });
|
|
2297
|
-
if (predicate.type === 'iri' && predicate.value === OWL_IMPORTS && objectNode.term.type === 'iri') this.imports.push(objectNode.term.value);
|
|
2298
|
-
} while (this.matchValue(','));
|
|
2299
|
-
if (this.matchValue(';')) {
|
|
2300
|
-
while (this.matchValue(';')) { /* tolerate repeated semicolons */ }
|
|
2301
|
-
if (terminators.some((value) => this.checkValue(value))) break;
|
|
2302
|
-
} else break;
|
|
2303
|
-
}
|
|
2304
|
-
return triples;
|
|
2305
|
-
}
|
|
3166
|
+
function isVariable(term) {
|
|
3167
|
+
return term && term.type === 'var';
|
|
3168
|
+
}
|
|
2306
3169
|
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
return { term: this.parseTerm(), triples: [] };
|
|
2311
|
-
}
|
|
3170
|
+
function isIRI(term) {
|
|
3171
|
+
return term && term.type === 'iri';
|
|
3172
|
+
}
|
|
2312
3173
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
if (this.matchValue(']')) return { term: node, triples: [] };
|
|
2317
|
-
const triples = this.parsePredicateObjectList(node, [']']);
|
|
2318
|
-
this.expectValue(']');
|
|
2319
|
-
return { term: node, triples };
|
|
2320
|
-
}
|
|
3174
|
+
function isBlank(term) {
|
|
3175
|
+
return term && term.type === 'blank';
|
|
3176
|
+
}
|
|
2321
3177
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
3178
|
+
function isLiteral(term) {
|
|
3179
|
+
return term && term.type === 'literal';
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function isTripleTerm(term) {
|
|
3183
|
+
return term && term.type === 'triple';
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
function termEquals(a, b) {
|
|
3187
|
+
return termKey(a) === termKey(b);
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
function literalKeyValue(value) {
|
|
3191
|
+
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
3192
|
+
return JSON.stringify(value);
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
function isNumericPrimitive(value) {
|
|
3196
|
+
return typeof value === 'number' || typeof value === 'bigint';
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
function compareNumericPrimitives(a, b) {
|
|
3200
|
+
if (typeof a === 'bigint' && typeof b === 'bigint') {
|
|
3201
|
+
if (a < b) return -1;
|
|
3202
|
+
if (a > b) return 1;
|
|
3203
|
+
return 0;
|
|
3204
|
+
}
|
|
3205
|
+
if (typeof a === 'bigint' && typeof b === 'number' && Number.isInteger(b) && Number.isSafeInteger(b)) {
|
|
3206
|
+
const bi = BigInt(b);
|
|
3207
|
+
if (a < bi) return -1;
|
|
3208
|
+
if (a > bi) return 1;
|
|
3209
|
+
return 0;
|
|
3210
|
+
}
|
|
3211
|
+
if (typeof a === 'number' && typeof b === 'bigint' && Number.isInteger(a) && Number.isSafeInteger(a)) {
|
|
3212
|
+
const ai = BigInt(a);
|
|
3213
|
+
if (ai < b) return -1;
|
|
3214
|
+
if (ai > b) return 1;
|
|
3215
|
+
return 0;
|
|
3216
|
+
}
|
|
3217
|
+
const diff = Number(a) - Number(b);
|
|
3218
|
+
if (diff < 0) return -1;
|
|
3219
|
+
if (diff > 0) return 1;
|
|
3220
|
+
return 0;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
function termKey(term) {
|
|
3224
|
+
if (!term) return 'null';
|
|
3225
|
+
if (term.type === 'iri') return `I:${term.value}`;
|
|
3226
|
+
if (term.type === 'blank') return `B:${term.value}`;
|
|
3227
|
+
if (term.type === 'var') return `V:${term.value}`;
|
|
3228
|
+
if (term.type === 'literal') return `L:${literalKeyValue(term.value)}^^${term.datatype || ''}@${term.lang || ''}--${term.langDir || ''}`;
|
|
3229
|
+
if (term.type === 'triple') return `T:${termKey(term.s)} ${termKey(term.p)} ${termKey(term.o)}`;
|
|
3230
|
+
return JSON.stringify(term);
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
function tripleKey(triple) {
|
|
3234
|
+
return `${termKey(triple.s)} ${termKey(triple.p)} ${termKey(triple.o)}`;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
function cloneTerm(term) {
|
|
3238
|
+
if (!term) return term;
|
|
3239
|
+
if (term.type === 'triple') return tripleTerm(cloneTerm(term.s), cloneTerm(term.p), cloneTerm(term.o));
|
|
3240
|
+
return { ...term };
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
function valueToTerm(value) {
|
|
3244
|
+
if (value && typeof value === 'object' && value.type) return value;
|
|
3245
|
+
return literal(value, inferDatatype(value));
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
function inferDatatype(value) {
|
|
3249
|
+
if (typeof value === 'boolean') return XSD_BOOLEAN;
|
|
3250
|
+
if (typeof value === 'bigint') return XSD_INTEGER;
|
|
3251
|
+
if (typeof value === 'number' && Number.isInteger(value)) return XSD_INTEGER;
|
|
3252
|
+
if (typeof value === 'number') return XSD_DECIMAL;
|
|
3253
|
+
if (typeof value === 'string') return XSD_STRING;
|
|
3254
|
+
return null;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
function termToPrimitive(term) {
|
|
3258
|
+
if (!term) return undefined;
|
|
3259
|
+
if (term.type === 'literal') return term.value;
|
|
3260
|
+
if (term.type === 'iri') return term.value;
|
|
3261
|
+
if (term.type === 'blank') return `_:${term.value}`;
|
|
3262
|
+
if (term.type === 'var') return undefined;
|
|
3263
|
+
if (term.type === 'triple') return term;
|
|
3264
|
+
return term;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
function termToString(term) {
|
|
3268
|
+
const value = termToPrimitive(term);
|
|
3269
|
+
if (value === undefined || value === null) return '';
|
|
3270
|
+
if (value && value.type === 'triple') return formatTerm(value);
|
|
3271
|
+
return String(value);
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
function booleanValue(value) {
|
|
3275
|
+
const primitive = value && value.type ? termToPrimitive(value) : value;
|
|
3276
|
+
if (primitive === undefined || primitive === null) return false;
|
|
3277
|
+
if (typeof primitive === 'boolean') return primitive;
|
|
3278
|
+
if (typeof primitive === 'bigint') return primitive !== 0n;
|
|
3279
|
+
if (typeof primitive === 'number') return primitive !== 0 && !Number.isNaN(primitive);
|
|
3280
|
+
if (typeof primitive === 'string') return primitive.length > 0 && primitive !== 'false';
|
|
3281
|
+
return Boolean(primitive);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
function comparePrimitives(a, b) {
|
|
3285
|
+
const av = a && a.type ? termToPrimitive(a) : a;
|
|
3286
|
+
const bv = b && b.type ? termToPrimitive(b) : b;
|
|
3287
|
+
if (isNumericPrimitive(av) && isNumericPrimitive(bv)) return compareNumericPrimitives(av, bv);
|
|
3288
|
+
const as = String(av);
|
|
3289
|
+
const bs = String(bv);
|
|
3290
|
+
if (as < bs) return -1;
|
|
3291
|
+
if (as > bs) return 1;
|
|
3292
|
+
return 0;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
function escapeString(value) {
|
|
3296
|
+
return String(value)
|
|
3297
|
+
.replace(/\\/g, '\\\\')
|
|
3298
|
+
.replace(/\n/g, '\\n')
|
|
3299
|
+
.replace(/\r/g, '\\r')
|
|
3300
|
+
.replace(/\t/g, '\\t')
|
|
3301
|
+
.replace(/"/g, '\\"');
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
function compactIRI(value, prefixes = {}) {
|
|
3305
|
+
if (value === RDF_TYPE) return 'a';
|
|
3306
|
+
const entries = Object.entries(prefixes)
|
|
3307
|
+
.filter(([, iriPrefix]) => iriPrefix && value.startsWith(iriPrefix))
|
|
3308
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
3309
|
+
if (entries.length > 0) {
|
|
3310
|
+
const [prefix, iriPrefix] = entries[0];
|
|
3311
|
+
const local = value.slice(iriPrefix.length);
|
|
3312
|
+
if (/^[A-Za-z_][A-Za-z0-9_\-]*$/.test(local) || /^[A-Za-z0-9_\-]+$/.test(local)) {
|
|
3313
|
+
return `${prefix}:${local}`;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
return `<${value}>`;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function formatTerm(term, prefixes = {}) {
|
|
3320
|
+
if (term.type === 'iri') return compactIRI(term.value, prefixes);
|
|
3321
|
+
if (term.type === 'blank') return `_:${term.value}`;
|
|
3322
|
+
if (term.type === 'var') return `?${term.value}`;
|
|
3323
|
+
if (term.type === 'triple') return `<<(${formatTerm(term.s, prefixes)} ${formatTerm(term.p, prefixes)} ${formatTerm(term.o, prefixes)})>>`;
|
|
3324
|
+
if (term.type === 'literal') {
|
|
3325
|
+
const v = term.value;
|
|
3326
|
+
if (typeof v === 'bigint' && !term.lang && (!term.datatype || term.datatype === XSD_INTEGER)) return String(v);
|
|
3327
|
+
if (typeof v === 'number' && Number.isFinite(v) && !term.lang && (!term.datatype || term.datatype === XSD_INTEGER || term.datatype === XSD_DECIMAL || term.datatype === XSD_DOUBLE)) return String(v);
|
|
3328
|
+
if (typeof v === 'boolean' && !term.lang && (!term.datatype || term.datatype === XSD_BOOLEAN)) return v ? 'true' : 'false';
|
|
3329
|
+
const lexical = `"${escapeString(v)}"`;
|
|
3330
|
+
if (term.lang) return `${lexical}@${term.lang}${term.langDir ? `--${term.langDir}` : ''}`;
|
|
3331
|
+
if (term.datatype && term.datatype !== XSD_STRING) return `${lexical}^^${compactIRI(term.datatype, prefixes)}`;
|
|
3332
|
+
return lexical;
|
|
3333
|
+
}
|
|
3334
|
+
return String(term.value ?? term);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function formatTriple(triple, prefixes = {}) {
|
|
3338
|
+
return `${formatTerm(triple.s, prefixes)} ${formatTerm(triple.p, prefixes)} ${formatTerm(triple.o, prefixes)} .`;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
module.exports = {
|
|
3342
|
+
RDF_NS,
|
|
3343
|
+
RDF_TYPE,
|
|
3344
|
+
RDF_FIRST,
|
|
3345
|
+
RDF_REST,
|
|
3346
|
+
RDF_NIL,
|
|
3347
|
+
RDF_REIFIES,
|
|
3348
|
+
XSD_STRING,
|
|
3349
|
+
XSD_BOOLEAN,
|
|
3350
|
+
XSD_INTEGER,
|
|
3351
|
+
XSD_DECIMAL,
|
|
3352
|
+
XSD_DOUBLE,
|
|
3353
|
+
iri,
|
|
3354
|
+
variable,
|
|
3355
|
+
blankNode,
|
|
3356
|
+
literal,
|
|
3357
|
+
tripleTerm,
|
|
3358
|
+
isVariable,
|
|
3359
|
+
isIRI,
|
|
3360
|
+
isBlank,
|
|
3361
|
+
isLiteral,
|
|
3362
|
+
isTripleTerm,
|
|
3363
|
+
termEquals,
|
|
3364
|
+
termKey,
|
|
3365
|
+
tripleKey,
|
|
3366
|
+
cloneTerm,
|
|
3367
|
+
valueToTerm,
|
|
3368
|
+
inferDatatype,
|
|
3369
|
+
termToPrimitive,
|
|
3370
|
+
termToString,
|
|
3371
|
+
booleanValue,
|
|
3372
|
+
comparePrimitives,
|
|
3373
|
+
compactIRI,
|
|
3374
|
+
formatTerm,
|
|
3375
|
+
formatTriple,
|
|
3376
|
+
};
|
|
3377
|
+
|
|
3378
|
+
},
|
|
3379
|
+
"src/builtins.js": function (require, module, exports) {
|
|
3380
|
+
'use strict';
|
|
3381
|
+
|
|
3382
|
+
const {
|
|
3383
|
+
iri,
|
|
3384
|
+
blankNode,
|
|
3385
|
+
literal,
|
|
3386
|
+
tripleTerm,
|
|
3387
|
+
termEquals,
|
|
3388
|
+
termToPrimitive,
|
|
3389
|
+
termToString,
|
|
3390
|
+
booleanValue,
|
|
3391
|
+
comparePrimitives,
|
|
3392
|
+
isIRI,
|
|
3393
|
+
isBlank,
|
|
3394
|
+
isLiteral,
|
|
3395
|
+
isTripleTerm,
|
|
3396
|
+
valueToTerm,
|
|
3397
|
+
inferDatatype,
|
|
3398
|
+
XSD_STRING,
|
|
3399
|
+
RDF_NS,
|
|
3400
|
+
XSD_INTEGER,
|
|
3401
|
+
XSD_DECIMAL,
|
|
3402
|
+
XSD_DOUBLE,
|
|
3403
|
+
} = require('./term.js');
|
|
3404
|
+
|
|
3405
|
+
const XSD_DATETIME = 'http://www.w3.org/2001/XMLSchema#dateTime';
|
|
3406
|
+
const XSD_DAYTIME_DURATION = 'http://www.w3.org/2001/XMLSchema#dayTimeDuration';
|
|
3407
|
+
const RDF_LANGSTRING = `${RDF_NS}langString`;
|
|
3408
|
+
const RDF_DIRLANGSTRING = `${RDF_NS}dirLangString`;
|
|
3409
|
+
const NUMERIC_DATATYPES = new Set([XSD_INTEGER, XSD_DECIMAL, XSD_DOUBLE]);
|
|
3410
|
+
|
|
3411
|
+
// This table is intentionally shaped by the SHACL 1.2 Rules grammar production BuiltInCall.
|
|
3412
|
+
// Keys are the canonical spellings used by the draft; lookup is case-insensitive so examples
|
|
3413
|
+
// may use SPARQL-style uppercase or lowercase spellings while still being checked against the
|
|
3414
|
+
// grammar's finite set of built-ins.
|
|
3415
|
+
const BUILTIN_SIGNATURES = Object.freeze({
|
|
3416
|
+
STR: { min: 1, max: 1 },
|
|
3417
|
+
LANG: { min: 1, max: 1 },
|
|
3418
|
+
LANGMATCHES: { min: 2, max: 2 },
|
|
3419
|
+
LANGDIR: { min: 1, max: 1 },
|
|
3420
|
+
DATATYPE: { min: 1, max: 1 },
|
|
3421
|
+
IRI: { min: 1, max: 1 },
|
|
3422
|
+
URI: { min: 1, max: 1 },
|
|
3423
|
+
BNODE: { min: 0, max: 1 },
|
|
3424
|
+
ABS: { min: 1, max: 1 },
|
|
3425
|
+
CEIL: { min: 1, max: 1 },
|
|
3426
|
+
FLOOR: { min: 1, max: 1 },
|
|
3427
|
+
ROUND: { min: 1, max: 1 },
|
|
3428
|
+
CONCAT: { min: 0, max: Infinity },
|
|
3429
|
+
SUBSTR: { min: 2, max: 3 },
|
|
3430
|
+
STRLEN: { min: 1, max: 1 },
|
|
3431
|
+
REPLACE: { min: 3, max: 4 },
|
|
3432
|
+
UCASE: { min: 1, max: 1 },
|
|
3433
|
+
LCASE: { min: 1, max: 1 },
|
|
3434
|
+
ENCODE_FOR_URI: { min: 1, max: 1 },
|
|
3435
|
+
CONTAINS: { min: 2, max: 2 },
|
|
3436
|
+
STRSTARTS: { min: 2, max: 2 },
|
|
3437
|
+
STRENDS: { min: 2, max: 2 },
|
|
3438
|
+
STRBEFORE: { min: 2, max: 2 },
|
|
3439
|
+
STRAFTER: { min: 2, max: 2 },
|
|
3440
|
+
YEAR: { min: 1, max: 1 },
|
|
3441
|
+
MONTH: { min: 1, max: 1 },
|
|
3442
|
+
DAY: { min: 1, max: 1 },
|
|
3443
|
+
HOURS: { min: 1, max: 1 },
|
|
3444
|
+
MINUTES: { min: 1, max: 1 },
|
|
3445
|
+
SECONDS: { min: 1, max: 1 },
|
|
3446
|
+
TIMEZONE: { min: 1, max: 1 },
|
|
3447
|
+
TZ: { min: 1, max: 1 },
|
|
3448
|
+
NOW: { min: 0, max: 0 },
|
|
3449
|
+
UUID: { min: 0, max: 0 },
|
|
3450
|
+
STRUUID: { min: 0, max: 0 },
|
|
3451
|
+
IF: { min: 3, max: 3, lazy: true },
|
|
3452
|
+
STRLANG: { min: 2, max: 2 },
|
|
3453
|
+
STRLANGDIR: { min: 3, max: 3 },
|
|
3454
|
+
STRDT: { min: 2, max: 2 },
|
|
3455
|
+
sameTerm: { min: 2, max: 2 },
|
|
3456
|
+
isIRI: { min: 1, max: 1 },
|
|
3457
|
+
isURI: { min: 1, max: 1 },
|
|
3458
|
+
isBLANK: { min: 1, max: 1 },
|
|
3459
|
+
isLITERAL: { min: 1, max: 1 },
|
|
3460
|
+
isNUMERIC: { min: 1, max: 1 },
|
|
3461
|
+
hasLANG: { min: 1, max: 1 },
|
|
3462
|
+
hasLANGDIR: { min: 1, max: 1 },
|
|
3463
|
+
REGEX: { min: 2, max: 3 },
|
|
3464
|
+
isTRIPLE: { min: 1, max: 1 },
|
|
3465
|
+
TRIPLE: { min: 3, max: 3 },
|
|
3466
|
+
SUBJECT: { min: 1, max: 1 },
|
|
3467
|
+
PREDICATE: { min: 1, max: 1 },
|
|
3468
|
+
OBJECT: { min: 1, max: 1 },
|
|
3469
|
+
});
|
|
3470
|
+
|
|
3471
|
+
const BUILTIN_BY_LOWER = new Map(Object.keys(BUILTIN_SIGNATURES).map((name) => [name.toLowerCase(), name]));
|
|
3472
|
+
|
|
3473
|
+
function canonicalBuiltinName(name) {
|
|
3474
|
+
return BUILTIN_BY_LOWER.get(String(name).toLowerCase()) || null;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
function isBuiltinName(name) {
|
|
3478
|
+
return canonicalBuiltinName(name) !== null;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
function builtinNames() {
|
|
3482
|
+
return Object.keys(BUILTIN_SIGNATURES);
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function evalExpression(expr, binding, options = {}) {
|
|
3486
|
+
switch (expr.type) {
|
|
3487
|
+
case 'literal':
|
|
3488
|
+
return expr.value;
|
|
3489
|
+
case 'term':
|
|
3490
|
+
return expr.value;
|
|
3491
|
+
case 'var':
|
|
3492
|
+
return binding[expr.name];
|
|
3493
|
+
case 'list':
|
|
3494
|
+
return expr.items.map((item) => evalExpression(item, binding, options));
|
|
3495
|
+
case 'unary': {
|
|
3496
|
+
const value = evalExpression(expr.expr, binding, options);
|
|
3497
|
+
if (expr.op === '!') return !booleanValue(value);
|
|
3498
|
+
if (expr.op === '-') return negateNumeric(termToPrimitive(valueToTermIfNeeded(value)));
|
|
3499
|
+
if (expr.op === '+') return unaryPlusNumeric(termToPrimitive(valueToTermIfNeeded(value)));
|
|
3500
|
+
throw new Error(`Unsupported unary operator ${expr.op}`);
|
|
2334
3501
|
}
|
|
2335
|
-
|
|
3502
|
+
case 'binary': {
|
|
3503
|
+
const left = evalExpression(expr.left, binding, options);
|
|
3504
|
+
if (expr.op === '&&') return booleanValue(left) && booleanValue(evalExpression(expr.right, binding, options));
|
|
3505
|
+
if (expr.op === '||') return booleanValue(left) || booleanValue(evalExpression(expr.right, binding, options));
|
|
3506
|
+
const right = evalExpression(expr.right, binding, options);
|
|
3507
|
+
return evalBinary(expr.op, left, right);
|
|
3508
|
+
}
|
|
3509
|
+
case 'call':
|
|
3510
|
+
return evalCallExpression(expr, binding, options);
|
|
3511
|
+
default:
|
|
3512
|
+
throw new Error(`Unsupported expression type ${expr.type}`);
|
|
2336
3513
|
}
|
|
3514
|
+
}
|
|
2337
3515
|
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
3516
|
+
function evalCallExpression(expr, binding, options) {
|
|
3517
|
+
const canonical = canonicalBuiltinName(expr.name);
|
|
3518
|
+
if (canonical === 'IF') {
|
|
3519
|
+
validateArity(canonical, expr.args.length);
|
|
3520
|
+
const condition = evalExpression(expr.args[0], binding, options);
|
|
3521
|
+
return evalExpression(booleanValue(condition) ? expr.args[1] : expr.args[2], binding, options);
|
|
2343
3522
|
}
|
|
3523
|
+
return callBuiltin(expr.name, expr.args.map((arg) => evalExpression(arg, binding, options)), binding, options);
|
|
3524
|
+
}
|
|
2344
3525
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
if (
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
3526
|
+
function evalBinary(op, left, right) {
|
|
3527
|
+
if (op === '=') return termishEquals(left, right);
|
|
3528
|
+
if (op === '!=') return !termishEquals(left, right);
|
|
3529
|
+
if (op === 'IN' || op === 'NOT IN') {
|
|
3530
|
+
const list = Array.isArray(right) ? right : [];
|
|
3531
|
+
const found = list.some((item) => termishEquals(left, item));
|
|
3532
|
+
return op === 'IN' ? found : !found;
|
|
3533
|
+
}
|
|
3534
|
+
if (['<', '<=', '>', '>='].includes(op)) {
|
|
3535
|
+
const cmp = comparePrimitives(left, right);
|
|
3536
|
+
if (op === '<') return cmp < 0;
|
|
3537
|
+
if (op === '<=') return cmp <= 0;
|
|
3538
|
+
if (op === '>') return cmp > 0;
|
|
3539
|
+
if (op === '>=') return cmp >= 0;
|
|
3540
|
+
}
|
|
3541
|
+
const lp = termToPrimitive(valueToTermIfNeeded(left));
|
|
3542
|
+
const rp = termToPrimitive(valueToTermIfNeeded(right));
|
|
3543
|
+
if (op === '+') {
|
|
3544
|
+
if (isNumericPrimitive(lp) && isNumericPrimitive(rp)) return addNumeric(lp, rp);
|
|
3545
|
+
return String(lp) + String(rp);
|
|
2364
3546
|
}
|
|
3547
|
+
if (op === '-') return subtractNumeric(lp, rp);
|
|
3548
|
+
if (op === '*') return multiplyNumeric(lp, rp);
|
|
3549
|
+
if (op === '/') return Number(lp) / Number(rp);
|
|
3550
|
+
throw new Error(`Unsupported binary operator ${op}`);
|
|
3551
|
+
}
|
|
2365
3552
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
3553
|
+
|
|
3554
|
+
function isNumericPrimitive(value) {
|
|
3555
|
+
return typeof value === 'number' || typeof value === 'bigint';
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
function isIntegerPrimitive(value) {
|
|
3559
|
+
return typeof value === 'bigint' || (typeof value === 'number' && Number.isInteger(value));
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
function toBigIntInteger(value) {
|
|
3563
|
+
if (typeof value === 'bigint') return value;
|
|
3564
|
+
if (typeof value === 'number' && Number.isInteger(value) && Number.isSafeInteger(value)) return BigInt(value);
|
|
3565
|
+
throw new Error(`Cannot convert ${String(value)} to BigInt safely`);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
function fromIntegerResult(value) {
|
|
3569
|
+
if (value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER)) return Number(value);
|
|
3570
|
+
return value;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function addNumeric(left, right) {
|
|
3574
|
+
if (isIntegerPrimitive(left) && isIntegerPrimitive(right)) {
|
|
3575
|
+
if (typeof left === 'bigint' || typeof right === 'bigint') return fromIntegerResult(toBigIntInteger(left) + toBigIntInteger(right));
|
|
3576
|
+
const result = left + right;
|
|
3577
|
+
if (Number.isSafeInteger(result)) return result;
|
|
3578
|
+
return toBigIntInteger(left) + toBigIntInteger(right);
|
|
2372
3579
|
}
|
|
3580
|
+
return Number(left) + Number(right);
|
|
3581
|
+
}
|
|
2373
3582
|
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
3583
|
+
function subtractNumeric(left, right) {
|
|
3584
|
+
if (isIntegerPrimitive(left) && isIntegerPrimitive(right)) {
|
|
3585
|
+
if (typeof left === 'bigint' || typeof right === 'bigint') return fromIntegerResult(toBigIntInteger(left) - toBigIntInteger(right));
|
|
3586
|
+
const result = left - right;
|
|
3587
|
+
if (Number.isSafeInteger(result)) return result;
|
|
3588
|
+
return toBigIntInteger(left) - toBigIntInteger(right);
|
|
3589
|
+
}
|
|
3590
|
+
return Number(left) - Number(right);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
function multiplyNumeric(left, right) {
|
|
3594
|
+
if (isIntegerPrimitive(left) && isIntegerPrimitive(right)) {
|
|
3595
|
+
if (typeof left === 'bigint' || typeof right === 'bigint') return fromIntegerResult(toBigIntInteger(left) * toBigIntInteger(right));
|
|
3596
|
+
const result = left * right;
|
|
3597
|
+
if (Number.isSafeInteger(result)) return result;
|
|
3598
|
+
return toBigIntInteger(left) * toBigIntInteger(right);
|
|
3599
|
+
}
|
|
3600
|
+
return Number(left) * Number(right);
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
function negateNumeric(value) {
|
|
3604
|
+
if (typeof value === 'bigint') return -value;
|
|
3605
|
+
return -Number(value);
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
function unaryPlusNumeric(value) {
|
|
3609
|
+
if (typeof value === 'bigint') return value;
|
|
3610
|
+
return Number(value);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
function valueToTermIfNeeded(value) {
|
|
3614
|
+
return value && value.type ? value : literal(value, inferDatatype(value));
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
function termishEquals(left, right) {
|
|
3618
|
+
if (left && left.type && right && right.type) return termEquals(left, right);
|
|
3619
|
+
const lp = left && left.type ? termToPrimitive(left) : left;
|
|
3620
|
+
const rp = right && right.type ? termToPrimitive(right) : right;
|
|
3621
|
+
return lp === rp;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
function callBuiltin(name, args, binding = {}, options = {}) {
|
|
3625
|
+
const injected = options.builtins && (options.builtins[name] || options.builtins[String(name).toLowerCase()]);
|
|
3626
|
+
if (injected) return injected(args, { binding, iri, blankNode, literal, tripleTerm, termToString, booleanValue, termToPrimitive });
|
|
3627
|
+
|
|
3628
|
+
if (localName(name).toLowerCase() === 'sudoku') {
|
|
3629
|
+
if (args.length !== 1) throw new Error(`SUDOKU expects 1 argument, got ${args.length}`);
|
|
3630
|
+
return solveSudoku(termToString(args[0]));
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
const canonical = canonicalBuiltinName(name);
|
|
3634
|
+
if (!canonical) throw new Error(`Unknown builtin ${name}`);
|
|
3635
|
+
validateArity(canonical, args.length);
|
|
3636
|
+
const key = canonical.toLowerCase();
|
|
3637
|
+
|
|
3638
|
+
if (key === 'str') return termToString(args[0]);
|
|
3639
|
+
if (key === 'iri' || key === 'uri') return makeIRI(termToString(args[0]), options);
|
|
3640
|
+
if (key === 'bnode') return makeBlankNode(args, options);
|
|
3641
|
+
if (key === 'concat') return args.map(termToString).join('');
|
|
3642
|
+
if (key === 'lcase') return termToString(args[0]).toLowerCase();
|
|
3643
|
+
if (key === 'ucase') return termToString(args[0]).toUpperCase();
|
|
3644
|
+
if (key === 'contains') return termToString(args[0]).includes(termToString(args[1]));
|
|
3645
|
+
if (key === 'strstarts') return termToString(args[0]).startsWith(termToString(args[1]));
|
|
3646
|
+
if (key === 'strends') return termToString(args[0]).endsWith(termToString(args[1]));
|
|
3647
|
+
if (key === 'strbefore') {
|
|
3648
|
+
const s = termToString(args[0]);
|
|
3649
|
+
const needle = termToString(args[1]);
|
|
3650
|
+
const index = s.indexOf(needle);
|
|
3651
|
+
return index < 0 ? '' : s.slice(0, index);
|
|
3652
|
+
}
|
|
3653
|
+
if (key === 'strafter') {
|
|
3654
|
+
const s = termToString(args[0]);
|
|
3655
|
+
const needle = termToString(args[1]);
|
|
3656
|
+
const index = s.indexOf(needle);
|
|
3657
|
+
return index < 0 ? '' : s.slice(index + needle.length);
|
|
3658
|
+
}
|
|
3659
|
+
if (key === 'encode_for_uri') return encodeURIComponent(termToString(args[0]));
|
|
3660
|
+
if (key === 'regex') return regex(args);
|
|
3661
|
+
if (key === 'replace') return replace(args);
|
|
3662
|
+
if (key === 'substr') return substr(args);
|
|
3663
|
+
if (key === 'sameterm') return termishEquals(args[0], args[1]);
|
|
3664
|
+
if (key === 'isiri' || key === 'isuri') return isIRI(args[0]);
|
|
3665
|
+
if (key === 'isblank') return isBlank(args[0]);
|
|
3666
|
+
if (key === 'isliteral') return isLiteral(args[0]);
|
|
3667
|
+
if (key === 'istriple') return isTripleTerm(args[0]);
|
|
3668
|
+
if (key === 'isnumeric') return isNumericValue(args[0]);
|
|
3669
|
+
if (key === 'datatype') return datatypeOf(args[0]);
|
|
3670
|
+
if (key === 'lang') return args[0] && args[0].type === 'literal' ? (args[0].lang || '') : '';
|
|
3671
|
+
if (key === 'langmatches') return langMatches(termToString(args[0]), termToString(args[1]));
|
|
3672
|
+
if (key === 'haslang') return !!(args[0] && args[0].type === 'literal' && args[0].lang);
|
|
3673
|
+
if (key === 'langdir') return args[0] && args[0].type === 'literal' ? (args[0].langDir || '') : '';
|
|
3674
|
+
if (key === 'haslangdir') return !!(args[0] && args[0].type === 'literal' && args[0].langDir);
|
|
3675
|
+
if (key === 'strlen') return termToString(args[0]).length;
|
|
3676
|
+
if (key === 'abs') return Math.abs(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
3677
|
+
if (key === 'floor') return Math.floor(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
3678
|
+
if (key === 'ceil') return Math.ceil(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
3679
|
+
if (key === 'round') return Math.round(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));
|
|
3680
|
+
if (key === 'if') return booleanValue(args[0]) ? args[1] : args[2];
|
|
3681
|
+
if (key === 'strdt') return literal(termToString(args[0]), termToString(args[1]));
|
|
3682
|
+
if (key === 'strlang') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase());
|
|
3683
|
+
if (key === 'strlangdir') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase(), termToString(args[2]).toLowerCase());
|
|
3684
|
+
if (key === 'triple') return tripleTerm(valueToTermIfNeeded(args[0]), valueToTermIfNeeded(args[1]), valueToTermIfNeeded(args[2]));
|
|
3685
|
+
if (key === 'subject') return isTripleTerm(args[0]) ? args[0].s : null;
|
|
3686
|
+
if (key === 'predicate') return isTripleTerm(args[0]) ? args[0].p : null;
|
|
3687
|
+
if (key === 'object') return isTripleTerm(args[0]) ? args[0].o : null;
|
|
3688
|
+
if (key === 'year') return datePart(args[0], 'year');
|
|
3689
|
+
if (key === 'month') return datePart(args[0], 'month');
|
|
3690
|
+
if (key === 'day') return datePart(args[0], 'day');
|
|
3691
|
+
if (key === 'hours') return datePart(args[0], 'hours');
|
|
3692
|
+
if (key === 'minutes') return datePart(args[0], 'minutes');
|
|
3693
|
+
if (key === 'seconds') return datePart(args[0], 'seconds');
|
|
3694
|
+
if (key === 'timezone') return timezoneDuration(args[0]);
|
|
3695
|
+
if (key === 'tz') return timezoneLexical(args[0]);
|
|
3696
|
+
if (key === 'now') return literal((options.now || new Date()).toISOString(), XSD_DATETIME);
|
|
3697
|
+
if (key === 'uuid') return iri(`urn:uuid:${freshUuid(options)}`);
|
|
3698
|
+
if (key === 'struuid') return freshUuid(options);
|
|
3699
|
+
throw new Error(`Unimplemented builtin ${name}`);
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
|
|
3703
|
+
function localName(name) {
|
|
3704
|
+
const text = String(name || '');
|
|
3705
|
+
const hash = text.lastIndexOf('#');
|
|
3706
|
+
const slash = text.lastIndexOf('/');
|
|
3707
|
+
const colon = text.lastIndexOf(':');
|
|
3708
|
+
const index = Math.max(hash, slash, colon);
|
|
3709
|
+
return index >= 0 ? text.slice(index + 1) : text;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
function solveSudoku(puzzle) {
|
|
3713
|
+
const text = String(puzzle || '').trim();
|
|
3714
|
+
if (!/^[0-9.]{81}$/.test(text)) throw new Error('SUDOKU expects an 81-character puzzle string containing digits or dots');
|
|
3715
|
+
const cells = Array.from(text, (ch) => (ch === '.' ? 0 : Number(ch)));
|
|
3716
|
+
const peers = sudokuPeers();
|
|
3717
|
+
|
|
3718
|
+
for (let i = 0; i < 81; i += 1) {
|
|
3719
|
+
const value = cells[i];
|
|
3720
|
+
if (value === 0) continue;
|
|
3721
|
+
for (const peer of peers[i]) {
|
|
3722
|
+
if (cells[peer] === value) throw new Error('SUDOKU puzzle has conflicting givens');
|
|
2383
3723
|
}
|
|
2384
|
-
return literal(token.value);
|
|
2385
3724
|
}
|
|
2386
3725
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
throw this.error(`Expected datatype IRI, got ${token.value}`, token);
|
|
2392
|
-
}
|
|
3726
|
+
const solved = solveSudokuCells(cells, peers);
|
|
3727
|
+
if (!solved) return '';
|
|
3728
|
+
return solved.join('');
|
|
3729
|
+
}
|
|
2393
3730
|
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
}
|
|
3731
|
+
function solveSudokuCells(cells, peers) {
|
|
3732
|
+
let bestIndex = -1;
|
|
3733
|
+
let bestCandidates = null;
|
|
2398
3734
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
3735
|
+
for (let i = 0; i < 81; i += 1) {
|
|
3736
|
+
if (cells[i] !== 0) continue;
|
|
3737
|
+
const candidates = sudokuCandidates(cells, peers[i]);
|
|
3738
|
+
if (candidates.length === 0) return null;
|
|
3739
|
+
if (!bestCandidates || candidates.length < bestCandidates.length) {
|
|
3740
|
+
bestIndex = i;
|
|
3741
|
+
bestCandidates = candidates;
|
|
3742
|
+
if (candidates.length === 1) break;
|
|
2404
3743
|
}
|
|
2405
|
-
return out;
|
|
2406
3744
|
}
|
|
2407
3745
|
|
|
2408
|
-
|
|
2409
|
-
const colon = value.indexOf(':');
|
|
2410
|
-
if (colon < 0) throw this.error(`Expected prefixed name, got ${value}`, token);
|
|
2411
|
-
const prefix = value.slice(0, colon);
|
|
2412
|
-
const local = value.slice(colon + 1);
|
|
2413
|
-
if (!Object.hasOwn(this.prefixes, prefix)) throw this.error(`Unknown prefix ${prefix}:`, token);
|
|
2414
|
-
return this.prefixes[prefix] + local;
|
|
2415
|
-
}
|
|
3746
|
+
if (bestIndex < 0) return cells;
|
|
2416
3747
|
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
3748
|
+
for (const value of bestCandidates) {
|
|
3749
|
+
const next = cells.slice();
|
|
3750
|
+
next[bestIndex] = value;
|
|
3751
|
+
const solved = solveSudokuCells(next, peers);
|
|
3752
|
+
if (solved) return solved;
|
|
2420
3753
|
}
|
|
3754
|
+
return null;
|
|
3755
|
+
}
|
|
2421
3756
|
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
3757
|
+
function sudokuCandidates(cells, peers) {
|
|
3758
|
+
const used = new Set();
|
|
3759
|
+
for (const peer of peers) if (cells[peer] !== 0) used.add(cells[peer]);
|
|
3760
|
+
const out = [];
|
|
3761
|
+
for (let value = 1; value <= 9; value += 1) if (!used.has(value)) out.push(value);
|
|
3762
|
+
return out;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
let SUDOKU_PEERS = null;
|
|
3766
|
+
function sudokuPeers() {
|
|
3767
|
+
if (SUDOKU_PEERS) return SUDOKU_PEERS;
|
|
3768
|
+
SUDOKU_PEERS = Array.from({ length: 81 }, (_, index) => {
|
|
3769
|
+
const row = Math.floor(index / 9);
|
|
3770
|
+
const col = index % 9;
|
|
3771
|
+
const boxRow = Math.floor(row / 3) * 3;
|
|
3772
|
+
const boxCol = Math.floor(col / 3) * 3;
|
|
3773
|
+
const peers = new Set();
|
|
3774
|
+
for (let c = 0; c < 9; c += 1) peers.add(row * 9 + c);
|
|
3775
|
+
for (let r = 0; r < 9; r += 1) peers.add(r * 9 + col);
|
|
3776
|
+
for (let r = boxRow; r < boxRow + 3; r += 1) {
|
|
3777
|
+
for (let c = boxCol; c < boxCol + 3; c += 1) peers.add(r * 9 + c);
|
|
2426
3778
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
3779
|
+
peers.delete(index);
|
|
3780
|
+
return Array.from(peers);
|
|
3781
|
+
});
|
|
3782
|
+
return SUDOKU_PEERS;
|
|
3783
|
+
}
|
|
2429
3784
|
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
expectValue(value) { const token = this.advance(); if (token.value !== value) throw this.error(`Expected ${value}, got ${token.value}`, token); return token; }
|
|
2440
|
-
error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token); }
|
|
3785
|
+
function validateArity(canonical, actual) {
|
|
3786
|
+
const sig = BUILTIN_SIGNATURES[canonical];
|
|
3787
|
+
if (!sig) throw new Error(`Unknown builtin ${canonical}`);
|
|
3788
|
+
const tooFew = actual < sig.min;
|
|
3789
|
+
const tooMany = actual > sig.max;
|
|
3790
|
+
if (tooFew || tooMany) {
|
|
3791
|
+
const expected = sig.min === sig.max ? `${sig.min}` : `${sig.min}${sig.max === Infinity ? '+' : `..${sig.max}`}`;
|
|
3792
|
+
throw new Error(`${canonical} expects ${expected} argument${expected === '1' ? '' : 's'}, got ${actual}`);
|
|
3793
|
+
}
|
|
2441
3794
|
}
|
|
2442
3795
|
|
|
2443
|
-
function
|
|
2444
|
-
|
|
3796
|
+
function makeIRI(value, options) {
|
|
3797
|
+
if (options.baseIRI && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) {
|
|
3798
|
+
try { return iri(new URL(value, options.baseIRI).href); } catch (_) { /* fall through */ }
|
|
3799
|
+
}
|
|
3800
|
+
return iri(value);
|
|
2445
3801
|
}
|
|
2446
3802
|
|
|
2447
|
-
function
|
|
2448
|
-
|
|
2449
|
-
|
|
3803
|
+
function makeBlankNode(args, options) {
|
|
3804
|
+
if (args.length === 0) return blankNode(freshId(options));
|
|
3805
|
+
const label = termToString(args[0]);
|
|
3806
|
+
if (!options.__bnodeLabels) options.__bnodeLabels = new Map();
|
|
3807
|
+
if (!options.__bnodeLabels.has(label)) options.__bnodeLabels.set(label, label || freshId(options));
|
|
3808
|
+
return blankNode(options.__bnodeLabels.get(label));
|
|
2450
3809
|
}
|
|
2451
3810
|
|
|
2452
|
-
function
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
|
|
3811
|
+
function regex(args) {
|
|
3812
|
+
const flags = regexFlags(termToString(args[2] || ''));
|
|
3813
|
+
return new RegExp(termToString(args[1]), flags).test(termToString(args[0]));
|
|
3814
|
+
}
|
|
2456
3815
|
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
data: [],
|
|
2463
|
-
rules: [],
|
|
2464
|
-
rdfSyntax: true,
|
|
2465
|
-
options: { shacl12Conformance: !!options.shacl12Conformance },
|
|
2466
|
-
ruleSets: ruleSetNodes.map((term) => formatTerm(term, document.prefixes)),
|
|
2467
|
-
};
|
|
3816
|
+
function replace(args) {
|
|
3817
|
+
const flags = regexFlags(termToString(args[3] || ''));
|
|
3818
|
+
const effectiveFlags = flags.includes('g') ? flags : `${flags}g`;
|
|
3819
|
+
return termToString(args[0]).replace(new RegExp(termToString(args[1]), effectiveFlags), termToString(args[2]));
|
|
3820
|
+
}
|
|
2468
3821
|
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
}
|
|
3822
|
+
function regexFlags(flags) {
|
|
3823
|
+
let out = '';
|
|
3824
|
+
for (const ch of String(flags)) {
|
|
3825
|
+
// JavaScript RegExp has no direct SPARQL/xpath "x" free-spacing flag, so ignore it.
|
|
3826
|
+
if (ch === 'x') continue;
|
|
3827
|
+
if ('imsuyg'.includes(ch) && !out.includes(ch)) out += ch;
|
|
2476
3828
|
}
|
|
2477
|
-
return
|
|
3829
|
+
return out;
|
|
2478
3830
|
}
|
|
2479
3831
|
|
|
2480
|
-
function
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
const typed = graph.subjects(RDF_TYPE, iri(SRL_RULE_SET));
|
|
2486
|
-
if (typed.length > 0) return uniqueTerms(typed);
|
|
2487
|
-
const byData = graph.subjectsWithPredicate(SRL_DATA);
|
|
2488
|
-
const byRules = graph.subjectsWithPredicate(SRL_RULES);
|
|
2489
|
-
return uniqueTerms([...byData, ...byRules]).filter((term) => graph.objects(term, SRL_RULES).length > 0 || graph.objects(term, SRL_DATA).length > 0);
|
|
3832
|
+
function substr(args) {
|
|
3833
|
+
const value = termToString(args[0]);
|
|
3834
|
+
const start = Math.max(0, Number(termToPrimitive(valueToTermIfNeeded(args[1]))) - 1);
|
|
3835
|
+
if (args.length >= 3) return value.substring(start, start + Number(termToPrimitive(valueToTermIfNeeded(args[2]))));
|
|
3836
|
+
return value.substring(start);
|
|
2490
3837
|
}
|
|
2491
3838
|
|
|
2492
|
-
function
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
if (
|
|
2496
|
-
if (
|
|
2497
|
-
return
|
|
3839
|
+
function datatypeOf(value) {
|
|
3840
|
+
const term = valueToTermIfNeeded(value);
|
|
3841
|
+
if (term.type !== 'literal') return null;
|
|
3842
|
+
if (term.langDir) return iri(RDF_DIRLANGSTRING);
|
|
3843
|
+
if (term.lang) return iri(RDF_LANGSTRING);
|
|
3844
|
+
return iri(term.datatype || inferDatatype(term.value) || XSD_STRING);
|
|
2498
3845
|
}
|
|
2499
3846
|
|
|
2500
|
-
function
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
if (
|
|
2504
|
-
|
|
2505
|
-
const head = graph.list(headLists[0]).map((item) => toTripleLike(item, graph));
|
|
2506
|
-
return { name: graph.label(ruleNode), head, body, runOnce: ruleNeedsRunOnce(head, body, options) };
|
|
3847
|
+
function isNumericValue(value) {
|
|
3848
|
+
const term = valueToTermIfNeeded(value);
|
|
3849
|
+
if (typeof termToPrimitive(term) === 'bigint') return true;
|
|
3850
|
+
if (typeof termToPrimitive(term) === 'number') return true;
|
|
3851
|
+
return term.type === 'literal' && NUMERIC_DATATYPES.has(term.datatype);
|
|
2507
3852
|
}
|
|
2508
3853
|
|
|
2509
|
-
function
|
|
2510
|
-
|
|
2511
|
-
const
|
|
2512
|
-
if (
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
if (
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
3854
|
+
function datePart(value, part) {
|
|
3855
|
+
const lexical = termToString(value);
|
|
3856
|
+
const match = lexical.match(/^(-?\d{4,})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:?\d{2})?)?/);
|
|
3857
|
+
if (!match) return null;
|
|
3858
|
+
const [, year, month, day, hours = '0', minutes = '0', seconds = '0'] = match;
|
|
3859
|
+
if (part === 'year') return Number(year);
|
|
3860
|
+
if (part === 'month') return Number(month);
|
|
3861
|
+
if (part === 'day') return Number(day);
|
|
3862
|
+
if (part === 'hours') return Number(hours);
|
|
3863
|
+
if (part === 'minutes') return Number(minutes);
|
|
3864
|
+
if (part === 'seconds') return Number(seconds);
|
|
3865
|
+
return null;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
function timezoneLexical(value) {
|
|
3869
|
+
const lexical = termToString(value);
|
|
3870
|
+
const match = lexical.match(/(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:?\d{2})$/);
|
|
3871
|
+
return match ? match[1] : '';
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
function timezoneDuration(value) {
|
|
3875
|
+
const zone = timezoneLexical(value);
|
|
3876
|
+
if (!zone) return null;
|
|
3877
|
+
if (zone === 'Z') return literal('PT0S', XSD_DAYTIME_DURATION);
|
|
3878
|
+
const match = zone.match(/^([+-])(\d{2}):?(\d{2})$/);
|
|
3879
|
+
if (!match) return null;
|
|
3880
|
+
const [, sign, hh, mm] = match;
|
|
3881
|
+
const hours = Number(hh);
|
|
3882
|
+
const minutes = Number(mm);
|
|
3883
|
+
const body = `${hours ? `${hours}H` : ''}${minutes ? `${minutes}M` : ''}` || '0S';
|
|
3884
|
+
return literal(`${sign === '-' ? '-' : ''}PT${body}`, XSD_DAYTIME_DURATION);
|
|
2538
3885
|
}
|
|
2539
3886
|
|
|
2540
|
-
function
|
|
2541
|
-
if (
|
|
2542
|
-
|
|
2543
|
-
const predicates = graph.objects(node, SRL_PREDICATE);
|
|
2544
|
-
const objects = graph.objects(node, SRL_OBJECT);
|
|
2545
|
-
if (subjects.length !== 1 || predicates.length !== 1 || objects.length !== 1) {
|
|
2546
|
-
throw new Error(`Triple node ${graph.label(node)} must have exactly one srl:subject, srl:predicate and srl:object`);
|
|
2547
|
-
}
|
|
2548
|
-
return {
|
|
2549
|
-
s: toVarOrTerm(subjects[0], graph),
|
|
2550
|
-
p: toVarOrTerm(predicates[0], graph),
|
|
2551
|
-
o: toVarOrTerm(objects[0], graph),
|
|
2552
|
-
};
|
|
3887
|
+
function langMatches(lang, range) {
|
|
3888
|
+
if (range === '*') return lang.length > 0;
|
|
3889
|
+
return lang.toLowerCase() === range.toLowerCase() || lang.toLowerCase().startsWith(`${range.toLowerCase()}-`);
|
|
2553
3890
|
}
|
|
2554
3891
|
|
|
2555
|
-
function
|
|
2556
|
-
|
|
3892
|
+
function freshUuid(options) {
|
|
3893
|
+
if (typeof options.uuidGenerator === 'function') return String(options.uuidGenerator());
|
|
3894
|
+
options.__eyelengUuidCounter = (options.__eyelengUuidCounter || 0) + 1;
|
|
3895
|
+
return `00000000-0000-4000-8000-${String(options.__eyelengUuidCounter).padStart(12, '0')}`;
|
|
2557
3896
|
}
|
|
2558
3897
|
|
|
2559
|
-
function
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Variable node ${graph.label(node)} must have exactly one string srl:varName`);
|
|
2563
|
-
return variable(String(varNames[0].value));
|
|
2564
|
-
}
|
|
2565
|
-
return node;
|
|
3898
|
+
function freshId(options) {
|
|
3899
|
+
options.__eyelengCounter = (options.__eyelengCounter || 0) + 1;
|
|
3900
|
+
return `eyeleng-${options.__eyelengCounter}`;
|
|
2566
3901
|
}
|
|
2567
3902
|
|
|
2568
|
-
function
|
|
2569
|
-
|
|
2570
|
-
if (varNames.length > 0) {
|
|
2571
|
-
if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Expression variable ${graph.label(node)} must name one variable`);
|
|
2572
|
-
return { type: 'var', name: String(varNames[0].value) };
|
|
2573
|
-
}
|
|
2574
|
-
if (node.type === 'literal') {
|
|
2575
|
-
if (node.datatype || node.lang) return { type: 'term', value: node };
|
|
2576
|
-
return { type: 'literal', value: node.value };
|
|
2577
|
-
}
|
|
2578
|
-
if (node.type === 'iri' || node.type === 'blank' || node.type === 'triple') {
|
|
2579
|
-
const call = graph.functionCall(node);
|
|
2580
|
-
if (call) return toFunctionExpression(call.name, call.args.map((arg) => toExpression(arg, graph)));
|
|
2581
|
-
if (node.type === 'blank' && graph.hasOutgoing(node)) return { type: 'term', value: node };
|
|
2582
|
-
return { type: 'term', value: toVarOrTerm(node, graph) };
|
|
2583
|
-
}
|
|
2584
|
-
return { type: 'term', value: node };
|
|
3903
|
+
function asTerm(value) {
|
|
3904
|
+
return valueToTerm(value);
|
|
2585
3905
|
}
|
|
2586
3906
|
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
3907
|
+
module.exports = {
|
|
3908
|
+
BUILTIN_SIGNATURES,
|
|
3909
|
+
builtinNames,
|
|
3910
|
+
canonicalBuiltinName,
|
|
3911
|
+
isBuiltinName,
|
|
3912
|
+
validateArity,
|
|
3913
|
+
evalExpression,
|
|
3914
|
+
booleanValue,
|
|
3915
|
+
asTerm,
|
|
3916
|
+
callBuiltin,
|
|
3917
|
+
evalBinary,
|
|
3918
|
+
};
|
|
3919
|
+
|
|
3920
|
+
},
|
|
3921
|
+
"src/rdfMessages.js": function (require, module, exports) {
|
|
3922
|
+
'use strict';
|
|
3923
|
+
|
|
3924
|
+
const { parseRdfDocument } = require('./rdfSyntax.js');
|
|
3925
|
+
const {
|
|
3926
|
+
iri,
|
|
3927
|
+
blankNode,
|
|
3928
|
+
literal,
|
|
3929
|
+
tripleTerm,
|
|
3930
|
+
RDF_TYPE,
|
|
3931
|
+
RDF_FIRST,
|
|
3932
|
+
RDF_REST,
|
|
3933
|
+
RDF_NIL,
|
|
3934
|
+
XSD_INTEGER,
|
|
3935
|
+
} = require('./term.js');
|
|
3936
|
+
|
|
3937
|
+
const RDF_MESSAGE_VERSION_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/im;
|
|
3938
|
+
const RDF_MESSAGE_VERSION_LINE_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/i;
|
|
3939
|
+
const RDF_DIRECTIVE_LINE_RE = /^\s*(?:@?(?:prefix|base)\b|PREFIX\b|BASE\b)/i;
|
|
3940
|
+
const RDF_MESSAGE_DELIMITER_LINE_RE = /^\s*(?:MESSAGE\b|@message\s*\.?)\s*(?:#.*)?$/i;
|
|
3941
|
+
|
|
3942
|
+
const EYMSG_NS = 'https://eyereasoner.github.io/eyeling/vocab/message#';
|
|
3943
|
+
const LOG_NS = 'http://www.w3.org/2000/10/swap/log#';
|
|
3944
|
+
const EYMSG = Object.freeze({
|
|
3945
|
+
RDFMessageStream: `${EYMSG_NS}RDFMessageStream`,
|
|
3946
|
+
MessageEnvelope: `${EYMSG_NS}MessageEnvelope`,
|
|
3947
|
+
envelope: `${EYMSG_NS}envelope`,
|
|
3948
|
+
firstEnvelope: `${EYMSG_NS}firstEnvelope`,
|
|
3949
|
+
lastEnvelope: `${EYMSG_NS}lastEnvelope`,
|
|
3950
|
+
orderedEnvelopes: `${EYMSG_NS}orderedEnvelopes`,
|
|
3951
|
+
messageCount: `${EYMSG_NS}messageCount`,
|
|
3952
|
+
offset: `${EYMSG_NS}offset`,
|
|
3953
|
+
nextEnvelope: `${EYMSG_NS}nextEnvelope`,
|
|
3954
|
+
payloadGraph: `${EYMSG_NS}payloadGraph`,
|
|
3955
|
+
payloadKind: `${EYMSG_NS}payloadKind`,
|
|
3956
|
+
payloadTriple: `${EYMSG_NS}payloadTriple`,
|
|
3957
|
+
tripleCount: `${EYMSG_NS}tripleCount`,
|
|
3958
|
+
empty: `${EYMSG_NS}empty`,
|
|
3959
|
+
nonEmpty: `${EYMSG_NS}nonEmpty`,
|
|
3960
|
+
});
|
|
3961
|
+
const LOG_NAME_OF = `${LOG_NS}nameOf`;
|
|
3962
|
+
|
|
3963
|
+
function simpleHashText(value) {
|
|
3964
|
+
let h = 0x811c9dc5;
|
|
3965
|
+
const text = String(value || '');
|
|
3966
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
3967
|
+
h ^= text.charCodeAt(i);
|
|
3968
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
2605
3969
|
}
|
|
2606
|
-
return
|
|
3970
|
+
return h.toString(16).padStart(8, '0');
|
|
2607
3971
|
}
|
|
2608
3972
|
|
|
2609
|
-
function
|
|
2610
|
-
|
|
2611
|
-
return { type: 'binary', op, left: args[0], right: args[1] };
|
|
3973
|
+
function looksLikeRdfMessageLog(source, options = {}) {
|
|
3974
|
+
return !!options.rdfMessages || RDF_MESSAGE_VERSION_RE.test(String(source || ''));
|
|
2612
3975
|
}
|
|
2613
3976
|
|
|
2614
|
-
function
|
|
2615
|
-
|
|
2616
|
-
return args.slice(1).reduce((left, right) => ({ type: 'binary', op, left, right }), args[0]);
|
|
3977
|
+
function splitPreservingLineEndings(text) {
|
|
3978
|
+
return String(text || '').match(/.*(?:\r\n|\n|\r)|.+$/g) || [];
|
|
2617
3979
|
}
|
|
2618
3980
|
|
|
2619
|
-
function
|
|
2620
|
-
return
|
|
3981
|
+
function isOnlyWhitespaceAndComments(text) {
|
|
3982
|
+
return splitPreservingLineEndings(text).every((line) => {
|
|
3983
|
+
const hash = line.indexOf('#');
|
|
3984
|
+
const body = hash >= 0 ? line.slice(0, hash) : line;
|
|
3985
|
+
return body.trim() === '';
|
|
3986
|
+
});
|
|
2621
3987
|
}
|
|
2622
3988
|
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
this.prefixes = prefixes;
|
|
2627
|
-
this.bySubject = new Map();
|
|
2628
|
-
for (const triple of triples) {
|
|
2629
|
-
const key = termKey(triple.s);
|
|
2630
|
-
if (!this.bySubject.has(key)) this.bySubject.set(key, []);
|
|
2631
|
-
this.bySubject.get(key).push(triple);
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
3989
|
+
function stripMessageVersionLines(text) {
|
|
3990
|
+
return splitPreservingLineEndings(text).filter((line) => !RDF_MESSAGE_VERSION_LINE_RE.test(line)).join('');
|
|
3991
|
+
}
|
|
2634
3992
|
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
}
|
|
3993
|
+
function stripDirectiveLines(text) {
|
|
3994
|
+
return splitPreservingLineEndings(text).filter((line) => !RDF_DIRECTIVE_LINE_RE.test(line) && !RDF_MESSAGE_VERSION_LINE_RE.test(line)).join('');
|
|
3995
|
+
}
|
|
2639
3996
|
|
|
2640
|
-
|
|
2641
|
-
|
|
3997
|
+
function collectDirectiveLines(text) {
|
|
3998
|
+
const seen = new Set();
|
|
3999
|
+
const out = [];
|
|
4000
|
+
for (const line of splitPreservingLineEndings(text)) {
|
|
4001
|
+
if (!RDF_DIRECTIVE_LINE_RE.test(line)) continue;
|
|
4002
|
+
const key = line.trim();
|
|
4003
|
+
if (!key || seen.has(key)) continue;
|
|
4004
|
+
seen.add(key);
|
|
4005
|
+
out.push(line.endsWith('\n') || line.endsWith('\r') ? line : `${line}\n`);
|
|
2642
4006
|
}
|
|
4007
|
+
return out;
|
|
4008
|
+
}
|
|
2643
4009
|
|
|
2644
|
-
|
|
2645
|
-
|
|
4010
|
+
function readStringAt(source, index) {
|
|
4011
|
+
const quote = source[index];
|
|
4012
|
+
const long = source.startsWith(quote.repeat(3), index);
|
|
4013
|
+
let i = index + (long ? 3 : 1);
|
|
4014
|
+
while (i < source.length) {
|
|
4015
|
+
if (source[i] === '\\') { i += 2; continue; }
|
|
4016
|
+
if (long && source.startsWith(quote.repeat(3), i)) return { end: i + 3 };
|
|
4017
|
+
if (!long && source[i] === quote) return { end: i + 1 };
|
|
4018
|
+
i += 1;
|
|
2646
4019
|
}
|
|
4020
|
+
return { end: source.length };
|
|
4021
|
+
}
|
|
2647
4022
|
|
|
2648
|
-
|
|
2649
|
-
|
|
4023
|
+
function readIriAt(source, index) {
|
|
4024
|
+
let i = index + 1;
|
|
4025
|
+
while (i < source.length) {
|
|
4026
|
+
if (source[i] === '\\') { i += 2; continue; }
|
|
4027
|
+
if (source[i] === '>') return { end: i + 1 };
|
|
4028
|
+
i += 1;
|
|
2650
4029
|
}
|
|
4030
|
+
return { end: source.length };
|
|
4031
|
+
}
|
|
2651
4032
|
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
seen.add(key);
|
|
2660
|
-
const first = this.objects(node, RDF_FIRST);
|
|
2661
|
-
const rest = this.objects(node, RDF_REST);
|
|
2662
|
-
if (first.length !== 1 || rest.length !== 1) throw new Error(`Expected RDF list node at ${this.label(node)}`);
|
|
2663
|
-
out.push(first[0]);
|
|
2664
|
-
node = rest[0];
|
|
4033
|
+
function skipWsAndComments(source, index) {
|
|
4034
|
+
let i = index;
|
|
4035
|
+
while (i < source.length) {
|
|
4036
|
+
if (/\s/.test(source[i])) { i += 1; continue; }
|
|
4037
|
+
if (source[i] === '#') {
|
|
4038
|
+
while (i < source.length && source[i] !== '\n' && source[i] !== '\r') i += 1;
|
|
4039
|
+
continue;
|
|
2665
4040
|
}
|
|
2666
|
-
|
|
4041
|
+
break;
|
|
2667
4042
|
}
|
|
4043
|
+
return i;
|
|
4044
|
+
}
|
|
2668
4045
|
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
4046
|
+
function isWordChar(ch) { return !!ch && /[A-Za-z0-9_\-]/.test(ch); }
|
|
4047
|
+
function startsWordAt(source, word, index) {
|
|
4048
|
+
return source.slice(index, index + word.length).toUpperCase() === word && !isWordChar(source[index - 1]) && !isWordChar(source[index + word.length]);
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
function findMessageDirectiveAt(source, index) {
|
|
4052
|
+
if (startsWordAt(source, 'MESSAGE', index)) return { start: index, end: index + 'MESSAGE'.length };
|
|
4053
|
+
if (source.slice(index, index + 8).toLowerCase() === '@message' && !isWordChar(source[index + 8])) {
|
|
4054
|
+
let end = index + 8;
|
|
4055
|
+
end = skipWsAndComments(source, end);
|
|
4056
|
+
if (source[end] === '.') end += 1;
|
|
4057
|
+
return { start: index, end };
|
|
2676
4058
|
}
|
|
4059
|
+
return null;
|
|
4060
|
+
}
|
|
2677
4061
|
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
4062
|
+
function splitRdfMessageLog(source) {
|
|
4063
|
+
const text = stripMessageVersionLines(source);
|
|
4064
|
+
const chunks = [];
|
|
4065
|
+
let i = 0;
|
|
4066
|
+
let start = 0;
|
|
4067
|
+
let braceDepth = 0;
|
|
4068
|
+
let bracketDepth = 0;
|
|
4069
|
+
let parenDepth = 0;
|
|
4070
|
+
let statementStart = true;
|
|
4071
|
+
let sawDelimiter = false;
|
|
4072
|
+
|
|
4073
|
+
while (i < text.length) {
|
|
4074
|
+
const ch = text[i];
|
|
4075
|
+
if (ch === '"' || ch === "'") { i = readStringAt(text, i).end; statementStart = false; continue; }
|
|
4076
|
+
if (ch === '<' && !text.startsWith('<<', i)) { i = readIriAt(text, i).end; statementStart = false; continue; }
|
|
4077
|
+
if (ch === '#') { while (i < text.length && text[i] !== '\n' && text[i] !== '\r') i += 1; statementStart = true; continue; }
|
|
4078
|
+
|
|
4079
|
+
if (statementStart && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
|
|
4080
|
+
const termStart = skipWsAndComments(text, i);
|
|
4081
|
+
const directive = findMessageDirectiveAt(text, termStart);
|
|
4082
|
+
if (directive) {
|
|
4083
|
+
chunks.push(text.slice(start, termStart));
|
|
4084
|
+
start = directive.end;
|
|
4085
|
+
i = directive.end;
|
|
4086
|
+
statementStart = true;
|
|
4087
|
+
sawDelimiter = true;
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
if (termStart !== i) { i = termStart; continue; }
|
|
2688
4091
|
}
|
|
2689
|
-
return iri(text);
|
|
2690
|
-
}
|
|
2691
4092
|
|
|
2692
|
-
|
|
2693
|
-
|
|
4093
|
+
if (ch === '{') braceDepth += 1;
|
|
4094
|
+
else if (ch === '}' && braceDepth > 0) braceDepth -= 1;
|
|
4095
|
+
else if (ch === '[') bracketDepth += 1;
|
|
4096
|
+
else if (ch === ']' && bracketDepth > 0) bracketDepth -= 1;
|
|
4097
|
+
else if (ch === '(') parenDepth += 1;
|
|
4098
|
+
else if (ch === ')' && parenDepth > 0) parenDepth -= 1;
|
|
2694
4099
|
|
|
2695
|
-
|
|
2696
|
-
|
|
4100
|
+
if (ch === '.' && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) statementStart = true;
|
|
4101
|
+
else if (ch === '\n' || ch === '\r') statementStart = true;
|
|
4102
|
+
else if (!/\s/.test(ch)) statementStart = false;
|
|
4103
|
+
i += 1;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
const tail = text.slice(start);
|
|
4107
|
+
if (!sawDelimiter || !isOnlyWhitespaceAndComments(tail)) chunks.push(tail);
|
|
4108
|
+
return chunks;
|
|
2697
4109
|
}
|
|
2698
4110
|
|
|
2699
|
-
function
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
4111
|
+
function rewriteMessageBlankLabels(source, messageIndex) {
|
|
4112
|
+
const prefix = `msg${String(messageIndex).padStart(3, '0')}_`;
|
|
4113
|
+
let out = '';
|
|
4114
|
+
let i = 0;
|
|
4115
|
+
while (i < source.length) {
|
|
4116
|
+
const ch = source[i];
|
|
4117
|
+
if (ch === '"' || ch === "'") {
|
|
4118
|
+
const end = readStringAt(source, i).end;
|
|
4119
|
+
out += source.slice(i, end);
|
|
4120
|
+
i = end;
|
|
4121
|
+
continue;
|
|
4122
|
+
}
|
|
4123
|
+
if (ch === '<' && !source.startsWith('<<', i)) {
|
|
4124
|
+
const end = readIriAt(source, i).end;
|
|
4125
|
+
out += source.slice(i, end);
|
|
4126
|
+
i = end;
|
|
4127
|
+
continue;
|
|
4128
|
+
}
|
|
4129
|
+
if (ch === '#') {
|
|
4130
|
+
while (i < source.length) {
|
|
4131
|
+
const c = source[i++]; out += c;
|
|
4132
|
+
if (c === '\n' || c === '\r') break;
|
|
4133
|
+
}
|
|
4134
|
+
continue;
|
|
4135
|
+
}
|
|
4136
|
+
if (source.startsWith('_:', i)) {
|
|
4137
|
+
let j = i + 2;
|
|
4138
|
+
while (j < source.length && !/\s/.test(source[j]) && !'{}[](),;.<>'.includes(source[j])) j += 1;
|
|
4139
|
+
const label = source.slice(i + 2, j);
|
|
4140
|
+
if (label) {
|
|
4141
|
+
out += `_:${prefix}${label.replace(/[^A-Za-z0-9_\-]/g, '_')}`;
|
|
4142
|
+
i = j;
|
|
4143
|
+
continue;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
out += ch;
|
|
4147
|
+
i += 1;
|
|
2705
4148
|
}
|
|
2706
4149
|
return out;
|
|
2707
4150
|
}
|
|
2708
4151
|
|
|
2709
|
-
function
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
4152
|
+
function messageChunkHasRdf(chunk) {
|
|
4153
|
+
return !isOnlyWhitespaceAndComments(stripDirectiveLines(chunk));
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
function listTriples(headTerm, items, data, makeBlank) {
|
|
4157
|
+
if (items.length === 0) return iri(RDF_NIL);
|
|
4158
|
+
const cells = items.map(() => makeBlank());
|
|
4159
|
+
for (let i = 0; i < cells.length; i += 1) {
|
|
4160
|
+
data.push({ s: cells[i], p: iri(RDF_FIRST), o: items[i] });
|
|
4161
|
+
data.push({ s: cells[i], p: iri(RDF_REST), o: i + 1 < cells.length ? cells[i + 1] : iri(RDF_NIL) });
|
|
4162
|
+
}
|
|
4163
|
+
return headTerm || cells[0];
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
function parseRdfMessageLog(source, options = {}) {
|
|
4167
|
+
const text = String(source || '');
|
|
4168
|
+
const directives = collectDirectiveLines(text);
|
|
4169
|
+
const chunks = splitRdfMessageLog(text);
|
|
4170
|
+
// Keep generated message-log IRIs stable across machines and checkout paths.
|
|
4171
|
+
// The previous seed used baseIRI/filename, which made golden outputs depend on
|
|
4172
|
+
// absolute local paths such as file:///home/.../examples/rdf-messages.trig.
|
|
4173
|
+
// A caller that needs a location-specific identity can still pass
|
|
4174
|
+
// options.messageBaseIRI explicitly.
|
|
4175
|
+
const hash = simpleHashText(text);
|
|
4176
|
+
const base = options.messageBaseIRI || `urn:eyeleng:message-log:${hash}`;
|
|
4177
|
+
const stream = iri(`${base}#stream`);
|
|
4178
|
+
const envelopes = chunks.map((unused, index) => iri(`${base}#m${String(index + 1).padStart(3, '0')}`));
|
|
4179
|
+
const payloads = chunks.map((unused, index) => iri(`${base}#m${String(index + 1).padStart(3, '0')}/payload`));
|
|
4180
|
+
const data = [];
|
|
4181
|
+
let bnodeCounter = 0;
|
|
4182
|
+
const makeBlank = () => blankNode(`msg${++bnodeCounter}`);
|
|
4183
|
+
const prefixes = {
|
|
4184
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
4185
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
4186
|
+
eymsg: EYMSG_NS,
|
|
4187
|
+
log: LOG_NS,
|
|
4188
|
+
};
|
|
2720
4189
|
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
if (
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
4190
|
+
data.push({ s: stream, p: iri(RDF_TYPE), o: iri(EYMSG.RDFMessageStream) });
|
|
4191
|
+
data.push({ s: stream, p: iri(EYMSG.messageCount), o: literal(chunks.length, XSD_INTEGER) });
|
|
4192
|
+
if (envelopes.length > 0) {
|
|
4193
|
+
data.push({ s: stream, p: iri(EYMSG.orderedEnvelopes), o: listTriples(null, envelopes, data, makeBlank) });
|
|
4194
|
+
data.push({ s: stream, p: iri(EYMSG.firstEnvelope), o: envelopes[0] });
|
|
4195
|
+
data.push({ s: stream, p: iri(EYMSG.lastEnvelope), o: envelopes[envelopes.length - 1] });
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
4199
|
+
const envelope = envelopes[i];
|
|
4200
|
+
const payload = payloads[i];
|
|
4201
|
+
const rawChunk = chunks[i];
|
|
4202
|
+
const chunk = rewriteMessageBlankLabels(rawChunk, i + 1);
|
|
4203
|
+
const hasBody = messageChunkHasRdf(chunk);
|
|
4204
|
+
const bodySource = `${directives.join('')}\n${stripDirectiveLines(chunk)}`;
|
|
4205
|
+
const parsed = hasBody ? parseRdfDocument(bodySource, { ...options, filename: `${options.filename || '<message>'}#m${i + 1}`, baseIRI: options.baseIRI }) : { triples: [], prefixes: {} };
|
|
4206
|
+
Object.assign(prefixes, parsed.prefixes || {});
|
|
4207
|
+
const payloadTriples = parsed.triples || [];
|
|
4208
|
+
const tripleTerms = payloadTriples.map((t) => tripleTerm(t.s, t.p, t.o));
|
|
4209
|
+
|
|
4210
|
+
data.push({ s: stream, p: iri(EYMSG.envelope), o: envelope });
|
|
4211
|
+
data.push({ s: envelope, p: iri(RDF_TYPE), o: iri(EYMSG.MessageEnvelope) });
|
|
4212
|
+
data.push({ s: envelope, p: iri(EYMSG.offset), o: literal(i + 1, XSD_INTEGER) });
|
|
4213
|
+
data.push({ s: envelope, p: iri(EYMSG.payloadKind), o: iri(hasBody ? EYMSG.nonEmpty : EYMSG.empty) });
|
|
4214
|
+
data.push({ s: envelope, p: iri(EYMSG.tripleCount), o: literal(payloadTriples.length, XSD_INTEGER) });
|
|
4215
|
+
if (i + 1 < envelopes.length) data.push({ s: envelope, p: iri(EYMSG.nextEnvelope), o: envelopes[i + 1] });
|
|
4216
|
+
if (hasBody) {
|
|
4217
|
+
data.push({ s: envelope, p: iri(EYMSG.payloadGraph), o: payload });
|
|
4218
|
+
data.push({ s: payload, p: iri(LOG_NAME_OF), o: listTriples(null, tripleTerms, data, makeBlank) });
|
|
4219
|
+
for (const term of tripleTerms) data.push({ s: payload, p: iri(EYMSG.payloadTriple), o: term });
|
|
4220
|
+
if (options.includeMessageFacts) data.push(...payloadTriples);
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
2727
4223
|
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
4224
|
+
return {
|
|
4225
|
+
baseIRI: options.baseIRI || null,
|
|
4226
|
+
version: '1.2-messages',
|
|
4227
|
+
imports: [],
|
|
4228
|
+
prefixes,
|
|
4229
|
+
data,
|
|
4230
|
+
rules: [],
|
|
4231
|
+
};
|
|
2733
4232
|
}
|
|
2734
4233
|
|
|
2735
4234
|
module.exports = {
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
SRL_NS,
|
|
2744
|
-
SHNEX_NS,
|
|
2745
|
-
SPARQL_NS,
|
|
2746
|
-
SRL_RULE_SET,
|
|
2747
|
-
SRL_RULE,
|
|
2748
|
-
},
|
|
4235
|
+
EYMSG_NS,
|
|
4236
|
+
EYMSG,
|
|
4237
|
+
LOG_NS,
|
|
4238
|
+
LOG_NAME_OF,
|
|
4239
|
+
looksLikeRdfMessageLog,
|
|
4240
|
+
splitRdfMessageLog,
|
|
4241
|
+
parseRdfMessageLog,
|
|
2749
4242
|
};
|
|
2750
4243
|
|
|
2751
4244
|
},
|
|
@@ -4144,7 +5637,7 @@
|
|
|
4144
5637
|
|
|
4145
5638
|
},
|
|
4146
5639
|
};
|
|
4147
|
-
const __mappings = {"src/tokenizer.js":{},"src/term.js":{},"src/
|
|
5640
|
+
const __mappings = {"src/tokenizer.js":{},"src/assignments.js":{},"src/term.js":{},"src/rdfSyntax.js":{"./tokenizer.js":"src/tokenizer.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/builtins.js":{"./term.js":"src/term.js"},"src/parser.js":{"./tokenizer.js":"src/tokenizer.js","./rdfSyntax.js":"src/rdfSyntax.js","./builtins.js":"src/builtins.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/rdfMessages.js":{"./rdfSyntax.js":"src/rdfSyntax.js","./term.js":"src/term.js"},"src/store.js":{"./term.js":"src/term.js"},"src/analyze.js":{"./term.js":"src/term.js","./assignments.js":"src/assignments.js"},"src/engine.js":{"./store.js":"src/store.js","./term.js":"src/term.js","./builtins.js":"src/builtins.js","./analyze.js":"src/analyze.js"},"src/format.js":{"./term.js":"src/term.js"},"src/query.js":{"./parser.js":"src/parser.js","./store.js":"src/store.js","./engine.js":"src/engine.js","./api.js":"src/api.js"},"src/output.js":{},"src/api.js":{"./parser.js":"src/parser.js","./rdfSyntax.js":"src/rdfSyntax.js","./rdfMessages.js":"src/rdfMessages.js","./engine.js":"src/engine.js","./analyze.js":"src/analyze.js","./format.js":"src/format.js","./query.js":"src/query.js","./output.js":"src/output.js"},"src/cli.js":{"./api.js":"src/api.js","./term.js":"src/term.js"}};
|
|
4148
5641
|
const __cache = {};
|
|
4149
5642
|
function __require(id) {
|
|
4150
5643
|
if (!id.startsWith("src/")) return __nativeRequire(id);
|