eyeling 1.10.21 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/HANDBOOK.md +2 -88
  2. package/eyeling-builtins.ttl +0 -48
  3. package/eyeling.js +67 -314
  4. package/lib/engine.js +67 -309
  5. package/lib/rules.js +0 -5
  6. package/package.json +1 -1
  7. package/test/n3gen.test.js +4 -4
  8. package/test/package.test.js +1 -1
  9. package/tools/n3gen.js +6 -1883
  10. package/examples/bind-builtins.n3 +0 -11
  11. package/examples/bind.n3 +0 -7
  12. package/examples/brussels-brew-club.n3 +0 -119
  13. package/examples/builtins-string-math.n3 +0 -11
  14. package/examples/builtins-triple-termtests.n3 +0 -7
  15. package/examples/family.n3 +0 -10
  16. package/examples/filter-demorgan.n3 +0 -9
  17. package/examples/filter-in-notin.n3 +0 -10
  18. package/examples/filter-nested-or.n3 +0 -10
  19. package/examples/filter.n3 +0 -8
  20. package/examples/input/bind-builtins.srl +0 -30
  21. package/examples/input/bind.srl +0 -12
  22. package/examples/input/builtins-string-math.srl +0 -38
  23. package/examples/input/builtins-triple-termtests.srl +0 -27
  24. package/examples/input/family.srl +0 -12
  25. package/examples/input/filter-demorgan.srl +0 -15
  26. package/examples/input/filter-in-notin.srl +0 -15
  27. package/examples/input/filter-nested-or.srl +0 -15
  28. package/examples/input/filter.srl +0 -9
  29. package/examples/input/snaf.srl +0 -6
  30. package/examples/it-is-about-time.n3 +0 -580
  31. package/examples/json-pointer.n3 +0 -75
  32. package/examples/json-reconcile-vat.n3 +0 -361
  33. package/examples/output/bind-builtins.n3 +0 -9
  34. package/examples/output/bind.n3 +0 -3
  35. package/examples/output/brussels-brew-club.n3 +0 -22
  36. package/examples/output/builtins-string-math.n3 +0 -0
  37. package/examples/output/builtins-triple-termtests.n3 +0 -0
  38. package/examples/output/family.n3 +0 -13
  39. package/examples/output/filter-demorgan.n3 +0 -3
  40. package/examples/output/filter-in-notin.n3 +0 -4
  41. package/examples/output/filter-nested-or.n3 +0 -4
  42. package/examples/output/filter.n3 +0 -3
  43. package/examples/output/it-is-about-time.n3 +0 -0
  44. package/examples/output/json-pointer.n3 +0 -13
  45. package/examples/output/json-reconcile-vat.n3 +0 -226
  46. package/examples/output/snaf.n3 +0 -3
  47. package/examples/snaf.n3 +0 -6
package/tools/n3gen.js CHANGED
@@ -2,21 +2,17 @@
2
2
  'use strict';
3
3
 
4
4
  /*
5
- * n3gen.js — Convert Turtle (.ttl), TriG (.trig) or SRL (.srl) to N3.
5
+ * n3gen.js — Convert Turtle (.ttl) or TriG (.trig) to N3.
6
6
  *
7
7
  * This tool always emits N3 to stdout. The input syntax is selected by the file
8
8
  * extension:
9
9
  * - .ttl (RDF 1.2 Turtle)
10
10
  * - .trig (RDF 1.2 TriG)
11
- * - .srl (SRL rules)
12
- *
11
+ * *
13
12
  * TriG → N3 mapping (named graphs)
14
13
  * TriG: <graphName> { ...triples... }
15
14
  * N3: <graphName> rdfg:isGraph { ...triples... } .
16
15
  *
17
- * SRL → N3 mapping (rules)
18
- * SRL: RULE { Head } WHERE { Body }
19
- * N3: { Body } => { Head } .
20
16
  *
21
17
  * RDF 1.2 Turtle-star / TriG-star
22
18
  * - triple terms: rdf:reifies <<( s p o )>>
@@ -28,7 +24,6 @@
28
24
  * Usage
29
25
  * n3gen file.ttl > file.n3
30
26
  * n3gen file.trig > file.n3
31
- * n3gen file.srl > file.n3
32
27
  */
33
28
 
34
29
  const fs = require('node:fs/promises');
@@ -110,20 +105,6 @@ const rdfg = {
110
105
  const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
111
106
  const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
112
107
  const OWL_NS = 'http://www.w3.org/2002/07/owl#';
113
- const LOG_NS = 'http://www.w3.org/2000/10/swap/log#';
114
- const MATH_NS = 'http://www.w3.org/2000/10/swap/math#';
115
- const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
116
- const LIST_NS = 'http://www.w3.org/2000/10/swap/list#';
117
- const CRYPTO_NS = 'http://www.w3.org/2000/10/swap/crypto#';
118
- const TIME_NS = 'http://www.w3.org/2000/10/swap/time#';
119
- const TIMEFN_NS = 'https://w3id.org/time-fn#';
120
- const TIMEFN_BUILTIN_NAMES = new Set([
121
- 'periodMinInclusive',
122
- 'periodMaxInclusive',
123
- 'periodMinExclusive',
124
- 'periodMaxExclusive',
125
- 'bindDefaultTimezone',
126
- ]);
127
108
 
128
109
  // Avoid literal triple-quote sequences in this source (helps embedding in tools).
129
110
  const DQ3 = '"'.repeat(3);
@@ -2059,1869 +2040,15 @@ function trigToN3(trigText) {
2059
2040
  return writeN3RdfgIsGraph({ datasetQuads: quads, prefixes });
2060
2041
  }
2061
2042
 
2062
- function prefixEnvFromSrlPrefixes(prefixLines) {
2063
- const env = PrefixEnv.newDefault();
2064
- if (Array.isArray(prefixLines)) {
2065
- for (const { label, iri } of prefixLines) {
2066
- const lab = (label || '').trim();
2067
- const base = (iri || '').trim();
2068
- if (!lab || !base) continue;
2069
- // SRL uses "PREFIX :" for default prefix; store it as "" in PrefixEnv.
2070
- let pfx = lab.replace(/:$/, '');
2071
- if (pfx === ':') pfx = '';
2072
- env.setPrefix(pfx, base);
2073
- }
2074
- }
2075
- return env;
2076
- }
2077
-
2078
- function parseTriplesBlockAllowImplicitDots(bodyText, env) {
2079
- const p = new TurtleParser(lex(bodyText));
2080
- if (env) p.prefixes = env;
2081
- const triples = [];
2082
-
2083
- function canStartSubject(tok) {
2084
- if (!tok) return false;
2085
- return (
2086
- tok.typ === 'IriRef' ||
2087
- tok.typ === 'Ident' ||
2088
- tok.typ === 'Var' ||
2089
- tok.typ === 'LBracket' ||
2090
- tok.typ === 'LParen' ||
2091
- tok.typ === 'LBrace'
2092
- );
2093
- }
2094
-
2095
- while (p.peek().typ !== 'EOF') {
2096
- // Skip stray dots (permissive)
2097
- if (p.peek().typ === 'Dot') {
2098
- p.next();
2099
- continue;
2100
- }
2101
-
2102
- const subj = p.parseTerm();
2103
-
2104
- let more;
2105
- if (p.peek().typ === 'Dot') {
2106
- more = [];
2107
- if (p.pendingTriples.length > 0) {
2108
- more = p.pendingTriples;
2109
- p.pendingTriples = [];
2110
- }
2111
- p.next(); // consume dot
2112
- } else {
2113
- more = p.parsePredicateObjectList(subj);
2114
- // In SPARQL graph patterns, the '.' between triple blocks is optional.
2115
- if (p.peek().typ === 'Dot') p.next();
2116
- else if (p.peek().typ === 'EOF') {
2117
- /* ok */
2118
- } else if (canStartSubject(p.peek())) {
2119
- /* implicit separator */
2120
- } else throw new Error(`Expected '.' or start of next triple, got ${p.peek().toString()}`);
2121
- }
2122
-
2123
- triples.push(...more);
2124
- }
2125
-
2126
- return triples;
2127
- }
2128
-
2129
- function triplesToN3Body(triples, env) {
2130
- // Render as explicit triple statements (with dots)
2131
- return normalizeInsideBracesKeepStyle(
2132
- triples.map((tr) => `${termToText(tr.s, env)} ${termToText(tr.p, env)} ${termToText(tr.o, env)} .`).join(' '),
2133
- );
2134
- }
2135
-
2136
- function readBalancedParens(s, i) {
2137
- if (s[i] !== '(') throw new Error("Unclosed '(...)'");
2138
- let depth = 0;
2139
- let j = i;
2140
- let inString = false;
2141
- let quote = null;
2142
- let escaped = false;
2143
-
2144
- for (; j < s.length; j++) {
2145
- const ch = s[j];
2146
-
2147
- if (inString) {
2148
- if (escaped) {
2149
- escaped = false;
2150
- continue;
2151
- }
2152
- if (ch === '\\') {
2153
- escaped = true;
2154
- continue;
2155
- }
2156
- if (ch === quote) {
2157
- inString = false;
2158
- quote = null;
2159
- }
2160
- continue;
2161
- }
2162
-
2163
- if (ch === '"' || ch === "'") {
2164
- inString = true;
2165
- quote = ch;
2166
- continue;
2167
- }
2168
-
2169
- if (ch === '(') depth++;
2170
- else if (ch === ')') {
2171
- depth--;
2172
- if (depth === 0) {
2173
- return { content: s.slice(i + 1, j), endIdx: j + 1 };
2174
- }
2175
- }
2176
- }
2177
-
2178
- throw new Error("Unclosed '(...)'");
2179
- }
2180
-
2181
- function extractSrlDataBlocks(text) {
2182
- const blocks = [];
2183
- let i = 0;
2184
- let out = '';
2185
- const s = text || '';
2186
-
2187
- while (i < s.length) {
2188
- const idx = s.indexOf('DATA', i);
2189
- if (idx < 0) {
2190
- out += s.slice(i);
2191
- break;
2192
- }
2193
-
2194
- const before = idx === 0 ? ' ' : s[idx - 1];
2195
- const after = idx + 4 < s.length ? s[idx + 4] : ' ';
2196
- if (/[A-Za-z0-9_]/.test(before) || /[A-Za-z0-9_]/.test(after)) {
2197
- i = idx + 4;
2198
- continue;
2199
- }
2200
-
2201
- out += s.slice(i, idx);
2202
- let j = idx + 4;
2203
- while (j < s.length && /\s/.test(s[j])) j++;
2204
- if (s[j] !== '{') {
2205
- // Not a DATA block, keep literal "DATA"
2206
- out += 'DATA';
2207
- i = idx + 4;
2208
- continue;
2209
- }
2210
-
2211
- const blk = readBalancedBraces(s, j);
2212
- blocks.push((blk.content || '').trim());
2213
- i = blk.endIdx;
2214
- }
2215
-
2216
- return { dataText: out.trim(), dataBlocks: blocks };
2217
- }
2218
-
2219
- // Extract SRL FILTER(...) clauses from a WHERE body and return them as raw expressions.
2220
- function extractSrlFilters(bodyRaw) {
2221
- const s = bodyRaw || '';
2222
- let i = 0;
2223
- let out = '';
2224
- const filters = [];
2225
-
2226
- while (i < s.length) {
2227
- const idx = s.indexOf('FILTER', i);
2228
- if (idx < 0) {
2229
- out += s.slice(i);
2230
- break;
2231
- }
2232
-
2233
- const before = idx === 0 ? ' ' : s[idx - 1];
2234
- const after = idx + 6 < s.length ? s[idx + 6] : ' ';
2235
- if (/[A-Za-z0-9_]/.test(before) || /[A-Za-z0-9_]/.test(after)) {
2236
- i = idx + 6;
2237
- continue;
2238
- }
2239
-
2240
- out += s.slice(i, idx);
2241
-
2242
- let j = idx + 6;
2243
- while (j < s.length && /\s/.test(s[j])) j++;
2244
- if (s[j] !== '(') {
2245
- // Not a FILTER(...), keep it
2246
- out += 'FILTER';
2247
- i = idx + 6;
2248
- continue;
2249
- }
2250
-
2251
- const par = readBalancedParens(s, j);
2252
- filters.push((par.content || '').trim());
2253
- i = par.endIdx;
2254
- }
2255
-
2256
- return { body: normalizeInsideBracesKeepStyle(out), filters };
2257
- }
2258
-
2259
- function stripOuterParensOnce(expr) {
2260
- const t = (expr || '').trim();
2261
- if (!t.startsWith('(') || !t.endsWith(')')) return t;
2262
- try {
2263
- const par = readBalancedParens(t, 0);
2264
- // Only strip if it consumes the whole string
2265
- if (par.endIdx === t.length) return (par.content || '').trim();
2266
- } catch {}
2267
- return t;
2268
- }
2269
-
2270
- function splitTopLevelOr(expr) {
2271
- // Split on '||' that occurs at paren depth 0 (ignoring strings).
2272
- const s = expr || '';
2273
- const parts = [];
2274
- let buf = '';
2275
- let depth = 0;
2276
- let inString = false;
2277
- let quote = null;
2278
- let escaped = false;
2279
-
2280
- for (let i = 0; i < s.length; i++) {
2281
- const ch = s[i];
2282
-
2283
- if (inString) {
2284
- buf += ch;
2285
- if (escaped) {
2286
- escaped = false;
2287
- continue;
2288
- }
2289
- if (ch === '\\') {
2290
- escaped = true;
2291
- continue;
2292
- }
2293
- if (ch === quote) {
2294
- inString = false;
2295
- quote = null;
2296
- }
2297
- continue;
2298
- }
2299
-
2300
- if (ch === '"' || ch === "'") {
2301
- inString = true;
2302
- quote = ch;
2303
- buf += ch;
2304
- continue;
2305
- }
2306
-
2307
- if (ch === '(') {
2308
- depth++;
2309
- buf += ch;
2310
- continue;
2311
- }
2312
- if (ch === ')') {
2313
- depth = Math.max(0, depth - 1);
2314
- buf += ch;
2315
- continue;
2316
- }
2317
-
2318
- if (depth === 0 && ch === '|' && s[i + 1] === '|') {
2319
- parts.push(buf.trim());
2320
- buf = '';
2321
- i++;
2322
- continue;
2323
- }
2324
-
2325
- buf += ch;
2326
- }
2327
-
2328
- if (buf.trim()) parts.push(buf.trim());
2329
- return parts.length > 1 ? parts : null;
2330
- }
2331
-
2332
- function splitTopLevelAnd(expr) {
2333
- // Split on '&&' that occurs at paren depth 0 (ignoring strings).
2334
- const s = expr || '';
2335
- const parts = [];
2336
- let buf = '';
2337
- let depth = 0;
2338
- let inString = false;
2339
- let quote = null;
2340
- let escaped = false;
2341
-
2342
- for (let i = 0; i < s.length; i++) {
2343
- const ch = s[i];
2344
-
2345
- if (inString) {
2346
- buf += ch;
2347
- if (escaped) {
2348
- escaped = false;
2349
- continue;
2350
- }
2351
- if (ch === '\\') {
2352
- escaped = true;
2353
- continue;
2354
- }
2355
- if (ch === quote) {
2356
- inString = false;
2357
- quote = null;
2358
- }
2359
- continue;
2360
- }
2361
-
2362
- if (ch === '"' || ch === "'") {
2363
- inString = true;
2364
- quote = ch;
2365
- buf += ch;
2366
- continue;
2367
- }
2368
-
2369
- if (ch === '(') {
2370
- depth++;
2371
- buf += ch;
2372
- continue;
2373
- }
2374
- if (ch === ')') {
2375
- depth = Math.max(0, depth - 1);
2376
- buf += ch;
2377
- continue;
2378
- }
2379
-
2380
- if (depth === 0 && ch === '&' && s[i + 1] === '&') {
2381
- parts.push(buf.trim());
2382
- buf = '';
2383
- i++;
2384
- continue;
2385
- }
2386
-
2387
- buf += ch;
2388
- }
2389
-
2390
- if (buf.trim()) parts.push(buf.trim());
2391
- return parts.length > 1 ? parts : null;
2392
- }
2393
-
2394
- function splitTopLevelCommaArgs(s) {
2395
- const parts = [];
2396
- let buf = '';
2397
- let depthPar = 0;
2398
- let depthBr = 0;
2399
- let depthSq = 0;
2400
- let inString = false;
2401
- let quote = null;
2402
- let escaped = false;
2403
-
2404
- for (let i = 0; i < (s || '').length; i++) {
2405
- const ch = s[i];
2406
-
2407
- if (inString) {
2408
- buf += ch;
2409
- if (escaped) {
2410
- escaped = false;
2411
- continue;
2412
- }
2413
- if (ch === '\\') {
2414
- escaped = true;
2415
- continue;
2416
- }
2417
- if (ch === quote) {
2418
- inString = false;
2419
- quote = null;
2420
- }
2421
- continue;
2422
- }
2423
-
2424
- if (ch === '"' || ch === "'") {
2425
- inString = true;
2426
- quote = ch;
2427
- buf += ch;
2428
- continue;
2429
- }
2430
-
2431
- if (ch === '(') {
2432
- depthPar++;
2433
- buf += ch;
2434
- continue;
2435
- }
2436
- if (ch === ')') {
2437
- depthPar--;
2438
- buf += ch;
2439
- continue;
2440
- }
2441
- if (ch === '{') {
2442
- depthBr++;
2443
- buf += ch;
2444
- continue;
2445
- }
2446
- if (ch === '}') {
2447
- depthBr--;
2448
- buf += ch;
2449
- continue;
2450
- }
2451
- if (ch === '[') {
2452
- depthSq++;
2453
- buf += ch;
2454
- continue;
2455
- }
2456
- if (ch === ']') {
2457
- depthSq--;
2458
- buf += ch;
2459
- continue;
2460
- }
2461
-
2462
- if (depthPar === 0 && depthBr === 0 && depthSq === 0 && ch === ',') {
2463
- if (buf.trim()) parts.push(buf.trim());
2464
- buf = '';
2465
- continue;
2466
- }
2467
-
2468
- buf += ch;
2469
- }
2470
-
2471
- if (buf.trim()) parts.push(buf.trim());
2472
- return parts;
2473
- }
2474
-
2475
-
2476
- function makeTempVarGenerator(prefix = '__e') {
2477
- let n = 0;
2478
- return () => `?${prefix}${++n}`;
2479
- }
2480
-
2481
- function stripOuterParensAll(expr) {
2482
- let t = (expr || '').trim();
2483
- while (t.startsWith('(')) {
2484
- try {
2485
- const par = readBalancedParens(t, 0);
2486
- if (par.endIdx === t.length) {
2487
- t = (par.content || '').trim();
2488
- continue;
2489
- }
2490
- } catch {}
2491
- break;
2492
- }
2493
- return t;
2494
- }
2495
-
2496
- function mergeUsed(a, b) {
2497
- const out = { ...a };
2498
- for (const k of Object.keys(b || {})) out[k] = out[k] || b[k];
2499
- return out;
2500
- }
2501
-
2502
- function isStringLiteral(s) {
2503
- const t = (s || '').trim();
2504
- return t.startsWith('"') || t.startsWith("'");
2505
- }
2506
-
2507
- function isNumericLike(s) {
2508
- const t = (s || '').trim();
2509
- // Plain numbers (with optional sign/decimal/exponent)
2510
- if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(t)) return true;
2511
- // Typed numeric literals like "1"^^xsd:integer or "1"^^<...#integer>
2512
- if (/^".*"\s*\^\^\s*(xsd:(?:integer|decimal|double|float)|<[^>]+#(?:integer|decimal|double|float)>)\s*$/i.test(t))
2513
- return true;
2514
- return false;
2515
- }
2516
-
2517
- function prevNonWsChar(s, i) {
2518
- for (let j = i; j >= 0; j--) {
2519
- const ch = s[j];
2520
- if (!/\s/.test(ch)) return ch;
2521
- }
2522
- return null;
2523
- }
2524
-
2525
- function findTopLevelBinaryOp(expr, ops, fromRight = true) {
2526
- // Scan left-to-right and then choose either the first or last match.
2527
- // This avoids incorrect parenthesis-depth tracking when scanning right-to-left.
2528
- const t = expr || '';
2529
- let depth = 0;
2530
- let inString = false;
2531
- let quote = null;
2532
- let escaped = false;
2533
-
2534
- const candidates = [];
2535
- // Prefer longer operators first (e.g., "NOT IN" before "IN", ">=" before ">").
2536
- const opsSorted = [...(ops || [])].sort((a, b) => b.length - a.length);
2537
-
2538
- for (let i = 0; i < t.length; i++) {
2539
- const ch = t[i];
2540
-
2541
- if (inString) {
2542
- if (escaped) {
2543
- escaped = false;
2544
- continue;
2545
- }
2546
- if (ch === '\\') {
2547
- escaped = true;
2548
- continue;
2549
- }
2550
- if (ch === quote) {
2551
- inString = false;
2552
- quote = null;
2553
- }
2554
- continue;
2555
- }
2556
-
2557
- if (ch === '"' || ch === "'") {
2558
- inString = true;
2559
- quote = ch;
2560
- continue;
2561
- }
2562
-
2563
- if (ch === '(') {
2564
- depth++;
2565
- continue;
2566
- }
2567
- if (ch === ')') {
2568
- depth = Math.max(0, depth - 1);
2569
- continue;
2570
- }
2571
- if (depth !== 0) continue;
2572
-
2573
- let matchedHere = false;
2574
-
2575
- for (const op of opsSorted) {
2576
- if (/^[A-Za-z]/.test(op)) {
2577
- // Word operator, case-insensitive, with simple boundary checks.
2578
- const slice = t.slice(i, i + op.length);
2579
- if (slice.length !== op.length) continue;
2580
- if (slice.toUpperCase() !== op.toUpperCase()) continue;
2581
- const beforeAdj = i > 0 ? t[i - 1] : ' ';
2582
- const afterAdj = t[i + op.length] || ' ';
2583
- if (/[A-Za-z0-9_]/.test(beforeAdj)) continue;
2584
- if (/[A-Za-z0-9_]/.test(afterAdj)) continue;
2585
- candidates.push({ idx: i, op, len: op.length });
2586
- i += op.length - 1;
2587
- matchedHere = true;
2588
- break;
2589
- } else {
2590
- const slice = t.slice(i, i + op.length);
2591
- if (slice !== op) continue;
2592
-
2593
- // For + and -, ensure it's binary (not unary)
2594
- if (op === '+' || op === '-') {
2595
- const before = prevNonWsChar(t, i - 1);
2596
- if (!before || /[\(\,\=\+\-\*\/\^\&\|\!\<\>]/.test(before)) continue;
2597
- }
2598
-
2599
- candidates.push({ idx: i, op, len: op.length });
2600
- i += op.length - 1;
2601
- matchedHere = true;
2602
- break;
2603
- }
2604
- }
2605
-
2606
- if (matchedHere) continue;
2607
- }
2608
-
2609
- if (!candidates.length) return null;
2610
- return fromRight ? candidates[candidates.length - 1] : candidates[0];
2611
- }
2612
-
2613
- function tryParseFunctionCall(expr) {
2614
- const t = stripOuterParensAll(expr);
2615
- // Look for the first '(' at top-level
2616
- let depth = 0;
2617
- let inString = false;
2618
- let quote = null;
2619
- let escaped = false;
2620
- let openIdx = -1;
2621
-
2622
- for (let i = 0; i < t.length; i++) {
2623
- const ch = t[i];
2624
-
2625
- if (inString) {
2626
- if (escaped) {
2627
- escaped = false;
2628
- continue;
2629
- }
2630
- if (ch === '\\') {
2631
- escaped = true;
2632
- continue;
2633
- }
2634
- if (ch === quote) {
2635
- inString = false;
2636
- quote = null;
2637
- }
2638
- continue;
2639
- }
2640
- if (ch === '"' || ch === "'") {
2641
- inString = true;
2642
- quote = ch;
2643
- continue;
2644
- }
2645
-
2646
- if (ch === '(') {
2647
- if (depth === 0) {
2648
- openIdx = i;
2649
- break;
2650
- }
2651
- depth++;
2652
- continue;
2653
- }
2654
- }
2655
-
2656
- if (openIdx < 0) return null;
2657
- const name = t.slice(0, openIdx).trim();
2658
- if (!name) return null;
2659
-
2660
- try {
2661
- const par = readBalancedParens(t, openIdx);
2662
- if (par.endIdx !== t.length) return null;
2663
- return { name, argsRaw: par.content || '' };
2664
- } catch {
2665
- return null;
2666
- }
2667
- }
2668
-
2669
- function normalizeFnName(name) {
2670
- const n = (name || '').trim();
2671
- // Strip surrounding <>
2672
- if (n.startsWith('<') && n.endsWith('>')) return n.slice(1, -1);
2673
- return n;
2674
- }
2675
-
2676
- function fnLocalName(name) {
2677
- const n = normalizeFnName(name);
2678
- const idx = n.lastIndexOf('#');
2679
- if (idx >= 0) return n.slice(idx + 1);
2680
- const c = n.indexOf(':');
2681
- if (c >= 0) return n.slice(c + 1);
2682
- return n;
2683
- }
2684
-
2685
- function compileValueExpr(expr, ctx, targetVar = null) {
2686
- const used0 = {
2687
- usedMath: false,
2688
- usedString: false,
2689
- usedTime: false,
2690
- usedLog: false,
2691
- usedList: false,
2692
- usedCrypto: false,
2693
- };
2694
-
2695
- let t = stripOuterParensAll(expr);
2696
-
2697
- // Atomic?
2698
- if (
2699
- /^\s*[$?][A-Za-z_][A-Za-z0-9_-]*\s*$/.test(t) ||
2700
- isNumericLike(t) ||
2701
- isStringLiteral(t) ||
2702
- /^\s*<[^>]*>\s*$/.test(t) ||
2703
- /^\s*[_:][A-Za-z0-9_][A-Za-z0-9_-]*\s*$/.test(t) ||
2704
- /^\s*[A-Za-z][A-Za-z0-9_-]*:[A-Za-z_][A-Za-z0-9._-]*\s*$/.test(t) ||
2705
- /^\s*(true|false)\s*$/i.test(t)
2706
- ) {
2707
- if (!targetVar) return { term: t.trim(), stmts: [], used: used0 };
2708
- return {
2709
- term: targetVar,
2710
- stmts: [`${targetVar} log:equalTo ${t.trim()} .`],
2711
- used: { ...used0, usedLog: true },
2712
- };
2713
- }
2714
-
2715
- // Unary '!' is value-error in SPARQL; keep as TODO.
2716
- // Unary minus (negation)
2717
- if (t.startsWith('-')) {
2718
- const rest = t.slice(1).trim();
2719
- // If "-<num>" then atomic already handled above; so here it's expression negation.
2720
- const out = targetVar || ctx.newVar();
2721
- const inner = compileValueExpr(rest, ctx, null);
2722
- const stmts = [...inner.stmts, `${inner.term} math:negation ${out} .`];
2723
- const used = mergeUsed(inner.used, { ...used0, usedMath: true });
2724
- return { term: out, stmts, used };
2725
- }
2726
-
2727
- // Function call?
2728
- const call = tryParseFunctionCall(t);
2729
- if (call) {
2730
- const local0 = fnLocalName(call.name);
2731
- const local = local0.toLowerCase();
2732
- const localKey = local.replace(/_/g, '');
2733
- const args = splitTopLevelCommaArgs(call.argsRaw).map((x) => x.trim()).filter(Boolean);
2734
-
2735
- // time-fn:* (keep existing mapping)
2736
- {
2737
- let timeFnLocal = null;
2738
- const norm = normalizeFnName(call.name);
2739
- if (norm.startsWith(TIMEFN_NS)) timeFnLocal = norm.slice(TIMEFN_NS.length);
2740
- if (!timeFnLocal && call.name.includes(':')) timeFnLocal = call.name.split(':').slice(-1)[0];
2741
- if (timeFnLocal && TIMEFN_BUILTIN_NAMES.has(timeFnLocal)) {
2742
- const out = targetVar || ctx.newVar();
2743
- if (timeFnLocal === 'bindDefaultTimezone') {
2744
- if (args.length !== 2) return { term: out, stmts: [`# TODO BIND: ${t}`], used: used0 };
2745
- } else {
2746
- if (args.length !== 1) return { term: out, stmts: [`# TODO BIND: ${t}`], used: used0 };
2747
- }
2748
- const list = `(${args.join(' ')})`;
2749
- return { term: out, stmts: [`${list} ${call.name.trim()} ${out} .`], used: used0 };
2750
- }
2751
- }
2752
-
2753
- const compiledArgs = args.map((a) => compileValueExpr(a, ctx, null));
2754
- let stmts = [];
2755
- let used = used0;
2756
- const argTerms = [];
2757
- for (const ca of compiledArgs) {
2758
- stmts = stmts.concat(ca.stmts);
2759
- used = mergeUsed(used, ca.used);
2760
- argTerms.push(ca.term);
2761
- }
2762
-
2763
- const out = targetVar || ctx.newVar();
2764
-
2765
- // SPARQL/XPath-ish builtins -> N3 builtins
2766
- if (local === 'concat') {
2767
- stmts.push(`(${argTerms.join(' ')}) string:concatenation ${out} .`);
2768
- used = mergeUsed(used, { usedString: true });
2769
- return { term: out, stmts, used };
2770
- }
2771
-
2772
- if (local === 'replace') {
2773
- if (argTerms.length !== 3) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2774
- stmts.push(`(${argTerms.join(' ')}) string:replace ${out} .`);
2775
- used = mergeUsed(used, { usedString: true });
2776
- return { term: out, stmts, used };
2777
- }
2778
-
2779
- if (local === 'format') {
2780
- if (argTerms.length < 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2781
- stmts.push(`(${argTerms.join(' ')}) string:format ${out} .`);
2782
- used = mergeUsed(used, { usedString: true });
2783
- return { term: out, stmts, used };
2784
- }
2785
-
2786
- if (local === 'abs') {
2787
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2788
- stmts.push(`${argTerms[0]} math:absoluteValue ${out} .`);
2789
- used = mergeUsed(used, { usedMath: true });
2790
- return { term: out, stmts, used };
2791
- }
2792
-
2793
- if (local === 'round') {
2794
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2795
- stmts.push(`${argTerms[0]} math:rounded ${out} .`);
2796
- used = mergeUsed(used, { usedMath: true });
2797
- return { term: out, stmts, used };
2798
- }
2799
-
2800
- if (local === 'ceil' || local === 'ceiling') {
2801
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2802
- stmts.push(`${argTerms[0]} math:ceiling ${out} .`);
2803
- used = mergeUsed(used, { usedMath: true });
2804
- return { term: out, stmts, used };
2805
- }
2806
-
2807
- if (local === 'floor') {
2808
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2809
- stmts.push(`${argTerms[0]} math:floor ${out} .`);
2810
- used = mergeUsed(used, { usedMath: true });
2811
- return { term: out, stmts, used };
2812
- }
2813
-
2814
- if (local === 'substr' || local === 'substring') {
2815
- if (argTerms.length !== 2 && argTerms.length !== 3) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2816
- stmts.push(`(${argTerms.join(' ')}) string:substring ${out} .`);
2817
- used = mergeUsed(used, { usedString: true });
2818
- return { term: out, stmts, used };
2819
- }
2820
-
2821
- if (local === 'strlen') {
2822
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2823
- stmts.push(`${argTerms[0]} string:length ${out} .`);
2824
- used = mergeUsed(used, { usedString: true });
2825
- return { term: out, stmts, used };
2826
- }
2827
-
2828
- if (local === 'ucase') {
2829
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2830
- stmts.push(`${argTerms[0]} string:upperCase ${out} .`);
2831
- used = mergeUsed(used, { usedString: true });
2832
- return { term: out, stmts, used };
2833
- }
2834
-
2835
- if (local === 'lcase') {
2836
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2837
- stmts.push(`${argTerms[0]} string:lowerCase ${out} .`);
2838
- used = mergeUsed(used, { usedString: true });
2839
- return { term: out, stmts, used };
2840
- }
2841
-
2842
- if (localKey === 'encodeforuri') {
2843
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2844
- stmts.push(`${argTerms[0]} string:encodeForURI ${out} .`);
2845
- used = mergeUsed(used, { usedString: true });
2846
- return { term: out, stmts, used };
2847
- }
2848
-
2849
- if (local === 'hours' || local === 'hour') {
2850
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2851
- stmts.push(`${argTerms[0]} time:hour ${out} .`);
2852
- used = mergeUsed(used, { usedTime: true });
2853
- return { term: out, stmts, used };
2854
- }
2855
-
2856
- if (local === 'tz') {
2857
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2858
- stmts.push(`${argTerms[0]} time:timeZone ${out} .`);
2859
- used = mergeUsed(used, { usedTime: true });
2860
- return { term: out, stmts, used };
2861
- }
2862
-
2863
- if (local === 'now') {
2864
- if (argTerms.length !== 0) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2865
- stmts.push(`"" time:localTime ${out} .`);
2866
- used = mergeUsed(used, { usedTime: true });
2867
- return { term: out, stmts, used };
2868
- }
2869
-
2870
- if (local === 'uuid') {
2871
- if (argTerms.length !== 0) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2872
- stmts.push(`"" log:uuid ${out} .`);
2873
- used = mergeUsed(used, { usedLog: true });
2874
- return { term: out, stmts, used };
2875
- }
2876
-
2877
- if (localKey === 'struuid') {
2878
- if (argTerms.length !== 0) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2879
- stmts.push(`"" log:struuid ${out} .`);
2880
- used = mergeUsed(used, { usedLog: true });
2881
- return { term: out, stmts, used };
2882
- }
2883
-
2884
- if (local === 'triple') {
2885
- if (argTerms.length !== 3) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2886
- const tripleTerm = `{ ${argTerms[0]} ${argTerms[1]} ${argTerms[2]} . }`;
2887
- stmts.push(`${out} log:equalTo ${tripleTerm} .`);
2888
- used = mergeUsed(used, { usedLog: true });
2889
- return { term: out, stmts, used };
2890
- }
2891
-
2892
- if (local === 'subject' || local === 'predicate' || local === 'object') {
2893
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2894
- const sVar = ctx.newVar();
2895
- const pVar = ctx.newVar();
2896
- const oVar = ctx.newVar();
2897
- if (local === 'subject') {
2898
- stmts.push(`${argTerms[0]} log:includes { ${out} ${pVar} ${oVar} . } .`);
2899
- } else if (local === 'predicate') {
2900
- stmts.push(`${argTerms[0]} log:includes { ${sVar} ${out} ${oVar} . } .`);
2901
- } else {
2902
- stmts.push(`${argTerms[0]} log:includes { ${sVar} ${pVar} ${out} . } .`);
2903
- }
2904
- used = mergeUsed(used, { usedLog: true });
2905
- return { term: out, stmts, used };
2906
- }
2907
-
2908
- if (local === 'md5' || local === 'sha256' || local === 'sha384' || local === 'sha512') {
2909
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2910
- const pred = local === 'md5' ? 'crypto:md5' : local === 'sha256' ? 'crypto:sha256' : local === 'sha384' ? 'crypto:sha384' : 'crypto:sha512';
2911
- stmts.push(`${argTerms[0]} ${pred} ${out} .`);
2912
- used = mergeUsed(used, { usedCrypto: true });
2913
- return { term: out, stmts, used };
2914
- }
2915
-
2916
- if (local === 'year' || local === 'month' || local === 'day' || local === 'minutes' || local === 'minute' || local === 'seconds' || local === 'second' || local === 'timezone') {
2917
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2918
- const pred =
2919
- local === 'minutes' || local === 'minute'
2920
- ? 'time:minute'
2921
- : local === 'seconds' || local === 'second'
2922
- ? 'time:second'
2923
- : local === 'timezone'
2924
- ? 'time:timeZone'
2925
- : `time:${local}`;
2926
- stmts.push(`${argTerms[0]} ${pred} ${out} .`);
2927
- used = mergeUsed(used, { usedTime: true });
2928
- return { term: out, stmts, used };
2929
- }
2930
-
2931
- if (local === 'strdt') {
2932
- if (argTerms.length !== 2) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2933
- stmts.push(`(${argTerms.join(' ')}) log:dtlit ${out} .`);
2934
- used = mergeUsed(used, { usedLog: true });
2935
- return { term: out, stmts, used };
2936
- }
2937
-
2938
- if (local === 'strlang') {
2939
- if (argTerms.length !== 2) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2940
- stmts.push(`(${argTerms.join(' ')}) log:langlit ${out} .`);
2941
- used = mergeUsed(used, { usedLog: true });
2942
- return { term: out, stmts, used };
2943
- }
2944
-
2945
- if (local === 'sha' || local === 'sha1') {
2946
- if (argTerms.length !== 1) return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2947
- stmts.push(`${argTerms[0]} crypto:sha ${out} .`);
2948
- used = mergeUsed(used, { usedCrypto: true });
2949
- return { term: out, stmts, used };
2950
- }
2951
-
2952
- return { term: out, stmts: [...stmts, `# TODO BIND: ${t}`], used };
2953
- }
2954
-
2955
- // Arithmetic ops (+,-,*,/,^,MOD,%)
2956
- // Precedence: ^, *,/,MOD,%, +,-
2957
- const opAdd = findTopLevelBinaryOp(t, ['+', '-'], true);
2958
- if (opAdd) {
2959
- const left = t.slice(0, opAdd.idx).trim();
2960
- const right = t.slice(opAdd.idx + opAdd.len).trim();
2961
- const out = targetVar || ctx.newVar();
2962
- const l = compileValueExpr(left, ctx, null);
2963
- const r = compileValueExpr(right, ctx, null);
2964
- const pred = opAdd.op === '+' ? 'math:sum' : 'math:difference';
2965
- const stmts = [...l.stmts, ...r.stmts, `(${l.term} ${r.term}) ${pred} ${out} .`];
2966
- const used = mergeUsed(mergeUsed(l.used, r.used), { ...used0, usedMath: true });
2967
- return { term: out, stmts, used };
2968
- }
2969
-
2970
- const opMul = findTopLevelBinaryOp(t, ['*', '/', 'MOD', '%'], true);
2971
- if (opMul) {
2972
- const left = t.slice(0, opMul.idx).trim();
2973
- const right = t.slice(opMul.idx + opMul.len).trim();
2974
- const out = targetVar || ctx.newVar();
2975
- const l = compileValueExpr(left, ctx, null);
2976
- const r = compileValueExpr(right, ctx, null);
2977
- const pred =
2978
- opMul.op === '*'
2979
- ? 'math:product'
2980
- : opMul.op === '/'
2981
- ? 'math:quotient'
2982
- : 'math:remainder';
2983
- const stmts = [...l.stmts, ...r.stmts, `(${l.term} ${r.term}) ${pred} ${out} .`];
2984
- const used = mergeUsed(mergeUsed(l.used, r.used), { ...used0, usedMath: true });
2985
- return { term: out, stmts, used };
2986
- }
2987
-
2988
- const opPow = findTopLevelBinaryOp(t, ['^'], false);
2989
- if (opPow) {
2990
- const left = t.slice(0, opPow.idx).trim();
2991
- const right = t.slice(opPow.idx + opPow.len).trim();
2992
- const out = targetVar || ctx.newVar();
2993
- const l = compileValueExpr(left, ctx, null);
2994
- const r = compileValueExpr(right, ctx, null);
2995
- const stmts = [...l.stmts, ...r.stmts, `(${l.term} ${r.term}) math:exponentiation ${out} .`];
2996
- const used = mergeUsed(mergeUsed(l.used, r.used), { ...used0, usedMath: true });
2997
- return { term: out, stmts, used };
2998
- }
2999
-
3000
- // Fallback: emit TODO
3001
- if (!targetVar) return { term: t.trim(), stmts: [`# TODO expr: ${t.trim()}`], used: used0 };
3002
- return { term: targetVar, stmts: [`# TODO BIND: ${t.trim()} => ${targetVar}`], used: used0 };
3003
- }
3004
-
3005
- function compileBooleanFactor(expr, ctx, invert = false) {
3006
- let t = stripOuterParensAll(expr).trim();
3007
- const used0 = {
3008
- usedMath: false,
3009
- usedString: false,
3010
- usedTime: false,
3011
- usedLog: false,
3012
- usedList: false,
3013
- usedCrypto: false,
3014
- };
3015
-
3016
- // Handle unary !
3017
- if (t.startsWith('!')) {
3018
- return compileBooleanFactor(t.slice(1), ctx, !invert);
3019
- }
3020
-
3021
- // IN / NOT IN
3022
- {
3023
- // Look for top-level IN keyword
3024
- const opIn = findTopLevelBinaryOp(t, ['NOT IN', 'IN'], true);
3025
- if (opIn && (opIn.op.toUpperCase() === 'IN' || opIn.op.toUpperCase() === 'NOT IN')) {
3026
- const leftExpr = t.slice(0, opIn.idx).trim();
3027
- const rightExpr = t.slice(opIn.idx + opIn.len).trim();
3028
- let listInner = rightExpr;
3029
- // Expect (...) list
3030
- listInner = stripOuterParensAll(listInner);
3031
- const items = splitTopLevelCommaArgs(listInner).map((x) => x.trim()).filter(Boolean);
3032
- const list = `(${items.join(' ')})`;
3033
- const left = compileValueExpr(leftExpr, ctx, null);
3034
- let stmts = [...left.stmts];
3035
- let used = mergeUsed(used0, left.used);
3036
- used = mergeUsed(used, { usedList: true });
3037
-
3038
- const coreStmt = `${left.term} list:in ${list} .`;
3039
-
3040
- const neg = invert || opIn.op.toUpperCase() === 'NOT IN';
3041
- if (neg) {
3042
- stmts.push(`?SCOPE log:notIncludes { ${coreStmt} } .`);
3043
- used = mergeUsed(used, { usedLog: true });
3044
- } else {
3045
- stmts.push(coreStmt);
3046
- }
3047
- return { stmts, used };
3048
- }
3049
- }
3050
-
3051
- // Function call booleans: CONTAINS, STRSTARTS, STRENDS, REGEX, sameTerm
3052
- const call = tryParseFunctionCall(t);
3053
- if (call) {
3054
- const local0 = fnLocalName(call.name);
3055
- const local = local0.toLowerCase();
3056
- const localKey = local.replace(/_/g, '');
3057
- const args = splitTopLevelCommaArgs(call.argsRaw).map((x) => x.trim()).filter(Boolean);
3058
- const compiledArgs = args.map((a) => compileValueExpr(a, ctx, null));
3059
-
3060
- let stmts = [];
3061
- let used = used0;
3062
- const argTerms = [];
3063
- for (const ca of compiledArgs) {
3064
- stmts = stmts.concat(ca.stmts);
3065
- used = mergeUsed(used, ca.used);
3066
- argTerms.push(ca.term);
3067
- }
3068
-
3069
- const unaryTest = (pred) => {
3070
- if (argTerms.length !== 1) return { stmts: [...stmts, `# TODO FILTER: ${t}`], used };
3071
- const innerStmt = `${argTerms[0]} ${pred} true .`;
3072
- used = mergeUsed(used, { usedLog: true });
3073
- if (invert) makeNegatedNAF(innerStmt);
3074
- else stmts.push(innerStmt);
3075
- return { stmts, used };
3076
- };
3077
-
3078
- if (localKey === 'isiri' || localKey === 'isuri') return unaryTest('log:isIRI');
3079
- if (localKey === 'isliteral') return unaryTest('log:isLiteral');
3080
- if (localKey === 'isblank') return unaryTest('log:isBlank');
3081
- if (localKey === 'isnumeric') return unaryTest('log:isNumeric');
3082
- if (localKey === 'istriple') return unaryTest('log:isTriple');
3083
-
3084
- const makeNegatedNAF = (innerStmt) => {
3085
- stmts.push(`?SCOPE log:notIncludes { ${innerStmt} } .`);
3086
- used = mergeUsed(used, { usedLog: true });
3087
- };
3088
-
3089
- if (local === 'contains' || local === 'strstarts' || local === 'strends') {
3090
- if (argTerms.length !== 2) return { stmts: [...stmts, `# TODO FILTER: ${t}`], used };
3091
- const pred =
3092
- local === 'contains' ? 'string:contains' : local === 'strstarts' ? 'string:startsWith' : 'string:endsWith';
3093
- const innerStmt = `${argTerms[0]} ${pred} ${argTerms[1]} .`;
3094
- used = mergeUsed(used, { usedString: true });
3095
- if (invert) makeNegatedNAF(innerStmt);
3096
- else stmts.push(innerStmt);
3097
- return { stmts, used };
3098
- }
3099
-
3100
- if (local === 'regex') {
3101
- if (argTerms.length < 2) return { stmts: [...stmts, `# TODO FILTER: ${t}`], used };
3102
- // Ignore SPARQL regex flags (3rd arg) for now.
3103
- const pred = invert ? 'string:notMatches' : 'string:matches';
3104
- const innerStmt = `${argTerms[0]} ${pred} ${argTerms[1]} .`;
3105
- used = mergeUsed(used, { usedString: true });
3106
- stmts.push(innerStmt);
3107
- return { stmts, used };
3108
- }
3109
-
3110
- if (local === 'sameterm') {
3111
- if (argTerms.length !== 2) return { stmts: [...stmts, `# TODO FILTER: ${t}`], used };
3112
- const pred = invert ? 'log:notEqualTo' : 'log:equalTo';
3113
- stmts.push(`${argTerms[0]} ${pred} ${argTerms[1]} .`);
3114
- used = mergeUsed(used, { usedLog: true });
3115
- return { stmts, used };
3116
- }
3117
- }
3118
-
3119
- // Comparison
3120
- const cmp = parseSimpleComparison(t);
3121
- if (!cmp) return null;
3122
-
3123
- const left = compileValueExpr(cmp.left, ctx, null);
3124
- const right = compileValueExpr(cmp.right, ctx, null);
3125
-
3126
- let stmts = [...left.stmts, ...right.stmts];
3127
- let used = mergeUsed(mergeUsed(used0, left.used), right.used);
3128
-
3129
- // Decide namespace: numeric => math; string literals => string for ordering; otherwise:
3130
- const leftNum = isNumericLike(cmp.left) || isNumericLike(left.term);
3131
- const rightNum = isNumericLike(cmp.right) || isNumericLike(right.term);
3132
- const anyNum = leftNum || rightNum;
3133
- const anyStr = isStringLiteral(cmp.left) || isStringLiteral(cmp.right);
3134
-
3135
- const neg = invert;
3136
-
3137
- // Equality/inequality: prefer math for numeric, log otherwise.
3138
- if (cmp.op === '=' || cmp.op === '!=') {
3139
- if (anyNum) {
3140
- const pred = neg
3141
- ? cmp.op === '='
3142
- ? 'math:notEqualTo'
3143
- : 'math:equalTo'
3144
- : `math:${cmp.pred}`;
3145
- stmts.push(`${left.term} ${pred} ${right.term} .`);
3146
- used = mergeUsed(used, { usedMath: true });
3147
- return { stmts, used };
3148
- }
3149
-
3150
- const pred = neg ? (cmp.op === '=' ? 'log:notEqualTo' : 'log:equalTo') : cmp.op === '=' ? 'log:equalTo' : 'log:notEqualTo';
3151
- stmts.push(`${left.term} ${pred} ${right.term} .`);
3152
- used = mergeUsed(used, { usedLog: true });
3153
- return { stmts, used };
3154
- }
3155
-
3156
- // Relational: numeric => math; string literal ordering => string; default math
3157
- if (!anyNum && anyStr) {
3158
- // string ordering
3159
- const map = (op, inv) => {
3160
- const base =
3161
- op === '>'
3162
- ? 'greaterThan'
3163
- : op === '<'
3164
- ? 'lessThan'
3165
- : op === '>='
3166
- ? 'notLessThan'
3167
- : op === '<='
3168
- ? 'notGreaterThan'
3169
- : null;
3170
- if (!base) return null;
3171
-
3172
- if (!inv) return `string:${base}`;
3173
-
3174
- // invert
3175
- if (op === '>') return 'string:notGreaterThan';
3176
- if (op === '<') return 'string:notLessThan';
3177
- if (op === '>=') return 'string:lessThan';
3178
- if (op === '<=') return 'string:greaterThan';
3179
- return null;
3180
- };
3181
- const pred = map(cmp.op, neg);
3182
- if (!pred) return null;
3183
- stmts.push(`${left.term} ${pred} ${right.term} .`);
3184
- used = mergeUsed(used, { usedString: true });
3185
- return { stmts, used };
3186
- }
3187
-
3188
- // numeric/default math
3189
- {
3190
- const map = (op, inv, pred) => {
3191
- if (!inv) return `math:${pred}`;
3192
- if (op === '>') return 'math:notGreaterThan';
3193
- if (op === '<') return 'math:notLessThan';
3194
- if (op === '>=') return 'math:lessThan';
3195
- if (op === '<=') return 'math:greaterThan';
3196
- return null;
3197
- };
3198
- const pred = map(cmp.op, neg, cmp.pred);
3199
- if (!pred) return null;
3200
- stmts.push(`${left.term} ${pred} ${right.term} .`);
3201
- used = mergeUsed(used, { usedMath: true });
3202
- return { stmts, used };
3203
- }
3204
- }
3205
-
3206
- function bindExprToN3Alternatives(bindInner, ctx) {
3207
- // bindInner is the inside of BIND(...), e.g. 'concat(?a," ",?b) AS ?x'
3208
- const s = (bindInner || '').trim();
3209
- const m = s.match(/^(.*)\s+AS\s+(\?[A-Za-z_][A-Za-z0-9_-]*)\s*$/i);
3210
- if (!m) return null;
3211
-
3212
- const expr = m[1].trim();
3213
- const outVar = m[2].trim();
3214
-
3215
- // Special forms that need rule-distribution instead of a single builtin triple.
3216
- const call = tryParseFunctionCall(expr);
3217
- if (call) {
3218
- const local0 = fnLocalName(call.name);
3219
- const local = local0.toLowerCase();
3220
- const localKey = local.replace(/_/g, '');
3221
- const args = splitTopLevelCommaArgs(call.argsRaw).map((x) => x.trim()).filter(Boolean);
3222
-
3223
- // IF(cond, then, else): distribute into rule alternatives using filter compilation.
3224
- if (localKey === 'if') {
3225
- if (args.length !== 3) return { alts: [[`# TODO BIND: ${bindInner}`]], used: { usedMath: false, usedString: false, usedTime: false, usedLog: false, usedList: false, usedCrypto: false } };
3226
-
3227
- const cond = filterExprToN3Alternatives(args[0], ctx);
3228
- if (!cond) return { alts: [[`# TODO BIND(IF): ${bindInner}`]], used: { usedMath: false, usedString: false, usedTime: false, usedLog: false, usedList: false, usedCrypto: false } };
3229
-
3230
- const thenC = compileValueExpr(args[1], ctx, outVar);
3231
- const elseC = compileValueExpr(args[2], ctx, outVar);
3232
-
3233
- let used = mergeUsed(cond.used, mergeUsed(thenC.used, elseC.used));
3234
- used = mergeUsed(used, { usedLog: true }); // else branch uses log:notIncludes guards
3235
-
3236
- const alts = [];
3237
- // THEN alts: each conjunction of the condition DNF
3238
- for (const conj of cond.alts) {
3239
- alts.push([...(conj || []), ...thenC.stmts]);
3240
- }
3241
-
3242
- // ELSE alt: AND_i not(conj_i)
3243
- const elseGuards = [];
3244
- for (const conj of cond.alts) {
3245
- const inner = (conj || []).join(' ');
3246
- elseGuards.push(`?SCOPE log:notIncludes { ${inner} } .`);
3247
- }
3248
- alts.push([...elseGuards, ...elseC.stmts]);
3249
-
3250
- return { alts, used };
3251
- }
3252
-
3253
- // COALESCE(e1,e2,...): pick first expression that is satisfiable under current bindings.
3254
- if (localKey === 'coalesce') {
3255
- if (args.length < 1) return { alts: [[`# TODO BIND: ${bindInner}`]], used: { usedMath: false, usedString: false, usedTime: false, usedLog: false, usedList: false, usedCrypto: false } };
3256
-
3257
- const compiled = args.map((a) => compileValueExpr(a, ctx, outVar));
3258
- let used = { usedMath: false, usedString: false, usedTime: false, usedLog: false, usedList: false, usedCrypto: false };
3259
- for (const c of compiled) used = mergeUsed(used, c.used);
3260
- used = mergeUsed(used, { usedLog: true }); // guards use log:notIncludes
3261
-
3262
- const alts = [];
3263
- for (let i = 0; i < compiled.length; i++) {
3264
- const guards = [];
3265
- for (let j = 0; j < i; j++) {
3266
- const inner = compiled[j].stmts.join(' ');
3267
- guards.push(`?SCOPE log:notIncludes { ${inner} } .`);
3268
- }
3269
- alts.push([...guards, ...compiled[i].stmts]);
3270
- }
3271
- return { alts, used };
3272
- }
3273
- }
3274
-
3275
- // Default: compile as a single value expression (one alternative).
3276
- const compiled = compileValueExpr(expr, ctx, outVar);
3277
- return { alts: [compiled.stmts], used: compiled.used };
3278
- }
3279
-
3280
-
3281
- function extractSrlBinds(bodyRaw, ctx) {
3282
- const s = bodyRaw || '';
3283
- let i = 0;
3284
- let out = '';
3285
-
3286
- /** @type {string[][]} */
3287
- let bindAlts = [[]];
3288
-
3289
- let used = {
3290
- usedMath: false,
3291
- usedString: false,
3292
- usedTime: false,
3293
- usedLog: false,
3294
- usedList: false,
3295
- usedCrypto: false,
3296
- };
3297
-
3298
- while (i < s.length) {
3299
- const idx = s.indexOf('BIND', i);
3300
- if (idx < 0) {
3301
- out += s.slice(i);
3302
- break;
3303
- }
3304
-
3305
- const before = idx === 0 ? ' ' : s[idx - 1];
3306
- const after = idx + 4 < s.length ? s[idx + 4] : ' ';
3307
- if (/[A-Za-z0-9_]/.test(before) || /[A-Za-z0-9_]/.test(after)) {
3308
- i = idx + 4;
3309
- continue;
3310
- }
3311
-
3312
- let j = idx + 4;
3313
- while (j < s.length && /\s/.test(s[j])) j++;
3314
- if (s[j] !== '(') {
3315
- out += s.slice(i, idx + 4);
3316
- i = idx + 4;
3317
- continue;
3318
- }
3319
-
3320
- out += s.slice(i, idx);
3321
- const blk = readBalancedParens(s, j);
3322
- const inner = (blk.content || '').trim();
3323
-
3324
- const conv = bindExprToN3Alternatives(inner, ctx);
3325
- if (!conv) throw new Error(`Unsupported SRL BIND expression for N3 mapping: ${inner}`);
3326
-
3327
- // Cross product with existing bind alternatives
3328
- const next = [];
3329
- for (const acc of bindAlts) {
3330
- for (const opt of conv.alts) next.push(acc.concat(opt || []));
3331
- }
3332
- bindAlts = next;
3333
-
3334
- used = mergeUsed(used, conv.used);
3335
-
3336
- i = blk.endIdx;
3337
- }
3338
-
3339
- return { body: normalizeInsideBracesKeepStyle(out), bindAlts, used };
3340
- }
3341
-
3342
-
3343
- function parseSimpleComparison(expr) {
3344
- const t0 = stripOuterParensOnce(expr);
3345
- const t = (t0 || '').trim();
3346
- if (!t) return null;
3347
-
3348
- const ops2 = ['>=', '<=', '!='];
3349
- const ops1 = ['=', '>', '<'];
3350
-
3351
- let depth = 0;
3352
- let inString = false;
3353
- let quote = null;
3354
- let escaped = false;
3355
-
3356
- function mapPred(op) {
3357
- return op === '>'
3358
- ? 'greaterThan'
3359
- : op === '<'
3360
- ? 'lessThan'
3361
- : op === '='
3362
- ? 'equalTo'
3363
- : op === '!='
3364
- ? 'notEqualTo'
3365
- : op === '>='
3366
- ? 'notLessThan'
3367
- : op === '<='
3368
- ? 'notGreaterThan'
3369
- : null;
3370
- }
3371
-
3372
- for (let i = 0; i < t.length; i++) {
3373
- const ch = t[i];
3374
-
3375
- if (inString) {
3376
- if (escaped) {
3377
- escaped = false;
3378
- } else if (ch === '\\') {
3379
- escaped = true;
3380
- } else if (ch === quote) {
3381
- inString = false;
3382
- quote = null;
3383
- }
3384
- continue;
3385
- }
3386
-
3387
- if (ch === '"' || ch === "'") {
3388
- inString = true;
3389
- quote = ch;
3390
- continue;
3391
- }
3392
-
3393
- if (ch === '(') {
3394
- depth++;
3395
- continue;
3396
- }
3397
- if (ch === ')') {
3398
- depth = Math.max(0, depth - 1);
3399
- continue;
3400
- }
3401
-
3402
- if (depth !== 0) continue;
3403
-
3404
- // 2-char ops first
3405
- const two = t.slice(i, i + 2);
3406
- if (ops2.includes(two)) {
3407
- const left = t.slice(0, i).trim();
3408
- const right = t.slice(i + 2).trim();
3409
- const pred = mapPred(two);
3410
- if (!left || !right || !pred) return null;
3411
- return { left, op: two, right, pred };
3412
- }
3413
-
3414
- if (ops1.includes(ch)) {
3415
- const left = t.slice(0, i).trim();
3416
- const right = t.slice(i + 1).trim();
3417
- const pred = mapPred(ch);
3418
- if (!left || !right || !pred) return null;
3419
- return { left, op: ch, right, pred };
3420
- }
3421
- }
3422
-
3423
- return null;
3424
- }
3425
-
3426
- function filterExprToN3Alternatives(expr, ctx) {
3427
- // Returns {alts, used} where:
3428
- // - alts is an array of alternatives
3429
- // - each alternative is an array of N3 statements (strings) to be added to the rule body
3430
- //
3431
- // Supports nested disjunction distribution, e.g.:
3432
- // a && (b || c) ==> (a && b) || (a && c)
3433
- // and De Morgan for leading '!' over composite subexpressions.
3434
- const e0 = (expr || '').trim();
3435
- if (!e0) return null;
3436
-
3437
- function crossProductDnfs(dnfA, dnfB) {
3438
- const next = [];
3439
- for (const a of dnfA) {
3440
- for (const b of dnfB) {
3441
- next.push(a.concat(b));
3442
- }
3443
- }
3444
- return next;
3445
- }
3446
-
3447
- function toDnf(rawExpr, negate = false) {
3448
- let t = (rawExpr || '').trim();
3449
- if (!t) return [[]];
3450
-
3451
- // Normalize leading '!' (can appear multiple times)
3452
- let neg = negate;
3453
- while (t.startsWith('!')) {
3454
- neg = !neg;
3455
- t = t.slice(1).trim();
3456
- }
3457
-
3458
- t = stripOuterParensAll(t);
3459
-
3460
- // OR has lower precedence than AND.
3461
- const orParts = splitTopLevelOr(t);
3462
- if (orParts) {
3463
- if (!neg) {
3464
- let out = [];
3465
- for (const p of orParts) out = out.concat(toDnf(p, false));
3466
- return out;
3467
- }
3468
- // NOT (A OR B) == (NOT A) AND (NOT B)
3469
- let out = [[]];
3470
- for (const p of orParts) {
3471
- out = crossProductDnfs(out, toDnf(p, true));
3472
- }
3473
- return out;
3474
- }
3475
-
3476
- const andParts = splitTopLevelAnd(t);
3477
- if (andParts) {
3478
- if (!neg) {
3479
- let out = [[]];
3480
- for (const p of andParts) out = crossProductDnfs(out, toDnf(p, false));
3481
- return out;
3482
- }
3483
- // NOT (A AND B) == (NOT A) OR (NOT B)
3484
- let out = [];
3485
- for (const p of andParts) out = out.concat(toDnf(p, true));
3486
- return out;
3487
- }
3488
-
3489
- // Atomic
3490
- return [[neg ? '!' + t : t]];
3491
- }
3492
-
3493
- const conjunctions = toDnf(e0, false);
3494
-
3495
- const alts = [];
3496
- let used = {
3497
- usedMath: false,
3498
- usedString: false,
3499
- usedTime: false,
3500
- usedLog: false,
3501
- usedList: false,
3502
- usedCrypto: false,
3503
- };
3504
-
3505
- for (const conj of conjunctions) {
3506
- let stmts = [];
3507
- for (const factor of conj) {
3508
- const bf = compileBooleanFactor(factor, ctx, false);
3509
- if (!bf) return null;
3510
- stmts = stmts.concat(bf.stmts);
3511
- used = mergeUsed(used, bf.used);
3512
- }
3513
- alts.push(stmts);
3514
- }
3515
-
3516
- return { alts, used };
3517
- }
3518
-
3519
-
3520
- function srlWhereBodyToN3Body(bodyRaw) {
3521
- const s = bodyRaw || '';
3522
- let i = 0;
3523
- let out = '';
3524
- let usedLog = false;
3525
-
3526
- while (i < s.length) {
3527
- const idx = s.indexOf('NOT', i);
3528
- if (idx < 0) {
3529
- out += s.slice(i);
3530
- break;
3531
- }
3532
-
3533
- // token boundary check for "NOT"
3534
- const before = idx === 0 ? ' ' : s[idx - 1];
3535
- const after = idx + 3 < s.length ? s[idx + 3] : ' ';
3536
- if (/[A-Za-z0-9_]/.test(before) || /[A-Za-z0-9_]/.test(after)) {
3537
- i = idx + 3;
3538
- continue;
3539
- }
3540
-
3541
- // Look ahead for "{"
3542
- let j = idx + 3;
3543
- while (j < s.length && /\s/.test(s[j])) j++;
3544
- if (s[j] !== '{') {
3545
- // Not a negation element; keep the text and continue
3546
- out += s.slice(i, idx + 3);
3547
- i = idx + 3;
3548
- continue;
3549
- }
3550
-
3551
- // Capture the NOT {...} block
3552
- out += s.slice(i, idx);
3553
- const blk = readBalancedBraces(s, j);
3554
- const inner = (blk.content || '').trim();
3555
-
3556
- out += ` ?SCOPE log:notIncludes { ${inner} } . `;
3557
- usedLog = true;
3558
- i = blk.endIdx;
3559
- }
3560
-
3561
- return { body: normalizeInsideBracesKeepStyle(out), usedLog };
3562
- }
3563
-
3564
- // N3 : ?SCOPE log:notIncludes { ... } .
3565
- // SRL: NOT { ... }
3566
- function stripOnlyWholeLineHashComments(src) {
3567
- // IMPORTANT: do NOT treat '#' as an inline comment marker here,
3568
- // because IRIs commonly contain '#', e.g. <http://example.org/#>.
3569
- return src
3570
- .split('\n')
3571
- .map((line) => (line.trim().startsWith('#') ? '' : line))
3572
- .join('\n');
3573
- }
3574
-
3575
- function normalizeInsideBracesKeepStyle(s) {
3576
- return (s || '').trim().replace(/\s+/g, ' ').trim();
3577
- }
3578
-
3579
- function srlDollarVarsToQVars(text) {
3580
- // SHACL SPARQL uses $this/$value; N3 uses ?vars.
3581
- // Convert $name -> ?name, ignoring occurrences inside strings.
3582
- const s = text || '';
3583
- let out = '';
3584
- let inString = false;
3585
- let quote = null;
3586
- let escaped = false;
3587
-
3588
- for (let i = 0; i < s.length; i++) {
3589
- const ch = s[i];
3590
-
3591
- if (inString) {
3592
- out += ch;
3593
- if (escaped) {
3594
- escaped = false;
3595
- continue;
3596
- }
3597
- if (ch === '\\') {
3598
- escaped = true;
3599
- continue;
3600
- }
3601
- if (ch === quote) {
3602
- inString = false;
3603
- quote = null;
3604
- }
3605
- continue;
3606
- }
3607
-
3608
- if (ch === '"' || ch === "'") {
3609
- inString = true;
3610
- quote = ch;
3611
- out += ch;
3612
- continue;
3613
- }
3614
-
3615
- if (ch === '$') {
3616
- let j = i + 1;
3617
- let name = '';
3618
- while (j < s.length && /[A-Za-z0-9_\-]/.test(s[j])) {
3619
- name += s[j];
3620
- j++;
3621
- }
3622
- if (name.length) {
3623
- out += '?' + name;
3624
- i = j - 1;
3625
- continue;
3626
- }
3627
- }
3628
-
3629
- out += ch;
3630
- }
3631
-
3632
- return out;
3633
- }
3634
-
3635
- function parseSrlPrefixLines(src) {
3636
- const prefixes = [];
3637
- const other = [];
3638
-
3639
- for (const rawLine of src.split('\n')) {
3640
- const line = rawLine.trim();
3641
- if (!line) continue;
3642
-
3643
- const m = line.match(/^PREFIX\s+([^\s]+)\s*<([^>]+)>\s*\.?\s*$/i);
3644
- if (m) prefixes.push({ label: m[1].trim(), iri: m[2].trim() });
3645
- else other.push(rawLine);
3646
- }
3647
-
3648
- return { prefixes, rest: other.join('\n') };
3649
- }
3650
-
3651
- function readBalancedBraces(src, startIdx) {
3652
- if (src[startIdx] !== '{') throw new Error("Expected '{'");
3653
-
3654
- let i = startIdx;
3655
- let depth = 0;
3656
- let inString = false;
3657
- let quote = null;
3658
- let escaped = false;
3659
- let out = '';
3660
-
3661
- for (; i < src.length; i++) {
3662
- const ch = src[i];
3663
-
3664
- if (inString) {
3665
- out += ch;
3666
-
3667
- if (escaped) {
3668
- escaped = false;
3669
- continue;
3670
- }
3671
- if (ch === '\\') {
3672
- escaped = true;
3673
- continue;
3674
- }
3675
- if (ch === quote) {
3676
- inString = false;
3677
- quote = null;
3678
- }
3679
- continue;
3680
- }
3681
-
3682
- // Treat \" or \' outside strings as literal quotes (common when SRL is copy-pasted from JS strings)
3683
- if ((ch === '"' || ch === "'") && i > 0 && src[i - 1] === '\\') {
3684
- out += ch;
3685
- continue;
3686
- }
3687
-
3688
- if (ch === '"' || ch === "'") {
3689
- inString = true;
3690
- quote = ch;
3691
- out += ch;
3692
- continue;
3693
- }
3694
-
3695
- if (ch === '{') {
3696
- depth++;
3697
- if (depth > 1) out += ch;
3698
- continue;
3699
- }
3700
-
3701
- if (ch === '}') {
3702
- depth--;
3703
- if (depth === 0) return { content: out, endIdx: i + 1 };
3704
- out += ch;
3705
- continue;
3706
- }
3707
-
3708
- if (depth >= 1) out += ch;
3709
- }
3710
-
3711
- throw new Error("Unclosed '{...}'");
3712
- }
3713
-
3714
- function extractSrlRules(src) {
3715
- const rules = [];
3716
- let i = 0;
3717
- let dataParts = [];
3718
- const s = src;
3719
-
3720
- while (i < s.length) {
3721
- const idx = s.indexOf('RULE', i);
3722
- if (idx < 0) {
3723
- dataParts.push(s.slice(i));
3724
- break;
3725
- }
3726
-
3727
- const before = idx === 0 ? ' ' : s[idx - 1];
3728
- const after = idx + 4 < s.length ? s[idx + 4] : ' ';
3729
- if (/[A-Za-z0-9_]/.test(before) || /[A-Za-z0-9_]/.test(after)) {
3730
- i = idx + 4;
3731
- continue;
3732
- }
3733
-
3734
- dataParts.push(s.slice(i, idx));
3735
- i = idx + 4;
3736
-
3737
- while (i < s.length && /\s/.test(s[i])) i++;
3738
- if (s[i] !== '{') throw new Error("SRL parse error: expected '{' after RULE");
3739
- const head = readBalancedBraces(s, i);
3740
- i = head.endIdx;
3741
-
3742
- while (i < s.length && /\s/.test(s[i])) i++;
3743
- if (!s.slice(i, i + 5).match(/^WHERE/i)) throw new Error('SRL parse error: expected WHERE');
3744
- i += 5;
3745
-
3746
- while (i < s.length && /\s/.test(s[i])) i++;
3747
- if (s[i] !== '{') throw new Error("SRL parse error: expected '{' after WHERE");
3748
- const body = readBalancedBraces(s, i);
3749
- i = body.endIdx;
3750
-
3751
- rules.push({ head: head.content, body: body.content });
3752
- }
3753
-
3754
- return { dataText: dataParts.join('').trim(), rules };
3755
- }
3756
-
3757
- function srlToN3(srlText) {
3758
- const cleaned = stripOnlyWholeLineHashComments(srlText);
3759
- const { prefixes, rest } = parseSrlPrefixLines(cleaned);
3760
- const { dataText, rules } = extractSrlRules(rest);
3761
- const env = prefixEnvFromSrlPrefixes(prefixes);
3762
-
3763
- // DATA { ... } blocks are SRL-only; map their content to plain N3 data triples.
3764
- const dataExtract = extractSrlDataBlocks(dataText);
3765
- const dataOutside = dataExtract.dataText || '';
3766
- const dataBlocks = dataExtract.dataBlocks || [];
3767
-
3768
- // Convert rules first so we know whether we need log:/math:
3769
- const renderedRules = [];
3770
- let needsLog = false;
3771
- let needsMath = false;
3772
- let needsString = false;
3773
- let needsList = false;
3774
- let needsCrypto = false;
3775
- let needsTime = false;
3776
-
3777
- for (const r of rules) {
3778
- const ctx = { newVar: makeTempVarGenerator('__e') };
3779
-
3780
- // 1) NOT { ... } -> ?SCOPE log:notIncludes { ... } .
3781
- const convNot = srlWhereBodyToN3Body(r.body);
3782
- needsLog = needsLog || convNot.usedLog;
3783
- if (convNot.usedLog && !env.map.log) env.setPrefix('log', LOG_NS);
3784
-
3785
- // 2) FILTER(...) -> math:* builtins (possibly multiple alternative rules for OR)
3786
- const convFilter = extractSrlFilters(convNot.body);
3787
- const bodyNoFilter = convFilter.body;
3788
- const filterExprs = convFilter.filters;
3789
-
3790
- // 3) BIND(concat(... ) AS ?v) -> string:concatenation builtins
3791
- const convBind = extractSrlBinds(bodyNoFilter, ctx);
3792
- const bodyNoBind = convBind.body;
3793
- const bindAlts = (convBind.bindAlts && convBind.bindAlts.length) ? convBind.bindAlts : [[]];
3794
- if (convBind.used.usedString) {
3795
- needsString = true;
3796
- if (!env.map.string) env.setPrefix('string', STRING_NS);
3797
- }
3798
- if (convBind.used.usedTime) {
3799
- needsTime = true;
3800
- if (!env.map.time) env.setPrefix('time', TIME_NS);
3801
- }
3802
- if (convBind.used.usedLog) {
3803
- needsLog = true;
3804
- if (!env.map.log) env.setPrefix('log', LOG_NS);
3805
- }
3806
- if (convBind.used.usedMath) {
3807
- needsMath = true;
3808
- if (!env.map.math) env.setPrefix('math', MATH_NS);
3809
- }
3810
- if (convBind.used.usedList) {
3811
- needsList = true;
3812
- if (!env.map.list) env.setPrefix('list', LIST_NS);
3813
- }
3814
- if (convBind.used.usedCrypto) {
3815
- needsCrypto = true;
3816
- if (!env.map.crypto) env.setPrefix('crypto', CRYPTO_NS);
3817
- }
3818
-
3819
- // Build rule alternatives (disjunction => multiple rules)
3820
- let alts = [{ builtins: [] }];
3821
-
3822
- for (const f of filterExprs) {
3823
- const conv = filterExprToN3Alternatives(f, ctx);
3824
- if (!conv) {
3825
- throw new Error(`Unsupported SRL FILTER expression for N3 mapping: ${f}`);
3826
- }
3827
-
3828
- // Ensure needed builtin prefixes
3829
- if (conv.used.usedMath) {
3830
- needsMath = true;
3831
- if (!env.map.math) env.setPrefix('math', MATH_NS);
3832
- }
3833
- if (conv.used.usedString) {
3834
- needsString = true;
3835
- if (!env.map.string) env.setPrefix('string', STRING_NS);
3836
- }
3837
- if (conv.used.usedLog) {
3838
- needsLog = true;
3839
- if (!env.map.log) env.setPrefix('log', LOG_NS);
3840
- }
3841
- if (conv.used.usedList) {
3842
- needsList = true;
3843
- if (!env.map.list) env.setPrefix('list', LIST_NS);
3844
- }
3845
- if (conv.used.usedCrypto) {
3846
- needsCrypto = true;
3847
- if (!env.map.crypto) env.setPrefix('crypto', CRYPTO_NS);
3848
- }
3849
- if (conv.used.usedTime) {
3850
- needsTime = true;
3851
- if (!env.map.time) env.setPrefix('time', TIME_NS);
3852
- }
3853
-
3854
- // Expand OR alternatives
3855
- const next = [];
3856
- for (const a of alts) {
3857
- for (const option of conv.alts) {
3858
- next.push({ builtins: a.builtins.concat(option) });
3859
- }
3860
- }
3861
- alts = next;
3862
- }
3863
-
3864
- const head = srlDollarVarsToQVars(normalizeInsideBracesKeepStyle(r.head));
3865
-
3866
- for (const a of alts) {
3867
- const extra = a.builtins.length ? ` ${a.builtins.join(' ')} ` : '';
3868
- for (const bindBuiltins of bindAlts) {
3869
- const bindExtra = bindBuiltins.length ? ` ${bindBuiltins.join(' ')} ` : '';
3870
- const combined = `${bodyNoBind} ${extra} ${bindExtra}`.trim();
3871
- const triples = parseTriplesBlockAllowImplicitDots(combined, env);
3872
- const body = triplesToN3Body(triples, env);
3873
- renderedRules.push(`{ ${body} } => { ${head} } .`);
3874
- }
3875
- }
3876
- }
3877
-
3878
- // Build body first (data + rules), then decide which prefixes are actually needed.
3879
- const bodyParts = [];
3880
-
3881
- // Emit "plain" data triples first (outside SRL DATA blocks)
3882
- if (dataOutside.trim()) bodyParts.push(dataOutside.trim(), '');
3883
-
3884
- // Emit each SRL DATA { ... } block as plain N3 data triples
3885
- for (const blk of dataBlocks) {
3886
- if (blk.trim()) bodyParts.push(blk.trim(), '');
3887
- }
3888
-
3889
- bodyParts.push(...renderedRules);
3890
-
3891
- let bodyText = bodyParts.join('\n').trim();
3892
-
3893
- // Decide whether rdf:/xsd: are needed in the final output (SRL emits a lot as raw text).
3894
- const usesRdf = /\brdf:/.test(bodyText) || bodyText.includes(`<${RDF_NS}`) || bodyText.includes(`^^<${RDF_NS}`);
3895
- const usesXsd = /\bxsd:/.test(bodyText) || bodyText.includes(`<${XSD_NS}`) || bodyText.includes(`^^<${XSD_NS}`);
3896
-
3897
- // Ensure prefixes requested by generated content are declared.
3898
- if (usesRdf && !env.map.rdf) env.setPrefix('rdf', RDF_NS);
3899
- if (usesXsd && !env.map.xsd) env.setPrefix('xsd', XSD_NS);
3900
-
3901
- // If we have xsd:, prefer qname datatypes in the body, e.g. ^^xsd:date.
3902
- if (env.map.xsd) {
3903
- const esc = XSD_NS.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3904
- bodyText = bodyText.replace(new RegExp(`\\^\\^<${esc}([^>]+)>`, 'g'), '^^xsd:$1');
3905
- }
3906
-
3907
- const out = [];
3908
- const pro = renderPrefixPrologue(env, { includeRdfg: false }).trim();
3909
- if (pro) out.push(pro, '');
3910
- if (bodyText) out.push(bodyText);
3911
-
3912
- return out.join('\n').trim() + '\n';
3913
- }
3914
-
3915
2043
  function printHelp() {
3916
2044
  process.stdout.write(`Usage:
3917
- n3gen <file.ttl|file.trig|file.srl>
2045
+ n3gen <file.ttl|file.trig>
3918
2046
 
3919
- Converts RDF 1.2 Turtle/TriG and SHACL 1.2 Rules to N3.
2047
+ Converts RDF 1.2 Turtle (.ttl) or TriG (.trig) to Notation 3 (.n3) and writes to stdout.
3920
2048
 
3921
2049
  Examples:
3922
- n3gen file.ttl > file.n3
2050
+ n3gen file.ttl > file.n3
3923
2051
  n3gen file.trig > file.n3
3924
- n3gen file.srl > file.n3
3925
2052
  `);
3926
2053
  }
3927
2054
 
@@ -3952,12 +2079,8 @@ async function main() {
3952
2079
  process.stdout.write(trigToN3(text));
3953
2080
  return;
3954
2081
  }
3955
- if (ext === '.srl') {
3956
- process.stdout.write(srlToN3(text));
3957
- return;
3958
- }
3959
2082
 
3960
- throw new Error(`Unsupported file extension "${ext}". Use .ttl, .trig or .srl`);
2083
+ throw new Error(`Unsupported file extension "${ext}". Use .ttl or .trig`);
3961
2084
  }
3962
2085
 
3963
2086
  main().catch((e) => {