@mrhenry/prettier-twig 0.1.1 → 0.1.3
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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/printer.js +123 -25
- package/test/prettier-twig.test.js +174 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.3 (2026-09-24)
|
|
4
|
+
|
|
5
|
+
* Normalize twig structures inside attribute values (`{ w:1256 , q: 45 }` becomes `{ w: 1256, q: 45 }`)
|
|
6
|
+
* Normalize mapping key/value spacing (`{a:1}` becomes `{ a: 1 }`), leaving ternaries and slices untouched
|
|
7
|
+
* Drop whitespace around the `|` filter operator (`image.imgix_src | add_query_arg` becomes `image.imgix_src|add_query_arg`)
|
|
8
|
+
|
|
9
|
+
## 0.1.2 (2026-09-22)
|
|
10
|
+
|
|
11
|
+
* Fix inline elements (text-level, ruby, inline-level replaced content and inline-level form controls) being pushed onto their own line instead of staying with the surrounding text
|
|
12
|
+
* Fix a comment written right after an opening tag being moved onto its own line
|
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]);
|
|
@@ -132,11 +139,25 @@ function isInlineChild(node) {
|
|
|
132
139
|
return node.type === 'element' && INLINE_ELEMENTS.has(node.name);
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
/**
|
|
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
|
+
*/
|
|
136
147
|
const INLINE_ELEMENTS = new Set([
|
|
137
|
-
|
|
138
|
-
'
|
|
139
|
-
'
|
|
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',
|
|
140
161
|
]);
|
|
141
162
|
|
|
142
163
|
/**
|
|
@@ -193,15 +214,21 @@ function hasBlankLine(source, from, to) {
|
|
|
193
214
|
|
|
194
215
|
/**
|
|
195
216
|
* Collapses runs of whitespace in a twig construct's body to single spaces,
|
|
196
|
-
* leaving the contents of string literals untouched.
|
|
217
|
+
* leaving the contents of string literals untouched. When `removeFilterSpaces`
|
|
218
|
+
* is set, whitespace around the `|` filter operator is dropped too (`a | b`
|
|
219
|
+
* becomes `a|b`); comment bodies keep their spaces by leaving it unset.
|
|
197
220
|
*
|
|
198
221
|
* @param {string} text
|
|
222
|
+
* @param {boolean} [removeFilterSpaces]
|
|
199
223
|
* @returns {string}
|
|
200
224
|
*/
|
|
201
|
-
function collapseWhitespace(text) {
|
|
225
|
+
function collapseWhitespace(text, removeFilterSpaces = false) {
|
|
202
226
|
let out = '';
|
|
203
227
|
let quote = '';
|
|
204
228
|
let pendingSpace = false;
|
|
229
|
+
// `|` is Twig's filter operator: no space goes before or after it
|
|
230
|
+
const noSpaceBefore = removeFilterSpaces ? ')]},|' : ')]},';
|
|
231
|
+
const noSpaceAfter = removeFilterSpaces ? '([{|' : '([{';
|
|
205
232
|
for (let i = 0; i < text.length; i += 1) {
|
|
206
233
|
const ch = text[i];
|
|
207
234
|
if (quote !== '') {
|
|
@@ -228,7 +255,7 @@ function collapseWhitespace(text) {
|
|
|
228
255
|
continue;
|
|
229
256
|
}
|
|
230
257
|
const previous = out[out.length - 1] ?? '';
|
|
231
|
-
if (pendingSpace && out !== '' && !
|
|
258
|
+
if (pendingSpace && out !== '' && !noSpaceAfter.includes(previous) && !noSpaceBefore.includes(ch)) {
|
|
232
259
|
out += ' ';
|
|
233
260
|
}
|
|
234
261
|
pendingSpace = false;
|
|
@@ -282,7 +309,7 @@ function twigDelimiters(raw) {
|
|
|
282
309
|
*/
|
|
283
310
|
function normalizeTwigTag(raw) {
|
|
284
311
|
const { open, close, body } = twigDelimiters(raw);
|
|
285
|
-
const normalized = collapseWhitespace(body).trim();
|
|
312
|
+
const normalized = collapseWhitespace(body, true).trim();
|
|
286
313
|
return normalized === '' ? `${open}${close}` : `${open} ${normalized} ${close}`;
|
|
287
314
|
}
|
|
288
315
|
|
|
@@ -338,7 +365,7 @@ function formatTwigHead(raw, options) {
|
|
|
338
365
|
const unit = options.useTabs === false ? ' '.repeat(options.tabWidth ?? 4) : '\t';
|
|
339
366
|
const lines = body
|
|
340
367
|
.split(/\r?\n/)
|
|
341
|
-
.map((/** @type {string} */ text) => collapseWhitespace(text).trim());
|
|
368
|
+
.map((/** @type {string} */ text) => collapseWhitespace(text, true).trim());
|
|
342
369
|
while (lines.length > 0 && lines[0] === '') {
|
|
343
370
|
lines.shift();
|
|
344
371
|
}
|
|
@@ -477,6 +504,43 @@ function formatStructures(text, forceBreak, multiline = [], trailingComma = true
|
|
|
477
504
|
return docs.length === 0 ? '' : docs;
|
|
478
505
|
}
|
|
479
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Normalizes the key/value separator of a mapping entry to `key: value` (one
|
|
509
|
+
* space after, none before). Only the first top-level colon is touched, so a
|
|
510
|
+
* ternary or slice inside the value keeps its spacing. Entries without a colon
|
|
511
|
+
* (a shorthand key) are returned unchanged.
|
|
512
|
+
*
|
|
513
|
+
* @param {string} entry
|
|
514
|
+
* @returns {string}
|
|
515
|
+
*/
|
|
516
|
+
function normalizeMappingEntry(entry) {
|
|
517
|
+
let depth = 0;
|
|
518
|
+
let quote = '';
|
|
519
|
+
for (let i = 0; i < entry.length; i += 1) {
|
|
520
|
+
const ch = entry[i];
|
|
521
|
+
if (quote !== '') {
|
|
522
|
+
if (ch === '\\') {
|
|
523
|
+
i += 1;
|
|
524
|
+
} else if (ch === quote) {
|
|
525
|
+
quote = '';
|
|
526
|
+
}
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (ch === '"' || ch === "'") {
|
|
530
|
+
quote = ch;
|
|
531
|
+
} else if (ch === '(' || ch === '{' || ch === '[') {
|
|
532
|
+
depth += 1;
|
|
533
|
+
} else if (ch === ')' || ch === '}' || ch === ']') {
|
|
534
|
+
depth -= 1;
|
|
535
|
+
} else if (ch === ':' && depth === 0) {
|
|
536
|
+
const key = entry.slice(0, i).trim();
|
|
537
|
+
const value = entry.slice(i + 1).trim();
|
|
538
|
+
return `${key}: ${value}`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return entry;
|
|
542
|
+
}
|
|
543
|
+
|
|
480
544
|
/**
|
|
481
545
|
* @param {string} text A single `{ … }` mapping or `[ … ]` sequence.
|
|
482
546
|
* @param {boolean} forceBreak Whether the container must expand.
|
|
@@ -491,7 +555,9 @@ function formatContainer(text, forceBreak, multiline = [], trailingComma = true)
|
|
|
491
555
|
if (entries.length === 0) {
|
|
492
556
|
return `${open}${close}`;
|
|
493
557
|
}
|
|
494
|
-
const parts = entries.map((entry) =>
|
|
558
|
+
const parts = entries.map((entry) =>
|
|
559
|
+
formatStructures(open === '{' ? normalizeMappingEntry(entry) : entry, false, multiline, trailingComma),
|
|
560
|
+
);
|
|
495
561
|
const trailing = trailingComma ? ifBreak(',') : '';
|
|
496
562
|
// mappings get inner spaces (`{ a: 1 }`), sequences do not (`[1, 2]`)
|
|
497
563
|
const edge = open === '{' ? line : softline;
|
|
@@ -512,7 +578,7 @@ function formatContainer(text, forceBreak, multiline = [], trailingComma = true)
|
|
|
512
578
|
*/
|
|
513
579
|
function formatTwigTag(raw, forceBreakStructures = false, trailingComma = true) {
|
|
514
580
|
const { open, close, body } = twigDelimiters(raw);
|
|
515
|
-
const normalized = collapseWhitespace(body).trim();
|
|
581
|
+
const normalized = collapseWhitespace(body, true).trim();
|
|
516
582
|
if (normalized === '') {
|
|
517
583
|
return `${open}${close}`;
|
|
518
584
|
}
|
|
@@ -521,14 +587,13 @@ function formatTwigTag(raw, forceBreakStructures = false, trailingComma = true)
|
|
|
521
587
|
}
|
|
522
588
|
|
|
523
589
|
/**
|
|
524
|
-
*
|
|
590
|
+
* Whether an element's start tag prints on a single line: at most one real
|
|
591
|
+
* attribute, no conditional attributes and no multi-line attribute value.
|
|
525
592
|
*
|
|
526
593
|
* @param {any} node
|
|
527
|
-
* @
|
|
528
|
-
* @param {string} source
|
|
529
|
-
* @returns {any} A Prettier doc.
|
|
594
|
+
* @returns {boolean}
|
|
530
595
|
*/
|
|
531
|
-
function
|
|
596
|
+
function isSingleLineStartTag(node) {
|
|
532
597
|
const attrs = node.attrs ?? [];
|
|
533
598
|
const realAttrs = attrs.filter(
|
|
534
599
|
(/** @type {any} */ a) => a.type !== 'twig' && a.type !== 'twigBlock' && a.type !== 'twigComment',
|
|
@@ -541,8 +606,20 @@ function printOpenTag(node, attrDocs, source) {
|
|
|
541
606
|
: /[\r\n]/.test(c.text),
|
|
542
607
|
),
|
|
543
608
|
);
|
|
609
|
+
return realAttrs.length <= 1 && !hasConditional && !hasComplexValue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Prints an element's start tag.
|
|
614
|
+
*
|
|
615
|
+
* @param {any} node
|
|
616
|
+
* @param {any[]} attrDocs
|
|
617
|
+
* @param {string} source
|
|
618
|
+
* @returns {any} A Prettier doc.
|
|
619
|
+
*/
|
|
620
|
+
function printOpenTag(node, attrDocs, source) {
|
|
544
621
|
const forcedSingleLine = FORCE_SINGLE_LINE.has(node.name);
|
|
545
|
-
const singleLineTag = forcedSingleLine || (
|
|
622
|
+
const singleLineTag = forcedSingleLine || isSingleLineStartTag(node);
|
|
546
623
|
return forcedSingleLine
|
|
547
624
|
? // `html`, `link` and `meta` are never broken across lines
|
|
548
625
|
collapseWhitespace(source.slice(node.rawStart, node.startTagEnd)).replace(
|
|
@@ -648,7 +725,11 @@ function isTextOnly(node) {
|
|
|
648
725
|
|
|
649
726
|
/**
|
|
650
727
|
* Whether an element's non-empty content is only phrasing content (text,
|
|
651
|
-
* prints, comments
|
|
728
|
+
* prints, comments, inline twig blocks and inline elements).
|
|
729
|
+
*
|
|
730
|
+
* Inline element children only count as phrasing when the element's own start
|
|
731
|
+
* tag prints on a single line: once a multi-line start tag puts its `>` on a
|
|
732
|
+
* line of its own, the content belongs on the line below it.
|
|
652
733
|
*
|
|
653
734
|
* @param {any} node
|
|
654
735
|
* @param {string} source
|
|
@@ -659,13 +740,15 @@ function isPhrasingContent(node, source) {
|
|
|
659
740
|
if (children.length === 0) {
|
|
660
741
|
return false;
|
|
661
742
|
}
|
|
743
|
+
const inlineElementsAttach = isSingleLineStartTag(node);
|
|
662
744
|
return children.every(
|
|
663
745
|
(/** @type {any} */ c) =>
|
|
664
746
|
c.type === 'text' ||
|
|
665
747
|
c.type === 'twig' ||
|
|
666
748
|
c.type === 'comment' ||
|
|
667
749
|
c.type === 'twigComment' ||
|
|
668
|
-
(c.type === 'twigBlock' && isInlineTwigBlock(c, source))
|
|
750
|
+
(c.type === 'twigBlock' && isInlineTwigBlock(c, source)) ||
|
|
751
|
+
(c.type === 'element' && inlineElementsAttach && INLINE_ELEMENTS.has(c.name)),
|
|
669
752
|
);
|
|
670
753
|
}
|
|
671
754
|
|
|
@@ -715,9 +798,10 @@ const RAWTEXT_ELEMENTS = new Set(['script', 'style', 'textarea', 'title', 'pre']
|
|
|
715
798
|
* Prints an attribute.
|
|
716
799
|
*
|
|
717
800
|
* @param {any} attr
|
|
801
|
+
* @param {any} options
|
|
718
802
|
* @returns {any} A Prettier doc.
|
|
719
803
|
*/
|
|
720
|
-
function printAttribute(attr) {
|
|
804
|
+
function printAttribute(attr, options) {
|
|
721
805
|
if (attr.quote === null && attr.valueChunks.length === 0) {
|
|
722
806
|
return attr.nameRaw;
|
|
723
807
|
}
|
|
@@ -729,10 +813,24 @@ function printAttribute(attr) {
|
|
|
729
813
|
const hasControlFlow = attr.valueChunks.some(
|
|
730
814
|
(/** @type {any} */ c) => c.type === 'twig' && c.atom.tag,
|
|
731
815
|
);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
.
|
|
735
|
-
|
|
816
|
+
let value;
|
|
817
|
+
if (isEventHandler || hasControlFlow) {
|
|
818
|
+
value = attr.valueChunks
|
|
819
|
+
.map((/** @type {any} */ c) => (c.type === 'twig' ? c.atom.raw : c.text))
|
|
820
|
+
.join('');
|
|
821
|
+
} else {
|
|
822
|
+
// Plain values get their twig expressions normalized (`{ w:1256 , q: 45 }`
|
|
823
|
+
// becomes `{ w:1256, q: 45 }`, `a | b` becomes `a|b`); the surrounding
|
|
824
|
+
// literal text is kept and the result is re-indented.
|
|
825
|
+
const formattedValue = attr.valueChunks
|
|
826
|
+
.map((/** @type {any} */ c) =>
|
|
827
|
+
c.type === 'twig'
|
|
828
|
+
? docToString(formatTwigTag(c.atom.raw, false, options.trailingComma !== 'none'), options)
|
|
829
|
+
: c.text,
|
|
830
|
+
)
|
|
831
|
+
.join('');
|
|
832
|
+
value = printAttributeValue(formattedValue);
|
|
833
|
+
}
|
|
736
834
|
if (attr.quote === null) {
|
|
737
835
|
return [attr.nameRaw, '=', value];
|
|
738
836
|
}
|
|
@@ -1222,7 +1320,7 @@ function printNode(node, options) {
|
|
|
1222
1320
|
case 'twigComment':
|
|
1223
1321
|
return printTwigComment(node);
|
|
1224
1322
|
case 'attr':
|
|
1225
|
-
return printAttribute(node);
|
|
1323
|
+
return printAttribute(node, options);
|
|
1226
1324
|
case 'text':
|
|
1227
1325
|
return printText(node);
|
|
1228
1326
|
case 'comment':
|
|
@@ -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
|
|
@@ -1155,6 +1205,50 @@ test('keeps inline comments next to content without adding line breaks', async (
|
|
|
1155
1205
|
}
|
|
1156
1206
|
});
|
|
1157
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
|
+
|
|
1158
1252
|
test('keeps html comments and doctype', async () => {
|
|
1159
1253
|
const out = await fmt(`<!doctype html>
|
|
1160
1254
|
<!-- c -->
|
|
@@ -1670,6 +1764,86 @@ test('aligns multi-line attribute values on one indent level', async () => {
|
|
|
1670
1764
|
`,
|
|
1671
1765
|
);
|
|
1672
1766
|
}
|
|
1767
|
+
|
|
1768
|
+
{
|
|
1769
|
+
// a value whose lines have no indentation is indented one level
|
|
1770
|
+
const out = await fmt(`<source
|
|
1771
|
+
srcset="
|
|
1772
|
+
{{ url|add_query_arg({ w: 120 }) }} 120w,
|
|
1773
|
+
{{ url|add_query_arg({ w: 250, q: 45 }) }} 250w,
|
|
1774
|
+
{{ url|add_query_arg({ w: 380, q: 45 }) }} 380w
|
|
1775
|
+
"
|
|
1776
|
+
sizes="calc(100vw - 96px - 48px)"
|
|
1777
|
+
media="(min-width: 35rem)"
|
|
1778
|
+
>`);
|
|
1779
|
+
assert.equal(
|
|
1780
|
+
out,
|
|
1781
|
+
`<source
|
|
1782
|
+
srcset="
|
|
1783
|
+
{{ url|add_query_arg({ w: 120 }) }} 120w,
|
|
1784
|
+
{{ url|add_query_arg({ w: 250, q: 45 }) }} 250w,
|
|
1785
|
+
{{ url|add_query_arg({ w: 380, q: 45 }) }} 380w
|
|
1786
|
+
"
|
|
1787
|
+
sizes="calc(100vw - 96px - 48px)"
|
|
1788
|
+
media="(min-width: 35rem)"
|
|
1789
|
+
>
|
|
1790
|
+
`,
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
test('normalizes twig structures inside attribute values', async () => {
|
|
1796
|
+
const out = await fmt(`<source
|
|
1797
|
+
media="(min-width: 90rem)"
|
|
1798
|
+
srcset="
|
|
1799
|
+
{{ url|add_query_arg({ w:1256 , q: 45 }) }} 2x,
|
|
1800
|
+
{{ url|add_query_arg({ w: 628 }) }}
|
|
1801
|
+
"
|
|
1802
|
+
>`);
|
|
1803
|
+
assert.equal(
|
|
1804
|
+
out,
|
|
1805
|
+
`<source
|
|
1806
|
+
media="(min-width: 90rem)"
|
|
1807
|
+
srcset="
|
|
1808
|
+
{{ url|add_query_arg({ w: 1256, q: 45 }) }} 2x,
|
|
1809
|
+
{{ url|add_query_arg({ w: 628 }) }}
|
|
1810
|
+
"
|
|
1811
|
+
>
|
|
1812
|
+
`,
|
|
1813
|
+
);
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
test('normalizes mapping key/value spacing without touching ternary or slices', async () => {
|
|
1817
|
+
assert.equal(await fmt(`{{ {a:1,b :2} }}`), `{{ { a: 1, b: 2 } }}\n`);
|
|
1818
|
+
assert.equal(await fmt(`{{ { x: a ? b : c, y: list[1:2] } }}`), `{{ { x: a ? b : c, y: list[1:2] } }}\n`);
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
test('drops whitespace around the filter operator', async () => {
|
|
1822
|
+
const source = `{% set url = image.imgix_src|add_query_arg({
|
|
1823
|
+
auto: 'format',
|
|
1824
|
+
crop: 'faces,entropy',
|
|
1825
|
+
ar: aspect_ratio,
|
|
1826
|
+
fit: 'crop',
|
|
1827
|
+
q: 60,
|
|
1828
|
+
}, false) %}
|
|
1829
|
+
`;
|
|
1830
|
+
assert.equal(await fmt(source), source);
|
|
1831
|
+
|
|
1832
|
+
// a subtle variation that does get formatted: spaces around the filters
|
|
1833
|
+
assert.equal(
|
|
1834
|
+
await fmt(`{% set url = image.imgix_src | add_query_arg({
|
|
1835
|
+
auto: 'format',
|
|
1836
|
+
crop: 'faces,entropy',
|
|
1837
|
+
ar: aspect_ratio,
|
|
1838
|
+
fit: 'crop',
|
|
1839
|
+
q: 60,
|
|
1840
|
+
}, false) %}
|
|
1841
|
+
`),
|
|
1842
|
+
source,
|
|
1843
|
+
);
|
|
1844
|
+
|
|
1845
|
+
// whitespace inside a string literal is untouched
|
|
1846
|
+
assert.equal(await fmt(`{{ 'a | b' | upper }}`), `{{ 'a | b'|upper }}\n`);
|
|
1673
1847
|
});
|
|
1674
1848
|
|
|
1675
1849
|
test('indents twig block bodies while section heads stay at block indent', async () => {
|