@coldsmirk/inkstone-codemirror 0.9.0 → 0.10.1
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/README.md +33 -3
- package/dist/{context-D86o2jK3.js → context-BJzyiP0A.js} +12 -5
- package/dist/index.d.ts +131 -3
- package/dist/index.js +216 -58
- package/dist/minijinja-D5ANOjtx.js +700 -0
- package/dist/sql-schema-DiPx5-bu.js +15 -0
- package/dist/sql-support-Gk7Wv9Kj.js +29 -0
- package/package.json +2 -1
- package/dist/minijinja-D_5erBa7.js +0 -418
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { i as resolveMembers, n as minijinjaContextField, o as typeAtPath, r as normalizeContext, t as elementType } from "./context-BJzyiP0A.js";
|
|
2
|
+
import { LanguageSupport, syntaxTree } from "@codemirror/language";
|
|
3
|
+
import { jinja, jinjaLanguage } from "@codemirror/lang-jinja";
|
|
4
|
+
import { linter } from "@codemirror/lint";
|
|
5
|
+
//#region src/minijinja-parser.ts
|
|
6
|
+
var MinijinjaCompatibleInput = class {
|
|
7
|
+
value;
|
|
8
|
+
length;
|
|
9
|
+
lineChunks = false;
|
|
10
|
+
constructor(value) {
|
|
11
|
+
this.value = value;
|
|
12
|
+
this.length = value.length;
|
|
13
|
+
}
|
|
14
|
+
chunk(from) {
|
|
15
|
+
return this.value.slice(from);
|
|
16
|
+
}
|
|
17
|
+
read(from, to) {
|
|
18
|
+
return this.value.slice(from, to);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function withMinijinjaSyntax(language) {
|
|
22
|
+
const { parser } = language;
|
|
23
|
+
return language.configure({ wrap(inner, input, fragments, ranges) {
|
|
24
|
+
const source = input.read(0, input.length);
|
|
25
|
+
const compatible = normalizeSyntax(source);
|
|
26
|
+
return compatible === source ? inner : parser.startParse(new MinijinjaCompatibleInput(compatible), fragments, ranges);
|
|
27
|
+
} }, "minijinja");
|
|
28
|
+
}
|
|
29
|
+
function normalizeSyntax(source) {
|
|
30
|
+
let output = null;
|
|
31
|
+
let searchFrom = 0;
|
|
32
|
+
let raw = false;
|
|
33
|
+
while (searchFrom < source.length) {
|
|
34
|
+
const statementOpen = source.indexOf("{%", searchFrom);
|
|
35
|
+
if (!raw) {
|
|
36
|
+
const interpolationOpen = source.indexOf("{{", searchFrom);
|
|
37
|
+
const commentOpen = source.indexOf("{#", searchFrom);
|
|
38
|
+
const nextOpen = Math.min(statementOpen === -1 ? source.length : statementOpen, interpolationOpen === -1 ? source.length : interpolationOpen, commentOpen === -1 ? source.length : commentOpen);
|
|
39
|
+
if (nextOpen === source.length) break;
|
|
40
|
+
if (nextOpen === commentOpen) {
|
|
41
|
+
const commentClose = source.indexOf("#}", commentOpen + 2);
|
|
42
|
+
if (commentClose === -1) break;
|
|
43
|
+
searchFrom = commentClose + 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (nextOpen === interpolationOpen) {
|
|
47
|
+
const interpolationClose = findInterpolationEnd(source, interpolationOpen + 2);
|
|
48
|
+
if (interpolationClose === -1) break;
|
|
49
|
+
if (source[interpolationOpen + 2] === "-" || source[interpolationOpen + 2] === "+") output = replaceAt(source, output, interpolationOpen + 2, " ");
|
|
50
|
+
if (source[interpolationClose - 1] === "-" || source[interpolationClose - 1] === "+") output = replaceAt(source, output, interpolationClose - 1, " ");
|
|
51
|
+
searchFrom = interpolationClose + 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (statementOpen === -1) break;
|
|
56
|
+
if (raw) {
|
|
57
|
+
const end = rawEndTag(source, statementOpen);
|
|
58
|
+
if (end) {
|
|
59
|
+
if (end.openControl !== null) output = replaceAt(source, output, statementOpen + 2, " ");
|
|
60
|
+
for (const index of end.whitespace) if (source[index] !== " " && source[index] !== "\n") output = replaceAt(source, output, index, " ");
|
|
61
|
+
if (end.closeControl === "+") output = replaceAt(source, output, end.close - 1, " ");
|
|
62
|
+
raw = false;
|
|
63
|
+
searchFrom = end.close + 2;
|
|
64
|
+
} else {
|
|
65
|
+
const upstreamKeyword = upstreamRawEndKeyword(source, statementOpen);
|
|
66
|
+
if (upstreamKeyword !== null) output = replaceAt(source, output, upstreamKeyword, "_");
|
|
67
|
+
searchFrom = statementOpen + 2;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let keyword = statementOpen + 2;
|
|
72
|
+
const close = findTagEnd(source, keyword);
|
|
73
|
+
if (close === -1) break;
|
|
74
|
+
if (source[keyword] === "-" || source[keyword] === "+") keyword += 1;
|
|
75
|
+
while (/\s/u.test(source[keyword] ?? "")) keyword += 1;
|
|
76
|
+
if (source[statementOpen + 2] === "+") output = replaceAt(source, output, statementOpen + 2, " ");
|
|
77
|
+
if (source[close - 1] === "+") output = replaceAt(source, output, close - 1, " ");
|
|
78
|
+
if (hasKeywordAt(source, keyword, "raw")) raw = true;
|
|
79
|
+
else if (hasKeywordAt(source, keyword, "set")) {
|
|
80
|
+
const bodyEnd = source[close - 1] === "-" || source[close - 1] === "+" ? close - 1 : close;
|
|
81
|
+
output = normalizeSetBody(source, output, keyword + 3, bodyEnd);
|
|
82
|
+
}
|
|
83
|
+
searchFrom = close + 2;
|
|
84
|
+
}
|
|
85
|
+
return output ? output.join("") : source;
|
|
86
|
+
}
|
|
87
|
+
function replaceAt(source, output, index, replacement) {
|
|
88
|
+
const next = output ?? source.split("");
|
|
89
|
+
next[index] = replacement;
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
function hasKeywordAt(source, from, keyword) {
|
|
93
|
+
return source.startsWith(keyword, from) && !/[\p{L}\p{N}_]/u.test(source[from + keyword.length] ?? "");
|
|
94
|
+
}
|
|
95
|
+
function rawEndTag(source, statementOpen) {
|
|
96
|
+
let index = statementOpen + 2;
|
|
97
|
+
const open = source[index];
|
|
98
|
+
const openControl = open === "-" || open === "+" ? open : null;
|
|
99
|
+
const whitespace = [];
|
|
100
|
+
if (openControl !== null) index += 1;
|
|
101
|
+
while (isAsciiWhitespace(source[index])) {
|
|
102
|
+
whitespace.push(index);
|
|
103
|
+
index += 1;
|
|
104
|
+
}
|
|
105
|
+
if (!source.startsWith("endraw", index)) return null;
|
|
106
|
+
index += 6;
|
|
107
|
+
while (isAsciiWhitespace(source[index])) {
|
|
108
|
+
whitespace.push(index);
|
|
109
|
+
index += 1;
|
|
110
|
+
}
|
|
111
|
+
const close = source[index];
|
|
112
|
+
const closeControl = close === "-" || close === "+" ? close : null;
|
|
113
|
+
if (closeControl !== null) index += 1;
|
|
114
|
+
return source[index] === "%" && source[index + 1] === "}" ? {
|
|
115
|
+
close: index,
|
|
116
|
+
closeControl,
|
|
117
|
+
openControl,
|
|
118
|
+
whitespace
|
|
119
|
+
} : null;
|
|
120
|
+
}
|
|
121
|
+
function upstreamRawEndKeyword(source, statementOpen) {
|
|
122
|
+
let index = statementOpen + 2;
|
|
123
|
+
while (source[index] === " " || source[index] === "\n") index += 1;
|
|
124
|
+
return source.startsWith("endraw", index) && !/[A-Za-z]/u.test(source[index + 6] ?? "") ? index : null;
|
|
125
|
+
}
|
|
126
|
+
function isAsciiWhitespace(character) {
|
|
127
|
+
if (character === void 0) return false;
|
|
128
|
+
const code = character.codePointAt(0);
|
|
129
|
+
return code === 9 || code === 10 || code === 12 || code === 13 || code === 32;
|
|
130
|
+
}
|
|
131
|
+
function normalizeSetBody(source, output, from, to) {
|
|
132
|
+
const assignment = findTopLevelAssignment(source, from, to);
|
|
133
|
+
let next = replaceTargetDots(source, output, from, assignment === -1 ? findCaptureTargetEnd(source, from, to) : assignment);
|
|
134
|
+
if (assignment === -1) return next;
|
|
135
|
+
forEachTopLevel(source, assignment + 1, to, (index) => {
|
|
136
|
+
if (source[index] !== ",") return;
|
|
137
|
+
next ??= source.split("");
|
|
138
|
+
next[index] = hasNonWhitespace(source, index + 1, to) ? "+" : " ";
|
|
139
|
+
});
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
function replaceTargetDots(source, output, from, to) {
|
|
143
|
+
let next = output;
|
|
144
|
+
for (let index = from; index < to; index++) {
|
|
145
|
+
if (source[index] !== ".") continue;
|
|
146
|
+
next ??= source.split("");
|
|
147
|
+
next[index] = "_";
|
|
148
|
+
}
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
function findTopLevelAssignment(source, from, to) {
|
|
152
|
+
let found = -1;
|
|
153
|
+
forEachTopLevel(source, from, to, (index) => {
|
|
154
|
+
if (found !== -1 || source[index] !== "=") return;
|
|
155
|
+
const before = source[index - 1] ?? "";
|
|
156
|
+
const after = source[index + 1] ?? "";
|
|
157
|
+
if (before !== "=" && before !== "!" && before !== "<" && before !== ">" && after !== "=") found = index;
|
|
158
|
+
});
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
function findCaptureTargetEnd(source, from, to) {
|
|
162
|
+
let index = from;
|
|
163
|
+
while (/\s/u.test(source[index] ?? "")) index += 1;
|
|
164
|
+
while (index < to && !/\s|\|/u.test(source[index] ?? "")) index += 1;
|
|
165
|
+
return index;
|
|
166
|
+
}
|
|
167
|
+
function forEachTopLevel(source, from, to, visit) {
|
|
168
|
+
let quote = null;
|
|
169
|
+
let depth = 0;
|
|
170
|
+
for (let index = from; index < to; index++) {
|
|
171
|
+
const character = source[index];
|
|
172
|
+
if (quote) {
|
|
173
|
+
if (character === "\\") index += 1;
|
|
174
|
+
else if (character === quote) quote = null;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (character === "\"" || character === "'") {
|
|
178
|
+
quote = character;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const depthChange = bracketDepthChange(character);
|
|
182
|
+
if (depthChange !== 0) {
|
|
183
|
+
depth = Math.max(0, depth + depthChange);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (depth === 0) visit(index);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function bracketDepthChange(character) {
|
|
190
|
+
if (character === "(" || character === "[" || character === "{") return 1;
|
|
191
|
+
if (character === ")" || character === "]" || character === "}") return -1;
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
function findTagEnd(source, from) {
|
|
195
|
+
let quote = null;
|
|
196
|
+
for (let index = from; index < source.length - 1; index++) {
|
|
197
|
+
const character = source[index];
|
|
198
|
+
if (quote) {
|
|
199
|
+
if (character === "\\") index += 1;
|
|
200
|
+
else if (character === quote) quote = null;
|
|
201
|
+
} else if (character === "\"" || character === "'") quote = character;
|
|
202
|
+
else if (character === "%" && source[index + 1] === "}") return index;
|
|
203
|
+
}
|
|
204
|
+
return -1;
|
|
205
|
+
}
|
|
206
|
+
function findInterpolationEnd(source, from) {
|
|
207
|
+
let quote = null;
|
|
208
|
+
let depth = 0;
|
|
209
|
+
for (let index = from; index < source.length - 1; index++) {
|
|
210
|
+
const character = source[index];
|
|
211
|
+
if (quote) {
|
|
212
|
+
if (character === "\\") index += 1;
|
|
213
|
+
else if (character === quote) quote = null;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (character === "\"" || character === "'") {
|
|
217
|
+
quote = character;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (character === "}" && source[index + 1] === "}" && depth === 0) return index;
|
|
221
|
+
depth = Math.max(0, depth + bracketDepthChange(character));
|
|
222
|
+
}
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
function hasNonWhitespace(source, from, to) {
|
|
226
|
+
for (let index = from; index < to; index++) if (!/\s/u.test(source[index] ?? "")) return true;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/minijinja.ts
|
|
231
|
+
const MINIJINJA_TAGS = /* @__PURE__ */ new Set([
|
|
232
|
+
"autoescape",
|
|
233
|
+
"endautoescape",
|
|
234
|
+
"block",
|
|
235
|
+
"endblock",
|
|
236
|
+
"call",
|
|
237
|
+
"endcall",
|
|
238
|
+
"filter",
|
|
239
|
+
"endfilter",
|
|
240
|
+
"for",
|
|
241
|
+
"endfor",
|
|
242
|
+
"if",
|
|
243
|
+
"elif",
|
|
244
|
+
"else",
|
|
245
|
+
"endif",
|
|
246
|
+
"macro",
|
|
247
|
+
"endmacro",
|
|
248
|
+
"set",
|
|
249
|
+
"endset",
|
|
250
|
+
"with",
|
|
251
|
+
"endwith",
|
|
252
|
+
"raw",
|
|
253
|
+
"endraw",
|
|
254
|
+
"extends",
|
|
255
|
+
"include",
|
|
256
|
+
"import",
|
|
257
|
+
"from",
|
|
258
|
+
"do",
|
|
259
|
+
"break",
|
|
260
|
+
"continue"
|
|
261
|
+
]);
|
|
262
|
+
const MINIJINJA_FILTERS = [
|
|
263
|
+
"abs",
|
|
264
|
+
"attr",
|
|
265
|
+
"batch",
|
|
266
|
+
"bool",
|
|
267
|
+
"capitalize",
|
|
268
|
+
"chain",
|
|
269
|
+
"count",
|
|
270
|
+
"d",
|
|
271
|
+
"default",
|
|
272
|
+
"dictsort",
|
|
273
|
+
"e",
|
|
274
|
+
"escape",
|
|
275
|
+
"first",
|
|
276
|
+
"float",
|
|
277
|
+
"format",
|
|
278
|
+
"groupby",
|
|
279
|
+
"indent",
|
|
280
|
+
"int",
|
|
281
|
+
"items",
|
|
282
|
+
"join",
|
|
283
|
+
"last",
|
|
284
|
+
"length",
|
|
285
|
+
"lines",
|
|
286
|
+
"list",
|
|
287
|
+
"lower",
|
|
288
|
+
"map",
|
|
289
|
+
"max",
|
|
290
|
+
"min",
|
|
291
|
+
"pprint",
|
|
292
|
+
"reject",
|
|
293
|
+
"rejectattr",
|
|
294
|
+
"replace",
|
|
295
|
+
"reverse",
|
|
296
|
+
"round",
|
|
297
|
+
"safe",
|
|
298
|
+
"select",
|
|
299
|
+
"selectattr",
|
|
300
|
+
"slice",
|
|
301
|
+
"sort",
|
|
302
|
+
"split",
|
|
303
|
+
"string",
|
|
304
|
+
"sum",
|
|
305
|
+
"title",
|
|
306
|
+
"tojson",
|
|
307
|
+
"trim",
|
|
308
|
+
"unique",
|
|
309
|
+
"upper",
|
|
310
|
+
"urlencode",
|
|
311
|
+
"zip"
|
|
312
|
+
];
|
|
313
|
+
const MINIJINJA_TESTS = [
|
|
314
|
+
"boolean",
|
|
315
|
+
"defined",
|
|
316
|
+
"divisibleby",
|
|
317
|
+
"endingwith",
|
|
318
|
+
"eq",
|
|
319
|
+
"equalto",
|
|
320
|
+
"escaped",
|
|
321
|
+
"even",
|
|
322
|
+
"false",
|
|
323
|
+
"filter",
|
|
324
|
+
"float",
|
|
325
|
+
"ge",
|
|
326
|
+
"greaterthan",
|
|
327
|
+
"in",
|
|
328
|
+
"int",
|
|
329
|
+
"integer",
|
|
330
|
+
"iterable",
|
|
331
|
+
"le",
|
|
332
|
+
"lessthan",
|
|
333
|
+
"lower",
|
|
334
|
+
"lt",
|
|
335
|
+
"gt",
|
|
336
|
+
"mapping",
|
|
337
|
+
"ne",
|
|
338
|
+
"none",
|
|
339
|
+
"number",
|
|
340
|
+
"odd",
|
|
341
|
+
"safe",
|
|
342
|
+
"sameas",
|
|
343
|
+
"sequence",
|
|
344
|
+
"startingwith",
|
|
345
|
+
"string",
|
|
346
|
+
"test",
|
|
347
|
+
"true",
|
|
348
|
+
"undefined",
|
|
349
|
+
"upper"
|
|
350
|
+
];
|
|
351
|
+
const MINIJINJA_FUNCTIONS = [
|
|
352
|
+
"debug",
|
|
353
|
+
"dict",
|
|
354
|
+
"namespace",
|
|
355
|
+
"range"
|
|
356
|
+
];
|
|
357
|
+
function toCompletions(labels, type) {
|
|
358
|
+
return [...labels].toSorted().map((label) => {
|
|
359
|
+
return {
|
|
360
|
+
label,
|
|
361
|
+
type
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const tagCompletions = toCompletions(MINIJINJA_TAGS, "keyword");
|
|
366
|
+
const filterCompletions = toCompletions(MINIJINJA_FILTERS, "function");
|
|
367
|
+
const testCompletions = toCompletions(MINIJINJA_TESTS, "function");
|
|
368
|
+
const functionCompletions = toCompletions(MINIJINJA_FUNCTIONS, "function");
|
|
369
|
+
function memberCompletions(members, type) {
|
|
370
|
+
return members.map((member) => {
|
|
371
|
+
return {
|
|
372
|
+
label: member.name,
|
|
373
|
+
type,
|
|
374
|
+
detail: `${irTypeToString(member.type)}${member.nullable ? "?" : ""}`,
|
|
375
|
+
info: member.doc
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function irTypeToString(type) {
|
|
380
|
+
switch (type.kind) {
|
|
381
|
+
case "object": return "object";
|
|
382
|
+
case "array": return `${irTypeToString(type.element)}[]`;
|
|
383
|
+
case "enum": return type.values.map((value) => typeof value === "string" ? `"${value}"` : String(value)).join(" | ");
|
|
384
|
+
case "ref": return type.name;
|
|
385
|
+
default: return type.kind;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function walkBase(state, node) {
|
|
389
|
+
if (!node) return null;
|
|
390
|
+
if (node.name === "VariableName" || node.name === "loop") return [state.sliceDoc(node.from, node.to)];
|
|
391
|
+
if (node.name === "MemberExpression") {
|
|
392
|
+
const inner = walkBase(state, node.firstChild);
|
|
393
|
+
const property = node.lastChild;
|
|
394
|
+
if (inner === null || !property || property.name !== "PropertyName") return null;
|
|
395
|
+
return [...inner, state.sliceDoc(property.from, property.to)];
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function contextTarget(state, pos) {
|
|
400
|
+
const node = syntaxTree(state).resolveInner(pos, -1);
|
|
401
|
+
if (node.name === "PropertyName" && node.parent?.name === "MemberExpression") {
|
|
402
|
+
const path = walkBase(state, node.parent.firstChild);
|
|
403
|
+
return path ? {
|
|
404
|
+
path,
|
|
405
|
+
from: node.from
|
|
406
|
+
} : null;
|
|
407
|
+
}
|
|
408
|
+
if (node.name === "." && node.parent?.name === "MemberExpression") {
|
|
409
|
+
const path = walkBase(state, node.parent.firstChild);
|
|
410
|
+
return path ? {
|
|
411
|
+
path,
|
|
412
|
+
from: pos
|
|
413
|
+
} : null;
|
|
414
|
+
}
|
|
415
|
+
if (node.name === "Definition") return null;
|
|
416
|
+
const word = state.wordAt(pos);
|
|
417
|
+
return {
|
|
418
|
+
path: [],
|
|
419
|
+
from: word ? word.from : pos
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const LOOP_TYPE = {
|
|
423
|
+
kind: "object",
|
|
424
|
+
fields: {
|
|
425
|
+
index: loopField("number"),
|
|
426
|
+
index0: loopField("number"),
|
|
427
|
+
revindex: loopField("number"),
|
|
428
|
+
revindex0: loopField("number"),
|
|
429
|
+
first: loopField("boolean"),
|
|
430
|
+
last: loopField("boolean"),
|
|
431
|
+
length: loopField("number"),
|
|
432
|
+
depth: loopField("number"),
|
|
433
|
+
depth0: loopField("number"),
|
|
434
|
+
previtem: loopField("any"),
|
|
435
|
+
nextitem: loopField("any"),
|
|
436
|
+
cycle: loopField("any"),
|
|
437
|
+
changed: loopField("any")
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
function loopField(kind) {
|
|
441
|
+
return {
|
|
442
|
+
type: { kind },
|
|
443
|
+
required: true,
|
|
444
|
+
nullable: false
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const ROOT_SCOPE = -1;
|
|
448
|
+
function scopeBindings(state, pos, ir) {
|
|
449
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
450
|
+
const enclosing = [];
|
|
451
|
+
for (let node = syntaxTree(state).resolveInner(pos, -1); node; node = node.parent) if (node.name === "ForStatement" || node.name === "MacroStatement" || node.name === "WithStatement" || node.name === "BlockStatement") enclosing.push(node);
|
|
452
|
+
const scopes = enclosing.toReversed();
|
|
453
|
+
const sets = collectSetBindings(state, pos, scopes);
|
|
454
|
+
const applySets = (scopeKey) => {
|
|
455
|
+
const definitions = sets.get(scopeKey) ?? [];
|
|
456
|
+
for (const definition of definitions) bindings.set(state.sliceDoc(definition.from, definition.to), { kind: "any" });
|
|
457
|
+
};
|
|
458
|
+
applySets(ROOT_SCOPE);
|
|
459
|
+
for (const node of scopes) {
|
|
460
|
+
collectScopeBindings(state, node, pos, ir, bindings);
|
|
461
|
+
applySets(node.from);
|
|
462
|
+
}
|
|
463
|
+
return bindings;
|
|
464
|
+
}
|
|
465
|
+
function collectScopeBindings(state, node, pos, ir, bindings) {
|
|
466
|
+
switch (node.name) {
|
|
467
|
+
case "ForStatement":
|
|
468
|
+
collectForBindings(state, node, pos, ir, bindings);
|
|
469
|
+
return;
|
|
470
|
+
case "MacroStatement":
|
|
471
|
+
collectMacroBindings(state, node, pos, bindings);
|
|
472
|
+
return;
|
|
473
|
+
case "WithStatement": collectWithBindings(state, node, pos, ir, bindings);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function collectForBindings(state, forNode, pos, ir, bindings) {
|
|
477
|
+
const tag = forNode.firstChild;
|
|
478
|
+
if (!tag || tag.name !== "Tag") return;
|
|
479
|
+
const definitions = [];
|
|
480
|
+
let iterable = null;
|
|
481
|
+
let filterKeyword = null;
|
|
482
|
+
let seenIn = false;
|
|
483
|
+
for (let child = tag.firstChild; child; child = child.nextSibling) if (child.name === "in") seenIn = true;
|
|
484
|
+
else if (!seenIn && child.name === "Definition") definitions.push(child);
|
|
485
|
+
else if (seenIn && !iterable && child.name !== "%}") iterable = child;
|
|
486
|
+
if (iterable?.name === "ConditionalExpression") {
|
|
487
|
+
for (let child = iterable.firstChild; child; child = child.nextSibling) if (child.name === "if") {
|
|
488
|
+
filterKeyword = child;
|
|
489
|
+
iterable = iterable.firstChild;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
let elseTag = null;
|
|
494
|
+
let endTag = null;
|
|
495
|
+
for (let child = tag.nextSibling; child; child = child.nextSibling) {
|
|
496
|
+
if (child.name === "EndTag") {
|
|
497
|
+
endTag = child;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (child.name === "Tag") {
|
|
501
|
+
const keyword = child.firstChild?.nextSibling;
|
|
502
|
+
if (keyword && state.sliceDoc(keyword.from, keyword.to) === "else") elseTag = child;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const inFilter = filterKeyword !== null && pos >= filterKeyword.to && pos <= tag.to;
|
|
506
|
+
const bodyEnd = elseTag?.from ?? endTag?.from ?? forNode.to;
|
|
507
|
+
const inUnclosedBodyAtEnd = elseTag === null && endTag === null && pos === bodyEnd;
|
|
508
|
+
const inBody = tagBodyStarted(tag, pos) && (pos < bodyEnd || inUnclosedBodyAtEnd);
|
|
509
|
+
if (!inFilter && !inBody) return;
|
|
510
|
+
let element = null;
|
|
511
|
+
if (definitions.length === 1 && iterable) {
|
|
512
|
+
const path = walkBase(state, iterable);
|
|
513
|
+
const iterableType = path ? typeAtPath(ir, path, bindings) : null;
|
|
514
|
+
element = iterableType ? elementType(ir, iterableType) : null;
|
|
515
|
+
}
|
|
516
|
+
if (inBody) bindings.set("loop", LOOP_TYPE);
|
|
517
|
+
for (const definition of definitions) bindings.set(state.sliceDoc(definition.from, definition.to), element ?? { kind: "any" });
|
|
518
|
+
}
|
|
519
|
+
function collectWithBindings(state, withNode, pos, ir, bindings) {
|
|
520
|
+
const tag = withNode.firstChild;
|
|
521
|
+
if (!tag || tag.name !== "Tag") return;
|
|
522
|
+
const inBody = tagBodyStarted(tag, pos);
|
|
523
|
+
let targets = [];
|
|
524
|
+
for (let child = tag.firstChild; child; child = child.nextSibling) {
|
|
525
|
+
if (child.name === "Definition") {
|
|
526
|
+
targets.push(child);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (child.name !== "AssignOp" || targets.length === 0) continue;
|
|
530
|
+
const value = child.nextSibling;
|
|
531
|
+
const delimiter = value?.nextSibling;
|
|
532
|
+
if (inBody || delimiter?.name === "," && delimiter.to <= pos) {
|
|
533
|
+
const path = targets.length === 1 && value ? walkBase(state, value) : null;
|
|
534
|
+
const type = path ? typeAtPath(ir, path, bindings) : null;
|
|
535
|
+
for (const target of targets) bindings.set(state.sliceDoc(target.from, target.to), type ?? { kind: "any" });
|
|
536
|
+
}
|
|
537
|
+
targets = [];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function collectMacroBindings(state, macroNode, pos, bindings) {
|
|
541
|
+
const tag = macroNode.firstChild;
|
|
542
|
+
if (!tag || !tagBodyStarted(tag, pos)) return;
|
|
543
|
+
const paramList = tag.getChild("ParamList");
|
|
544
|
+
if (!paramList) return;
|
|
545
|
+
for (let child = paramList.firstChild; child; child = child.nextSibling) {
|
|
546
|
+
const definition = child.name === "Definition" ? child : child.name === "OptionalParameter" ? child.getChild("Definition") : null;
|
|
547
|
+
if (definition) bindings.set(state.sliceDoc(definition.from, definition.to), { kind: "any" });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function tagBodyStarted(tag, pos) {
|
|
551
|
+
return tag.name === "Tag" && tag.getChild("%}") !== null && pos >= tag.to;
|
|
552
|
+
}
|
|
553
|
+
function collectSetBindings(state, pos, scopes) {
|
|
554
|
+
const scopeStarts = new Set(scopes.map((scope) => scope.from));
|
|
555
|
+
const caretBranches = /* @__PURE__ */ new Map();
|
|
556
|
+
const sets = /* @__PURE__ */ new Map();
|
|
557
|
+
for (let node = syntaxTree(state).resolveInner(pos, -1); node; node = node.parent) if (node.name === "IfStatement" || node.name === "ForStatement") caretBranches.set(node.from, branchAt(state, node, pos));
|
|
558
|
+
syntaxTree(state).iterate({
|
|
559
|
+
to: pos,
|
|
560
|
+
enter: (node) => {
|
|
561
|
+
if (node.name !== "Tag" || node.to > pos) return;
|
|
562
|
+
const keyword = node.node.firstChild?.nextSibling;
|
|
563
|
+
if (!keyword || state.sliceDoc(keyword.from, keyword.to) !== "set") return;
|
|
564
|
+
const definitions = [];
|
|
565
|
+
let assignment = false;
|
|
566
|
+
for (let child = keyword.nextSibling; child; child = child.nextSibling) {
|
|
567
|
+
if (child.name === "AssignOp") {
|
|
568
|
+
assignment = true;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
if (child.name === "Definition" && !state.sliceDoc(child.from, child.to).includes(".")) definitions.push(child);
|
|
572
|
+
}
|
|
573
|
+
if (definitions.length === 0) return;
|
|
574
|
+
const statement = node.node.parent;
|
|
575
|
+
if (assignment) {
|
|
576
|
+
if (!node.node.getChild("%}")) return;
|
|
577
|
+
} else if (statement?.name !== "SetStatement" || statement.to > pos || !statement.getChild("EndTag")) return;
|
|
578
|
+
let scopeKey = ROOT_SCOPE;
|
|
579
|
+
for (let { parent } = node.node; parent; parent = parent.parent) {
|
|
580
|
+
if (parent.name !== "IfStatement" && parent.name !== "ForStatement") continue;
|
|
581
|
+
const caretBranch = caretBranches.get(parent.from);
|
|
582
|
+
if (caretBranch !== void 0 && branchAt(state, parent, node.from) !== caretBranch) return;
|
|
583
|
+
}
|
|
584
|
+
for (let { parent } = node.node; parent; parent = parent.parent) if (parent.name === "ForStatement" || parent.name === "MacroStatement" || parent.name === "WithStatement" || parent.name === "BlockStatement") {
|
|
585
|
+
if (!scopeStarts.has(parent.from)) return;
|
|
586
|
+
scopeKey = parent.from;
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
const bucket = sets.get(scopeKey);
|
|
590
|
+
if (bucket) bucket.push(...definitions);
|
|
591
|
+
else sets.set(scopeKey, definitions);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
return sets;
|
|
595
|
+
}
|
|
596
|
+
function branchAt(state, statement, pos) {
|
|
597
|
+
let branch = 0;
|
|
598
|
+
for (let child = statement.firstChild; child && child.from <= pos; child = child.nextSibling) {
|
|
599
|
+
if (child.name !== "Tag") continue;
|
|
600
|
+
const keyword = child.firstChild?.nextSibling;
|
|
601
|
+
if (!keyword) continue;
|
|
602
|
+
const name = state.sliceDoc(keyword.from, keyword.to);
|
|
603
|
+
if (name === "else" || statement.name === "IfStatement" && name === "elif") branch += 1;
|
|
604
|
+
}
|
|
605
|
+
return branch;
|
|
606
|
+
}
|
|
607
|
+
function jinjaContext(state, pos, side) {
|
|
608
|
+
for (let node = syntaxTree(state).resolveInner(pos, side); node; node = node.parent) {
|
|
609
|
+
const { name } = node;
|
|
610
|
+
if (name === "Comment") return "comment";
|
|
611
|
+
if (name === "StringLiteral" || name === "NumberLiteral") return null;
|
|
612
|
+
if (name === "Interpolation" || name === "Tag" || name === "EndTag") return "expr";
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
function minijinjaCompletion(context) {
|
|
617
|
+
const word = context.matchBefore(/\w*/);
|
|
618
|
+
if (!word) return null;
|
|
619
|
+
const { from } = word;
|
|
620
|
+
const before = context.state.sliceDoc(Math.max(0, from - 24), from);
|
|
621
|
+
let side = from === context.pos ? 1 : -1;
|
|
622
|
+
if (side === 1 && context.pos === context.state.doc.length) {
|
|
623
|
+
const left = syntaxTree(context.state).resolveInner(context.pos, -1);
|
|
624
|
+
if (left.name !== "}}" && left.name !== "%}" && left.name !== "#}") side = -1;
|
|
625
|
+
}
|
|
626
|
+
if (jinjaContext(context.state, context.pos, side) !== "expr") return null;
|
|
627
|
+
if (/\{%[-+]?\s*$/.test(before)) return {
|
|
628
|
+
from,
|
|
629
|
+
options: tagCompletions,
|
|
630
|
+
validFor: /^\w*$/
|
|
631
|
+
};
|
|
632
|
+
if (/\|\s*$/.test(before)) return {
|
|
633
|
+
from,
|
|
634
|
+
options: filterCompletions,
|
|
635
|
+
validFor: /^\w*$/
|
|
636
|
+
};
|
|
637
|
+
if (/\bis\s+(?:not\s+)?$/.test(before)) return {
|
|
638
|
+
from,
|
|
639
|
+
options: testCompletions,
|
|
640
|
+
validFor: /^\w*$/
|
|
641
|
+
};
|
|
642
|
+
const ir = context.state.field(minijinjaContextField, false);
|
|
643
|
+
if (ir) {
|
|
644
|
+
const target = contextTarget(context.state, context.pos);
|
|
645
|
+
if (!target) return null;
|
|
646
|
+
const scope = scopeBindings(context.state, context.pos, ir);
|
|
647
|
+
const members = resolveMembers(ir, target.path, scope);
|
|
648
|
+
if (target.path.length > 0) return members.length > 0 ? {
|
|
649
|
+
from: target.from,
|
|
650
|
+
options: memberCompletions(members, "property"),
|
|
651
|
+
validFor: /^\w*$/
|
|
652
|
+
} : null;
|
|
653
|
+
if (target.from === context.pos && !context.explicit) return null;
|
|
654
|
+
const memberOptions = memberCompletions(members, "variable");
|
|
655
|
+
const memberNames = new Set(memberOptions.map((option) => option.label));
|
|
656
|
+
return {
|
|
657
|
+
from: target.from,
|
|
658
|
+
options: [...memberOptions, ...functionCompletions.filter((option) => !memberNames.has(option.label))],
|
|
659
|
+
validFor: /^\w*$/
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (from === context.pos && !context.explicit) return null;
|
|
663
|
+
return {
|
|
664
|
+
from,
|
|
665
|
+
options: functionCompletions,
|
|
666
|
+
validFor: /^\w*$/
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function minijinjaDiagnostics(state) {
|
|
670
|
+
const diagnostics = [];
|
|
671
|
+
const { doc } = state;
|
|
672
|
+
syntaxTree(state).iterate({ enter: (node) => {
|
|
673
|
+
if (node.name !== "Tag" && node.name !== "EndTag") return;
|
|
674
|
+
const keywordNode = node.node.firstChild?.nextSibling;
|
|
675
|
+
if (!keywordNode || keywordNode.type.isError) return;
|
|
676
|
+
const keyword = doc.sliceString(keywordNode.from, keywordNode.to);
|
|
677
|
+
if (!MINIJINJA_TAGS.has(keyword)) diagnostics.push({
|
|
678
|
+
from: keywordNode.from,
|
|
679
|
+
to: keywordNode.to,
|
|
680
|
+
severity: "error",
|
|
681
|
+
message: `"${keyword}" is not a MiniJinja tag.`
|
|
682
|
+
});
|
|
683
|
+
} });
|
|
684
|
+
return diagnostics;
|
|
685
|
+
}
|
|
686
|
+
const minijinjaLinter = linter((view) => minijinjaDiagnostics(view.state));
|
|
687
|
+
function minijinja(config = {}) {
|
|
688
|
+
const { base, context } = config;
|
|
689
|
+
const language = withMinijinjaSyntax(base ? jinja({ base }).language : jinjaLanguage);
|
|
690
|
+
const contextField = context ? minijinjaContextField.init(() => normalizeContext(context)) : minijinjaContextField;
|
|
691
|
+
const support = [
|
|
692
|
+
language.data.of({ autocomplete: minijinjaCompletion }),
|
|
693
|
+
minijinjaLinter,
|
|
694
|
+
contextField
|
|
695
|
+
];
|
|
696
|
+
if (base) support.push(base.support);
|
|
697
|
+
return new LanguageSupport(language, support);
|
|
698
|
+
}
|
|
699
|
+
//#endregion
|
|
700
|
+
export { minijinja, minijinjaCompletion, minijinjaDiagnostics };
|