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.
- package/HANDBOOK.md +2 -88
- package/eyeling-builtins.ttl +0 -48
- package/eyeling.js +67 -314
- package/lib/engine.js +67 -309
- package/lib/rules.js +0 -5
- package/package.json +1 -1
- package/test/n3gen.test.js +4 -4
- package/test/package.test.js +1 -1
- package/tools/n3gen.js +6 -1883
- package/examples/bind-builtins.n3 +0 -11
- package/examples/bind.n3 +0 -7
- package/examples/brussels-brew-club.n3 +0 -119
- package/examples/builtins-string-math.n3 +0 -11
- package/examples/builtins-triple-termtests.n3 +0 -7
- package/examples/family.n3 +0 -10
- package/examples/filter-demorgan.n3 +0 -9
- package/examples/filter-in-notin.n3 +0 -10
- package/examples/filter-nested-or.n3 +0 -10
- package/examples/filter.n3 +0 -8
- package/examples/input/bind-builtins.srl +0 -30
- package/examples/input/bind.srl +0 -12
- package/examples/input/builtins-string-math.srl +0 -38
- package/examples/input/builtins-triple-termtests.srl +0 -27
- package/examples/input/family.srl +0 -12
- package/examples/input/filter-demorgan.srl +0 -15
- package/examples/input/filter-in-notin.srl +0 -15
- package/examples/input/filter-nested-or.srl +0 -15
- package/examples/input/filter.srl +0 -9
- package/examples/input/snaf.srl +0 -6
- package/examples/it-is-about-time.n3 +0 -580
- package/examples/json-pointer.n3 +0 -75
- package/examples/json-reconcile-vat.n3 +0 -361
- package/examples/output/bind-builtins.n3 +0 -9
- package/examples/output/bind.n3 +0 -3
- package/examples/output/brussels-brew-club.n3 +0 -22
- package/examples/output/builtins-string-math.n3 +0 -0
- package/examples/output/builtins-triple-termtests.n3 +0 -0
- package/examples/output/family.n3 +0 -13
- package/examples/output/filter-demorgan.n3 +0 -3
- package/examples/output/filter-in-notin.n3 +0 -4
- package/examples/output/filter-nested-or.n3 +0 -4
- package/examples/output/filter.n3 +0 -3
- package/examples/output/it-is-about-time.n3 +0 -0
- package/examples/output/json-pointer.n3 +0 -13
- package/examples/output/json-reconcile-vat.n3 +0 -226
- package/examples/output/snaf.n3 +0 -3
- 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)
|
|
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
|
-
*
|
|
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
|
|
2045
|
+
n3gen <file.ttl|file.trig>
|
|
3918
2046
|
|
|
3919
|
-
Converts RDF 1.2 Turtle
|
|
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
|
|
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
|
|
2083
|
+
throw new Error(`Unsupported file extension "${ext}". Use .ttl or .trig`);
|
|
3961
2084
|
}
|
|
3962
2085
|
|
|
3963
2086
|
main().catch((e) => {
|