@matdata/sparql-formatter 1.0.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/LICENSE +21 -0
- package/README.md +245 -0
- package/dist/ast.d.ts +447 -0
- package/dist/browser.d.ts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2621 -0
- package/dist/errors.d.ts +12 -0
- package/dist/index.cjs +2573 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +2540 -0
- package/dist/lexer.d.ts +47 -0
- package/dist/parser.d.ts +12 -0
- package/dist/printer.d.ts +89 -0
- package/dist/spfmt.min.js +8 -0
- package/dist/writer.d.ts +73 -0
- package/package.json +70 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2621 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
|
|
7
|
+
// src/errors.ts
|
|
8
|
+
var SparqlSyntaxError = class extends Error {
|
|
9
|
+
constructor(message, offset, line, column, expected = []) {
|
|
10
|
+
super(`${message} (line ${line}, column ${column})`);
|
|
11
|
+
this.name = "SparqlSyntaxError";
|
|
12
|
+
this.offset = offset;
|
|
13
|
+
this.line = line;
|
|
14
|
+
this.column = column;
|
|
15
|
+
this.expected = expected;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/lexer.ts
|
|
20
|
+
var PN_CHARS_BASE = "A-Za-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\u{10000}-\\u{EFFFF}";
|
|
21
|
+
var PN_CHARS_U = PN_CHARS_BASE + "_";
|
|
22
|
+
var PN_CHARS = PN_CHARS_U + "\\-0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
|
|
23
|
+
var PLX = "%[0-9A-Fa-f]{2}|\\\\[_~.\\-!$&'()*+,;=/?#@%]";
|
|
24
|
+
var PN_PREFIX = `[${PN_CHARS_BASE}](?:[${PN_CHARS}.]*[${PN_CHARS}])?`;
|
|
25
|
+
var PN_LOCAL = `(?:[${PN_CHARS_U}:0-9]|${PLX})(?:(?:[${PN_CHARS}.:]|${PLX})*(?:[${PN_CHARS}:]|${PLX}))?`;
|
|
26
|
+
var RE_PNAME = new RegExp(`(?:${PN_PREFIX})?:(?:${PN_LOCAL})?`, "uy");
|
|
27
|
+
var RE_VAR = new RegExp(`[?$][${PN_CHARS_U}0-9][${PN_CHARS_U}0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*`, "uy");
|
|
28
|
+
var RE_BNODE = new RegExp(`_:[${PN_CHARS_U}0-9](?:[${PN_CHARS}.]*[${PN_CHARS}])?`, "uy");
|
|
29
|
+
var RE_IRIREF = /<(?:[^<>"{}|^`\\\u0000- ]|\\u[0-9A-Fa-f]{4}|\\U[0-9A-Fa-f]{8})*>/y;
|
|
30
|
+
var RE_LANG_DIR = /@[a-zA-Z]+(?:-[a-zA-Z0-9]+)*(?:--[a-zA-Z]+)?/y;
|
|
31
|
+
var RE_DOUBLE = /(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)[eE][+-]?[0-9]+/y;
|
|
32
|
+
var RE_DECIMAL = /[0-9]*\.[0-9]+/y;
|
|
33
|
+
var RE_INTEGER = /[0-9]+/y;
|
|
34
|
+
var RE_NAME = /[A-Za-z_][A-Za-z0-9_]*/y;
|
|
35
|
+
var ECHAR_UCHAR = String.raw`\\[tbnrf\\"']|\\u[0-9A-Fa-f]{4}|\\U[0-9A-Fa-f]{8}`;
|
|
36
|
+
var RE_STRING_LONG1 = new RegExp(String.raw`'''(?:(?:'|'')?(?:[^'\\]|${ECHAR_UCHAR}))*'''`, "y");
|
|
37
|
+
var RE_STRING_LONG2 = new RegExp(String.raw`"""(?:(?:"|"")?(?:[^"\\]|${ECHAR_UCHAR}))*"""`, "y");
|
|
38
|
+
var RE_STRING1 = new RegExp(String.raw`'(?:[^'\\\n\r]|${ECHAR_UCHAR})*'`, "y");
|
|
39
|
+
var RE_STRING2 = new RegExp(String.raw`"(?:[^"\\\n\r]|${ECHAR_UCHAR})*"`, "y");
|
|
40
|
+
var RE_SURROGATE_ESCAPE = /\\(?:u|U0000)[dD][89a-fA-F][0-9a-fA-F]{2}/;
|
|
41
|
+
var PUNCTUATION = [
|
|
42
|
+
"<<(",
|
|
43
|
+
")>>",
|
|
44
|
+
"<<",
|
|
45
|
+
">>",
|
|
46
|
+
"{|",
|
|
47
|
+
"|}",
|
|
48
|
+
"^^",
|
|
49
|
+
"&&",
|
|
50
|
+
"||",
|
|
51
|
+
"!=",
|
|
52
|
+
"<=",
|
|
53
|
+
">=",
|
|
54
|
+
"{",
|
|
55
|
+
"}",
|
|
56
|
+
"(",
|
|
57
|
+
")",
|
|
58
|
+
"[",
|
|
59
|
+
"]",
|
|
60
|
+
".",
|
|
61
|
+
",",
|
|
62
|
+
";",
|
|
63
|
+
"*",
|
|
64
|
+
"/",
|
|
65
|
+
"|",
|
|
66
|
+
"^",
|
|
67
|
+
"?",
|
|
68
|
+
"+",
|
|
69
|
+
"-",
|
|
70
|
+
"!",
|
|
71
|
+
"=",
|
|
72
|
+
"<",
|
|
73
|
+
">",
|
|
74
|
+
"~"
|
|
75
|
+
];
|
|
76
|
+
function match(re, input, pos) {
|
|
77
|
+
re.lastIndex = pos;
|
|
78
|
+
const m = re.exec(input);
|
|
79
|
+
return m ? m[0] : null;
|
|
80
|
+
}
|
|
81
|
+
function tokenize(input) {
|
|
82
|
+
const tokens = [];
|
|
83
|
+
let pos = 0;
|
|
84
|
+
let line = 1;
|
|
85
|
+
let lineStart = 0;
|
|
86
|
+
let pendingComments = [];
|
|
87
|
+
let newlinesSinceLast = 0;
|
|
88
|
+
let sawSpace = false;
|
|
89
|
+
let prevToken = null;
|
|
90
|
+
let prevTokenLine = -1;
|
|
91
|
+
const advanceLines = (from, to) => {
|
|
92
|
+
for (let i = from; i < to; i++) {
|
|
93
|
+
const c = input.charCodeAt(i);
|
|
94
|
+
if (c === 10 || c === 13 && input.charCodeAt(i + 1) !== 10) {
|
|
95
|
+
line++;
|
|
96
|
+
lineStart = i + 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const error = (message) => {
|
|
101
|
+
throw new SparqlSyntaxError(message, pos, line, pos - lineStart + 1);
|
|
102
|
+
};
|
|
103
|
+
while (pos < input.length) {
|
|
104
|
+
const ch = input[pos];
|
|
105
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
106
|
+
if (ch === "\n" || ch === "\r" && input[pos + 1] !== "\n") {
|
|
107
|
+
newlinesSinceLast++;
|
|
108
|
+
line++;
|
|
109
|
+
lineStart = pos + 1;
|
|
110
|
+
}
|
|
111
|
+
sawSpace = true;
|
|
112
|
+
pos++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ch === "#") {
|
|
116
|
+
let end = pos;
|
|
117
|
+
while (end < input.length && input[end] !== "\n" && input[end] !== "\r") end++;
|
|
118
|
+
const comment = {
|
|
119
|
+
text: input.slice(pos, end).replace(/\s+$/, ""),
|
|
120
|
+
offset: pos,
|
|
121
|
+
line,
|
|
122
|
+
column: pos - lineStart + 1,
|
|
123
|
+
blankLinesBefore: Math.max(0, newlinesSinceLast - 1)
|
|
124
|
+
};
|
|
125
|
+
if (prevToken && prevTokenLine === line && pendingComments.length === 0 && !prevToken.trailing) {
|
|
126
|
+
prevToken.trailing = comment;
|
|
127
|
+
} else {
|
|
128
|
+
pendingComments.push(comment);
|
|
129
|
+
}
|
|
130
|
+
newlinesSinceLast = 0;
|
|
131
|
+
sawSpace = true;
|
|
132
|
+
pos = end;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const startLine = line;
|
|
136
|
+
const startColumn = pos - lineStart + 1;
|
|
137
|
+
let type;
|
|
138
|
+
let value = null;
|
|
139
|
+
if (ch === "<") {
|
|
140
|
+
if (input.startsWith("<<(", pos)) {
|
|
141
|
+
type = "PUNCT";
|
|
142
|
+
value = "<<(";
|
|
143
|
+
} else if (input.startsWith("<<", pos)) {
|
|
144
|
+
type = "PUNCT";
|
|
145
|
+
value = "<<";
|
|
146
|
+
} else if (value = match(RE_IRIREF, input, pos)) {
|
|
147
|
+
type = "IRIREF";
|
|
148
|
+
} else {
|
|
149
|
+
type = "PUNCT";
|
|
150
|
+
value = input.startsWith("<=", pos) ? "<=" : "<";
|
|
151
|
+
}
|
|
152
|
+
} else if (ch === "?" || ch === "$") {
|
|
153
|
+
value = match(RE_VAR, input, pos);
|
|
154
|
+
if (value) {
|
|
155
|
+
type = "VAR";
|
|
156
|
+
} else if (ch === "?") {
|
|
157
|
+
type = "PUNCT";
|
|
158
|
+
value = "?";
|
|
159
|
+
} else {
|
|
160
|
+
return error("Invalid variable name");
|
|
161
|
+
}
|
|
162
|
+
} else if (ch === '"' || ch === "'") {
|
|
163
|
+
value = ch === '"' ? match(RE_STRING_LONG2, input, pos) ?? match(RE_STRING2, input, pos) : match(RE_STRING_LONG1, input, pos) ?? match(RE_STRING1, input, pos);
|
|
164
|
+
if (!value) return error("Unterminated or invalid string literal");
|
|
165
|
+
type = "STRING";
|
|
166
|
+
} else if (ch === "@") {
|
|
167
|
+
value = match(RE_LANG_DIR, input, pos);
|
|
168
|
+
if (!value) return error("Invalid language tag");
|
|
169
|
+
type = "LANG_DIR";
|
|
170
|
+
} else if (ch === "_" && input[pos + 1] === ":") {
|
|
171
|
+
value = match(RE_BNODE, input, pos);
|
|
172
|
+
if (!value) return error("Invalid blank node label");
|
|
173
|
+
type = "BLANK_NODE_LABEL";
|
|
174
|
+
} else if (ch >= "0" && ch <= "9" || ch === "." && /[0-9]/.test(input[pos + 1] ?? "")) {
|
|
175
|
+
if (value = match(RE_DOUBLE, input, pos)) type = "DOUBLE";
|
|
176
|
+
else if (value = match(RE_DECIMAL, input, pos)) type = "DECIMAL";
|
|
177
|
+
else {
|
|
178
|
+
value = match(RE_INTEGER, input, pos);
|
|
179
|
+
type = "INTEGER";
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
const pname = match(RE_PNAME, input, pos);
|
|
183
|
+
if (pname) {
|
|
184
|
+
value = pname;
|
|
185
|
+
type = pname.endsWith(":") && pname.indexOf(":") === pname.length - 1 ? "PNAME_NS" : "PNAME_LN";
|
|
186
|
+
} else if (value = match(RE_NAME, input, pos)) {
|
|
187
|
+
type = "NAME";
|
|
188
|
+
} else {
|
|
189
|
+
value = PUNCTUATION.find((p) => input.startsWith(p, pos)) ?? null;
|
|
190
|
+
if (!value) return error(`Unexpected character '${String.fromCodePoint(input.codePointAt(pos))}'`);
|
|
191
|
+
type = "PUNCT";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if ((type === "STRING" || type === "IRIREF") && RE_SURROGATE_ESCAPE.test(value)) {
|
|
195
|
+
return error("Escape sequences for surrogate code points (U+D800 to U+DFFF) are not allowed");
|
|
196
|
+
}
|
|
197
|
+
const token = {
|
|
198
|
+
type,
|
|
199
|
+
value,
|
|
200
|
+
offset: pos,
|
|
201
|
+
end: pos + value.length,
|
|
202
|
+
line: startLine,
|
|
203
|
+
column: startColumn,
|
|
204
|
+
index: tokens.length,
|
|
205
|
+
leading: pendingComments,
|
|
206
|
+
trailing: null,
|
|
207
|
+
blankLinesBefore: Math.max(0, newlinesSinceLast - 1),
|
|
208
|
+
spaceBefore: sawSpace
|
|
209
|
+
};
|
|
210
|
+
tokens.push(token);
|
|
211
|
+
advanceLines(pos, token.end);
|
|
212
|
+
pos = token.end;
|
|
213
|
+
prevToken = token;
|
|
214
|
+
prevTokenLine = line;
|
|
215
|
+
pendingComments = [];
|
|
216
|
+
newlinesSinceLast = 0;
|
|
217
|
+
sawSpace = false;
|
|
218
|
+
}
|
|
219
|
+
tokens.push({
|
|
220
|
+
type: "EOF",
|
|
221
|
+
value: "",
|
|
222
|
+
offset: input.length,
|
|
223
|
+
end: input.length,
|
|
224
|
+
line,
|
|
225
|
+
column: input.length - lineStart + 1,
|
|
226
|
+
index: tokens.length,
|
|
227
|
+
leading: pendingComments,
|
|
228
|
+
trailing: null,
|
|
229
|
+
blankLinesBefore: Math.max(0, newlinesSinceLast - 1),
|
|
230
|
+
spaceBefore: sawSpace
|
|
231
|
+
});
|
|
232
|
+
return tokens;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/parser.ts
|
|
236
|
+
var BUILTINS = /* @__PURE__ */ new Set([
|
|
237
|
+
"COUNT",
|
|
238
|
+
"SUM",
|
|
239
|
+
"MIN",
|
|
240
|
+
"MAX",
|
|
241
|
+
"AVG",
|
|
242
|
+
"SAMPLE",
|
|
243
|
+
"GROUP_CONCAT",
|
|
244
|
+
"STR",
|
|
245
|
+
"LANG",
|
|
246
|
+
"LANGMATCHES",
|
|
247
|
+
"LANGDIR",
|
|
248
|
+
"DATATYPE",
|
|
249
|
+
"BOUND",
|
|
250
|
+
"IRI",
|
|
251
|
+
"URI",
|
|
252
|
+
"BNODE",
|
|
253
|
+
"RAND",
|
|
254
|
+
"ABS",
|
|
255
|
+
"CEIL",
|
|
256
|
+
"FLOOR",
|
|
257
|
+
"ROUND",
|
|
258
|
+
"CONCAT",
|
|
259
|
+
"SUBSTR",
|
|
260
|
+
"STRLEN",
|
|
261
|
+
"REPLACE",
|
|
262
|
+
"UCASE",
|
|
263
|
+
"LCASE",
|
|
264
|
+
"ENCODE_FOR_URI",
|
|
265
|
+
"CONTAINS",
|
|
266
|
+
"STRSTARTS",
|
|
267
|
+
"STRENDS",
|
|
268
|
+
"STRBEFORE",
|
|
269
|
+
"STRAFTER",
|
|
270
|
+
"YEAR",
|
|
271
|
+
"MONTH",
|
|
272
|
+
"DAY",
|
|
273
|
+
"HOURS",
|
|
274
|
+
"MINUTES",
|
|
275
|
+
"SECONDS",
|
|
276
|
+
"TIMEZONE",
|
|
277
|
+
"TZ",
|
|
278
|
+
"NOW",
|
|
279
|
+
"UUID",
|
|
280
|
+
"STRUUID",
|
|
281
|
+
"MD5",
|
|
282
|
+
"SHA1",
|
|
283
|
+
"SHA256",
|
|
284
|
+
"SHA384",
|
|
285
|
+
"SHA512",
|
|
286
|
+
"COALESCE",
|
|
287
|
+
"IF",
|
|
288
|
+
"STRLANG",
|
|
289
|
+
"STRLANGDIR",
|
|
290
|
+
"STRDT",
|
|
291
|
+
"SAMETERM",
|
|
292
|
+
"ISIRI",
|
|
293
|
+
"ISURI",
|
|
294
|
+
"ISBLANK",
|
|
295
|
+
"ISLITERAL",
|
|
296
|
+
"ISNUMERIC",
|
|
297
|
+
"HASLANG",
|
|
298
|
+
"HASLANGDIR",
|
|
299
|
+
"REGEX",
|
|
300
|
+
"ISTRIPLE",
|
|
301
|
+
"TRIPLE",
|
|
302
|
+
"SUBJECT",
|
|
303
|
+
"PREDICATE",
|
|
304
|
+
"OBJECT"
|
|
305
|
+
]);
|
|
306
|
+
var AGGREGATES = /* @__PURE__ */ new Set(["COUNT", "SUM", "MIN", "MAX", "AVG", "SAMPLE", "GROUP_CONCAT"]);
|
|
307
|
+
var NO_ARGS = /* @__PURE__ */ new Set(["RAND", "NOW", "UUID", "STRUUID"]);
|
|
308
|
+
var ARITY = {
|
|
309
|
+
LANGMATCHES: [2, 2],
|
|
310
|
+
CONTAINS: [2, 2],
|
|
311
|
+
STRSTARTS: [2, 2],
|
|
312
|
+
STRENDS: [2, 2],
|
|
313
|
+
STRBEFORE: [2, 2],
|
|
314
|
+
STRAFTER: [2, 2],
|
|
315
|
+
STRLANG: [2, 2],
|
|
316
|
+
STRDT: [2, 2],
|
|
317
|
+
SAMETERM: [2, 2],
|
|
318
|
+
IF: [3, 3],
|
|
319
|
+
STRLANGDIR: [3, 3],
|
|
320
|
+
TRIPLE: [3, 3],
|
|
321
|
+
REGEX: [2, 3],
|
|
322
|
+
SUBSTR: [2, 3],
|
|
323
|
+
REPLACE: [3, 4],
|
|
324
|
+
CONCAT: [0, Infinity],
|
|
325
|
+
COALESCE: [0, Infinity],
|
|
326
|
+
BNODE: [0, 1]
|
|
327
|
+
};
|
|
328
|
+
var QUERY_FORMS = /* @__PURE__ */ new Set(["SELECT", "CONSTRUCT", "DESCRIBE", "ASK"]);
|
|
329
|
+
var UPDATE_KEYWORDS = /* @__PURE__ */ new Set(["LOAD", "CLEAR", "DROP", "CREATE", "ADD", "MOVE", "COPY", "INSERT", "DELETE", "WITH"]);
|
|
330
|
+
function parse(input) {
|
|
331
|
+
return new Parser(tokenize(input)).parseUnit();
|
|
332
|
+
}
|
|
333
|
+
var Parser = class {
|
|
334
|
+
constructor(tokens) {
|
|
335
|
+
this.pos = 0;
|
|
336
|
+
this.tokens = tokens;
|
|
337
|
+
}
|
|
338
|
+
// -------------------------------------------------------------------------
|
|
339
|
+
// Token helpers
|
|
340
|
+
// -------------------------------------------------------------------------
|
|
341
|
+
peek(offset = 0) {
|
|
342
|
+
return this.tokens[Math.min(this.pos + offset, this.tokens.length - 1)];
|
|
343
|
+
}
|
|
344
|
+
next() {
|
|
345
|
+
const t = this.tokens[this.pos];
|
|
346
|
+
if (t.type !== "EOF") this.pos++;
|
|
347
|
+
return t;
|
|
348
|
+
}
|
|
349
|
+
/** Is the token the given keyword (case-insensitive)? */
|
|
350
|
+
isKw(t, ...keywords) {
|
|
351
|
+
return t.type === "NAME" && keywords.includes(t.value.toUpperCase());
|
|
352
|
+
}
|
|
353
|
+
isPunct(t, ...values) {
|
|
354
|
+
return t.type === "PUNCT" && values.includes(t.value);
|
|
355
|
+
}
|
|
356
|
+
fail(t, expected) {
|
|
357
|
+
const found = t.type === "EOF" ? "end of input" : `'${t.value}'`;
|
|
358
|
+
throw new SparqlSyntaxError(`Expected ${expected} but found ${found}`, t.offset, t.line, t.column, [expected]);
|
|
359
|
+
}
|
|
360
|
+
expectKw(keyword) {
|
|
361
|
+
const t = this.peek();
|
|
362
|
+
if (!this.isKw(t, keyword)) this.fail(t, keyword);
|
|
363
|
+
return this.next();
|
|
364
|
+
}
|
|
365
|
+
expectPunct(value) {
|
|
366
|
+
const t = this.peek();
|
|
367
|
+
if (!this.isPunct(t, value)) this.fail(t, `'${value}'`);
|
|
368
|
+
return this.next();
|
|
369
|
+
}
|
|
370
|
+
optKw(...keywords) {
|
|
371
|
+
return this.isKw(this.peek(), ...keywords) ? this.next() : null;
|
|
372
|
+
}
|
|
373
|
+
optPunct(value) {
|
|
374
|
+
return this.isPunct(this.peek(), value) ? this.next() : null;
|
|
375
|
+
}
|
|
376
|
+
term(...tokens) {
|
|
377
|
+
return { type: "Term", tokens };
|
|
378
|
+
}
|
|
379
|
+
/** `(` directly followed by `)` (NIL) or `[` followed by `]` (ANON). */
|
|
380
|
+
isEmptyPair(open, close) {
|
|
381
|
+
return this.isPunct(this.peek(), open) && this.isPunct(this.peek(1), close);
|
|
382
|
+
}
|
|
383
|
+
// -------------------------------------------------------------------------
|
|
384
|
+
// Units
|
|
385
|
+
// -------------------------------------------------------------------------
|
|
386
|
+
parseUnit() {
|
|
387
|
+
const start = this.pos;
|
|
388
|
+
const prologue = this.parsePrologue();
|
|
389
|
+
if (QUERY_FORMS.has(this.peek().type === "NAME" ? this.peek().value.toUpperCase() : "")) {
|
|
390
|
+
const form = this.parseQueryForm();
|
|
391
|
+
const values = this.isKw(this.peek(), "VALUES") ? this.parseValues() : null;
|
|
392
|
+
const eof = this.peek();
|
|
393
|
+
if (eof.type !== "EOF") this.fail(eof, "end of query");
|
|
394
|
+
return { type: "Query", prologue, form, values, eof };
|
|
395
|
+
}
|
|
396
|
+
this.pos = start;
|
|
397
|
+
return this.parseUpdate();
|
|
398
|
+
}
|
|
399
|
+
parsePrologue() {
|
|
400
|
+
const decls = [];
|
|
401
|
+
for (; ; ) {
|
|
402
|
+
const t = this.peek();
|
|
403
|
+
if (this.isKw(t, "BASE")) {
|
|
404
|
+
const keyword = this.next();
|
|
405
|
+
const value = this.next();
|
|
406
|
+
if (value.type !== "IRIREF") this.fail(value, "an IRI");
|
|
407
|
+
decls.push({ type: "Base", keyword, prefix: null, value });
|
|
408
|
+
} else if (this.isKw(t, "PREFIX")) {
|
|
409
|
+
const keyword = this.next();
|
|
410
|
+
const prefix = this.next();
|
|
411
|
+
if (prefix.type !== "PNAME_NS") this.fail(prefix, "a prefix name such as 'ex:'");
|
|
412
|
+
const value = this.next();
|
|
413
|
+
if (value.type !== "IRIREF") this.fail(value, "an IRI");
|
|
414
|
+
decls.push({ type: "Prefix", keyword, prefix, value });
|
|
415
|
+
} else if (this.isKw(t, "VERSION")) {
|
|
416
|
+
const keyword = this.next();
|
|
417
|
+
const value = this.next();
|
|
418
|
+
if (value.type !== "STRING" || value.value.startsWith("'''") || value.value.startsWith('"""')) {
|
|
419
|
+
this.fail(value, "a version string");
|
|
420
|
+
}
|
|
421
|
+
decls.push({ type: "Version", keyword, prefix: null, value });
|
|
422
|
+
} else {
|
|
423
|
+
return decls;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
parseQueryForm() {
|
|
428
|
+
const t = this.peek();
|
|
429
|
+
switch (t.value.toUpperCase()) {
|
|
430
|
+
case "SELECT":
|
|
431
|
+
return this.parseSelect();
|
|
432
|
+
case "CONSTRUCT":
|
|
433
|
+
return this.parseConstruct();
|
|
434
|
+
case "DESCRIBE":
|
|
435
|
+
return this.parseDescribe();
|
|
436
|
+
default:
|
|
437
|
+
return this.parseAsk();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
parseSelectClause() {
|
|
441
|
+
const keyword = this.expectKw("SELECT");
|
|
442
|
+
const modifier = this.optKw("DISTINCT", "REDUCED");
|
|
443
|
+
const star = this.optPunct("*");
|
|
444
|
+
const items = [];
|
|
445
|
+
if (!star) {
|
|
446
|
+
for (; ; ) {
|
|
447
|
+
const t = this.peek();
|
|
448
|
+
if (t.type === "VAR") {
|
|
449
|
+
items.push(this.term(this.next()));
|
|
450
|
+
} else if (this.isPunct(t, "(")) {
|
|
451
|
+
const open = this.next();
|
|
452
|
+
const expression = this.parseExpression();
|
|
453
|
+
const as = this.expectKw("AS");
|
|
454
|
+
const variable = this.parseVar();
|
|
455
|
+
const close = this.expectPunct(")");
|
|
456
|
+
items.push({ type: "Projection", open, expression, as, variable, close });
|
|
457
|
+
} else {
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (items.length === 0) this.fail(this.peek(), "'*', a variable or '(' after SELECT");
|
|
462
|
+
}
|
|
463
|
+
return { type: "SelectClause", keyword, modifier, star, items };
|
|
464
|
+
}
|
|
465
|
+
parseSelect() {
|
|
466
|
+
const select = this.parseSelectClause();
|
|
467
|
+
const datasets = this.parseDatasets();
|
|
468
|
+
const where = this.parseWhereClause(true);
|
|
469
|
+
const modifiers = this.parseSolutionModifier();
|
|
470
|
+
return { type: "Select", select, datasets, where, modifiers };
|
|
471
|
+
}
|
|
472
|
+
parseSubSelect() {
|
|
473
|
+
const select = this.parseSelectClause();
|
|
474
|
+
const where = this.parseWhereClause(true);
|
|
475
|
+
const modifiers = this.parseSolutionModifier();
|
|
476
|
+
const values = this.isKw(this.peek(), "VALUES") ? this.parseValues() : null;
|
|
477
|
+
return { type: "SubSelect", select, datasets: [], where, modifiers, values };
|
|
478
|
+
}
|
|
479
|
+
parseConstruct() {
|
|
480
|
+
const keyword = this.expectKw("CONSTRUCT");
|
|
481
|
+
if (this.isPunct(this.peek(), "{")) {
|
|
482
|
+
const template2 = this.parseTriplesTemplate(false);
|
|
483
|
+
const datasets2 = this.parseDatasets();
|
|
484
|
+
const where = this.parseWhereClause(true);
|
|
485
|
+
const modifiers2 = this.parseSolutionModifier();
|
|
486
|
+
return { type: "Construct", keyword, template: template2, datasets: datasets2, where, shortForm: null, modifiers: modifiers2 };
|
|
487
|
+
}
|
|
488
|
+
const datasets = this.parseDatasets();
|
|
489
|
+
const whereKw = this.expectKw("WHERE");
|
|
490
|
+
const template = this.parseTriplesTemplate(false);
|
|
491
|
+
const modifiers = this.parseSolutionModifier();
|
|
492
|
+
return {
|
|
493
|
+
type: "Construct",
|
|
494
|
+
keyword,
|
|
495
|
+
template: null,
|
|
496
|
+
datasets,
|
|
497
|
+
where: null,
|
|
498
|
+
shortForm: { keyword: whereKw, template },
|
|
499
|
+
modifiers
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
parseDescribe() {
|
|
503
|
+
const keyword = this.expectKw("DESCRIBE");
|
|
504
|
+
const star = this.optPunct("*");
|
|
505
|
+
const items = [];
|
|
506
|
+
if (!star) {
|
|
507
|
+
while (this.isVarOrIri(this.peek())) items.push(this.term(this.next()));
|
|
508
|
+
if (items.length === 0) this.fail(this.peek(), "'*', a variable or an IRI after DESCRIBE");
|
|
509
|
+
}
|
|
510
|
+
const datasets = this.parseDatasets();
|
|
511
|
+
const where = this.isKw(this.peek(), "WHERE") || this.isPunct(this.peek(), "{") ? this.parseWhereClause(true) : null;
|
|
512
|
+
const modifiers = this.parseSolutionModifier();
|
|
513
|
+
return { type: "Describe", keyword, star, items, datasets, where, modifiers };
|
|
514
|
+
}
|
|
515
|
+
parseAsk() {
|
|
516
|
+
const keyword = this.expectKw("ASK");
|
|
517
|
+
const datasets = this.parseDatasets();
|
|
518
|
+
const where = this.parseWhereClause(true);
|
|
519
|
+
const modifiers = this.parseSolutionModifier();
|
|
520
|
+
return { type: "Ask", keyword, datasets, where, modifiers };
|
|
521
|
+
}
|
|
522
|
+
parseDatasets() {
|
|
523
|
+
const datasets = [];
|
|
524
|
+
while (this.isKw(this.peek(), "FROM")) {
|
|
525
|
+
const from = this.next();
|
|
526
|
+
const named = this.optKw("NAMED");
|
|
527
|
+
datasets.push({ type: "DatasetClause", from, named, iri: this.parseIri() });
|
|
528
|
+
}
|
|
529
|
+
return datasets;
|
|
530
|
+
}
|
|
531
|
+
parseWhereClause(optionalKeyword) {
|
|
532
|
+
const keyword = optionalKeyword ? this.optKw("WHERE") : this.expectKw("WHERE");
|
|
533
|
+
return { keyword, group: this.parseGroupGraphPattern() };
|
|
534
|
+
}
|
|
535
|
+
parseSolutionModifier() {
|
|
536
|
+
const modifiers = { groupBy: null, having: null, orderBy: null, limitOffset: [] };
|
|
537
|
+
if (this.isKw(this.peek(), "GROUP")) {
|
|
538
|
+
const group = this.next();
|
|
539
|
+
const by = this.expectKw("BY");
|
|
540
|
+
const conditions = [];
|
|
541
|
+
while (this.startsCondition(this.peek())) {
|
|
542
|
+
if (this.isPunct(this.peek(), "(")) {
|
|
543
|
+
const open = this.next();
|
|
544
|
+
const expression = this.parseExpression();
|
|
545
|
+
const as = this.optKw("AS");
|
|
546
|
+
const variable = as ? this.parseVar() : null;
|
|
547
|
+
const close = this.expectPunct(")");
|
|
548
|
+
conditions.push({ type: "Projection", open, expression, as, variable, close });
|
|
549
|
+
} else if (this.peek().type === "VAR") {
|
|
550
|
+
conditions.push(this.term(this.next()));
|
|
551
|
+
} else {
|
|
552
|
+
conditions.push(this.parseConstraint());
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (conditions.length === 0) this.fail(this.peek(), "a GROUP BY condition");
|
|
556
|
+
modifiers.groupBy = { group, by, conditions };
|
|
557
|
+
}
|
|
558
|
+
if (this.isKw(this.peek(), "HAVING")) {
|
|
559
|
+
const keyword = this.next();
|
|
560
|
+
const conditions = [];
|
|
561
|
+
while (this.startsCondition(this.peek()) && this.peek().type !== "VAR") {
|
|
562
|
+
conditions.push(this.parseConstraint());
|
|
563
|
+
}
|
|
564
|
+
if (conditions.length === 0) this.fail(this.peek(), "a HAVING condition");
|
|
565
|
+
modifiers.having = { keyword, conditions };
|
|
566
|
+
}
|
|
567
|
+
if (this.isKw(this.peek(), "ORDER")) {
|
|
568
|
+
const order = this.next();
|
|
569
|
+
const by = this.expectKw("BY");
|
|
570
|
+
const conditions = [];
|
|
571
|
+
for (; ; ) {
|
|
572
|
+
const t = this.peek();
|
|
573
|
+
if (this.isKw(t, "ASC", "DESC")) {
|
|
574
|
+
const direction = this.next();
|
|
575
|
+
conditions.push({ type: "OrderCondition", direction, expression: this.parseBracketed() });
|
|
576
|
+
} else if (t.type === "VAR") {
|
|
577
|
+
conditions.push(this.term(this.next()));
|
|
578
|
+
} else if (this.startsCondition(t)) {
|
|
579
|
+
conditions.push(this.parseConstraint());
|
|
580
|
+
} else {
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (conditions.length === 0) this.fail(this.peek(), "an ORDER BY condition");
|
|
585
|
+
modifiers.orderBy = { order, by, conditions };
|
|
586
|
+
}
|
|
587
|
+
const first = this.optKw("LIMIT", "OFFSET");
|
|
588
|
+
if (first) {
|
|
589
|
+
modifiers.limitOffset.push({ keyword: first, value: this.parseInteger() });
|
|
590
|
+
const second = this.optKw(this.isKw(first, "LIMIT") ? "OFFSET" : "LIMIT");
|
|
591
|
+
if (second) modifiers.limitOffset.push({ keyword: second, value: this.parseInteger() });
|
|
592
|
+
}
|
|
593
|
+
return modifiers;
|
|
594
|
+
}
|
|
595
|
+
parseInteger() {
|
|
596
|
+
const t = this.peek();
|
|
597
|
+
if (t.type !== "INTEGER") this.fail(t, "an integer");
|
|
598
|
+
return this.next();
|
|
599
|
+
}
|
|
600
|
+
/** Can this token start a GROUP BY / HAVING / ORDER BY condition? */
|
|
601
|
+
startsCondition(t) {
|
|
602
|
+
return t.type === "VAR" || this.isPunct(t, "(") || t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS" || t.type === "NAME" && (BUILTINS.has(t.value.toUpperCase()) || this.isKw(t, "EXISTS", "NOT"));
|
|
603
|
+
}
|
|
604
|
+
/** Constraint: BrackettedExpression | BuiltInCall | FunctionCall */
|
|
605
|
+
parseConstraint() {
|
|
606
|
+
const t = this.peek();
|
|
607
|
+
if (this.isPunct(t, "(")) return this.parseBracketed();
|
|
608
|
+
if (t.type === "NAME") return this.parseBuiltInCall();
|
|
609
|
+
if (t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS") {
|
|
610
|
+
const iri = this.parseIri();
|
|
611
|
+
if (!this.isPunct(this.peek(), "(")) this.fail(this.peek(), "'(' of a function call");
|
|
612
|
+
return this.parseArgList(iri, false);
|
|
613
|
+
}
|
|
614
|
+
return this.fail(t, "'(', a built-in call or a function call");
|
|
615
|
+
}
|
|
616
|
+
// -------------------------------------------------------------------------
|
|
617
|
+
// Updates
|
|
618
|
+
// -------------------------------------------------------------------------
|
|
619
|
+
parseUpdate() {
|
|
620
|
+
const parts = [];
|
|
621
|
+
for (; ; ) {
|
|
622
|
+
const prologue = this.parsePrologue();
|
|
623
|
+
const t = this.peek();
|
|
624
|
+
let operation = null;
|
|
625
|
+
if (t.type === "NAME" && UPDATE_KEYWORDS.has(t.value.toUpperCase())) {
|
|
626
|
+
operation = this.parseUpdateOperation();
|
|
627
|
+
} else if (t.type !== "EOF") {
|
|
628
|
+
this.fail(t, parts.length === 0 && prologue.length === 0 ? "a query or update" : "an update operation");
|
|
629
|
+
}
|
|
630
|
+
const semicolon = operation ? this.optPunct(";") : null;
|
|
631
|
+
parts.push({ prologue, operation, semicolon });
|
|
632
|
+
if (!semicolon) break;
|
|
633
|
+
}
|
|
634
|
+
const eof = this.peek();
|
|
635
|
+
if (eof.type !== "EOF") this.fail(eof, "';' or end of update");
|
|
636
|
+
return { type: "Update", parts, eof };
|
|
637
|
+
}
|
|
638
|
+
parseUpdateOperation() {
|
|
639
|
+
const t = this.peek();
|
|
640
|
+
const kw = t.value.toUpperCase();
|
|
641
|
+
switch (kw) {
|
|
642
|
+
case "LOAD": {
|
|
643
|
+
const keyword = this.next();
|
|
644
|
+
const silent = this.optKw("SILENT");
|
|
645
|
+
const iri = this.parseIri();
|
|
646
|
+
let into = null;
|
|
647
|
+
if (this.isKw(this.peek(), "INTO")) {
|
|
648
|
+
const intoKw = this.next();
|
|
649
|
+
const graph = this.expectKw("GRAPH");
|
|
650
|
+
into = { type: "Words", tokens: [intoKw, graph, this.parseIri()] };
|
|
651
|
+
}
|
|
652
|
+
return { type: "Load", keyword, silent, iri, into };
|
|
653
|
+
}
|
|
654
|
+
case "CLEAR":
|
|
655
|
+
case "DROP":
|
|
656
|
+
case "CREATE": {
|
|
657
|
+
const keyword = this.next();
|
|
658
|
+
const silent = this.optKw("SILENT");
|
|
659
|
+
let target;
|
|
660
|
+
if (kw !== "CREATE" && this.isKw(this.peek(), "DEFAULT", "NAMED", "ALL")) {
|
|
661
|
+
target = { type: "Words", tokens: [this.next()] };
|
|
662
|
+
} else {
|
|
663
|
+
const graph = this.expectKw("GRAPH");
|
|
664
|
+
target = { type: "Words", tokens: [graph, this.parseIri()] };
|
|
665
|
+
}
|
|
666
|
+
return { type: "GraphManagement", keyword, silent, target };
|
|
667
|
+
}
|
|
668
|
+
case "ADD":
|
|
669
|
+
case "MOVE":
|
|
670
|
+
case "COPY": {
|
|
671
|
+
const keyword = this.next();
|
|
672
|
+
const silent = this.optKw("SILENT");
|
|
673
|
+
const from = this.parseGraphOrDefault();
|
|
674
|
+
const to = this.expectKw("TO");
|
|
675
|
+
const target = this.parseGraphOrDefault();
|
|
676
|
+
return { type: "GraphTransfer", keyword, silent, from, to, target };
|
|
677
|
+
}
|
|
678
|
+
case "INSERT": {
|
|
679
|
+
if (this.isKw(this.peek(1), "DATA")) {
|
|
680
|
+
const keywords = [this.next(), this.next()];
|
|
681
|
+
return { type: "QuadData", keywords, quads: this.checkQuads(this.parseQuads(), true, false) };
|
|
682
|
+
}
|
|
683
|
+
return this.parseModify();
|
|
684
|
+
}
|
|
685
|
+
case "DELETE": {
|
|
686
|
+
if (this.isKw(this.peek(1), "DATA", "WHERE")) {
|
|
687
|
+
const keywords = [this.next(), this.next()];
|
|
688
|
+
const noVars = this.isKw(keywords[1], "DATA");
|
|
689
|
+
return { type: "QuadData", keywords, quads: this.checkQuads(this.parseQuads(), noVars, true) };
|
|
690
|
+
}
|
|
691
|
+
return this.parseModify();
|
|
692
|
+
}
|
|
693
|
+
default:
|
|
694
|
+
return this.parseModify();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
parseGraphOrDefault() {
|
|
698
|
+
if (this.isKw(this.peek(), "DEFAULT")) return { type: "Words", tokens: [this.next()] };
|
|
699
|
+
const graph = this.optKw("GRAPH");
|
|
700
|
+
const iri = this.parseIri();
|
|
701
|
+
return { type: "Words", tokens: graph ? [graph, iri] : [iri] };
|
|
702
|
+
}
|
|
703
|
+
parseModify() {
|
|
704
|
+
let withClause = null;
|
|
705
|
+
if (this.isKw(this.peek(), "WITH")) {
|
|
706
|
+
const keyword = this.next();
|
|
707
|
+
withClause = { keyword, iri: this.parseIri() };
|
|
708
|
+
}
|
|
709
|
+
let del = null;
|
|
710
|
+
let ins = null;
|
|
711
|
+
if (this.isKw(this.peek(), "DELETE")) {
|
|
712
|
+
const keyword = this.next();
|
|
713
|
+
del = { keyword, quads: this.checkQuads(this.parseQuads(), false, true) };
|
|
714
|
+
}
|
|
715
|
+
if (this.isKw(this.peek(), "INSERT")) {
|
|
716
|
+
const keyword = this.next();
|
|
717
|
+
ins = { keyword, quads: this.parseQuads() };
|
|
718
|
+
}
|
|
719
|
+
if (!del && !ins) this.fail(this.peek(), "DELETE or INSERT");
|
|
720
|
+
const using = [];
|
|
721
|
+
while (this.isKw(this.peek(), "USING")) {
|
|
722
|
+
const keyword = this.next();
|
|
723
|
+
const named = this.optKw("NAMED");
|
|
724
|
+
using.push({ keyword, named, iri: this.parseIri() });
|
|
725
|
+
}
|
|
726
|
+
const where = this.parseWhereClause(false);
|
|
727
|
+
return { type: "Modify", with: withClause, delete: del, insert: ins, using, where };
|
|
728
|
+
}
|
|
729
|
+
/** `{ TriplesTemplate? }` of CONSTRUCT and of `GRAPH x { }` in quads */
|
|
730
|
+
parseTriplesTemplate(path) {
|
|
731
|
+
const open = this.expectPunct("{");
|
|
732
|
+
const items = [];
|
|
733
|
+
while (!this.isPunct(this.peek(), "}")) {
|
|
734
|
+
if (!this.startsTriples(this.peek())) this.fail(this.peek(), "a triple pattern or '}'");
|
|
735
|
+
const node = this.parseTriplesSameSubject(path);
|
|
736
|
+
const dot = this.optPunct(".");
|
|
737
|
+
items.push({ node, dot });
|
|
738
|
+
if (!dot && !this.isPunct(this.peek(), "}")) this.fail(this.peek(), "'.' or '}'");
|
|
739
|
+
}
|
|
740
|
+
const close = this.next();
|
|
741
|
+
return { type: "TriplesTemplate", open, items, close };
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Notes 8 and 9 of the grammar: no variables in INSERT DATA / DELETE DATA,
|
|
745
|
+
* no blank nodes in DELETE DATA, DELETE WHERE and DELETE templates.
|
|
746
|
+
*/
|
|
747
|
+
checkQuads(quads, noVars, noBlankNodes) {
|
|
748
|
+
const reject = (t, what) => {
|
|
749
|
+
throw new SparqlSyntaxError(`${what} are not allowed here`, t.offset, t.line, t.column);
|
|
750
|
+
};
|
|
751
|
+
const visit = (value) => {
|
|
752
|
+
if (value === null || typeof value !== "object") return;
|
|
753
|
+
if (Array.isArray(value)) {
|
|
754
|
+
value.forEach((v, i) => {
|
|
755
|
+
if (noBlankNodes && v?.type === "AnnotationBlock" && (i === 0 || value[i - 1].type !== "Reifier")) {
|
|
756
|
+
reject(v.open, "Blank nodes");
|
|
757
|
+
}
|
|
758
|
+
visit(v);
|
|
759
|
+
});
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const node = value;
|
|
763
|
+
if (typeof node.offset === "number" && typeof node.value === "string") {
|
|
764
|
+
const t = node;
|
|
765
|
+
if (noVars && t.type === "VAR") reject(t, "Variables");
|
|
766
|
+
if (noBlankNodes && (t.type === "BLANK_NODE_LABEL" || this.isPunct(t, "["))) reject(t, "Blank nodes");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (noBlankNodes && node.type === "Reifier" && !node.id) reject(node.tilde, "Blank nodes");
|
|
770
|
+
if (noBlankNodes && node.type === "ReifiedTriple" && !node.reifier) reject(node.open, "Blank nodes");
|
|
771
|
+
for (const child of Object.values(node)) visit(child);
|
|
772
|
+
};
|
|
773
|
+
visit(quads.items);
|
|
774
|
+
return quads;
|
|
775
|
+
}
|
|
776
|
+
/** `{ Quads }`: triples and `GRAPH x { triples }` blocks */
|
|
777
|
+
parseQuads() {
|
|
778
|
+
const open = this.expectPunct("{");
|
|
779
|
+
const items = [];
|
|
780
|
+
while (!this.isPunct(this.peek(), "}")) {
|
|
781
|
+
if (this.isKw(this.peek(), "GRAPH")) {
|
|
782
|
+
const keyword = this.next();
|
|
783
|
+
const name = this.parseVarOrIri();
|
|
784
|
+
const template = this.parseTriplesTemplate(false);
|
|
785
|
+
items.push({ node: { type: "QuadsGraph", keyword, name, template }, dot: this.optPunct(".") });
|
|
786
|
+
} else if (this.startsTriples(this.peek())) {
|
|
787
|
+
const node = this.parseTriplesSameSubject(false);
|
|
788
|
+
const dot = this.optPunct(".");
|
|
789
|
+
items.push({ node, dot });
|
|
790
|
+
if (!dot && !this.isPunct(this.peek(), "}") && !this.isKw(this.peek(), "GRAPH")) {
|
|
791
|
+
this.fail(this.peek(), "'.' or '}'");
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
this.fail(this.peek(), "a triple pattern, GRAPH or '}'");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const close = this.next();
|
|
798
|
+
return { type: "TriplesTemplate", open, items, close };
|
|
799
|
+
}
|
|
800
|
+
// -------------------------------------------------------------------------
|
|
801
|
+
// Graph patterns
|
|
802
|
+
// -------------------------------------------------------------------------
|
|
803
|
+
parseGroupGraphPattern() {
|
|
804
|
+
const open = this.expectPunct("{");
|
|
805
|
+
if (this.isKw(this.peek(), "SELECT")) {
|
|
806
|
+
const subSelect = this.parseSubSelect();
|
|
807
|
+
const close2 = this.expectPunct("}");
|
|
808
|
+
return { type: "GroupGraphPattern", open, subSelect, items: [], close: close2 };
|
|
809
|
+
}
|
|
810
|
+
const items = [];
|
|
811
|
+
let needsDot = false;
|
|
812
|
+
for (; ; ) {
|
|
813
|
+
const t = this.peek();
|
|
814
|
+
if (this.isPunct(t, "}")) break;
|
|
815
|
+
if (this.startsTriples(t)) {
|
|
816
|
+
if (needsDot) this.fail(t, "'.' before the next triple pattern");
|
|
817
|
+
const node = this.parseTriplesSameSubject(true);
|
|
818
|
+
const dot = this.optPunct(".");
|
|
819
|
+
items.push({ node, dot });
|
|
820
|
+
needsDot = !dot;
|
|
821
|
+
} else {
|
|
822
|
+
const node = this.parseGraphPatternNotTriples();
|
|
823
|
+
items.push({ node, dot: this.optPunct(".") });
|
|
824
|
+
needsDot = false;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const close = this.next();
|
|
828
|
+
return { type: "GroupGraphPattern", open, subSelect: null, items, close };
|
|
829
|
+
}
|
|
830
|
+
parseGraphPatternNotTriples() {
|
|
831
|
+
const t = this.peek();
|
|
832
|
+
if (this.isPunct(t, "{")) {
|
|
833
|
+
const groups = [this.parseGroupGraphPattern()];
|
|
834
|
+
const keywords = [];
|
|
835
|
+
while (this.isKw(this.peek(), "UNION")) {
|
|
836
|
+
keywords.push(this.next());
|
|
837
|
+
groups.push(this.parseGroupGraphPattern());
|
|
838
|
+
}
|
|
839
|
+
return groups.length === 1 ? groups[0] : { type: "Union", groups, keywords };
|
|
840
|
+
}
|
|
841
|
+
if (t.type === "NAME") {
|
|
842
|
+
switch (t.value.toUpperCase()) {
|
|
843
|
+
case "OPTIONAL": {
|
|
844
|
+
const keyword = this.next();
|
|
845
|
+
return { type: "Optional", keyword, group: this.parseGroupGraphPattern() };
|
|
846
|
+
}
|
|
847
|
+
case "MINUS": {
|
|
848
|
+
const keyword = this.next();
|
|
849
|
+
return { type: "Minus", keyword, group: this.parseGroupGraphPattern() };
|
|
850
|
+
}
|
|
851
|
+
case "GRAPH": {
|
|
852
|
+
const keyword = this.next();
|
|
853
|
+
const name = this.parseVarOrIri();
|
|
854
|
+
return { type: "Graph", keyword, name, group: this.parseGroupGraphPattern() };
|
|
855
|
+
}
|
|
856
|
+
case "SERVICE": {
|
|
857
|
+
const keyword = this.next();
|
|
858
|
+
const silent = this.optKw("SILENT");
|
|
859
|
+
const name = this.parseVarOrIri();
|
|
860
|
+
return { type: "Service", keyword, silent, name, group: this.parseGroupGraphPattern() };
|
|
861
|
+
}
|
|
862
|
+
case "FILTER": {
|
|
863
|
+
const keyword = this.next();
|
|
864
|
+
return { type: "Filter", keyword, constraint: this.parseConstraint() };
|
|
865
|
+
}
|
|
866
|
+
case "BIND": {
|
|
867
|
+
const keyword = this.next();
|
|
868
|
+
const open = this.expectPunct("(");
|
|
869
|
+
const expression = this.parseExpression();
|
|
870
|
+
const as = this.expectKw("AS");
|
|
871
|
+
const variable = this.parseVar();
|
|
872
|
+
const close = this.expectPunct(")");
|
|
873
|
+
return { type: "Bind", keyword, open, expression, as, variable, close };
|
|
874
|
+
}
|
|
875
|
+
case "VALUES":
|
|
876
|
+
return this.parseValues();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return this.fail(t, "a triple pattern, a graph pattern or '}'");
|
|
880
|
+
}
|
|
881
|
+
parseValues() {
|
|
882
|
+
const keyword = this.expectKw("VALUES");
|
|
883
|
+
return { type: "Values", keyword, block: this.parseDataBlock() };
|
|
884
|
+
}
|
|
885
|
+
parseDataBlock() {
|
|
886
|
+
const t = this.peek();
|
|
887
|
+
if (t.type === "VAR") {
|
|
888
|
+
const variable = this.term(this.next());
|
|
889
|
+
const open2 = this.expectPunct("{");
|
|
890
|
+
const values = [];
|
|
891
|
+
while (!this.isPunct(this.peek(), "}")) values.push(this.parseDataBlockValue());
|
|
892
|
+
const close2 = this.next();
|
|
893
|
+
return { type: "DataBlock", variable, variables: null, open: open2, values, rows: [], close: close2 };
|
|
894
|
+
}
|
|
895
|
+
const varsOpen = this.expectPunct("(");
|
|
896
|
+
const vars = [];
|
|
897
|
+
while (this.peek().type === "VAR") vars.push(this.term(this.next()));
|
|
898
|
+
const varsClose = this.expectPunct(")");
|
|
899
|
+
const open = this.expectPunct("{");
|
|
900
|
+
const rows = [];
|
|
901
|
+
while (!this.isPunct(this.peek(), "}")) {
|
|
902
|
+
const rowOpen = this.expectPunct("(");
|
|
903
|
+
const values = [];
|
|
904
|
+
while (!this.isPunct(this.peek(), ")")) values.push(this.parseDataBlockValue());
|
|
905
|
+
if (values.length !== vars.length) {
|
|
906
|
+
throw new SparqlSyntaxError(
|
|
907
|
+
`Expected ${vars.length} value(s) in this VALUES row, found ${values.length}`,
|
|
908
|
+
rowOpen.offset,
|
|
909
|
+
rowOpen.line,
|
|
910
|
+
rowOpen.column
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
rows.push({ open: rowOpen, values, close: this.next() });
|
|
914
|
+
}
|
|
915
|
+
const close = this.next();
|
|
916
|
+
return {
|
|
917
|
+
type: "DataBlock",
|
|
918
|
+
variable: null,
|
|
919
|
+
variables: { open: varsOpen, vars, close: varsClose },
|
|
920
|
+
open,
|
|
921
|
+
values: [],
|
|
922
|
+
rows,
|
|
923
|
+
close
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
parseDataBlockValue() {
|
|
927
|
+
const t = this.peek();
|
|
928
|
+
if (this.isKw(t, "UNDEF")) return this.term(this.next());
|
|
929
|
+
if (this.isPunct(t, "<<(")) return this.parseTripleTerm("data");
|
|
930
|
+
if (t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS") return this.parseIri();
|
|
931
|
+
const literal = this.tryParseLiteral();
|
|
932
|
+
if (literal) return literal;
|
|
933
|
+
return this.fail(t, "a data value (IRI, literal, UNDEF or triple term)");
|
|
934
|
+
}
|
|
935
|
+
// -------------------------------------------------------------------------
|
|
936
|
+
// Triples
|
|
937
|
+
// -------------------------------------------------------------------------
|
|
938
|
+
startsTriples(t) {
|
|
939
|
+
switch (t.type) {
|
|
940
|
+
case "VAR":
|
|
941
|
+
case "IRIREF":
|
|
942
|
+
case "PNAME_LN":
|
|
943
|
+
case "PNAME_NS":
|
|
944
|
+
case "BLANK_NODE_LABEL":
|
|
945
|
+
case "STRING":
|
|
946
|
+
case "INTEGER":
|
|
947
|
+
case "DECIMAL":
|
|
948
|
+
case "DOUBLE":
|
|
949
|
+
return true;
|
|
950
|
+
case "NAME":
|
|
951
|
+
return this.isKw(t, "TRUE", "FALSE");
|
|
952
|
+
case "PUNCT":
|
|
953
|
+
return ["(", "[", "<<", "<<(", "+", "-"].includes(t.value);
|
|
954
|
+
default:
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/** TriplesSameSubject(Path) */
|
|
959
|
+
parseTriplesSameSubject(path) {
|
|
960
|
+
const t = this.peek();
|
|
961
|
+
let subject;
|
|
962
|
+
let required = true;
|
|
963
|
+
if (this.isPunct(t, "[") && !this.isEmptyPair("[", "]")) {
|
|
964
|
+
subject = this.parseBlankNodePropertyList(path);
|
|
965
|
+
required = false;
|
|
966
|
+
} else if (this.isPunct(t, "(") && !this.isEmptyPair("(", ")")) {
|
|
967
|
+
subject = this.parseCollection(path);
|
|
968
|
+
required = false;
|
|
969
|
+
} else if (this.isPunct(t, "<<")) {
|
|
970
|
+
subject = this.parseReifiedTriple();
|
|
971
|
+
required = false;
|
|
972
|
+
} else {
|
|
973
|
+
subject = this.parseVarOrTerm();
|
|
974
|
+
}
|
|
975
|
+
const properties = required || this.startsVerb(this.peek(), path) ? this.parsePropertyListNotEmpty(path) : null;
|
|
976
|
+
return { type: "Triples", subject, properties };
|
|
977
|
+
}
|
|
978
|
+
startsVerb(t, path) {
|
|
979
|
+
if (t.type === "VAR" || t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS") return true;
|
|
980
|
+
if (t.type === "NAME" && t.value === "a") return true;
|
|
981
|
+
return path && this.isPunct(t, "^", "!", "(");
|
|
982
|
+
}
|
|
983
|
+
parsePropertyListNotEmpty(path) {
|
|
984
|
+
const entries = [];
|
|
985
|
+
for (; ; ) {
|
|
986
|
+
const verb = this.parseVerb(path);
|
|
987
|
+
const objects = [];
|
|
988
|
+
for (; ; ) {
|
|
989
|
+
const node = this.parseGraphNode(path);
|
|
990
|
+
const annotations = this.parseAnnotations(path);
|
|
991
|
+
if (annotations.length > 0 && verb.type !== "Term") {
|
|
992
|
+
const t = annotations[0].type === "Reifier" ? annotations[0].tilde : annotations[0].open;
|
|
993
|
+
throw new SparqlSyntaxError(
|
|
994
|
+
"A reifier or annotation can only follow a simple predicate, not a property path",
|
|
995
|
+
t.offset,
|
|
996
|
+
t.line,
|
|
997
|
+
t.column
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const entry = { node, annotations, comma: this.optPunct(",") };
|
|
1001
|
+
objects.push(entry);
|
|
1002
|
+
if (!entry.comma) break;
|
|
1003
|
+
}
|
|
1004
|
+
const semicolons = [];
|
|
1005
|
+
while (this.isPunct(this.peek(), ";")) semicolons.push(this.next());
|
|
1006
|
+
entries.push({ verb, objects, semicolons });
|
|
1007
|
+
if (semicolons.length === 0 || !this.startsVerb(this.peek(), path)) break;
|
|
1008
|
+
}
|
|
1009
|
+
return { type: "PropertyList", entries };
|
|
1010
|
+
}
|
|
1011
|
+
parseVerb(path) {
|
|
1012
|
+
const t = this.peek();
|
|
1013
|
+
if (t.type === "VAR") return this.term(this.next());
|
|
1014
|
+
if (!path) {
|
|
1015
|
+
if (t.type === "NAME" && t.value === "a") return this.term(this.next());
|
|
1016
|
+
return this.parseIri("a predicate");
|
|
1017
|
+
}
|
|
1018
|
+
return this.parsePath();
|
|
1019
|
+
}
|
|
1020
|
+
/** Verb of a reified triple or triple term: Var | iri | 'a' */
|
|
1021
|
+
parseSimpleVerb(allowVar) {
|
|
1022
|
+
const t = this.peek();
|
|
1023
|
+
if (t.type === "VAR" && allowVar) return this.term(this.next());
|
|
1024
|
+
if (t.type === "NAME" && t.value === "a") return this.term(this.next());
|
|
1025
|
+
return this.parseIri("a predicate");
|
|
1026
|
+
}
|
|
1027
|
+
parseAnnotations(path) {
|
|
1028
|
+
const annotations = [];
|
|
1029
|
+
for (; ; ) {
|
|
1030
|
+
const t = this.peek();
|
|
1031
|
+
if (this.isPunct(t, "~")) {
|
|
1032
|
+
annotations.push(this.parseReifier());
|
|
1033
|
+
} else if (this.isPunct(t, "{|")) {
|
|
1034
|
+
const open = this.next();
|
|
1035
|
+
const properties = this.parsePropertyListNotEmpty(path);
|
|
1036
|
+
const close = this.expectPunct("|}");
|
|
1037
|
+
annotations.push({ type: "AnnotationBlock", open, properties, close });
|
|
1038
|
+
} else {
|
|
1039
|
+
return annotations;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
parseReifier() {
|
|
1044
|
+
const tilde = this.expectPunct("~");
|
|
1045
|
+
const t = this.peek();
|
|
1046
|
+
let id = null;
|
|
1047
|
+
if (t.type === "VAR" || t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS") {
|
|
1048
|
+
id = this.term(this.next());
|
|
1049
|
+
} else if (t.type === "BLANK_NODE_LABEL") {
|
|
1050
|
+
id = this.term(this.next());
|
|
1051
|
+
} else if (this.isEmptyPair("[", "]")) {
|
|
1052
|
+
id = this.term(this.next(), this.next());
|
|
1053
|
+
}
|
|
1054
|
+
return { type: "Reifier", tilde, id };
|
|
1055
|
+
}
|
|
1056
|
+
parseGraphNode(path) {
|
|
1057
|
+
const t = this.peek();
|
|
1058
|
+
if (this.isPunct(t, "[") && !this.isEmptyPair("[", "]")) return this.parseBlankNodePropertyList(path);
|
|
1059
|
+
if (this.isPunct(t, "(") && !this.isEmptyPair("(", ")")) return this.parseCollection(path);
|
|
1060
|
+
if (this.isPunct(t, "<<")) return this.parseReifiedTriple();
|
|
1061
|
+
return this.parseVarOrTerm();
|
|
1062
|
+
}
|
|
1063
|
+
parseBlankNodePropertyList(path) {
|
|
1064
|
+
const open = this.expectPunct("[");
|
|
1065
|
+
const properties = this.parsePropertyListNotEmpty(path);
|
|
1066
|
+
const close = this.expectPunct("]");
|
|
1067
|
+
return { type: "BlankNodePropertyList", open, properties, close };
|
|
1068
|
+
}
|
|
1069
|
+
parseCollection(path) {
|
|
1070
|
+
const open = this.expectPunct("(");
|
|
1071
|
+
const items = [];
|
|
1072
|
+
while (!this.isPunct(this.peek(), ")")) items.push(this.parseGraphNode(path));
|
|
1073
|
+
const close = this.next();
|
|
1074
|
+
return { type: "Collection", open, items, close };
|
|
1075
|
+
}
|
|
1076
|
+
parseReifiedTriple() {
|
|
1077
|
+
const open = this.expectPunct("<<");
|
|
1078
|
+
const subject = this.parseReifiedTripleTerm();
|
|
1079
|
+
const predicate = this.parseSimpleVerb(true);
|
|
1080
|
+
const object = this.parseReifiedTripleTerm();
|
|
1081
|
+
const reifier = this.isPunct(this.peek(), "~") ? this.parseReifier() : null;
|
|
1082
|
+
const close = this.expectPunct(">>");
|
|
1083
|
+
return { type: "ReifiedTriple", open, subject, predicate, object, reifier, close };
|
|
1084
|
+
}
|
|
1085
|
+
/** ReifiedTripleSubject / ReifiedTripleObject */
|
|
1086
|
+
parseReifiedTripleTerm() {
|
|
1087
|
+
const t = this.peek();
|
|
1088
|
+
if (this.isPunct(t, "<<")) return this.parseReifiedTriple();
|
|
1089
|
+
if (this.isPunct(t, "<<(")) return this.parseTripleTerm("pattern");
|
|
1090
|
+
if (this.isPunct(t, "(")) this.fail(t, "a variable, IRI, literal, blank node or triple");
|
|
1091
|
+
if (this.isPunct(t, "[") && !this.isEmptyPair("[", "]")) {
|
|
1092
|
+
this.fail(t, "a variable, IRI, literal, blank node or triple");
|
|
1093
|
+
}
|
|
1094
|
+
return this.parseVarOrTerm();
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* `<<( s p o )>>`
|
|
1098
|
+
* - pattern: TripleTerm (subject/object: var, iri, literal, bnode, triple term)
|
|
1099
|
+
* - expression: ExprTripleTerm (subject: iri | var; object: iri, literal, var, triple term)
|
|
1100
|
+
* - data: TripleTermData (subject: iri; object: iri, literal, triple term)
|
|
1101
|
+
*/
|
|
1102
|
+
parseTripleTerm(kind) {
|
|
1103
|
+
const open = this.expectPunct("<<(");
|
|
1104
|
+
const part = (isSubject) => {
|
|
1105
|
+
const t = this.peek();
|
|
1106
|
+
if (this.isPunct(t, "<<(")) {
|
|
1107
|
+
if (isSubject && kind !== "pattern") this.fail(t, "an IRI or variable");
|
|
1108
|
+
return this.parseTripleTerm(kind);
|
|
1109
|
+
}
|
|
1110
|
+
if (t.type === "VAR") {
|
|
1111
|
+
if (kind === "data") this.fail(t, "an IRI or literal (variables are not allowed in VALUES)");
|
|
1112
|
+
return this.term(this.next());
|
|
1113
|
+
}
|
|
1114
|
+
if (t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS") return this.parseIri();
|
|
1115
|
+
if (t.type === "BLANK_NODE_LABEL" || this.isEmptyPair("[", "]")) {
|
|
1116
|
+
if (kind !== "pattern") this.fail(t, "an IRI, literal or variable");
|
|
1117
|
+
return t.type === "BLANK_NODE_LABEL" ? this.term(this.next()) : this.term(this.next(), this.next());
|
|
1118
|
+
}
|
|
1119
|
+
if (isSubject && kind !== "pattern") this.fail(t, kind === "data" ? "an IRI" : "an IRI or variable");
|
|
1120
|
+
const literal = this.tryParseLiteral();
|
|
1121
|
+
if (literal) return literal;
|
|
1122
|
+
return this.fail(t, "an RDF term");
|
|
1123
|
+
};
|
|
1124
|
+
const subject = part(true);
|
|
1125
|
+
const predicate = this.parseSimpleVerb(kind !== "data");
|
|
1126
|
+
const object = part(false);
|
|
1127
|
+
const close = this.expectPunct(")>>");
|
|
1128
|
+
return { type: "TripleTerm", open, subject, predicate, object, close };
|
|
1129
|
+
}
|
|
1130
|
+
// -------------------------------------------------------------------------
|
|
1131
|
+
// Terms
|
|
1132
|
+
// -------------------------------------------------------------------------
|
|
1133
|
+
isVarOrIri(t) {
|
|
1134
|
+
return t.type === "VAR" || t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS";
|
|
1135
|
+
}
|
|
1136
|
+
parseVar() {
|
|
1137
|
+
const t = this.peek();
|
|
1138
|
+
if (t.type !== "VAR") this.fail(t, "a variable");
|
|
1139
|
+
return this.term(this.next());
|
|
1140
|
+
}
|
|
1141
|
+
parseIri(expected = "an IRI") {
|
|
1142
|
+
const t = this.peek();
|
|
1143
|
+
if (t.type !== "IRIREF" && t.type !== "PNAME_LN" && t.type !== "PNAME_NS") this.fail(t, expected);
|
|
1144
|
+
return this.term(this.next());
|
|
1145
|
+
}
|
|
1146
|
+
parseVarOrIri() {
|
|
1147
|
+
const t = this.peek();
|
|
1148
|
+
if (!this.isVarOrIri(t)) this.fail(t, "a variable or an IRI");
|
|
1149
|
+
return this.term(this.next());
|
|
1150
|
+
}
|
|
1151
|
+
/** RDFLiteral | NumericLiteral | BooleanLiteral, or null */
|
|
1152
|
+
tryParseLiteral() {
|
|
1153
|
+
const t = this.peek();
|
|
1154
|
+
if (t.type === "STRING") {
|
|
1155
|
+
const tokens = [this.next()];
|
|
1156
|
+
const n = this.peek();
|
|
1157
|
+
if (n.type === "LANG_DIR") {
|
|
1158
|
+
tokens.push(this.next());
|
|
1159
|
+
} else if (this.isPunct(n, "^^")) {
|
|
1160
|
+
tokens.push(this.next());
|
|
1161
|
+
tokens.push(...this.parseIri().tokens);
|
|
1162
|
+
}
|
|
1163
|
+
return this.term(...tokens);
|
|
1164
|
+
}
|
|
1165
|
+
if (t.type === "INTEGER" || t.type === "DECIMAL" || t.type === "DOUBLE") return this.term(this.next());
|
|
1166
|
+
if (this.isPunct(t, "+", "-")) {
|
|
1167
|
+
const n = this.peek(1);
|
|
1168
|
+
if ((n.type === "INTEGER" || n.type === "DECIMAL" || n.type === "DOUBLE") && !n.spaceBefore) {
|
|
1169
|
+
return this.term(this.next(), this.next());
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
if (t.type === "NAME" && this.isKw(t, "TRUE", "FALSE")) return this.term(this.next());
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
/** VarOrTerm: Var | iri | RDFLiteral | NumericLiteral | BooleanLiteral | BlankNode | NIL | TripleTerm */
|
|
1177
|
+
parseVarOrTerm() {
|
|
1178
|
+
const t = this.peek();
|
|
1179
|
+
switch (t.type) {
|
|
1180
|
+
case "VAR":
|
|
1181
|
+
case "IRIREF":
|
|
1182
|
+
case "PNAME_LN":
|
|
1183
|
+
case "PNAME_NS":
|
|
1184
|
+
case "BLANK_NODE_LABEL":
|
|
1185
|
+
return this.term(this.next());
|
|
1186
|
+
}
|
|
1187
|
+
if (this.isEmptyPair("(", ")") || this.isEmptyPair("[", "]")) return this.term(this.next(), this.next());
|
|
1188
|
+
if (this.isPunct(t, "<<(")) return this.parseTripleTerm("pattern");
|
|
1189
|
+
const literal = this.tryParseLiteral();
|
|
1190
|
+
if (literal) return literal;
|
|
1191
|
+
return this.fail(t, "an RDF term or a variable");
|
|
1192
|
+
}
|
|
1193
|
+
// -------------------------------------------------------------------------
|
|
1194
|
+
// Property paths
|
|
1195
|
+
// -------------------------------------------------------------------------
|
|
1196
|
+
parsePath() {
|
|
1197
|
+
const items = [this.parsePathSequence()];
|
|
1198
|
+
const separators = [];
|
|
1199
|
+
while (this.isPunct(this.peek(), "|")) {
|
|
1200
|
+
separators.push(this.next());
|
|
1201
|
+
items.push(this.parsePathSequence());
|
|
1202
|
+
}
|
|
1203
|
+
return items.length === 1 ? items[0] : { type: "PathAlternative", items, separators };
|
|
1204
|
+
}
|
|
1205
|
+
parsePathSequence() {
|
|
1206
|
+
const items = [this.parsePathEltOrInverse()];
|
|
1207
|
+
const separators = [];
|
|
1208
|
+
while (this.isPunct(this.peek(), "/")) {
|
|
1209
|
+
separators.push(this.next());
|
|
1210
|
+
items.push(this.parsePathEltOrInverse());
|
|
1211
|
+
}
|
|
1212
|
+
return items.length === 1 ? items[0] : { type: "PathSequence", items, separators };
|
|
1213
|
+
}
|
|
1214
|
+
parsePathEltOrInverse() {
|
|
1215
|
+
if (this.isPunct(this.peek(), "^")) {
|
|
1216
|
+
const caret = this.next();
|
|
1217
|
+
return { type: "PathInverse", caret, item: this.parsePathElt() };
|
|
1218
|
+
}
|
|
1219
|
+
return this.parsePathElt();
|
|
1220
|
+
}
|
|
1221
|
+
parsePathElt() {
|
|
1222
|
+
const primary = this.parsePathPrimary();
|
|
1223
|
+
const t = this.peek();
|
|
1224
|
+
if (this.isPunct(t, "?", "*", "+") && !this.isPathModAmbiguous(t)) {
|
|
1225
|
+
return { type: "PathModified", item: primary, modifier: this.next() };
|
|
1226
|
+
}
|
|
1227
|
+
return primary;
|
|
1228
|
+
}
|
|
1229
|
+
/** `:p ?x` must not read `?` as a modifier: the lexer makes `?x` a VAR, so only a bare `?` is a modifier. */
|
|
1230
|
+
isPathModAmbiguous(t) {
|
|
1231
|
+
if (this.isPunct(t, "+", "-")) {
|
|
1232
|
+
const n = this.peek(1);
|
|
1233
|
+
return (n.type === "INTEGER" || n.type === "DECIMAL" || n.type === "DOUBLE") && !n.spaceBefore;
|
|
1234
|
+
}
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
parsePathPrimary() {
|
|
1238
|
+
const t = this.peek();
|
|
1239
|
+
if (t.type === "NAME" && t.value === "a") return this.term(this.next());
|
|
1240
|
+
if (this.isPunct(t, "!")) {
|
|
1241
|
+
const bang = this.next();
|
|
1242
|
+
return { type: "PathNegated", bang, item: this.parsePathNegatedPropertySet() };
|
|
1243
|
+
}
|
|
1244
|
+
if (this.isPunct(t, "(")) {
|
|
1245
|
+
const open = this.next();
|
|
1246
|
+
const path = this.parsePath();
|
|
1247
|
+
const close = this.expectPunct(")");
|
|
1248
|
+
return { type: "PathGroup", open, path, close };
|
|
1249
|
+
}
|
|
1250
|
+
return this.parseIri("a predicate or property path");
|
|
1251
|
+
}
|
|
1252
|
+
parsePathNegatedPropertySet() {
|
|
1253
|
+
if (this.isPunct(this.peek(), "(")) {
|
|
1254
|
+
const open = this.next();
|
|
1255
|
+
if (this.isPunct(this.peek(), ")")) return { type: "PathGroup", open, path: null, close: this.next() };
|
|
1256
|
+
const items = [this.parsePathOneInPropertySet()];
|
|
1257
|
+
const separators = [];
|
|
1258
|
+
while (this.isPunct(this.peek(), "|")) {
|
|
1259
|
+
separators.push(this.next());
|
|
1260
|
+
items.push(this.parsePathOneInPropertySet());
|
|
1261
|
+
}
|
|
1262
|
+
const close = this.expectPunct(")");
|
|
1263
|
+
const path = items.length === 1 ? items[0] : { type: "PathAlternative", items, separators };
|
|
1264
|
+
return { type: "PathGroup", open, path, close };
|
|
1265
|
+
}
|
|
1266
|
+
return this.parsePathOneInPropertySet();
|
|
1267
|
+
}
|
|
1268
|
+
parsePathOneInPropertySet() {
|
|
1269
|
+
const t = this.peek();
|
|
1270
|
+
if (this.isPunct(t, "^")) {
|
|
1271
|
+
const caret = this.next();
|
|
1272
|
+
const n = this.peek();
|
|
1273
|
+
const item = n.type === "NAME" && n.value === "a" ? this.term(this.next()) : this.parseIri();
|
|
1274
|
+
return { type: "PathInverse", caret, item };
|
|
1275
|
+
}
|
|
1276
|
+
if (t.type === "NAME" && t.value === "a") return this.term(this.next());
|
|
1277
|
+
return this.parseIri();
|
|
1278
|
+
}
|
|
1279
|
+
// -------------------------------------------------------------------------
|
|
1280
|
+
// Expressions
|
|
1281
|
+
// -------------------------------------------------------------------------
|
|
1282
|
+
parseExpression() {
|
|
1283
|
+
let left = this.parseAnd();
|
|
1284
|
+
while (this.isPunct(this.peek(), "||")) {
|
|
1285
|
+
const operator = this.next();
|
|
1286
|
+
left = { type: "Binary", left, operator, right: this.parseAnd() };
|
|
1287
|
+
}
|
|
1288
|
+
return left;
|
|
1289
|
+
}
|
|
1290
|
+
parseAnd() {
|
|
1291
|
+
let left = this.parseRelational();
|
|
1292
|
+
while (this.isPunct(this.peek(), "&&")) {
|
|
1293
|
+
const operator = this.next();
|
|
1294
|
+
left = { type: "Binary", left, operator, right: this.parseRelational() };
|
|
1295
|
+
}
|
|
1296
|
+
return left;
|
|
1297
|
+
}
|
|
1298
|
+
parseRelational() {
|
|
1299
|
+
const left = this.parseAdditive();
|
|
1300
|
+
const t = this.peek();
|
|
1301
|
+
if (this.isPunct(t, "=", "!=", "<", ">", "<=", ">=")) {
|
|
1302
|
+
const operator = this.next();
|
|
1303
|
+
return { type: "Binary", left, operator, right: this.parseAdditive() };
|
|
1304
|
+
}
|
|
1305
|
+
if (this.isKw(t, "IN")) {
|
|
1306
|
+
const inKw = this.next();
|
|
1307
|
+
return { type: "In", expression: left, not: null, in: inKw, list: this.parseExpressionList() };
|
|
1308
|
+
}
|
|
1309
|
+
if (this.isKw(t, "NOT") && this.isKw(this.peek(1), "IN")) {
|
|
1310
|
+
const not = this.next();
|
|
1311
|
+
const inKw = this.next();
|
|
1312
|
+
return { type: "In", expression: left, not, in: inKw, list: this.parseExpressionList() };
|
|
1313
|
+
}
|
|
1314
|
+
return left;
|
|
1315
|
+
}
|
|
1316
|
+
parseAdditive() {
|
|
1317
|
+
let left = this.parseMultiplicative();
|
|
1318
|
+
while (this.isPunct(this.peek(), "+", "-")) {
|
|
1319
|
+
const operator = this.next();
|
|
1320
|
+
left = { type: "Binary", left, operator, right: this.parseMultiplicative() };
|
|
1321
|
+
}
|
|
1322
|
+
return left;
|
|
1323
|
+
}
|
|
1324
|
+
parseMultiplicative() {
|
|
1325
|
+
let left = this.parseUnary();
|
|
1326
|
+
while (this.isPunct(this.peek(), "*", "/")) {
|
|
1327
|
+
const operator = this.next();
|
|
1328
|
+
left = { type: "Binary", left, operator, right: this.parseUnary() };
|
|
1329
|
+
}
|
|
1330
|
+
return left;
|
|
1331
|
+
}
|
|
1332
|
+
parseUnary() {
|
|
1333
|
+
const t = this.peek();
|
|
1334
|
+
if (this.isPunct(t, "!")) {
|
|
1335
|
+
const operator = this.next();
|
|
1336
|
+
return { type: "Unary", operator, expression: this.parseUnary() };
|
|
1337
|
+
}
|
|
1338
|
+
if (this.isPunct(t, "+", "-")) {
|
|
1339
|
+
const n = this.peek(1);
|
|
1340
|
+
if ((n.type === "INTEGER" || n.type === "DECIMAL" || n.type === "DOUBLE") && !n.spaceBefore) {
|
|
1341
|
+
return this.term(this.next(), this.next());
|
|
1342
|
+
}
|
|
1343
|
+
const operator = this.next();
|
|
1344
|
+
return { type: "Unary", operator, expression: this.parsePrimary() };
|
|
1345
|
+
}
|
|
1346
|
+
return this.parsePrimary();
|
|
1347
|
+
}
|
|
1348
|
+
parsePrimary() {
|
|
1349
|
+
const t = this.peek();
|
|
1350
|
+
if (this.isPunct(t, "(")) return this.parseBracketed();
|
|
1351
|
+
if (this.isPunct(t, "<<(")) return this.parseTripleTerm("expression");
|
|
1352
|
+
if (t.type === "VAR") return this.term(this.next());
|
|
1353
|
+
if (t.type === "IRIREF" || t.type === "PNAME_LN" || t.type === "PNAME_NS") {
|
|
1354
|
+
const iri = this.parseIri();
|
|
1355
|
+
return this.isPunct(this.peek(), "(") ? this.parseArgList(iri, false) : iri;
|
|
1356
|
+
}
|
|
1357
|
+
if (t.type === "NAME" && !this.isKw(t, "TRUE", "FALSE")) return this.parseBuiltInCall();
|
|
1358
|
+
const literal = this.tryParseLiteral();
|
|
1359
|
+
if (literal) return literal;
|
|
1360
|
+
return this.fail(t, "an expression");
|
|
1361
|
+
}
|
|
1362
|
+
parseBracketed() {
|
|
1363
|
+
const open = this.expectPunct("(");
|
|
1364
|
+
const expression = this.parseExpression();
|
|
1365
|
+
const close = this.expectPunct(")");
|
|
1366
|
+
return { type: "Bracketed", open, expression, close };
|
|
1367
|
+
}
|
|
1368
|
+
parseExpressionList() {
|
|
1369
|
+
const open = this.expectPunct("(");
|
|
1370
|
+
const items = [];
|
|
1371
|
+
const commas = [];
|
|
1372
|
+
if (!this.isPunct(this.peek(), ")")) {
|
|
1373
|
+
items.push(this.parseExpression());
|
|
1374
|
+
while (this.isPunct(this.peek(), ",")) {
|
|
1375
|
+
commas.push(this.next());
|
|
1376
|
+
items.push(this.parseExpression());
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const close = this.expectPunct(")");
|
|
1380
|
+
return { type: "ExpressionList", open, items, commas, close };
|
|
1381
|
+
}
|
|
1382
|
+
parseBuiltInCall() {
|
|
1383
|
+
const t = this.peek();
|
|
1384
|
+
const name = t.value.toUpperCase();
|
|
1385
|
+
if (name === "EXISTS") {
|
|
1386
|
+
const keyword = this.next();
|
|
1387
|
+
return { type: "Exists", not: null, keyword, group: this.parseGroupGraphPattern() };
|
|
1388
|
+
}
|
|
1389
|
+
if (name === "NOT" && this.isKw(this.peek(1), "EXISTS")) {
|
|
1390
|
+
const not = this.next();
|
|
1391
|
+
const keyword = this.next();
|
|
1392
|
+
return { type: "Exists", not, keyword, group: this.parseGroupGraphPattern() };
|
|
1393
|
+
}
|
|
1394
|
+
if (t.type !== "NAME" || !BUILTINS.has(name)) return this.fail(t, "an expression");
|
|
1395
|
+
const nameToken = this.next();
|
|
1396
|
+
if (!this.isPunct(this.peek(), "(")) this.fail(this.peek(), `'(' after ${nameToken.value}`);
|
|
1397
|
+
const call = this.parseArgList(nameToken, true);
|
|
1398
|
+
const n = call.args.length;
|
|
1399
|
+
if (NO_ARGS.has(name)) {
|
|
1400
|
+
if (n > 0) this.fail(call.args.length ? this.tokenOf(call.args[0]) : call.close, "')'");
|
|
1401
|
+
} else if (ARITY[name]) {
|
|
1402
|
+
const [min, max] = ARITY[name];
|
|
1403
|
+
if (n < min || n > max) {
|
|
1404
|
+
throw new SparqlSyntaxError(
|
|
1405
|
+
`${nameToken.value} expects ${min === max ? min : `${min} to ${max}`} argument(s), found ${n}`,
|
|
1406
|
+
nameToken.offset,
|
|
1407
|
+
nameToken.line,
|
|
1408
|
+
nameToken.column
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
} else if (n !== 1) {
|
|
1412
|
+
throw new SparqlSyntaxError(
|
|
1413
|
+
`${nameToken.value} expects 1 argument, found ${n}`,
|
|
1414
|
+
nameToken.offset,
|
|
1415
|
+
nameToken.line,
|
|
1416
|
+
nameToken.column
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
if (call.distinct && !AGGREGATES.has(name)) this.fail(call.distinct, "an expression");
|
|
1420
|
+
if (call.separator && name !== "GROUP_CONCAT") this.fail(call.separator.semicolon, "')'");
|
|
1421
|
+
if (name === "BOUND" && (call.args[0].type !== "Term" || call.args[0].tokens[0].type !== "VAR")) {
|
|
1422
|
+
this.fail(this.tokenOf(call.args[0]), "a variable");
|
|
1423
|
+
}
|
|
1424
|
+
return call;
|
|
1425
|
+
}
|
|
1426
|
+
/** First token of a node, used for error positions. */
|
|
1427
|
+
tokenOf(node) {
|
|
1428
|
+
switch (node.type) {
|
|
1429
|
+
case "Term":
|
|
1430
|
+
return node.tokens[0];
|
|
1431
|
+
case "Binary":
|
|
1432
|
+
return this.tokenOf(node.left);
|
|
1433
|
+
case "In":
|
|
1434
|
+
return this.tokenOf(node.expression);
|
|
1435
|
+
case "Call":
|
|
1436
|
+
return node.name.type === "Term" ? node.name.tokens[0] : node.name;
|
|
1437
|
+
default:
|
|
1438
|
+
return "open" in node ? node.open : this.peek();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/** `name( [DISTINCT] args [; SEPARATOR = "x"] )` */
|
|
1442
|
+
parseArgList(name, builtin) {
|
|
1443
|
+
const open = this.expectPunct("(");
|
|
1444
|
+
const upper = name.type === "Term" ? "" : name.value.toUpperCase();
|
|
1445
|
+
const distinct = this.optKw("DISTINCT");
|
|
1446
|
+
const args = [];
|
|
1447
|
+
const commas = [];
|
|
1448
|
+
let separator = null;
|
|
1449
|
+
if (!this.isPunct(this.peek(), ")")) {
|
|
1450
|
+
if (upper === "COUNT" && this.isPunct(this.peek(), "*")) {
|
|
1451
|
+
args.push(this.term(this.next()));
|
|
1452
|
+
} else {
|
|
1453
|
+
args.push(this.parseExpression());
|
|
1454
|
+
}
|
|
1455
|
+
while (this.isPunct(this.peek(), ",")) {
|
|
1456
|
+
commas.push(this.next());
|
|
1457
|
+
args.push(this.parseExpression());
|
|
1458
|
+
}
|
|
1459
|
+
if (this.isPunct(this.peek(), ";")) {
|
|
1460
|
+
const semicolon = this.next();
|
|
1461
|
+
const keyword = this.expectKw("SEPARATOR");
|
|
1462
|
+
const equals = this.expectPunct("=");
|
|
1463
|
+
const value = this.peek();
|
|
1464
|
+
if (value.type !== "STRING") this.fail(value, "a string");
|
|
1465
|
+
separator = { semicolon, keyword, equals, value: this.term(this.next()) };
|
|
1466
|
+
}
|
|
1467
|
+
} else if (distinct) {
|
|
1468
|
+
this.fail(this.peek(), "an expression");
|
|
1469
|
+
}
|
|
1470
|
+
const close = this.expectPunct(")");
|
|
1471
|
+
return {
|
|
1472
|
+
type: "Call",
|
|
1473
|
+
name,
|
|
1474
|
+
builtin,
|
|
1475
|
+
open,
|
|
1476
|
+
distinct,
|
|
1477
|
+
args,
|
|
1478
|
+
commas,
|
|
1479
|
+
separator,
|
|
1480
|
+
close
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
// src/writer.ts
|
|
1486
|
+
var ATTACHABLE = /* @__PURE__ */ new Set([".", ",", ";"]);
|
|
1487
|
+
var Writer = class {
|
|
1488
|
+
constructor(options) {
|
|
1489
|
+
this.lines = [];
|
|
1490
|
+
this.current = "";
|
|
1491
|
+
this.currentIndent = "";
|
|
1492
|
+
this.level = 0;
|
|
1493
|
+
/** Indent to use for the next line instead of the structural indent (alignment). */
|
|
1494
|
+
this.nextIndent = null;
|
|
1495
|
+
this.pendingSpace = false;
|
|
1496
|
+
this.pendingTrailing = [];
|
|
1497
|
+
this.leadingDone = /* @__PURE__ */ new Set();
|
|
1498
|
+
/** Leading comments of dropped tokens, written before the next output. */
|
|
1499
|
+
this.deferred = [];
|
|
1500
|
+
/** Number of comments written; used to check that no comment was lost. */
|
|
1501
|
+
this.commentCount = 0;
|
|
1502
|
+
this.options = options;
|
|
1503
|
+
}
|
|
1504
|
+
// -------------------------------------------------------------------------
|
|
1505
|
+
// Layout
|
|
1506
|
+
// -------------------------------------------------------------------------
|
|
1507
|
+
get atLineStart() {
|
|
1508
|
+
return this.current === "";
|
|
1509
|
+
}
|
|
1510
|
+
indent() {
|
|
1511
|
+
this.level++;
|
|
1512
|
+
}
|
|
1513
|
+
dedent() {
|
|
1514
|
+
this.level--;
|
|
1515
|
+
}
|
|
1516
|
+
structuralIndent() {
|
|
1517
|
+
return this.options.indent.repeat(this.level);
|
|
1518
|
+
}
|
|
1519
|
+
/** Column (0-based) at which the next text will be written. */
|
|
1520
|
+
column() {
|
|
1521
|
+
if (this.atLineStart) return (this.nextIndent ?? this.structuralIndent()).length;
|
|
1522
|
+
return this.currentIndent.length + this.current.length + (this.pendingSpace ? 1 : 0);
|
|
1523
|
+
}
|
|
1524
|
+
space() {
|
|
1525
|
+
if (!this.atLineStart) this.pendingSpace = true;
|
|
1526
|
+
}
|
|
1527
|
+
/** End the current line. Does nothing on an empty line unless comments are pending. */
|
|
1528
|
+
newline() {
|
|
1529
|
+
if (this.atLineStart && this.pendingTrailing.length === 0) {
|
|
1530
|
+
this.nextIndent = null;
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const [first, ...rest] = this.pendingTrailing;
|
|
1534
|
+
let line = this.current;
|
|
1535
|
+
if (first !== void 0) {
|
|
1536
|
+
line = line === "" ? first.text : `${line} ${first.text}`;
|
|
1537
|
+
this.commentCount++;
|
|
1538
|
+
}
|
|
1539
|
+
this.lines.push((this.currentIndent + line).replace(/\s+$/, ""));
|
|
1540
|
+
this.current = "";
|
|
1541
|
+
this.pendingSpace = false;
|
|
1542
|
+
this.pendingTrailing = [];
|
|
1543
|
+
this.deferred.push(...rest);
|
|
1544
|
+
}
|
|
1545
|
+
/** Indentation of the current line (or of the next line when nothing was written yet). */
|
|
1546
|
+
lineIndent() {
|
|
1547
|
+
return this.atLineStart ? this.nextIndent ?? this.structuralIndent() : this.currentIndent;
|
|
1548
|
+
}
|
|
1549
|
+
/** Indent string that aligns a new line with the current column. */
|
|
1550
|
+
alignment() {
|
|
1551
|
+
const base = this.lineIndent();
|
|
1552
|
+
return base + " ".repeat(Math.max(0, this.column() - base.length));
|
|
1553
|
+
}
|
|
1554
|
+
/** Indent string one level deeper than the current line. */
|
|
1555
|
+
continuation() {
|
|
1556
|
+
return this.lineIndent() + this.options.indent;
|
|
1557
|
+
}
|
|
1558
|
+
/** Start a new line with the given indent string (from `alignment()` or `continuation()`). */
|
|
1559
|
+
newlineWith(indent) {
|
|
1560
|
+
this.newline();
|
|
1561
|
+
this.nextIndent = indent;
|
|
1562
|
+
}
|
|
1563
|
+
/** Make sure the output ends with exactly one empty line (unless at the very start or after an opening brace). */
|
|
1564
|
+
blankLine() {
|
|
1565
|
+
this.newline();
|
|
1566
|
+
const last = this.lines[this.lines.length - 1];
|
|
1567
|
+
if (last === void 0 || last === "" || /[{(\[]$/.test(last)) return;
|
|
1568
|
+
this.lines.push("");
|
|
1569
|
+
}
|
|
1570
|
+
/** Continue the current statement on a new line (used when a comment forces a break). */
|
|
1571
|
+
breakLine() {
|
|
1572
|
+
if (this.atLineStart) return;
|
|
1573
|
+
this.newline();
|
|
1574
|
+
this.nextIndent = this.structuralIndent() + this.options.indent;
|
|
1575
|
+
}
|
|
1576
|
+
write(text) {
|
|
1577
|
+
if (text === "") return;
|
|
1578
|
+
if (this.atLineStart) {
|
|
1579
|
+
this.currentIndent = this.nextIndent ?? this.structuralIndent();
|
|
1580
|
+
this.nextIndent = null;
|
|
1581
|
+
this.current = text;
|
|
1582
|
+
} else {
|
|
1583
|
+
this.current += (this.pendingSpace ? " " : "") + text;
|
|
1584
|
+
}
|
|
1585
|
+
this.pendingSpace = false;
|
|
1586
|
+
}
|
|
1587
|
+
// -------------------------------------------------------------------------
|
|
1588
|
+
// Content
|
|
1589
|
+
// -------------------------------------------------------------------------
|
|
1590
|
+
leadingComments(t, blank) {
|
|
1591
|
+
if (this.options.ignoreComments) return;
|
|
1592
|
+
const comments = [...this.deferred];
|
|
1593
|
+
this.deferred = [];
|
|
1594
|
+
if (t && t.leading.length > 0 && !this.leadingDone.has(t)) {
|
|
1595
|
+
this.leadingDone.add(t);
|
|
1596
|
+
comments.push(...t.leading);
|
|
1597
|
+
}
|
|
1598
|
+
if (comments.length > 0) {
|
|
1599
|
+
this.breakLine();
|
|
1600
|
+
const indent = this.nextIndent;
|
|
1601
|
+
for (const comment of comments) {
|
|
1602
|
+
if (comment.blankLinesBefore > 0 && this.options.preserveBlankLines) this.blankLine();
|
|
1603
|
+
this.nextIndent = indent;
|
|
1604
|
+
this.write(comment.text);
|
|
1605
|
+
this.commentCount++;
|
|
1606
|
+
this.newline();
|
|
1607
|
+
}
|
|
1608
|
+
this.nextIndent = indent;
|
|
1609
|
+
}
|
|
1610
|
+
if (t && blank && t.blankLinesBefore > 0 && this.options.preserveBlankLines && this.atLineStart) {
|
|
1611
|
+
this.blankLine();
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
trailingComment(t) {
|
|
1615
|
+
if (this.options.ignoreComments) return;
|
|
1616
|
+
if (t.trailing) {
|
|
1617
|
+
this.pendingTrailing.push(t.trailing);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Write a source token (optionally spelled differently, e.g. upper-cased).
|
|
1622
|
+
* `blank`: keep a blank line that precedes the token in the source.
|
|
1623
|
+
*/
|
|
1624
|
+
token(t, text = t.value, blank = false) {
|
|
1625
|
+
if (this.pendingTrailing.length > 0 && !ATTACHABLE.has(text)) this.breakLine();
|
|
1626
|
+
this.leadingComments(t, blank);
|
|
1627
|
+
this.write(text);
|
|
1628
|
+
this.trailingComment(t);
|
|
1629
|
+
}
|
|
1630
|
+
/** Write only the comments of a token that is not printed. */
|
|
1631
|
+
drop(t) {
|
|
1632
|
+
if (this.options.ignoreComments) return;
|
|
1633
|
+
if (!this.leadingDone.has(t)) {
|
|
1634
|
+
this.leadingDone.add(t);
|
|
1635
|
+
this.deferred.push(...t.leading);
|
|
1636
|
+
}
|
|
1637
|
+
if (this.deferred.length > 0 && t.trailing) this.deferred.push(t.trailing);
|
|
1638
|
+
else this.trailingComment(t);
|
|
1639
|
+
}
|
|
1640
|
+
/** Write only the leading comments of a token (used before closing brackets at the inner indent). */
|
|
1641
|
+
commentsBefore(t) {
|
|
1642
|
+
this.leadingComments(t, false);
|
|
1643
|
+
}
|
|
1644
|
+
/** Write text that doesn't come from a source token. */
|
|
1645
|
+
text(text) {
|
|
1646
|
+
if (this.pendingTrailing.length > 0 && !ATTACHABLE.has(text)) this.breakLine();
|
|
1647
|
+
if (this.deferred.length > 0) this.leadingComments(null, false);
|
|
1648
|
+
this.write(text);
|
|
1649
|
+
}
|
|
1650
|
+
/** Has anything been written to the current line or earlier? */
|
|
1651
|
+
get isEmpty() {
|
|
1652
|
+
return this.lines.length === 0 && this.atLineStart;
|
|
1653
|
+
}
|
|
1654
|
+
/** The text written so far, if it fits on a single line without comments. */
|
|
1655
|
+
singleLine() {
|
|
1656
|
+
if (this.lines.length > 0 || this.pendingTrailing.length > 0 || this.deferred.length > 0 || this.commentCount > 0) {
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
return this.current;
|
|
1660
|
+
}
|
|
1661
|
+
toString() {
|
|
1662
|
+
this.leadingComments(null, false);
|
|
1663
|
+
this.newline();
|
|
1664
|
+
while (this.lines.length > 0 && this.lines[this.lines.length - 1] === "") this.lines.pop();
|
|
1665
|
+
return this.lines.join("\n");
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
// src/printer.ts
|
|
1670
|
+
function resolveOptions(options = {}) {
|
|
1671
|
+
const indent = typeof options.indent === "string" ? options.indent : " ".repeat(Math.max(0, Math.floor(options.indent ?? 2)));
|
|
1672
|
+
return {
|
|
1673
|
+
indent,
|
|
1674
|
+
keywordCase: options.keywordCase ?? "upper",
|
|
1675
|
+
functionCase: options.functionCase ?? "preserve",
|
|
1676
|
+
alignPredicates: options.alignPredicates ?? true,
|
|
1677
|
+
preserveBlankLines: options.preserveBlankLines ?? true,
|
|
1678
|
+
compact: options.compact ?? false,
|
|
1679
|
+
lineWidth: options.lineWidth ?? 100,
|
|
1680
|
+
insertWhere: options.insertWhere ?? true
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function applyCase(text, c) {
|
|
1684
|
+
return c === "upper" ? text.toUpperCase() : c === "lower" ? text.toLowerCase() : text;
|
|
1685
|
+
}
|
|
1686
|
+
var Printer = class {
|
|
1687
|
+
constructor(options) {
|
|
1688
|
+
this.options = options;
|
|
1689
|
+
this.w = this.newWriter();
|
|
1690
|
+
}
|
|
1691
|
+
newWriter(ignoreComments = false) {
|
|
1692
|
+
return new Writer({
|
|
1693
|
+
indent: this.options.indent,
|
|
1694
|
+
preserveBlankLines: this.options.preserveBlankLines,
|
|
1695
|
+
ignoreComments
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
print(ast) {
|
|
1699
|
+
if (ast.type === "Query") this.query(ast);
|
|
1700
|
+
else this.update(ast);
|
|
1701
|
+
return { text: this.w.toString(), comments: this.w.commentCount };
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Render `fn` on a scratch writer and return the result if it fits on one
|
|
1705
|
+
* line (no comments, no line breaks) within `width` characters.
|
|
1706
|
+
*/
|
|
1707
|
+
measure(fn, width, ignoreComments = false) {
|
|
1708
|
+
const saved = this.w;
|
|
1709
|
+
this.w = this.newWriter(ignoreComments);
|
|
1710
|
+
try {
|
|
1711
|
+
fn();
|
|
1712
|
+
const line = this.w.singleLine();
|
|
1713
|
+
return line !== null && line.length <= width ? line : null;
|
|
1714
|
+
} finally {
|
|
1715
|
+
this.w = saved;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
remainingWidth() {
|
|
1719
|
+
return this.options.lineWidth - this.w.column();
|
|
1720
|
+
}
|
|
1721
|
+
// -------------------------------------------------------------------------
|
|
1722
|
+
// Tokens
|
|
1723
|
+
// -------------------------------------------------------------------------
|
|
1724
|
+
kw(t, blank = false) {
|
|
1725
|
+
this.w.token(t, applyCase(t.value, this.options.keywordCase), blank);
|
|
1726
|
+
}
|
|
1727
|
+
/** A keyword that was not in the source (e.g. an inserted WHERE). */
|
|
1728
|
+
synthKw(text) {
|
|
1729
|
+
this.w.text(applyCase(text, this.options.keywordCase));
|
|
1730
|
+
}
|
|
1731
|
+
tok(t, blank = false) {
|
|
1732
|
+
this.w.token(t, t.value, blank);
|
|
1733
|
+
}
|
|
1734
|
+
sp() {
|
|
1735
|
+
this.w.space();
|
|
1736
|
+
}
|
|
1737
|
+
nl() {
|
|
1738
|
+
this.w.newline();
|
|
1739
|
+
}
|
|
1740
|
+
// -------------------------------------------------------------------------
|
|
1741
|
+
// Units
|
|
1742
|
+
// -------------------------------------------------------------------------
|
|
1743
|
+
prologue(decls) {
|
|
1744
|
+
for (const decl of decls) {
|
|
1745
|
+
this.nl();
|
|
1746
|
+
this.kw(decl.keyword, true);
|
|
1747
|
+
this.sp();
|
|
1748
|
+
if (decl.prefix) {
|
|
1749
|
+
this.tok(decl.prefix);
|
|
1750
|
+
this.sp();
|
|
1751
|
+
}
|
|
1752
|
+
this.tok(decl.value);
|
|
1753
|
+
}
|
|
1754
|
+
if (decls.length > 0) this.w.blankLine();
|
|
1755
|
+
}
|
|
1756
|
+
query(q) {
|
|
1757
|
+
this.prologue(q.prologue);
|
|
1758
|
+
const form = q.form;
|
|
1759
|
+
switch (form.type) {
|
|
1760
|
+
case "Select":
|
|
1761
|
+
this.selectClause(form.select);
|
|
1762
|
+
this.datasets(form.datasets);
|
|
1763
|
+
this.whereClause(form.where, this.options.insertWhere);
|
|
1764
|
+
this.solutionModifier(form.modifiers);
|
|
1765
|
+
break;
|
|
1766
|
+
case "Construct":
|
|
1767
|
+
this.nl();
|
|
1768
|
+
this.kw(form.keyword);
|
|
1769
|
+
if (form.template) {
|
|
1770
|
+
this.sp();
|
|
1771
|
+
this.triplesTemplate(form.template);
|
|
1772
|
+
this.datasets(form.datasets);
|
|
1773
|
+
this.whereClause(form.where, this.options.insertWhere);
|
|
1774
|
+
} else {
|
|
1775
|
+
this.datasets(form.datasets);
|
|
1776
|
+
this.nl();
|
|
1777
|
+
this.kw(form.shortForm.keyword);
|
|
1778
|
+
this.sp();
|
|
1779
|
+
this.triplesTemplate(form.shortForm.template);
|
|
1780
|
+
}
|
|
1781
|
+
this.solutionModifier(form.modifiers);
|
|
1782
|
+
break;
|
|
1783
|
+
case "Describe":
|
|
1784
|
+
this.nl();
|
|
1785
|
+
this.kw(form.keyword);
|
|
1786
|
+
if (form.star) {
|
|
1787
|
+
this.sp();
|
|
1788
|
+
this.tok(form.star);
|
|
1789
|
+
}
|
|
1790
|
+
for (const item of form.items) {
|
|
1791
|
+
this.sp();
|
|
1792
|
+
this.term(item);
|
|
1793
|
+
}
|
|
1794
|
+
this.datasets(form.datasets);
|
|
1795
|
+
if (form.where) this.whereClause(form.where, this.options.insertWhere);
|
|
1796
|
+
this.solutionModifier(form.modifiers);
|
|
1797
|
+
break;
|
|
1798
|
+
case "Ask":
|
|
1799
|
+
this.nl();
|
|
1800
|
+
this.kw(form.keyword);
|
|
1801
|
+
if (form.datasets.length === 0 && !form.where.keyword) {
|
|
1802
|
+
this.sp();
|
|
1803
|
+
this.group(form.where.group);
|
|
1804
|
+
} else {
|
|
1805
|
+
this.datasets(form.datasets);
|
|
1806
|
+
this.whereClause(form.where, true);
|
|
1807
|
+
}
|
|
1808
|
+
this.solutionModifier(form.modifiers);
|
|
1809
|
+
break;
|
|
1810
|
+
}
|
|
1811
|
+
if (q.values) {
|
|
1812
|
+
this.nl();
|
|
1813
|
+
this.values(q.values);
|
|
1814
|
+
}
|
|
1815
|
+
this.nl();
|
|
1816
|
+
this.w.drop(q.eof);
|
|
1817
|
+
}
|
|
1818
|
+
selectClause(s) {
|
|
1819
|
+
this.nl();
|
|
1820
|
+
this.kw(s.keyword);
|
|
1821
|
+
if (s.modifier) {
|
|
1822
|
+
this.sp();
|
|
1823
|
+
this.kw(s.modifier);
|
|
1824
|
+
}
|
|
1825
|
+
if (s.star) {
|
|
1826
|
+
this.sp();
|
|
1827
|
+
this.tok(s.star);
|
|
1828
|
+
}
|
|
1829
|
+
for (const item of s.items) {
|
|
1830
|
+
this.sp();
|
|
1831
|
+
if (item.type === "Term") this.term(item);
|
|
1832
|
+
else this.projection(item);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
projection(p) {
|
|
1836
|
+
this.tok(p.open);
|
|
1837
|
+
this.expr(p.expression);
|
|
1838
|
+
if (p.as && p.variable) {
|
|
1839
|
+
this.sp();
|
|
1840
|
+
this.kw(p.as);
|
|
1841
|
+
this.sp();
|
|
1842
|
+
this.term(p.variable);
|
|
1843
|
+
}
|
|
1844
|
+
this.tok(p.close);
|
|
1845
|
+
}
|
|
1846
|
+
datasets(datasets) {
|
|
1847
|
+
for (const d of datasets) {
|
|
1848
|
+
this.nl();
|
|
1849
|
+
this.kw(d.from);
|
|
1850
|
+
if (d.named) {
|
|
1851
|
+
this.sp();
|
|
1852
|
+
this.kw(d.named);
|
|
1853
|
+
}
|
|
1854
|
+
this.sp();
|
|
1855
|
+
this.term(d.iri);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
whereClause(where, insertWhere) {
|
|
1859
|
+
this.nl();
|
|
1860
|
+
if (where.keyword) {
|
|
1861
|
+
this.kw(where.keyword);
|
|
1862
|
+
this.sp();
|
|
1863
|
+
} else if (insertWhere) {
|
|
1864
|
+
this.synthKw("WHERE");
|
|
1865
|
+
this.sp();
|
|
1866
|
+
}
|
|
1867
|
+
this.group(where.group);
|
|
1868
|
+
}
|
|
1869
|
+
solutionModifier(m) {
|
|
1870
|
+
if (m.groupBy) {
|
|
1871
|
+
this.nl();
|
|
1872
|
+
this.kw(m.groupBy.group);
|
|
1873
|
+
this.sp();
|
|
1874
|
+
this.kw(m.groupBy.by);
|
|
1875
|
+
for (const c of m.groupBy.conditions) {
|
|
1876
|
+
this.sp();
|
|
1877
|
+
this.condition(c);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
if (m.having) {
|
|
1881
|
+
this.nl();
|
|
1882
|
+
this.kw(m.having.keyword);
|
|
1883
|
+
for (const c of m.having.conditions) {
|
|
1884
|
+
this.sp();
|
|
1885
|
+
this.condition(c);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (m.orderBy) {
|
|
1889
|
+
this.nl();
|
|
1890
|
+
this.kw(m.orderBy.order);
|
|
1891
|
+
this.sp();
|
|
1892
|
+
this.kw(m.orderBy.by);
|
|
1893
|
+
for (const c of m.orderBy.conditions) {
|
|
1894
|
+
this.sp();
|
|
1895
|
+
this.condition(c);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
for (const lo of m.limitOffset) {
|
|
1899
|
+
this.nl();
|
|
1900
|
+
this.kw(lo.keyword);
|
|
1901
|
+
this.sp();
|
|
1902
|
+
this.tok(lo.value);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
condition(c) {
|
|
1906
|
+
if (c.type === "Projection") this.projection(c);
|
|
1907
|
+
else if (c.type === "OrderCondition") {
|
|
1908
|
+
if (c.direction) this.kw(c.direction);
|
|
1909
|
+
this.expr(c.expression);
|
|
1910
|
+
} else this.expr(c);
|
|
1911
|
+
}
|
|
1912
|
+
update(u) {
|
|
1913
|
+
let first = true;
|
|
1914
|
+
for (const part of u.parts) {
|
|
1915
|
+
if (!first && (part.prologue.length > 0 || part.operation)) this.w.blankLine();
|
|
1916
|
+
this.prologue(part.prologue);
|
|
1917
|
+
if (part.operation) {
|
|
1918
|
+
this.updateOperation(part.operation);
|
|
1919
|
+
first = false;
|
|
1920
|
+
}
|
|
1921
|
+
if (part.semicolon) {
|
|
1922
|
+
this.sp();
|
|
1923
|
+
this.tok(part.semicolon);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
this.nl();
|
|
1927
|
+
this.w.drop(u.eof);
|
|
1928
|
+
}
|
|
1929
|
+
words(words) {
|
|
1930
|
+
words.tokens.forEach((t, i) => {
|
|
1931
|
+
if (i > 0) this.sp();
|
|
1932
|
+
if ("tokens" in t) this.term(t);
|
|
1933
|
+
else this.kw(t);
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
updateOperation(op) {
|
|
1937
|
+
this.nl();
|
|
1938
|
+
switch (op.type) {
|
|
1939
|
+
case "Load":
|
|
1940
|
+
this.kw(op.keyword);
|
|
1941
|
+
if (op.silent) {
|
|
1942
|
+
this.sp();
|
|
1943
|
+
this.kw(op.silent);
|
|
1944
|
+
}
|
|
1945
|
+
this.sp();
|
|
1946
|
+
this.term(op.iri);
|
|
1947
|
+
if (op.into) {
|
|
1948
|
+
this.sp();
|
|
1949
|
+
this.words(op.into);
|
|
1950
|
+
}
|
|
1951
|
+
break;
|
|
1952
|
+
case "GraphManagement":
|
|
1953
|
+
this.kw(op.keyword);
|
|
1954
|
+
if (op.silent) {
|
|
1955
|
+
this.sp();
|
|
1956
|
+
this.kw(op.silent);
|
|
1957
|
+
}
|
|
1958
|
+
this.sp();
|
|
1959
|
+
this.words(op.target);
|
|
1960
|
+
break;
|
|
1961
|
+
case "GraphTransfer":
|
|
1962
|
+
this.kw(op.keyword);
|
|
1963
|
+
if (op.silent) {
|
|
1964
|
+
this.sp();
|
|
1965
|
+
this.kw(op.silent);
|
|
1966
|
+
}
|
|
1967
|
+
this.sp();
|
|
1968
|
+
this.words(op.from);
|
|
1969
|
+
this.sp();
|
|
1970
|
+
this.kw(op.to);
|
|
1971
|
+
this.sp();
|
|
1972
|
+
this.words(op.target);
|
|
1973
|
+
break;
|
|
1974
|
+
case "QuadData":
|
|
1975
|
+
this.kw(op.keywords[0]);
|
|
1976
|
+
this.sp();
|
|
1977
|
+
this.kw(op.keywords[1]);
|
|
1978
|
+
this.sp();
|
|
1979
|
+
this.triplesTemplate(op.quads);
|
|
1980
|
+
break;
|
|
1981
|
+
case "Modify":
|
|
1982
|
+
if (op.with) {
|
|
1983
|
+
this.kw(op.with.keyword);
|
|
1984
|
+
this.sp();
|
|
1985
|
+
this.term(op.with.iri);
|
|
1986
|
+
this.nl();
|
|
1987
|
+
}
|
|
1988
|
+
if (op.delete) {
|
|
1989
|
+
this.kw(op.delete.keyword);
|
|
1990
|
+
this.sp();
|
|
1991
|
+
this.triplesTemplate(op.delete.quads);
|
|
1992
|
+
this.nl();
|
|
1993
|
+
}
|
|
1994
|
+
if (op.insert) {
|
|
1995
|
+
this.kw(op.insert.keyword);
|
|
1996
|
+
this.sp();
|
|
1997
|
+
this.triplesTemplate(op.insert.quads);
|
|
1998
|
+
this.nl();
|
|
1999
|
+
}
|
|
2000
|
+
for (const using of op.using) {
|
|
2001
|
+
this.kw(using.keyword);
|
|
2002
|
+
if (using.named) {
|
|
2003
|
+
this.sp();
|
|
2004
|
+
this.kw(using.named);
|
|
2005
|
+
}
|
|
2006
|
+
this.sp();
|
|
2007
|
+
this.term(using.iri);
|
|
2008
|
+
this.nl();
|
|
2009
|
+
}
|
|
2010
|
+
this.whereClause(op.where, true);
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
// -------------------------------------------------------------------------
|
|
2015
|
+
// Blocks
|
|
2016
|
+
// -------------------------------------------------------------------------
|
|
2017
|
+
/**
|
|
2018
|
+
* Print `open`, the items on their own lines one level deeper, and `close`.
|
|
2019
|
+
* Comments before `close` stay inside the block.
|
|
2020
|
+
*/
|
|
2021
|
+
block(open, close, isEmpty, body, openText = open.value) {
|
|
2022
|
+
this.w.token(open, openText);
|
|
2023
|
+
if (isEmpty && close.leading.length === 0 && !open.trailing) {
|
|
2024
|
+
this.tok(close);
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
this.w.indent();
|
|
2028
|
+
body();
|
|
2029
|
+
this.nl();
|
|
2030
|
+
this.w.commentsBefore(close);
|
|
2031
|
+
this.w.dedent();
|
|
2032
|
+
this.nl();
|
|
2033
|
+
this.tok(close);
|
|
2034
|
+
}
|
|
2035
|
+
/** Try to print a block on a single line (compact mode). */
|
|
2036
|
+
inlineBlock(open, close, body) {
|
|
2037
|
+
if (!this.options.compact) return false;
|
|
2038
|
+
const line = this.measure(() => {
|
|
2039
|
+
this.tok(open);
|
|
2040
|
+
this.sp();
|
|
2041
|
+
body();
|
|
2042
|
+
this.sp();
|
|
2043
|
+
this.tok(close);
|
|
2044
|
+
}, this.remainingWidth());
|
|
2045
|
+
if (line === null) return false;
|
|
2046
|
+
this.tok(open);
|
|
2047
|
+
this.sp();
|
|
2048
|
+
body();
|
|
2049
|
+
this.sp();
|
|
2050
|
+
this.tok(close);
|
|
2051
|
+
return true;
|
|
2052
|
+
}
|
|
2053
|
+
group(g) {
|
|
2054
|
+
if (g.subSelect) {
|
|
2055
|
+
const sub = g.subSelect;
|
|
2056
|
+
this.block(g.open, g.close, false, () => {
|
|
2057
|
+
this.selectClause(sub.select);
|
|
2058
|
+
this.whereClause(sub.where, this.options.insertWhere);
|
|
2059
|
+
this.solutionModifier(sub.modifiers);
|
|
2060
|
+
if (sub.values) {
|
|
2061
|
+
this.nl();
|
|
2062
|
+
this.values(sub.values);
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (g.items.length === 1 && g.items[0].node.type === "Triples" && this.inlineBlock(g.open, g.close, () => this.patternItem(g.items[0], true))) {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
this.block(g.open, g.close, g.items.length === 0, () => {
|
|
2071
|
+
g.items.forEach((item) => {
|
|
2072
|
+
this.nl();
|
|
2073
|
+
this.patternItem(item, false);
|
|
2074
|
+
});
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
triplesTemplate(t) {
|
|
2078
|
+
if (t.items.length === 1 && t.items[0].node.type === "Triples" && this.inlineBlock(t.open, t.close, () => this.patternItem(t.items[0], true))) {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
this.block(t.open, t.close, t.items.length === 0, () => {
|
|
2082
|
+
for (const item of t.items) {
|
|
2083
|
+
this.nl();
|
|
2084
|
+
this.patternItem(item, false);
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
/** An item of a group: triples end with ` .`, other patterns have no dot. */
|
|
2089
|
+
patternItem(item, inline) {
|
|
2090
|
+
const node = item.node;
|
|
2091
|
+
if (node.type === "Triples") {
|
|
2092
|
+
this.triples(node, true);
|
|
2093
|
+
if (inline) {
|
|
2094
|
+
if (item.dot) this.w.drop(item.dot);
|
|
2095
|
+
} else {
|
|
2096
|
+
this.sp();
|
|
2097
|
+
if (item.dot) this.tok(item.dot);
|
|
2098
|
+
else this.w.text(".");
|
|
2099
|
+
}
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
this.pattern(node, true);
|
|
2103
|
+
if (item.dot) this.w.drop(item.dot);
|
|
2104
|
+
}
|
|
2105
|
+
pattern(node, first) {
|
|
2106
|
+
switch (node.type) {
|
|
2107
|
+
case "GroupGraphPattern":
|
|
2108
|
+
this.markBlank(node.open, first);
|
|
2109
|
+
this.group(node);
|
|
2110
|
+
break;
|
|
2111
|
+
case "Union":
|
|
2112
|
+
node.groups.forEach((g, i) => {
|
|
2113
|
+
if (i > 0) {
|
|
2114
|
+
this.nl();
|
|
2115
|
+
this.kw(node.keywords[i - 1]);
|
|
2116
|
+
this.nl();
|
|
2117
|
+
this.group(g);
|
|
2118
|
+
} else {
|
|
2119
|
+
this.markBlank(g.open, first);
|
|
2120
|
+
this.group(g);
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
break;
|
|
2124
|
+
case "Optional":
|
|
2125
|
+
case "Minus":
|
|
2126
|
+
this.kw(node.keyword, first);
|
|
2127
|
+
this.sp();
|
|
2128
|
+
this.group(node.group);
|
|
2129
|
+
break;
|
|
2130
|
+
case "Graph":
|
|
2131
|
+
this.kw(node.keyword, first);
|
|
2132
|
+
this.sp();
|
|
2133
|
+
this.term(node.name);
|
|
2134
|
+
this.sp();
|
|
2135
|
+
this.group(node.group);
|
|
2136
|
+
break;
|
|
2137
|
+
case "Service":
|
|
2138
|
+
this.kw(node.keyword, first);
|
|
2139
|
+
if (node.silent) {
|
|
2140
|
+
this.sp();
|
|
2141
|
+
this.kw(node.silent);
|
|
2142
|
+
}
|
|
2143
|
+
this.sp();
|
|
2144
|
+
this.term(node.name);
|
|
2145
|
+
this.sp();
|
|
2146
|
+
this.group(node.group);
|
|
2147
|
+
break;
|
|
2148
|
+
case "Filter":
|
|
2149
|
+
this.kw(node.keyword, first);
|
|
2150
|
+
this.sp();
|
|
2151
|
+
this.expr(node.constraint);
|
|
2152
|
+
break;
|
|
2153
|
+
case "Bind":
|
|
2154
|
+
this.kw(node.keyword, first);
|
|
2155
|
+
this.sp();
|
|
2156
|
+
this.tok(node.open);
|
|
2157
|
+
this.expr(node.expression);
|
|
2158
|
+
this.sp();
|
|
2159
|
+
this.kw(node.as);
|
|
2160
|
+
this.sp();
|
|
2161
|
+
this.term(node.variable);
|
|
2162
|
+
this.tok(node.close);
|
|
2163
|
+
break;
|
|
2164
|
+
case "Values":
|
|
2165
|
+
this.markBlank(node.keyword, first);
|
|
2166
|
+
this.values(node);
|
|
2167
|
+
break;
|
|
2168
|
+
case "QuadsGraph":
|
|
2169
|
+
this.kw(node.keyword, first);
|
|
2170
|
+
this.sp();
|
|
2171
|
+
this.term(node.name);
|
|
2172
|
+
this.sp();
|
|
2173
|
+
this.triplesTemplate(node.template);
|
|
2174
|
+
break;
|
|
2175
|
+
default:
|
|
2176
|
+
throw new Error(`Unexpected pattern ${node.type}`);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
/** Honour a blank line before the first token of a statement. */
|
|
2180
|
+
markBlank(t, blank) {
|
|
2181
|
+
if (blank && t.blankLinesBefore > 0 && this.options.preserveBlankLines && this.w.atLineStart) {
|
|
2182
|
+
this.w.commentsBefore(t);
|
|
2183
|
+
this.w.blankLine();
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
values(v) {
|
|
2187
|
+
this.kw(v.keyword, true);
|
|
2188
|
+
this.sp();
|
|
2189
|
+
const b = v.block;
|
|
2190
|
+
if (b.variable) {
|
|
2191
|
+
this.term(b.variable);
|
|
2192
|
+
this.sp();
|
|
2193
|
+
const values = b.values;
|
|
2194
|
+
const inline = this.measure(() => this.valuesOneVar(b, true), this.remainingWidth());
|
|
2195
|
+
if (inline !== null || values.length === 0) {
|
|
2196
|
+
this.valuesOneVar(b, true);
|
|
2197
|
+
} else {
|
|
2198
|
+
this.valuesOneVar(b, false);
|
|
2199
|
+
}
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const vars = b.variables;
|
|
2203
|
+
this.tok(vars.open);
|
|
2204
|
+
vars.vars.forEach((variable, i) => {
|
|
2205
|
+
if (i > 0) this.sp();
|
|
2206
|
+
this.term(variable);
|
|
2207
|
+
});
|
|
2208
|
+
this.tok(vars.close);
|
|
2209
|
+
this.sp();
|
|
2210
|
+
this.block(b.open, b.close, b.rows.length === 0, () => {
|
|
2211
|
+
for (const row of b.rows) {
|
|
2212
|
+
this.nl();
|
|
2213
|
+
this.tok(row.open);
|
|
2214
|
+
row.values.forEach((value, i) => {
|
|
2215
|
+
if (i > 0) this.sp();
|
|
2216
|
+
this.node(value);
|
|
2217
|
+
});
|
|
2218
|
+
this.tok(row.close);
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
valuesOneVar(b, inline) {
|
|
2223
|
+
if (inline) {
|
|
2224
|
+
this.tok(b.open);
|
|
2225
|
+
for (const value of b.values) {
|
|
2226
|
+
this.sp();
|
|
2227
|
+
this.node(value);
|
|
2228
|
+
}
|
|
2229
|
+
if (b.values.length > 0) this.sp();
|
|
2230
|
+
this.tok(b.close);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
this.block(b.open, b.close, false, () => {
|
|
2234
|
+
for (const value of b.values) {
|
|
2235
|
+
this.nl();
|
|
2236
|
+
this.node(value);
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
// -------------------------------------------------------------------------
|
|
2241
|
+
// Triples
|
|
2242
|
+
// -------------------------------------------------------------------------
|
|
2243
|
+
triples(t, blank) {
|
|
2244
|
+
this.node(t.subject, blank);
|
|
2245
|
+
if (t.properties) {
|
|
2246
|
+
this.sp();
|
|
2247
|
+
this.propertyList(t.properties);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
propertyList(pl, inline = false) {
|
|
2251
|
+
const indent = this.options.alignPredicates ? this.w.alignment() : this.w.continuation();
|
|
2252
|
+
pl.entries.forEach((entry, i) => {
|
|
2253
|
+
if (i > 0) {
|
|
2254
|
+
if (inline) this.sp();
|
|
2255
|
+
else this.w.newlineWith(indent);
|
|
2256
|
+
}
|
|
2257
|
+
this.node(entry.verb);
|
|
2258
|
+
this.sp();
|
|
2259
|
+
const object = (o) => {
|
|
2260
|
+
this.node(o.node);
|
|
2261
|
+
for (const a of o.annotations) {
|
|
2262
|
+
this.sp();
|
|
2263
|
+
this.node(a);
|
|
2264
|
+
}
|
|
2265
|
+
if (o.comma) this.tok(o.comma);
|
|
2266
|
+
};
|
|
2267
|
+
const wrap = !inline && entry.objects.length > 1 && this.measure(
|
|
2268
|
+
() => entry.objects.forEach((o, j) => (j > 0 && this.sp(), object(o))),
|
|
2269
|
+
this.remainingWidth() - 2,
|
|
2270
|
+
true
|
|
2271
|
+
) === null;
|
|
2272
|
+
const objectIndent = wrap ? this.w.alignment() : "";
|
|
2273
|
+
entry.objects.forEach((o, j) => {
|
|
2274
|
+
if (j > 0) {
|
|
2275
|
+
if (wrap) this.w.newlineWith(objectIndent);
|
|
2276
|
+
else this.sp();
|
|
2277
|
+
}
|
|
2278
|
+
object(o);
|
|
2279
|
+
});
|
|
2280
|
+
const last = i === pl.entries.length - 1;
|
|
2281
|
+
entry.semicolons.forEach((semi, k) => {
|
|
2282
|
+
if (!last && k === 0) {
|
|
2283
|
+
this.sp();
|
|
2284
|
+
this.tok(semi);
|
|
2285
|
+
} else {
|
|
2286
|
+
this.w.drop(semi);
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
// -------------------------------------------------------------------------
|
|
2292
|
+
// Nodes (terms, paths, expressions)
|
|
2293
|
+
// -------------------------------------------------------------------------
|
|
2294
|
+
term(t, blank = false) {
|
|
2295
|
+
t.tokens.forEach((token, i) => {
|
|
2296
|
+
if (token.type === "NAME" && token.value !== "a" && !/^(true|false)$/i.test(token.value)) {
|
|
2297
|
+
this.kw(token, blank && i === 0);
|
|
2298
|
+
} else {
|
|
2299
|
+
this.tok(token, blank && i === 0);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
node(n, blank = false) {
|
|
2304
|
+
switch (n.type) {
|
|
2305
|
+
case "Term":
|
|
2306
|
+
this.term(n, blank);
|
|
2307
|
+
break;
|
|
2308
|
+
case "TripleTerm":
|
|
2309
|
+
this.tok(n.open, blank);
|
|
2310
|
+
this.sp();
|
|
2311
|
+
this.node(n.subject);
|
|
2312
|
+
this.sp();
|
|
2313
|
+
this.term(n.predicate);
|
|
2314
|
+
this.sp();
|
|
2315
|
+
this.node(n.object);
|
|
2316
|
+
this.sp();
|
|
2317
|
+
this.tok(n.close);
|
|
2318
|
+
break;
|
|
2319
|
+
case "ReifiedTriple":
|
|
2320
|
+
this.tok(n.open, blank);
|
|
2321
|
+
this.sp();
|
|
2322
|
+
this.node(n.subject);
|
|
2323
|
+
this.sp();
|
|
2324
|
+
this.term(n.predicate);
|
|
2325
|
+
this.sp();
|
|
2326
|
+
this.node(n.object);
|
|
2327
|
+
if (n.reifier) {
|
|
2328
|
+
this.sp();
|
|
2329
|
+
this.node(n.reifier);
|
|
2330
|
+
}
|
|
2331
|
+
this.sp();
|
|
2332
|
+
this.tok(n.close);
|
|
2333
|
+
break;
|
|
2334
|
+
case "Reifier":
|
|
2335
|
+
this.tok(n.tilde);
|
|
2336
|
+
if (n.id) {
|
|
2337
|
+
this.sp();
|
|
2338
|
+
this.term(n.id);
|
|
2339
|
+
}
|
|
2340
|
+
break;
|
|
2341
|
+
case "AnnotationBlock": {
|
|
2342
|
+
const render = (inline) => {
|
|
2343
|
+
this.tok(n.open);
|
|
2344
|
+
this.sp();
|
|
2345
|
+
this.propertyList(n.properties, inline);
|
|
2346
|
+
this.sp();
|
|
2347
|
+
this.tok(n.close);
|
|
2348
|
+
};
|
|
2349
|
+
render(this.measure(() => render(true), this.remainingWidth()) !== null);
|
|
2350
|
+
break;
|
|
2351
|
+
}
|
|
2352
|
+
case "BlankNodePropertyList":
|
|
2353
|
+
this.tok(n.open, blank);
|
|
2354
|
+
this.sp();
|
|
2355
|
+
this.propertyList(n.properties);
|
|
2356
|
+
this.sp();
|
|
2357
|
+
this.tok(n.close);
|
|
2358
|
+
break;
|
|
2359
|
+
case "Collection":
|
|
2360
|
+
this.tok(n.open, blank);
|
|
2361
|
+
for (const item of n.items) {
|
|
2362
|
+
this.sp();
|
|
2363
|
+
this.node(item);
|
|
2364
|
+
}
|
|
2365
|
+
this.sp();
|
|
2366
|
+
this.tok(n.close);
|
|
2367
|
+
break;
|
|
2368
|
+
case "PathAlternative":
|
|
2369
|
+
case "PathSequence":
|
|
2370
|
+
n.items.forEach((item, i) => {
|
|
2371
|
+
if (i > 0) this.tok(n.separators[i - 1]);
|
|
2372
|
+
this.node(item);
|
|
2373
|
+
});
|
|
2374
|
+
break;
|
|
2375
|
+
case "PathInverse":
|
|
2376
|
+
this.tok(n.caret);
|
|
2377
|
+
this.node(n.item);
|
|
2378
|
+
break;
|
|
2379
|
+
case "PathModified":
|
|
2380
|
+
this.node(n.item);
|
|
2381
|
+
this.tok(n.modifier);
|
|
2382
|
+
break;
|
|
2383
|
+
case "PathNegated":
|
|
2384
|
+
this.tok(n.bang);
|
|
2385
|
+
this.node(n.item);
|
|
2386
|
+
break;
|
|
2387
|
+
case "PathGroup":
|
|
2388
|
+
this.tok(n.open);
|
|
2389
|
+
if (n.path) this.node(n.path);
|
|
2390
|
+
this.tok(n.close);
|
|
2391
|
+
break;
|
|
2392
|
+
default:
|
|
2393
|
+
this.expr(n);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
expr(n) {
|
|
2397
|
+
switch (n.type) {
|
|
2398
|
+
case "Binary":
|
|
2399
|
+
this.expr(n.left);
|
|
2400
|
+
this.sp();
|
|
2401
|
+
this.tok(n.operator);
|
|
2402
|
+
this.sp();
|
|
2403
|
+
this.expr(n.right);
|
|
2404
|
+
break;
|
|
2405
|
+
case "In":
|
|
2406
|
+
this.expr(n.expression);
|
|
2407
|
+
this.sp();
|
|
2408
|
+
if (n.not) {
|
|
2409
|
+
this.kw(n.not);
|
|
2410
|
+
this.sp();
|
|
2411
|
+
}
|
|
2412
|
+
this.kw(n.in);
|
|
2413
|
+
this.sp();
|
|
2414
|
+
this.expr(n.list);
|
|
2415
|
+
break;
|
|
2416
|
+
case "Unary":
|
|
2417
|
+
this.tok(n.operator);
|
|
2418
|
+
this.expr(n.expression);
|
|
2419
|
+
break;
|
|
2420
|
+
case "Bracketed":
|
|
2421
|
+
this.tok(n.open);
|
|
2422
|
+
this.expr(n.expression);
|
|
2423
|
+
this.tok(n.close);
|
|
2424
|
+
break;
|
|
2425
|
+
case "ExpressionList":
|
|
2426
|
+
this.tok(n.open);
|
|
2427
|
+
n.items.forEach((item, i) => {
|
|
2428
|
+
if (i > 0) {
|
|
2429
|
+
this.tok(n.commas[i - 1]);
|
|
2430
|
+
this.sp();
|
|
2431
|
+
}
|
|
2432
|
+
this.expr(item);
|
|
2433
|
+
});
|
|
2434
|
+
this.tok(n.close);
|
|
2435
|
+
break;
|
|
2436
|
+
case "Call":
|
|
2437
|
+
if (n.name.type === "Term") this.term(n.name);
|
|
2438
|
+
else this.w.token(n.name, applyCase(n.name.value, this.options.functionCase));
|
|
2439
|
+
this.tok(n.open);
|
|
2440
|
+
if (n.distinct) {
|
|
2441
|
+
this.kw(n.distinct);
|
|
2442
|
+
this.sp();
|
|
2443
|
+
}
|
|
2444
|
+
n.args.forEach((arg, i) => {
|
|
2445
|
+
if (i > 0) {
|
|
2446
|
+
this.tok(n.commas[i - 1]);
|
|
2447
|
+
this.sp();
|
|
2448
|
+
}
|
|
2449
|
+
this.expr(arg);
|
|
2450
|
+
});
|
|
2451
|
+
if (n.separator) {
|
|
2452
|
+
this.tok(n.separator.semicolon);
|
|
2453
|
+
this.sp();
|
|
2454
|
+
this.kw(n.separator.keyword);
|
|
2455
|
+
this.sp();
|
|
2456
|
+
this.tok(n.separator.equals);
|
|
2457
|
+
this.sp();
|
|
2458
|
+
this.term(n.separator.value);
|
|
2459
|
+
}
|
|
2460
|
+
this.tok(n.close);
|
|
2461
|
+
break;
|
|
2462
|
+
case "Exists":
|
|
2463
|
+
if (n.not) {
|
|
2464
|
+
this.kw(n.not);
|
|
2465
|
+
this.sp();
|
|
2466
|
+
}
|
|
2467
|
+
this.kw(n.keyword);
|
|
2468
|
+
this.sp();
|
|
2469
|
+
this.group(n.group);
|
|
2470
|
+
break;
|
|
2471
|
+
case "OrderCondition":
|
|
2472
|
+
case "Projection":
|
|
2473
|
+
this.condition(n);
|
|
2474
|
+
break;
|
|
2475
|
+
case "TripleTerm":
|
|
2476
|
+
case "Term":
|
|
2477
|
+
this.node(n);
|
|
2478
|
+
break;
|
|
2479
|
+
default:
|
|
2480
|
+
throw new Error(`Unexpected expression ${n.type}`);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
|
|
2485
|
+
// src/index.ts
|
|
2486
|
+
function countComments(ast) {
|
|
2487
|
+
let count = 0;
|
|
2488
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2489
|
+
const visit = (value) => {
|
|
2490
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return;
|
|
2491
|
+
seen.add(value);
|
|
2492
|
+
if (Array.isArray(value)) {
|
|
2493
|
+
value.forEach(visit);
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
const obj = value;
|
|
2497
|
+
if (typeof obj.offset === "number" && Array.isArray(obj.leading)) {
|
|
2498
|
+
count += obj.leading.length + (obj.trailing ? 1 : 0);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
Object.values(obj).forEach(visit);
|
|
2502
|
+
};
|
|
2503
|
+
visit(ast);
|
|
2504
|
+
return count;
|
|
2505
|
+
}
|
|
2506
|
+
function formatAst(ast, options) {
|
|
2507
|
+
const { text, comments } = new Printer(resolveOptions(options)).print(ast);
|
|
2508
|
+
const expected = countComments(ast);
|
|
2509
|
+
if (comments !== expected) {
|
|
2510
|
+
throw new Error(`Internal formatter error: ${expected - comments} comment(s) could not be placed`);
|
|
2511
|
+
}
|
|
2512
|
+
return text;
|
|
2513
|
+
}
|
|
2514
|
+
function format(query, options) {
|
|
2515
|
+
return formatAst(parse(query), options);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/cli.ts
|
|
2519
|
+
var HELP = `Usage: sparql-formatter [options] [file ...]
|
|
2520
|
+
|
|
2521
|
+
Formats SPARQL 1.2 queries and updates. Reads from stdin when no file is given.
|
|
2522
|
+
|
|
2523
|
+
Options:
|
|
2524
|
+
-i, --indent <n|tab> indentation: number of spaces or "tab" (default: 2)
|
|
2525
|
+
--keyword-case <case> upper | lower | preserve (default: upper)
|
|
2526
|
+
--function-case <case> upper | lower | preserve (default: preserve)
|
|
2527
|
+
--no-align don't align predicates under the first predicate
|
|
2528
|
+
--no-blank-lines remove blank lines instead of keeping them
|
|
2529
|
+
--compact put small group patterns on one line
|
|
2530
|
+
--line-width <n> maximum line width for --compact and VALUES (default: 100)
|
|
2531
|
+
-w, --write rewrite the files in place
|
|
2532
|
+
-c, --check exit with code 1 if a file is not formatted
|
|
2533
|
+
-v, --version print the version
|
|
2534
|
+
-h, --help print this help
|
|
2535
|
+
`;
|
|
2536
|
+
function parseCase(value, name) {
|
|
2537
|
+
if (value === void 0) return void 0;
|
|
2538
|
+
if (value === "upper" || value === "lower" || value === "preserve") return value;
|
|
2539
|
+
throw new Error(`--${name} must be one of upper, lower, preserve`);
|
|
2540
|
+
}
|
|
2541
|
+
function main(argv) {
|
|
2542
|
+
const { values, positionals } = parseArgs({
|
|
2543
|
+
args: argv,
|
|
2544
|
+
allowPositionals: true,
|
|
2545
|
+
options: {
|
|
2546
|
+
indent: { type: "string", short: "i" },
|
|
2547
|
+
"keyword-case": { type: "string" },
|
|
2548
|
+
"function-case": { type: "string" },
|
|
2549
|
+
"no-align": { type: "boolean" },
|
|
2550
|
+
"no-blank-lines": { type: "boolean" },
|
|
2551
|
+
compact: { type: "boolean" },
|
|
2552
|
+
"line-width": { type: "string" },
|
|
2553
|
+
write: { type: "boolean", short: "w" },
|
|
2554
|
+
check: { type: "boolean", short: "c" },
|
|
2555
|
+
version: { type: "boolean", short: "v" },
|
|
2556
|
+
help: { type: "boolean", short: "h" }
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
if (values.help) {
|
|
2560
|
+
process.stdout.write(HELP);
|
|
2561
|
+
return 0;
|
|
2562
|
+
}
|
|
2563
|
+
if (values.version) {
|
|
2564
|
+
process.stdout.write(`${"1.0.0"}
|
|
2565
|
+
`);
|
|
2566
|
+
return 0;
|
|
2567
|
+
}
|
|
2568
|
+
const options = {
|
|
2569
|
+
keywordCase: parseCase(values["keyword-case"], "keyword-case"),
|
|
2570
|
+
functionCase: parseCase(values["function-case"], "function-case"),
|
|
2571
|
+
alignPredicates: !values["no-align"],
|
|
2572
|
+
preserveBlankLines: !values["no-blank-lines"],
|
|
2573
|
+
compact: values.compact ?? false
|
|
2574
|
+
};
|
|
2575
|
+
if (values.indent !== void 0) {
|
|
2576
|
+
if (values.indent === "tab") options.indent = " ";
|
|
2577
|
+
else if (/^\d+$/.test(values.indent)) options.indent = Number(values.indent);
|
|
2578
|
+
else throw new Error('--indent must be a number or "tab"');
|
|
2579
|
+
}
|
|
2580
|
+
if (values["line-width"] !== void 0) options.lineWidth = Number(values["line-width"]);
|
|
2581
|
+
const inputs = positionals.length > 0 ? positionals : ["-"];
|
|
2582
|
+
if (values.write && positionals.length === 0) throw new Error("--write needs at least one file");
|
|
2583
|
+
let status = 0;
|
|
2584
|
+
for (const file of inputs) {
|
|
2585
|
+
const source = readFileSync(file === "-" ? 0 : file, "utf8");
|
|
2586
|
+
let formatted;
|
|
2587
|
+
try {
|
|
2588
|
+
formatted = format(source, options);
|
|
2589
|
+
} catch (e) {
|
|
2590
|
+
if (e instanceof SparqlSyntaxError) {
|
|
2591
|
+
process.stderr.write(`${file === "-" ? "<stdin>" : file}:${e.line}:${e.column}: ${e.message}
|
|
2592
|
+
`);
|
|
2593
|
+
status = 2;
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
throw e;
|
|
2597
|
+
}
|
|
2598
|
+
const output = formatted === "" ? "" : `${formatted}
|
|
2599
|
+
`;
|
|
2600
|
+
if (values.check) {
|
|
2601
|
+
if (output !== source) {
|
|
2602
|
+
process.stderr.write(`${file === "-" ? "<stdin>" : file}: not formatted
|
|
2603
|
+
`);
|
|
2604
|
+
status = Math.max(status, 1);
|
|
2605
|
+
}
|
|
2606
|
+
} else if (values.write) {
|
|
2607
|
+
if (output !== source) writeFileSync(file, output);
|
|
2608
|
+
} else {
|
|
2609
|
+
process.stdout.write(output);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return status;
|
|
2613
|
+
}
|
|
2614
|
+
try {
|
|
2615
|
+
process.exitCode = main(process.argv.slice(2));
|
|
2616
|
+
} catch (e) {
|
|
2617
|
+
process.stderr.write(`sparql-formatter: ${e.message}
|
|
2618
|
+
`);
|
|
2619
|
+
process.exitCode = 2;
|
|
2620
|
+
}
|
|
2621
|
+
//# sourceMappingURL=cli.js.map
|