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