@mrhenry/prettier-twig 0.1.0 → 0.1.2
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/package.json +1 -1
- package/src/printer.js +150 -23
- package/test/prettier-twig.test.js +311 -0
package/package.json
CHANGED
package/src/printer.js
CHANGED
|
@@ -70,6 +70,13 @@ function printChildren(path, printCallback, source, leadingHardline = true, opti
|
|
|
70
70
|
docs.push(childDoc);
|
|
71
71
|
} else if (blankBefore) {
|
|
72
72
|
docs.push([hardline, hardline, childDoc]);
|
|
73
|
+
} else if (
|
|
74
|
+
first &&
|
|
75
|
+
!gap.includes('\n') &&
|
|
76
|
+
(child.type === 'comment' || child.type === 'twigComment')
|
|
77
|
+
) {
|
|
78
|
+
// a comment written right after the opening tag stays there
|
|
79
|
+
docs.push(gap === '' ? childDoc : [' ', childDoc]);
|
|
73
80
|
} else if (!gap.includes('\n') && previousWasInline && inline) {
|
|
74
81
|
// consecutive inline expressions on one source line stay together
|
|
75
82
|
docs.push([gap === '' ? '' : ' ', childDoc]);
|
|
@@ -109,9 +116,9 @@ function danglingEndIndent(raw) {
|
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
|
-
* Whether a node is inline content (a `{{ … }}` expression
|
|
113
|
-
* share a line with adjacent content when
|
|
114
|
-
* them.
|
|
119
|
+
* Whether a node is inline content (a `{{ … }}` expression, text, a single-line
|
|
120
|
+
* comment or an inline element) that may share a line with adjacent content when
|
|
121
|
+
* the source has no line break between them.
|
|
115
122
|
*
|
|
116
123
|
* @param {any} node
|
|
117
124
|
* @returns {boolean}
|
|
@@ -123,14 +130,34 @@ function isInlineChild(node) {
|
|
|
123
130
|
if (node.type === 'twig') {
|
|
124
131
|
return Boolean(node.atom.isPrint);
|
|
125
132
|
}
|
|
133
|
+
if (node.type === 'twigComment') {
|
|
134
|
+
return !/[\r\n]/.test(node.atom.raw);
|
|
135
|
+
}
|
|
136
|
+
if (node.type === 'comment') {
|
|
137
|
+
return !/[\r\n]/.test(node.raw);
|
|
138
|
+
}
|
|
126
139
|
return node.type === 'element' && INLINE_ELEMENTS.has(node.name);
|
|
127
140
|
}
|
|
128
141
|
|
|
129
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Elements that are `display: inline` by default (and the inline-level ruby,
|
|
144
|
+
* replaced and form-control elements), so they may share a line with
|
|
145
|
+
* neighbouring text instead of being moved onto their own line.
|
|
146
|
+
*/
|
|
130
147
|
const INLINE_ELEMENTS = new Set([
|
|
131
|
-
|
|
132
|
-
'
|
|
133
|
-
'
|
|
148
|
+
// text-level semantics
|
|
149
|
+
'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'br', 'cite', 'code',
|
|
150
|
+
'data', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr',
|
|
151
|
+
'q', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup',
|
|
152
|
+
'time', 'tt', 'u', 'var', 'wbr',
|
|
153
|
+
// ruby annotations
|
|
154
|
+
'rp', 'rt', 'ruby',
|
|
155
|
+
// embedded / replaced inline-level content
|
|
156
|
+
'audio', 'canvas', 'embed', 'iframe', 'img', 'map', 'math', 'object',
|
|
157
|
+
'picture', 'svg', 'video',
|
|
158
|
+
// inline-level form controls
|
|
159
|
+
'button', 'input', 'label', 'meter', 'output', 'progress', 'select',
|
|
160
|
+
'textarea',
|
|
134
161
|
]);
|
|
135
162
|
|
|
136
163
|
/**
|
|
@@ -280,6 +307,81 @@ function normalizeTwigTag(raw) {
|
|
|
280
307
|
return normalized === '' ? `${open}${close}` : `${open} ${normalized} ${close}`;
|
|
281
308
|
}
|
|
282
309
|
|
|
310
|
+
/**
|
|
311
|
+
* The net parenthesis depth a line opens (positive) or closes (negative),
|
|
312
|
+
* ignoring parentheses inside string literals.
|
|
313
|
+
*
|
|
314
|
+
* @param {string} text
|
|
315
|
+
* @returns {number}
|
|
316
|
+
*/
|
|
317
|
+
function parenDelta(text) {
|
|
318
|
+
let depth = 0;
|
|
319
|
+
let quote = '';
|
|
320
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
321
|
+
const ch = text[i];
|
|
322
|
+
if (quote !== '') {
|
|
323
|
+
if (ch === '\\') {
|
|
324
|
+
i += 1;
|
|
325
|
+
} else if (ch === quote) {
|
|
326
|
+
quote = '';
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (ch === '"' || ch === "'") {
|
|
331
|
+
quote = ch;
|
|
332
|
+
} else if (ch === '(') {
|
|
333
|
+
depth += 1;
|
|
334
|
+
} else if (ch === ')') {
|
|
335
|
+
depth -= 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return depth;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Prints a `{% if … %}` / `{% elseif … %}` head. A condition written over
|
|
343
|
+
* several lines and made complex by boolean operators or parentheses keeps its
|
|
344
|
+
* line breaks, one operand per line, re-indented one level per parenthesis it
|
|
345
|
+
* is nested in. Anything else is normalized to a single line.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} raw
|
|
348
|
+
* @param {any} options
|
|
349
|
+
* @returns {any} A Prettier doc.
|
|
350
|
+
*/
|
|
351
|
+
function formatTwigHead(raw, options) {
|
|
352
|
+
const { open, close, body } = twigDelimiters(raw);
|
|
353
|
+
const keywordMatch = body.match(/^\s*(if|elseif)\b/);
|
|
354
|
+
const condition = keywordMatch ? body.slice(keywordMatch[0].length) : '';
|
|
355
|
+
const complex = /\b(and|or)\b/.test(condition) || condition.includes('(');
|
|
356
|
+
if (!keywordMatch || !/[\r\n]/.test(body) || !complex) {
|
|
357
|
+
return normalizeTwigTag(raw);
|
|
358
|
+
}
|
|
359
|
+
const unit = options.useTabs === false ? ' '.repeat(options.tabWidth ?? 4) : '\t';
|
|
360
|
+
const lines = body
|
|
361
|
+
.split(/\r?\n/)
|
|
362
|
+
.map((/** @type {string} */ text) => collapseWhitespace(text).trim());
|
|
363
|
+
while (lines.length > 0 && lines[0] === '') {
|
|
364
|
+
lines.shift();
|
|
365
|
+
}
|
|
366
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
367
|
+
lines.pop();
|
|
368
|
+
}
|
|
369
|
+
if (lines.length === 0) {
|
|
370
|
+
return `${open}${close}`;
|
|
371
|
+
}
|
|
372
|
+
const [first, ...rest] = lines;
|
|
373
|
+
let depth = parenDelta(first);
|
|
374
|
+
/** @type {any[]} */
|
|
375
|
+
const continuation = [];
|
|
376
|
+
for (const conditionLine of rest) {
|
|
377
|
+
const leadingClose = (conditionLine.match(/^\)+/) ?? [''])[0].length;
|
|
378
|
+
const level = Math.max(0, depth - leadingClose);
|
|
379
|
+
continuation.push([hardline, unit.repeat(level), conditionLine]);
|
|
380
|
+
depth += parenDelta(conditionLine);
|
|
381
|
+
}
|
|
382
|
+
return [open, ' ', first, indent(continuation), hardline, close];
|
|
383
|
+
}
|
|
384
|
+
|
|
283
385
|
/**
|
|
284
386
|
* Whether a mapping/sequence was written multi-line in the source (a newline
|
|
285
387
|
* between the opening delimiter and the first entry). Such containers stay
|
|
@@ -440,14 +542,13 @@ function formatTwigTag(raw, forceBreakStructures = false, trailingComma = true)
|
|
|
440
542
|
}
|
|
441
543
|
|
|
442
544
|
/**
|
|
443
|
-
*
|
|
545
|
+
* Whether an element's start tag prints on a single line: at most one real
|
|
546
|
+
* attribute, no conditional attributes and no multi-line attribute value.
|
|
444
547
|
*
|
|
445
548
|
* @param {any} node
|
|
446
|
-
* @
|
|
447
|
-
* @param {string} source
|
|
448
|
-
* @returns {any} A Prettier doc.
|
|
549
|
+
* @returns {boolean}
|
|
449
550
|
*/
|
|
450
|
-
function
|
|
551
|
+
function isSingleLineStartTag(node) {
|
|
451
552
|
const attrs = node.attrs ?? [];
|
|
452
553
|
const realAttrs = attrs.filter(
|
|
453
554
|
(/** @type {any} */ a) => a.type !== 'twig' && a.type !== 'twigBlock' && a.type !== 'twigComment',
|
|
@@ -460,8 +561,20 @@ function printOpenTag(node, attrDocs, source) {
|
|
|
460
561
|
: /[\r\n]/.test(c.text),
|
|
461
562
|
),
|
|
462
563
|
);
|
|
564
|
+
return realAttrs.length <= 1 && !hasConditional && !hasComplexValue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Prints an element's start tag.
|
|
569
|
+
*
|
|
570
|
+
* @param {any} node
|
|
571
|
+
* @param {any[]} attrDocs
|
|
572
|
+
* @param {string} source
|
|
573
|
+
* @returns {any} A Prettier doc.
|
|
574
|
+
*/
|
|
575
|
+
function printOpenTag(node, attrDocs, source) {
|
|
463
576
|
const forcedSingleLine = FORCE_SINGLE_LINE.has(node.name);
|
|
464
|
-
const singleLineTag = forcedSingleLine || (
|
|
577
|
+
const singleLineTag = forcedSingleLine || isSingleLineStartTag(node);
|
|
465
578
|
return forcedSingleLine
|
|
466
579
|
? // `html`, `link` and `meta` are never broken across lines
|
|
467
580
|
collapseWhitespace(source.slice(node.rawStart, node.startTagEnd)).replace(
|
|
@@ -567,7 +680,11 @@ function isTextOnly(node) {
|
|
|
567
680
|
|
|
568
681
|
/**
|
|
569
682
|
* Whether an element's non-empty content is only phrasing content (text,
|
|
570
|
-
* prints, comments
|
|
683
|
+
* prints, comments, inline twig blocks and inline elements).
|
|
684
|
+
*
|
|
685
|
+
* Inline element children only count as phrasing when the element's own start
|
|
686
|
+
* tag prints on a single line: once a multi-line start tag puts its `>` on a
|
|
687
|
+
* line of its own, the content belongs on the line below it.
|
|
571
688
|
*
|
|
572
689
|
* @param {any} node
|
|
573
690
|
* @param {string} source
|
|
@@ -578,13 +695,15 @@ function isPhrasingContent(node, source) {
|
|
|
578
695
|
if (children.length === 0) {
|
|
579
696
|
return false;
|
|
580
697
|
}
|
|
698
|
+
const inlineElementsAttach = isSingleLineStartTag(node);
|
|
581
699
|
return children.every(
|
|
582
700
|
(/** @type {any} */ c) =>
|
|
583
701
|
c.type === 'text' ||
|
|
584
702
|
c.type === 'twig' ||
|
|
585
703
|
c.type === 'comment' ||
|
|
586
704
|
c.type === 'twigComment' ||
|
|
587
|
-
(c.type === 'twigBlock' && isInlineTwigBlock(c, source))
|
|
705
|
+
(c.type === 'twigBlock' && isInlineTwigBlock(c, source)) ||
|
|
706
|
+
(c.type === 'element' && inlineElementsAttach && INLINE_ELEMENTS.has(c.name)),
|
|
588
707
|
);
|
|
589
708
|
}
|
|
590
709
|
|
|
@@ -700,11 +819,12 @@ function printAttributeValue(raw) {
|
|
|
700
819
|
* @param {any} path
|
|
701
820
|
* @param {(selector: any, ...rest: any[]) => any} printCallback
|
|
702
821
|
* @param {string} source
|
|
822
|
+
* @param {any} options
|
|
703
823
|
* @returns {any} A Prettier doc.
|
|
704
824
|
*/
|
|
705
|
-
function printTwigBlock(path, printCallback, source) {
|
|
825
|
+
function printTwigBlock(path, printCallback, source, options) {
|
|
706
826
|
const node = path.node;
|
|
707
|
-
const open =
|
|
827
|
+
const open = formatTwigHead(node.open.atom.raw, options);
|
|
708
828
|
const close = normalizeTwigTag(node.close.atom.raw);
|
|
709
829
|
/** @type {any[]} */
|
|
710
830
|
const docs = [];
|
|
@@ -717,7 +837,7 @@ function printTwigBlock(path, printCallback, source) {
|
|
|
717
837
|
for (const section of node.sections ?? []) {
|
|
718
838
|
if (section.head) {
|
|
719
839
|
// mid tags (`{% else %}`, `{% elseif %}`) sit at the block's own indent
|
|
720
|
-
const head =
|
|
840
|
+
const head = formatTwigHead(section.head.atom.raw, options);
|
|
721
841
|
docs.push(hasBlankLine(source, previousEnd, section.head.atom.rawStart) ? [hardline, hardline, head] : [hardline, head]);
|
|
722
842
|
previousEnd = section.head.atom.rawEnd;
|
|
723
843
|
}
|
|
@@ -885,8 +1005,11 @@ function printTwigComment(node) {
|
|
|
885
1005
|
* @returns {boolean}
|
|
886
1006
|
*/
|
|
887
1007
|
function isInlineTwigBlock(node, source) {
|
|
888
|
-
|
|
889
|
-
|
|
1008
|
+
// Trim-consumed whitespace before the `{%` is part of the node's raw span;
|
|
1009
|
+
// the open/close atoms describe the block itself, so a block that only looks
|
|
1010
|
+
// multi-line because of that leading trivia is still recognised as inline.
|
|
1011
|
+
const { start, end } = contentBounds(node);
|
|
1012
|
+
if (source.slice(start, end).includes('\n')) {
|
|
890
1013
|
return false;
|
|
891
1014
|
}
|
|
892
1015
|
const children = (node.children ?? []).filter((/** @type {any} */ c) => !isWhitespaceOnly(c));
|
|
@@ -1111,9 +1234,13 @@ export function print(path, options, printCallback) {
|
|
|
1111
1234
|
case 'element':
|
|
1112
1235
|
return printElement(path, printCallback, source);
|
|
1113
1236
|
case 'twigBlock':
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1237
|
+
if (isInlineTwigBlock(node, source)) {
|
|
1238
|
+
// slice from the atoms so trim-consumed whitespace before the `{%`
|
|
1239
|
+
// is not duplicated (the containing printer owns that gap)
|
|
1240
|
+
const { start, end } = contentBounds(node);
|
|
1241
|
+
return source.slice(start, end);
|
|
1242
|
+
}
|
|
1243
|
+
return printTwigBlock(path, printCallback, source, options);
|
|
1117
1244
|
default:
|
|
1118
1245
|
return printNode(node, options);
|
|
1119
1246
|
}
|
|
@@ -265,6 +265,56 @@ test('keeps text around inline elements on one line', async () => {
|
|
|
265
265
|
);
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
+
test('keeps inline elements from being pushed onto their own line', async () => {
|
|
269
|
+
{
|
|
270
|
+
const source = `<p><strong>If you are seeing this message you are viewing an incomplete page.<br>Everything you see here is WIP.</strong></p>
|
|
271
|
+
`;
|
|
272
|
+
assert.equal(await fmt(source), source);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
{
|
|
276
|
+
const source = `<p class="type-d"><b>{{ __('There are no results.', 'mr') }}</b></p>
|
|
277
|
+
`;
|
|
278
|
+
assert.equal(await fmt(source), source);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
{
|
|
282
|
+
// no text sibling: the whole inline run stays on one line
|
|
283
|
+
const source = `<p><del>old</del><ins>new</ins></p>
|
|
284
|
+
`;
|
|
285
|
+
assert.equal(await fmt(source), source);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
{
|
|
289
|
+
// inline-level form controls and embedded content are covered too
|
|
290
|
+
const source = `<p><label for="x">Name</label><input id="x"><img src="a.png"></p>
|
|
291
|
+
`;
|
|
292
|
+
assert.equal(await fmt(source), source);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
// ruby annotations and legacy inline elements
|
|
297
|
+
const source = `<p><ruby>漢<rt>kan</rt></ruby><tt>mono</tt><big>big</big></p>
|
|
298
|
+
`;
|
|
299
|
+
assert.equal(await fmt(source), source);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
// a multi-line start tag puts its `>` on its own line, so the inline
|
|
304
|
+
// content belongs on the line below it (unchanged behaviour)
|
|
305
|
+
assert.equal(
|
|
306
|
+
await fmt(`<div class="a" data-x="1"><br></div>`),
|
|
307
|
+
`<div
|
|
308
|
+
class="a"
|
|
309
|
+
data-x="1"
|
|
310
|
+
>
|
|
311
|
+
<br>
|
|
312
|
+
</div>
|
|
313
|
+
`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
268
318
|
test('places the closing > at the opening indent in multi-line tags', async () => {
|
|
269
319
|
{
|
|
270
320
|
const out = await fmt(`<div
|
|
@@ -485,6 +535,150 @@ test('formats conditional attributes', async () => {
|
|
|
485
535
|
}
|
|
486
536
|
});
|
|
487
537
|
|
|
538
|
+
test('keeps a complex multi-line condition across lines with paren-depth indent', async () => {
|
|
539
|
+
{
|
|
540
|
+
const out = await fmt(`<div
|
|
541
|
+
class="teaser-a"
|
|
542
|
+
{% if teaser.path %}
|
|
543
|
+
data-has-link
|
|
544
|
+
{% endif %}
|
|
545
|
+
{% if
|
|
546
|
+
show_region or
|
|
547
|
+
teaser.email or
|
|
548
|
+
teaser.telephone or
|
|
549
|
+
teaser.street or
|
|
550
|
+
teaser.postal or
|
|
551
|
+
teaser.city or
|
|
552
|
+
(teaser.type == 'Contact' and (
|
|
553
|
+
teaser.opening_hours.days|length > 0 or
|
|
554
|
+
teaser.opening_hours.text or
|
|
555
|
+
teaser.opening_hours_b.days|length > 0 or
|
|
556
|
+
teaser.opening_hours_b.text or
|
|
557
|
+
teaser.links|length > 0
|
|
558
|
+
))
|
|
559
|
+
%}
|
|
560
|
+
data-has-side
|
|
561
|
+
{% endif %}
|
|
562
|
+
>
|
|
563
|
+
`);
|
|
564
|
+
assert.equal(
|
|
565
|
+
out,
|
|
566
|
+
`<div
|
|
567
|
+
class="teaser-a"
|
|
568
|
+
{% if teaser.path %}
|
|
569
|
+
data-has-link
|
|
570
|
+
{% endif %}
|
|
571
|
+
{% if
|
|
572
|
+
show_region or
|
|
573
|
+
teaser.email or
|
|
574
|
+
teaser.telephone or
|
|
575
|
+
teaser.street or
|
|
576
|
+
teaser.postal or
|
|
577
|
+
teaser.city or
|
|
578
|
+
(teaser.type == 'Contact' and (
|
|
579
|
+
teaser.opening_hours.days|length > 0 or
|
|
580
|
+
teaser.opening_hours.text or
|
|
581
|
+
teaser.opening_hours_b.days|length > 0 or
|
|
582
|
+
teaser.opening_hours_b.text or
|
|
583
|
+
teaser.links|length > 0
|
|
584
|
+
))
|
|
585
|
+
%}
|
|
586
|
+
data-has-side
|
|
587
|
+
{% endif %}
|
|
588
|
+
>
|
|
589
|
+
`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
{
|
|
594
|
+
const out = await fmt(`<div
|
|
595
|
+
class="teaser-a"
|
|
596
|
+
{% if teaser.path %}
|
|
597
|
+
data-has-link
|
|
598
|
+
{% endif %}
|
|
599
|
+
{% if
|
|
600
|
+
show_region or
|
|
601
|
+
teaser.email or
|
|
602
|
+
teaser.telephone or
|
|
603
|
+
teaser.street or
|
|
604
|
+
teaser.postal or
|
|
605
|
+
teaser.city or
|
|
606
|
+
(
|
|
607
|
+
teaser.type == 'Contact' and (
|
|
608
|
+
teaser.opening_hours.days|length > 0 or
|
|
609
|
+
teaser.opening_hours.text or
|
|
610
|
+
teaser.opening_hours_b.days|length > 0 or
|
|
611
|
+
teaser.opening_hours_b.text or
|
|
612
|
+
teaser.links|length > 0
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
%}
|
|
616
|
+
data-has-side
|
|
617
|
+
{% endif %}
|
|
618
|
+
>
|
|
619
|
+
`);
|
|
620
|
+
assert.equal(
|
|
621
|
+
out,
|
|
622
|
+
`<div
|
|
623
|
+
class="teaser-a"
|
|
624
|
+
{% if teaser.path %}
|
|
625
|
+
data-has-link
|
|
626
|
+
{% endif %}
|
|
627
|
+
{% if
|
|
628
|
+
show_region or
|
|
629
|
+
teaser.email or
|
|
630
|
+
teaser.telephone or
|
|
631
|
+
teaser.street or
|
|
632
|
+
teaser.postal or
|
|
633
|
+
teaser.city or
|
|
634
|
+
(
|
|
635
|
+
teaser.type == 'Contact' and (
|
|
636
|
+
teaser.opening_hours.days|length > 0 or
|
|
637
|
+
teaser.opening_hours.text or
|
|
638
|
+
teaser.opening_hours_b.days|length > 0 or
|
|
639
|
+
teaser.opening_hours_b.text or
|
|
640
|
+
teaser.links|length > 0
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
%}
|
|
644
|
+
data-has-side
|
|
645
|
+
{% endif %}
|
|
646
|
+
>
|
|
647
|
+
`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test('keeps multi-line elseif conditions across lines', async () => {
|
|
653
|
+
const source = `{% if
|
|
654
|
+
a or
|
|
655
|
+
b
|
|
656
|
+
%}
|
|
657
|
+
<p>x</p>
|
|
658
|
+
{% elseif
|
|
659
|
+
c and
|
|
660
|
+
d
|
|
661
|
+
%}
|
|
662
|
+
<p>y</p>
|
|
663
|
+
{% endif %}
|
|
664
|
+
`;
|
|
665
|
+
assert.equal(await fmt(source), source);
|
|
666
|
+
|
|
667
|
+
// a simple condition broken across lines is still collapsed
|
|
668
|
+
assert.equal(
|
|
669
|
+
await fmt(`{% if
|
|
670
|
+
x
|
|
671
|
+
%}
|
|
672
|
+
<p>a</p>
|
|
673
|
+
{% endif %}
|
|
674
|
+
`),
|
|
675
|
+
`{% if x %}
|
|
676
|
+
<p>a</p>
|
|
677
|
+
{% endif %}
|
|
678
|
+
`,
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
|
|
488
682
|
test('indents twig block bodies', async () => {
|
|
489
683
|
{
|
|
490
684
|
const out = await fmt(`{% if x %}
|
|
@@ -955,6 +1149,106 @@ test('format twig comments', async () => {
|
|
|
955
1149
|
}
|
|
956
1150
|
});
|
|
957
1151
|
|
|
1152
|
+
test('keeps inline comments next to content without adding line breaks', async () => {
|
|
1153
|
+
{
|
|
1154
|
+
// the comment sits after the text on the same line, so it stays there
|
|
1155
|
+
const source = `<p>
|
|
1156
|
+
Start of main content. {# TODO: Translate if necessary. #}
|
|
1157
|
+
</p>
|
|
1158
|
+
`;
|
|
1159
|
+
assert.equal(await fmt(source), source);
|
|
1160
|
+
|
|
1161
|
+
// a subtle variation that does get formatted: spaces in the comment
|
|
1162
|
+
assert.equal(
|
|
1163
|
+
await fmt(`<p>
|
|
1164
|
+
Start of main content. {# TODO: Translate if necessary. #}
|
|
1165
|
+
</p>
|
|
1166
|
+
`),
|
|
1167
|
+
source,
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
{
|
|
1172
|
+
// in between text on one line
|
|
1173
|
+
const source = `<div>
|
|
1174
|
+
text {# c #} more
|
|
1175
|
+
</div>
|
|
1176
|
+
`;
|
|
1177
|
+
assert.equal(await fmt(source), source);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
{
|
|
1181
|
+
// in between text and an inline element
|
|
1182
|
+
const source = `<p>
|
|
1183
|
+
Go back to the <a href="/">homepage</a>. {# c #}
|
|
1184
|
+
</p>
|
|
1185
|
+
`;
|
|
1186
|
+
assert.equal(await fmt(source), source);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
{
|
|
1190
|
+
// html comments behave like twig comments
|
|
1191
|
+
const source = `<div>
|
|
1192
|
+
text <!-- c --> more
|
|
1193
|
+
</div>
|
|
1194
|
+
`;
|
|
1195
|
+
assert.equal(await fmt(source), source);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
{
|
|
1199
|
+
// at the document root
|
|
1200
|
+
assert.equal(
|
|
1201
|
+
await fmt(`Start of main content. {# TODO: Translate if necessary. #}`),
|
|
1202
|
+
`Start of main content. {# TODO: Translate if necessary. #}
|
|
1203
|
+
`,
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
test('keeps a comment written after the opening tag on that line', async () => {
|
|
1209
|
+
{
|
|
1210
|
+
const source = `<html lang="en" class="no-js"> {# TODO: set lang correctly #}
|
|
1211
|
+
<head></head>
|
|
1212
|
+
</html>
|
|
1213
|
+
`;
|
|
1214
|
+
assert.equal(await fmt(source), source);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
{
|
|
1218
|
+
// open-ended partial: the comment still follows the opening tag
|
|
1219
|
+
const source = `<html lang="en" class="no-js"> {# TODO: set lang correctly #}
|
|
1220
|
+
`;
|
|
1221
|
+
assert.equal(await fmt(source), source);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
{
|
|
1225
|
+
const source = `<div class="a"> {# c #}
|
|
1226
|
+
<p>x</p>
|
|
1227
|
+
</div>
|
|
1228
|
+
`;
|
|
1229
|
+
assert.equal(await fmt(source), source);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
{
|
|
1233
|
+
// html comments behave like twig comments
|
|
1234
|
+
const source = `<div class="a"> <!-- c -->
|
|
1235
|
+
<p>x</p>
|
|
1236
|
+
</div>
|
|
1237
|
+
`;
|
|
1238
|
+
assert.equal(await fmt(source), source);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
{
|
|
1242
|
+
// a newline after the opening tag still puts the comment on its own line
|
|
1243
|
+
const source = `<div class="a">
|
|
1244
|
+
{# c #}
|
|
1245
|
+
<p>x</p>
|
|
1246
|
+
</div>
|
|
1247
|
+
`;
|
|
1248
|
+
assert.equal(await fmt(source), source);
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
|
|
958
1252
|
test('keeps html comments and doctype', async () => {
|
|
959
1253
|
const out = await fmt(`<!doctype html>
|
|
960
1254
|
<!-- c -->
|
|
@@ -1674,6 +1968,23 @@ test('indents nested loops inside elements', async () => {
|
|
|
1674
1968
|
);
|
|
1675
1969
|
});
|
|
1676
1970
|
|
|
1971
|
+
test('keeps an inline twig block on one line after whitespace-trimmed siblings', async () => {
|
|
1972
|
+
const source = `{% if regions|length > 0 %}
|
|
1973
|
+
Regio
|
|
1974
|
+
|
|
1975
|
+
{% for region in regions -%}
|
|
1976
|
+
{{- region.name -}}
|
|
1977
|
+
{%- if not loop.last -%}, {% endif -%}
|
|
1978
|
+
{%- endfor -%}
|
|
1979
|
+
|
|
1980
|
+
{% if page_fields.job_meta_location or page_fields.job_meta_apply_until or tags|length > 0 %}
|
|
1981
|
+
<br>
|
|
1982
|
+
{% endif %}
|
|
1983
|
+
{% endif %}
|
|
1984
|
+
`;
|
|
1985
|
+
assert.equal(await fmt(source), source);
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1677
1988
|
test('nests control structures inside elements', async () => {
|
|
1678
1989
|
const out = await fmt(`{% for group in groups %}<div><h2>{{ group.name }}</h2><ul>{% for item in group.items %}<li>{{ item }}</li>{% endfor %}</ul></div>{% endfor %}`);
|
|
1679
1990
|
assert.equal(
|