eyeleng 1.0.4 → 1.0.6

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