@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/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
+ }