@mrhenry/twig-parser 0.1.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 +24 -0
- package/package.json +13 -0
- package/src/array-expression.js +69 -0
- package/src/callables.js +127 -0
- package/src/expression-parser.js +1103 -0
- package/src/index.js +19 -0
- package/src/node.js +474 -0
- package/src/parser.js +1776 -0
- package/src/printer.js +752 -0
package/src/printer.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* The Twig source printer used by {@link Node#serialize}.
|
|
4
|
+
*
|
|
5
|
+
* Renders a node subtree back to Twig source from its current contents. Only
|
|
6
|
+
* nodes whose subtree changed since parsing are printed; unchanged descendants
|
|
7
|
+
* still return their retained source text (see `Node#_serialize`).
|
|
8
|
+
*
|
|
9
|
+
* @module twig-parser
|
|
10
|
+
*/
|
|
11
|
+
import { Node, NodeType } from './node.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {unknown} value
|
|
15
|
+
* @returns {string} A single-quoted Twig string literal.
|
|
16
|
+
*/
|
|
17
|
+
function quoteString(value) {
|
|
18
|
+
const text = String(value);
|
|
19
|
+
return `'${text.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {unknown} value
|
|
24
|
+
* @returns {string} A number as it appears in Twig source.
|
|
25
|
+
*/
|
|
26
|
+
function formatNumber(value) {
|
|
27
|
+
const numeric = Number(value);
|
|
28
|
+
if (Number.isNaN(numeric)) {
|
|
29
|
+
return '0';
|
|
30
|
+
}
|
|
31
|
+
return String(numeric);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Serializes a node, reusing retained source where possible.
|
|
36
|
+
*
|
|
37
|
+
* @param {Node} node
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function serialize(node) {
|
|
41
|
+
return node._serialize();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Serializes the content of a body child.
|
|
46
|
+
*
|
|
47
|
+
* @param {Node|null|undefined} node
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function printBody(node) {
|
|
51
|
+
if (!node) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
return serialize(node);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Prints a call argument list (`arguments` is an array of `{name, value}`).
|
|
59
|
+
*
|
|
60
|
+
* @param {Array<{name: string|null, value: Node}>|null|undefined} args
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function printArguments(args) {
|
|
64
|
+
if (!args || !args.length) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
const parts = args.map((arg) => (arg.name === null || arg.name === undefined
|
|
68
|
+
? serialize(arg.value)
|
|
69
|
+
: `${arg.name}: ${serialize(arg.value)}`));
|
|
70
|
+
return `(${parts.join(', ')})`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Prints the argument list held by an `array` node (used for method/macro
|
|
75
|
+
* calls, where named arguments are preserved as constant string keys).
|
|
76
|
+
*
|
|
77
|
+
* @param {Node|null|undefined} array
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function printArrayArguments(array) {
|
|
81
|
+
if (!array || array.type !== NodeType.ArrayExpr) {
|
|
82
|
+
return '()';
|
|
83
|
+
}
|
|
84
|
+
const pairs = /** @type {Array<[Node, Node]>} */ (
|
|
85
|
+
/** @type {any} */ (array).getKeyValuePairs()
|
|
86
|
+
);
|
|
87
|
+
const parts = pairs.map(([key, value]) => {
|
|
88
|
+
if (
|
|
89
|
+
key.type === NodeType.Constant &&
|
|
90
|
+
typeof key.getAttribute('value') === 'string' &&
|
|
91
|
+
!/^[0-9]+$/.test(String(key.getAttribute('value')))
|
|
92
|
+
) {
|
|
93
|
+
return `${key.getAttribute('value')}: ${serialize(value)}`;
|
|
94
|
+
}
|
|
95
|
+
return serialize(value);
|
|
96
|
+
});
|
|
97
|
+
return `(${parts.join(', ')})`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Prints an expression node.
|
|
102
|
+
*
|
|
103
|
+
* @param {Node} node
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
function printExpression(node) {
|
|
107
|
+
switch (node.type) {
|
|
108
|
+
case NodeType.Constant:
|
|
109
|
+
return printConstant(node.getAttribute('value'));
|
|
110
|
+
case NodeType.ContextVariable:
|
|
111
|
+
case NodeType.AssignContextVariable:
|
|
112
|
+
return String(node.getAttribute('name') ?? '');
|
|
113
|
+
case NodeType.LocalVariable: {
|
|
114
|
+
const name = node.getAttribute('name');
|
|
115
|
+
return name === null || name === undefined ? '' : String(name);
|
|
116
|
+
}
|
|
117
|
+
case NodeType.ArrayExpr:
|
|
118
|
+
return printArray(node);
|
|
119
|
+
case NodeType.ListExpr: {
|
|
120
|
+
const names = /** @type {Node[]} */ (node.getAttribute('names') ?? []);
|
|
121
|
+
return `(${names.map((name) => printExpression(name)).join(', ')})`;
|
|
122
|
+
}
|
|
123
|
+
case NodeType.GetAttr:
|
|
124
|
+
return printGetAttr(node);
|
|
125
|
+
case NodeType.MacroReference:
|
|
126
|
+
return printMacroReference(node);
|
|
127
|
+
case NodeType.Filter:
|
|
128
|
+
return printFilter(node);
|
|
129
|
+
case NodeType.FunctionCall: {
|
|
130
|
+
const name = String(node.getAttribute('name') ?? '');
|
|
131
|
+
return `${name}${printArguments(/** @type {any} */ (node.children.arguments))}`;
|
|
132
|
+
}
|
|
133
|
+
case NodeType.Test: {
|
|
134
|
+
const name = String(node.getAttribute('name') ?? '');
|
|
135
|
+
const args = /** @type {Array<{name: string|null, value: Node}>|null} */ (
|
|
136
|
+
node.children.arguments ?? null
|
|
137
|
+
);
|
|
138
|
+
return `${serialize(/** @type {Node} */ (node.getNode('node')))} is ${name}${printArguments(args)}`;
|
|
139
|
+
}
|
|
140
|
+
case NodeType.ArrowFunction: {
|
|
141
|
+
const args = node.getNode('arguments');
|
|
142
|
+
const params = args && args.type === NodeType.ListExpr
|
|
143
|
+
? printExpression(args)
|
|
144
|
+
: `(${args ? printExpression(args) : ''})`;
|
|
145
|
+
return `${params} => ${serialize(/** @type {Node} */ (node.getNode('body')))}`;
|
|
146
|
+
}
|
|
147
|
+
case NodeType.Binary: {
|
|
148
|
+
const operator = String(node.getAttribute('operator') ?? '');
|
|
149
|
+
return `${serialize(/** @type {Node} */ (node.getNode('left')))} ${operator} ${serialize(
|
|
150
|
+
/** @type {Node} */ (node.getNode('right')),
|
|
151
|
+
)}`;
|
|
152
|
+
}
|
|
153
|
+
case NodeType.Unary: {
|
|
154
|
+
const operator = String(node.getAttribute('operator') ?? '');
|
|
155
|
+
const operand = serialize(/** @type {Node} */ (node.getNode('node')));
|
|
156
|
+
return operator === 'not' ? `not ${operand}` : `${operator}${operand}`;
|
|
157
|
+
}
|
|
158
|
+
case NodeType.SetBinary:
|
|
159
|
+
return `${serialize(/** @type {Node} */ (node.getNode('left')))} = ${serialize(
|
|
160
|
+
/** @type {Node} */ (node.getNode('right')),
|
|
161
|
+
)}`;
|
|
162
|
+
case NodeType.SequenceDestructuringSet:
|
|
163
|
+
return `${printDestructuringTarget(/** @type {Node} */ (node.getNode('left')))} = ${serialize(
|
|
164
|
+
/** @type {Node} */ (node.getNode('right')),
|
|
165
|
+
)}`;
|
|
166
|
+
case NodeType.ObjectDestructuringSet:
|
|
167
|
+
return `${printDestructuringTarget(/** @type {Node} */ (node.getNode('left')))} = ${serialize(
|
|
168
|
+
/** @type {Node} */ (node.getNode('right')),
|
|
169
|
+
)}`;
|
|
170
|
+
case NodeType.Conditional: {
|
|
171
|
+
const cond = serialize(/** @type {Node} */ (node.getNode('cond')));
|
|
172
|
+
const then = serialize(/** @type {Node} */ (node.getNode('then')));
|
|
173
|
+
const elseNode = node.getNode('else');
|
|
174
|
+
if (
|
|
175
|
+
elseNode &&
|
|
176
|
+
elseNode.type === NodeType.Constant &&
|
|
177
|
+
elseNode.getAttribute('value') === ''
|
|
178
|
+
) {
|
|
179
|
+
return `${cond} ? ${then}`;
|
|
180
|
+
}
|
|
181
|
+
return `${cond} ? ${then} : ${elseNode ? serialize(elseNode) : ''}`;
|
|
182
|
+
}
|
|
183
|
+
case NodeType.Empty:
|
|
184
|
+
return '';
|
|
185
|
+
default:
|
|
186
|
+
return printNode(node);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {unknown} value
|
|
192
|
+
* @returns {string}
|
|
193
|
+
*/
|
|
194
|
+
function printConstant(value) {
|
|
195
|
+
if (typeof value === 'string') {
|
|
196
|
+
return quoteString(value);
|
|
197
|
+
}
|
|
198
|
+
if (typeof value === 'number') {
|
|
199
|
+
return formatNumber(value);
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === 'boolean') {
|
|
202
|
+
return value ? 'true' : 'false';
|
|
203
|
+
}
|
|
204
|
+
if (value === null || value === undefined) {
|
|
205
|
+
return 'null';
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
return `[${value.map((item) => printConstant(item)).join(', ')}]`;
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === 'object') {
|
|
211
|
+
const object = /** @type {{name?: unknown, alias?: unknown}} */ (value);
|
|
212
|
+
if ('name' in object) {
|
|
213
|
+
const alias = object.alias;
|
|
214
|
+
if (alias !== undefined && alias !== null && alias !== object.name) {
|
|
215
|
+
return `${String(object.name)} as ${String(alias)}`;
|
|
216
|
+
}
|
|
217
|
+
return String(object.name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return String(value);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {Node} node
|
|
225
|
+
* @returns {string}
|
|
226
|
+
*/
|
|
227
|
+
function printArray(node) {
|
|
228
|
+
const array = /** @type {any} */ (node);
|
|
229
|
+
const pairs = /** @type {Array<[Node, Node]>} */ (array.getKeyValuePairs());
|
|
230
|
+
const sequence = typeof array.isSequence === 'function' ? array.isSequence() : false;
|
|
231
|
+
if (sequence) {
|
|
232
|
+
return `[${pairs.map(([, value]) => serialize(value)).join(', ')}]`;
|
|
233
|
+
}
|
|
234
|
+
return `{${pairs
|
|
235
|
+
.map(([key, value]) => `${serialize(key)}: ${serialize(value)}`)
|
|
236
|
+
.join(', ')}}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Prints a `.` attribute (a bare name or number), not a quoted string.
|
|
241
|
+
*
|
|
242
|
+
* @param {Node} node
|
|
243
|
+
* @returns {string}
|
|
244
|
+
*/
|
|
245
|
+
function printDotAttribute(node) {
|
|
246
|
+
if (node.type === NodeType.Constant) {
|
|
247
|
+
const value = node.getAttribute('value');
|
|
248
|
+
if (typeof value === 'string' && /^[a-zA-Z_\u007f-\uffff][a-zA-Z0-9_\u007f-\uffff]*$/.test(value)) {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
if (typeof value === 'number') {
|
|
252
|
+
return formatNumber(value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return serialize(node);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Prints the left-hand side of a destructuring assignment.
|
|
260
|
+
*
|
|
261
|
+
* @param {Node} node
|
|
262
|
+
* @returns {string}
|
|
263
|
+
*/
|
|
264
|
+
function printDestructuringTarget(node) {
|
|
265
|
+
if (node.type !== NodeType.ArrayExpr) {
|
|
266
|
+
return printExpression(node);
|
|
267
|
+
}
|
|
268
|
+
const array = /** @type {any} */ (node);
|
|
269
|
+
const pairs = /** @type {Array<[Node, Node]>} */ (array.getKeyValuePairs());
|
|
270
|
+
if (typeof array.isSequence === 'function' && array.isSequence()) {
|
|
271
|
+
return `[${pairs.map(([, value]) => printExpression(value)).join(', ')}]`;
|
|
272
|
+
}
|
|
273
|
+
const parts = pairs.map(([key, value]) => {
|
|
274
|
+
const keyName =
|
|
275
|
+
key.type === NodeType.Constant && typeof key.getAttribute('value') === 'string'
|
|
276
|
+
? String(key.getAttribute('value'))
|
|
277
|
+
: null;
|
|
278
|
+
const valueName =
|
|
279
|
+
value && (value.type === NodeType.ContextVariable || value.type === NodeType.AssignContextVariable)
|
|
280
|
+
? String(value.getAttribute('name'))
|
|
281
|
+
: null;
|
|
282
|
+
if (keyName !== null && valueName === keyName) {
|
|
283
|
+
return keyName;
|
|
284
|
+
}
|
|
285
|
+
return `${printMappingKey(key)}: ${printExpression(value)}`;
|
|
286
|
+
});
|
|
287
|
+
return `{${parts.join(', ')}}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Prints a mapping key (bare when it is a valid name).
|
|
292
|
+
*
|
|
293
|
+
* @param {Node} key
|
|
294
|
+
* @returns {string}
|
|
295
|
+
*/
|
|
296
|
+
function printMappingKey(key) {
|
|
297
|
+
if (key.type === NodeType.Constant) {
|
|
298
|
+
const value = key.getAttribute('value');
|
|
299
|
+
if (typeof value === 'string' && /^[a-zA-Z_\u007f-\uffff][a-zA-Z0-9_\u007f-\uffff]*$/.test(value)) {
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return printExpression(key);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {Node} node
|
|
308
|
+
* @returns {string}
|
|
309
|
+
*/
|
|
310
|
+
function printGetAttr(node) {
|
|
311
|
+
const base = serialize(/** @type {Node} */ (node.getNode('node')));
|
|
312
|
+
const attributeNode = /** @type {Node} */ (node.getNode('attribute'));
|
|
313
|
+
const type = String(node.getAttribute('type') ?? 'any');
|
|
314
|
+
const nullSafe = Boolean(node.getAttribute('nullSafe'));
|
|
315
|
+
if (type === 'array') {
|
|
316
|
+
return `${base}[${serialize(attributeNode)}]`;
|
|
317
|
+
}
|
|
318
|
+
const attribute = printDotAttribute(attributeNode);
|
|
319
|
+
if (type === 'method') {
|
|
320
|
+
return `${base}${nullSafe ? '?.' : '.'}${attribute}${printArrayArguments(node.getNode('arguments'))}`;
|
|
321
|
+
}
|
|
322
|
+
return `${base}${nullSafe ? '?.' : '.'}${attribute}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {Node} node
|
|
327
|
+
* @returns {string}
|
|
328
|
+
*/
|
|
329
|
+
function printMacroReference(node) {
|
|
330
|
+
const target = node.getNode('var');
|
|
331
|
+
const name = node.getNode('name');
|
|
332
|
+
const prefix = target ? printExpression(target) : '';
|
|
333
|
+
const attribute = name ? printDotAttribute(name) : '';
|
|
334
|
+
const args = printArrayArguments(node.getNode('arguments'));
|
|
335
|
+
if (node.getAttribute('hasCallParentheses') === false) {
|
|
336
|
+
return `${prefix}.${attribute}`;
|
|
337
|
+
}
|
|
338
|
+
return `${prefix}.${attribute}${args}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {Node} node
|
|
343
|
+
* @returns {string}
|
|
344
|
+
*/
|
|
345
|
+
function printFilter(node) {
|
|
346
|
+
const base = serialize(/** @type {Node} */ (node.getNode('node')));
|
|
347
|
+
const name = String(node.getAttribute('name') ?? '');
|
|
348
|
+
const args = printArguments(/** @type {any} */ (node.children.arguments));
|
|
349
|
+
return `${base}|${name}${args}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Prints the filter chain of an `apply` tag (whose base is a captured
|
|
354
|
+
* reference that must not be printed).
|
|
355
|
+
*
|
|
356
|
+
* @param {Node} node
|
|
357
|
+
* @returns {string}
|
|
358
|
+
*/
|
|
359
|
+
function printApplyFilter(node) {
|
|
360
|
+
const parts = [];
|
|
361
|
+
/** @type {Node|null} */
|
|
362
|
+
let current = node;
|
|
363
|
+
while (current && current.type === NodeType.Filter) {
|
|
364
|
+
const name = String(current.getAttribute('name') ?? '');
|
|
365
|
+
const args = printArguments(/** @type {any} */ (current.children.arguments));
|
|
366
|
+
parts.unshift(`${name}${args}`);
|
|
367
|
+
current = /** @type {Node|null} */ (current.getNode('node'));
|
|
368
|
+
}
|
|
369
|
+
return parts.join('|');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @param {Node} node
|
|
374
|
+
* @returns {string}
|
|
375
|
+
*/
|
|
376
|
+
function printNodes(node) {
|
|
377
|
+
const children = node.getNodes('nodes');
|
|
378
|
+
return children.map((child) => serialize(child)).join('');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {Node} node
|
|
383
|
+
* @returns {string}
|
|
384
|
+
*/
|
|
385
|
+
function printIf(node) {
|
|
386
|
+
const tests = /** @type {Node[]} */ (node.children.tests ?? []);
|
|
387
|
+
const elseBody = node.getNode('elseBody');
|
|
388
|
+
let out = '{% if ';
|
|
389
|
+
for (let i = 0; i < tests.length; i += 2) {
|
|
390
|
+
const condition = serialize(tests[i]);
|
|
391
|
+
const body = tests[i + 1] ? serialize(tests[i + 1]) : '';
|
|
392
|
+
if (i === 0) {
|
|
393
|
+
out += `${condition} %}${body}`;
|
|
394
|
+
} else {
|
|
395
|
+
out += `{% elseif ${condition} %}${body}`;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (elseBody) {
|
|
399
|
+
out += `{% else %}${printBody(elseBody)}`;
|
|
400
|
+
}
|
|
401
|
+
return `${out}{% endif %}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @param {Node} node
|
|
406
|
+
* @returns {string}
|
|
407
|
+
*/
|
|
408
|
+
function printFor(node) {
|
|
409
|
+
const keyTarget = /** @type {Node|null} */ (node.getNode('keyTarget'));
|
|
410
|
+
const valueTarget = /** @type {Node|null} */ (node.getNode('valueTarget'));
|
|
411
|
+
const seq = serialize(/** @type {Node} */ (node.getNode('seq')));
|
|
412
|
+
const body = printBody(node.getNode('body'));
|
|
413
|
+
const elseBody = node.getNode('elseBody');
|
|
414
|
+
/** @type {string[]} */
|
|
415
|
+
const targets = [];
|
|
416
|
+
if (keyTarget && keyTarget.getAttribute('name') !== '_key') {
|
|
417
|
+
targets.push(serialize(keyTarget));
|
|
418
|
+
}
|
|
419
|
+
if (valueTarget) {
|
|
420
|
+
targets.push(serialize(valueTarget));
|
|
421
|
+
}
|
|
422
|
+
const elsePart = elseBody ? `{% else %}${printBody(elseBody)}` : '';
|
|
423
|
+
return `{% for ${targets.join(', ')} in ${seq} %}${body}${elsePart}{% endfor %}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @param {Node} node
|
|
428
|
+
* @returns {string}
|
|
429
|
+
*/
|
|
430
|
+
function printSet(node) {
|
|
431
|
+
const capture = Boolean(node.getAttribute('capture'));
|
|
432
|
+
const namesNode = node.getNode('names');
|
|
433
|
+
const names = namesNode ? namesNode.getNodes('nodes') : [];
|
|
434
|
+
if (capture) {
|
|
435
|
+
return `{% set ${names.map((name) => serialize(name)).join(', ')} %}${printBody(
|
|
436
|
+
node.getNode('body'),
|
|
437
|
+
)}{% endset %}`;
|
|
438
|
+
}
|
|
439
|
+
const valuesNode = node.getNode('values');
|
|
440
|
+
const values = valuesNode ? valuesNode.getNodes('nodes') : [];
|
|
441
|
+
return `{% set ${names.map((name) => serialize(name)).join(', ')} = ${values
|
|
442
|
+
.map((value) => serialize(value))
|
|
443
|
+
.join(', ')} %}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* @param {Node} node
|
|
448
|
+
* @returns {string}
|
|
449
|
+
*/
|
|
450
|
+
function printBlock(node) {
|
|
451
|
+
const name = String(node.getAttribute('name') ?? '');
|
|
452
|
+
return `{% block ${name} %}${printBody(node.getNode('body'))}{% endblock %}`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @param {Node} node
|
|
457
|
+
* @returns {string}
|
|
458
|
+
*/
|
|
459
|
+
function printMacro(node) {
|
|
460
|
+
const name = String(node.getAttribute('name') ?? '');
|
|
461
|
+
const variadicName = node.getAttribute('variadicName');
|
|
462
|
+
const args = /** @type {Array<[string, Node]>} */ (node.getAttribute('arguments') ?? []);
|
|
463
|
+
const parts = args.map(([argumentName, defaultNode]) => {
|
|
464
|
+
if (defaultNode && defaultNode.getAttribute('is_implicit')) {
|
|
465
|
+
return argumentName;
|
|
466
|
+
}
|
|
467
|
+
return `${argumentName} = ${serialize(defaultNode)}`;
|
|
468
|
+
});
|
|
469
|
+
if (variadicName) {
|
|
470
|
+
parts.push(`...${variadicName}`);
|
|
471
|
+
}
|
|
472
|
+
return `{% macro ${name}(${parts.join(', ')}) %}${printBody(
|
|
473
|
+
node.getNode('body'),
|
|
474
|
+
)}{% endmacro %}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @param {Node} node
|
|
479
|
+
* @returns {string}
|
|
480
|
+
*/
|
|
481
|
+
function printInclude(node) {
|
|
482
|
+
const expr = serialize(/** @type {Node} */ (node.getNode('expr')));
|
|
483
|
+
const ignoreMissing = node.getNode('ignoreMissing')?.getAttribute('value');
|
|
484
|
+
const variables = node.getNode('variables');
|
|
485
|
+
const only = node.getNode('only')?.getAttribute('value');
|
|
486
|
+
let out = `{% include ${expr}`;
|
|
487
|
+
if (ignoreMissing) {
|
|
488
|
+
out += ' ignore missing';
|
|
489
|
+
}
|
|
490
|
+
if (variables) {
|
|
491
|
+
out += ` with ${serialize(variables)}`;
|
|
492
|
+
}
|
|
493
|
+
if (only) {
|
|
494
|
+
out += ' only';
|
|
495
|
+
}
|
|
496
|
+
return `${out} %}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @param {Node} node
|
|
501
|
+
* @returns {string}
|
|
502
|
+
*/
|
|
503
|
+
function printImport(node) {
|
|
504
|
+
const expr = serialize(/** @type {Node} */ (node.getNode('expr')));
|
|
505
|
+
const targets = node.getNode('targets');
|
|
506
|
+
if (targets) {
|
|
507
|
+
const parts = targets.getNodes('nodes').map((target) => printConstant(target.getAttribute('value')));
|
|
508
|
+
return `{% from ${expr} import ${parts.join(', ')} %}`;
|
|
509
|
+
}
|
|
510
|
+
const variable = node.getNode('var');
|
|
511
|
+
const name = variable ? String(variable.getAttribute('name') ?? '') : '';
|
|
512
|
+
return `{% import ${expr} as ${name} %}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @param {Node} node
|
|
517
|
+
* @returns {string}
|
|
518
|
+
*/
|
|
519
|
+
function printUse(node) {
|
|
520
|
+
const template = serialize(/** @type {Node} */ (node.getNode('template')));
|
|
521
|
+
const targets = node.getNode('targets');
|
|
522
|
+
const parts = targets
|
|
523
|
+
? targets.getNodes('nodes').map((target) => printConstant(target.getAttribute('value')))
|
|
524
|
+
: [];
|
|
525
|
+
const withPart = parts.length ? ` with ${parts.join(', ')}` : '';
|
|
526
|
+
return `{% use ${template}${withPart} %}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* @param {Node} node
|
|
531
|
+
* @returns {string}
|
|
532
|
+
*/
|
|
533
|
+
function printWith(node) {
|
|
534
|
+
const variables = node.getNode('variables');
|
|
535
|
+
const only = Boolean(node.getAttribute('only'));
|
|
536
|
+
let out = '{% with';
|
|
537
|
+
if (variables) {
|
|
538
|
+
out += ` ${serialize(variables)}`;
|
|
539
|
+
}
|
|
540
|
+
if (only) {
|
|
541
|
+
out += ' only';
|
|
542
|
+
}
|
|
543
|
+
return `${out} %}${printBody(node.getNode('body'))}{% endwith %}`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* @param {Node} node
|
|
548
|
+
* @returns {string}
|
|
549
|
+
*/
|
|
550
|
+
function printAutoEscape(node) {
|
|
551
|
+
const value = node.getAttribute('value');
|
|
552
|
+
const strategy = value === undefined || value === null ? '' : ` ${printConstant(value)}`;
|
|
553
|
+
return `{% autoescape${strategy} %}${printBody(node.getNode('body'))}{% endautoescape %}`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* @param {Node} node
|
|
558
|
+
* @returns {string}
|
|
559
|
+
*/
|
|
560
|
+
function printDeprecated(node) {
|
|
561
|
+
let out = `{% deprecated ${serialize(/** @type {Node} */ (node.getNode('expr')))}`;
|
|
562
|
+
const packageNode = node.getNode('package');
|
|
563
|
+
const versionNode = node.getNode('version');
|
|
564
|
+
if (packageNode) {
|
|
565
|
+
out += ` package=${serialize(packageNode)}`;
|
|
566
|
+
}
|
|
567
|
+
if (versionNode) {
|
|
568
|
+
out += ` version=${serialize(versionNode)}`;
|
|
569
|
+
}
|
|
570
|
+
return `${out} %}`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* @param {Node} node
|
|
575
|
+
* @returns {string}
|
|
576
|
+
*/
|
|
577
|
+
function printTypes(node) {
|
|
578
|
+
const types = /** @type {Array<{name: string, type: string, optional: boolean}>} */ (
|
|
579
|
+
node.getAttribute('types') ?? []
|
|
580
|
+
);
|
|
581
|
+
const parts = types.map(
|
|
582
|
+
(type) => `${type.name}${type.optional ? '?' : ''}: ${quoteString(type.type)}`,
|
|
583
|
+
);
|
|
584
|
+
const body = node.getAttribute('enclosed') ? `{${parts.join(', ')}}` : parts.join(', ');
|
|
585
|
+
return `{% types ${body} %}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @param {Node} node
|
|
590
|
+
* @returns {string}
|
|
591
|
+
*/
|
|
592
|
+
function printApply(node) {
|
|
593
|
+
const children = node.getNodes('nodes');
|
|
594
|
+
if (children.length !== 2) {
|
|
595
|
+
return printNodes(node);
|
|
596
|
+
}
|
|
597
|
+
const setNode = children[0];
|
|
598
|
+
const printNode2 = children[1];
|
|
599
|
+
const filter = printNode2.getNode('expr');
|
|
600
|
+
if (!filter) {
|
|
601
|
+
return printNodes(node);
|
|
602
|
+
}
|
|
603
|
+
return `{% apply ${printApplyFilter(filter)} %}${printBody(
|
|
604
|
+
setNode.getNode('body'),
|
|
605
|
+
)}{% endapply %}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* @param {Node} node
|
|
610
|
+
* @returns {string}
|
|
611
|
+
*/
|
|
612
|
+
function printGuard(node) {
|
|
613
|
+
const guardType = String(node.getAttribute('guardType') ?? '');
|
|
614
|
+
const guardName = String(node.getAttribute('guardName') ?? '');
|
|
615
|
+
const exists = Boolean(node.getAttribute('guardExists'));
|
|
616
|
+
const body = node.getNodes('nodes')[0];
|
|
617
|
+
const elsePart = exists ? '' : '{% else %}';
|
|
618
|
+
return `{% guard ${guardType} ${guardName} %}${elsePart}${printBody(body)}{% endguard %}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* @param {Node} node
|
|
623
|
+
* @returns {string}
|
|
624
|
+
*/
|
|
625
|
+
function printEmbed(node) {
|
|
626
|
+
const exprNode = /** @type {Node|null} */ (node.getAttribute('parent') ?? node.getNode('expr'));
|
|
627
|
+
const expr = exprNode ? serialize(exprNode) : '';
|
|
628
|
+
const ignoreMissing = node.getNode('ignoreMissing')?.getAttribute('value');
|
|
629
|
+
const variables = node.getNode('variables');
|
|
630
|
+
const only = node.getNode('only')?.getAttribute('value');
|
|
631
|
+
let out = `{% embed ${expr}`;
|
|
632
|
+
if (ignoreMissing) {
|
|
633
|
+
out += ' ignore missing';
|
|
634
|
+
}
|
|
635
|
+
if (variables) {
|
|
636
|
+
out += ` with ${serialize(variables)}`;
|
|
637
|
+
}
|
|
638
|
+
if (only) {
|
|
639
|
+
out += ' only';
|
|
640
|
+
}
|
|
641
|
+
const embedded = /** @type {Node|null} */ (node.getAttribute('embedded') ?? null);
|
|
642
|
+
const body = embedded ? printEmbeddedBody(embedded) : '';
|
|
643
|
+
return `${out} %}${body}{% endembed %}`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Prints the body of an embedded template: its block definitions in order
|
|
648
|
+
* (the injected `extends` empties the inline body).
|
|
649
|
+
*
|
|
650
|
+
* @param {Node} module
|
|
651
|
+
* @returns {string}
|
|
652
|
+
*/
|
|
653
|
+
function printEmbeddedBody(module) {
|
|
654
|
+
const blocks = module.children.blocks;
|
|
655
|
+
if (blocks && typeof blocks === 'object' && !Array.isArray(blocks) && !(blocks instanceof Node)) {
|
|
656
|
+
const definitions = Object.values(/** @type {Record<string, Node>} */ (blocks));
|
|
657
|
+
if (definitions.length) {
|
|
658
|
+
return definitions.map((block) => printBlock(block)).join('');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return printBody(module.getNode('body'));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Prints a node subtree as Twig source.
|
|
666
|
+
*
|
|
667
|
+
* @param {Node} node
|
|
668
|
+
* @returns {string}
|
|
669
|
+
*/
|
|
670
|
+
export function printNode(node) {
|
|
671
|
+
// `NodeType.Set` and `NodeType.SetBinary` share the value "set" (the
|
|
672
|
+
// reference implementation uses one string for both), so disambiguate by
|
|
673
|
+
// shape before switching.
|
|
674
|
+
if (node.type === NodeType.Set && node.children.left !== undefined && node.children.names === undefined) {
|
|
675
|
+
return printExpression(node);
|
|
676
|
+
}
|
|
677
|
+
switch (node.type) {
|
|
678
|
+
case NodeType.Module: {
|
|
679
|
+
const body = node.getNode('body');
|
|
680
|
+
const trailing = String(node.getAttribute('trailing') ?? '');
|
|
681
|
+
return `${body ? serialize(body) : ''}${trailing}`;
|
|
682
|
+
}
|
|
683
|
+
case NodeType.Body:
|
|
684
|
+
return printNodes(node);
|
|
685
|
+
case NodeType.Nodes: {
|
|
686
|
+
if (node.getAttribute('desugared') === 'apply') {
|
|
687
|
+
return printApply(node);
|
|
688
|
+
}
|
|
689
|
+
if (node.getAttribute('guardType') !== undefined) {
|
|
690
|
+
return printGuard(node);
|
|
691
|
+
}
|
|
692
|
+
return printNodes(node);
|
|
693
|
+
}
|
|
694
|
+
case NodeType.Text:
|
|
695
|
+
return String(node.getAttribute('data') ?? '');
|
|
696
|
+
case NodeType.Print:
|
|
697
|
+
return `{{ ${serialize(/** @type {Node} */ (node.getNode('expr')))} }}`;
|
|
698
|
+
case NodeType.Set:
|
|
699
|
+
return printSet(node);
|
|
700
|
+
case NodeType.For:
|
|
701
|
+
return printFor(node);
|
|
702
|
+
case NodeType.If:
|
|
703
|
+
return printIf(node);
|
|
704
|
+
case NodeType.Block:
|
|
705
|
+
return printBlock(node);
|
|
706
|
+
case NodeType.BlockReference: {
|
|
707
|
+
const block = /** @type {Node|null} */ (node.getAttribute('block') ?? null);
|
|
708
|
+
return block ? printBlock(block) : '';
|
|
709
|
+
}
|
|
710
|
+
case NodeType.Macro:
|
|
711
|
+
return printMacro(node);
|
|
712
|
+
case NodeType.MacroDeclaration: {
|
|
713
|
+
const macro = /** @type {Node|null} */ (node.getAttribute('macro') ?? null);
|
|
714
|
+
return macro ? printMacro(macro) : '';
|
|
715
|
+
}
|
|
716
|
+
case NodeType.Extends: {
|
|
717
|
+
const parent = /** @type {Node|null} */ (node.getAttribute('parent') ?? null);
|
|
718
|
+
return `{% extends ${parent ? serialize(parent) : ''} %}`;
|
|
719
|
+
}
|
|
720
|
+
case NodeType.Include:
|
|
721
|
+
return printInclude(node);
|
|
722
|
+
case NodeType.Import:
|
|
723
|
+
return printImport(node);
|
|
724
|
+
case NodeType.Use:
|
|
725
|
+
return printUse(node);
|
|
726
|
+
case NodeType.With:
|
|
727
|
+
return printWith(node);
|
|
728
|
+
case NodeType.Do:
|
|
729
|
+
return `{% do ${serialize(/** @type {Node} */ (node.getNode('expr')))} %}`;
|
|
730
|
+
case NodeType.AutoEscape:
|
|
731
|
+
return printAutoEscape(node);
|
|
732
|
+
case NodeType.Sandbox:
|
|
733
|
+
return `{% sandbox %}${printBody(node.getNode('body'))}{% endsandbox %}`;
|
|
734
|
+
case NodeType.Deprecated:
|
|
735
|
+
return printDeprecated(node);
|
|
736
|
+
case NodeType.Flush:
|
|
737
|
+
return '{% flush %}';
|
|
738
|
+
case NodeType.Types:
|
|
739
|
+
return printTypes(node);
|
|
740
|
+
case NodeType.Embed:
|
|
741
|
+
return printEmbed(node);
|
|
742
|
+
case NodeType.Guard:
|
|
743
|
+
return printGuard(node);
|
|
744
|
+
case NodeType.Empty:
|
|
745
|
+
return '';
|
|
746
|
+
default:
|
|
747
|
+
if (node.raw !== null) {
|
|
748
|
+
return node.raw;
|
|
749
|
+
}
|
|
750
|
+
return printExpression(node);
|
|
751
|
+
}
|
|
752
|
+
}
|