@mrhenry/prettier-twig 0.1.1 → 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 +50 -12
- package/test/prettier-twig.test.js +94 -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]);
|
|
@@ -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
|
/**
|
|
@@ -521,14 +542,13 @@ function formatTwigTag(raw, forceBreakStructures = false, trailingComma = true)
|
|
|
521
542
|
}
|
|
522
543
|
|
|
523
544
|
/**
|
|
524
|
-
*
|
|
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.
|
|
525
547
|
*
|
|
526
548
|
* @param {any} node
|
|
527
|
-
* @
|
|
528
|
-
* @param {string} source
|
|
529
|
-
* @returns {any} A Prettier doc.
|
|
549
|
+
* @returns {boolean}
|
|
530
550
|
*/
|
|
531
|
-
function
|
|
551
|
+
function isSingleLineStartTag(node) {
|
|
532
552
|
const attrs = node.attrs ?? [];
|
|
533
553
|
const realAttrs = attrs.filter(
|
|
534
554
|
(/** @type {any} */ a) => a.type !== 'twig' && a.type !== 'twigBlock' && a.type !== 'twigComment',
|
|
@@ -541,8 +561,20 @@ function printOpenTag(node, attrDocs, source) {
|
|
|
541
561
|
: /[\r\n]/.test(c.text),
|
|
542
562
|
),
|
|
543
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) {
|
|
544
576
|
const forcedSingleLine = FORCE_SINGLE_LINE.has(node.name);
|
|
545
|
-
const singleLineTag = forcedSingleLine || (
|
|
577
|
+
const singleLineTag = forcedSingleLine || isSingleLineStartTag(node);
|
|
546
578
|
return forcedSingleLine
|
|
547
579
|
? // `html`, `link` and `meta` are never broken across lines
|
|
548
580
|
collapseWhitespace(source.slice(node.rawStart, node.startTagEnd)).replace(
|
|
@@ -648,7 +680,11 @@ function isTextOnly(node) {
|
|
|
648
680
|
|
|
649
681
|
/**
|
|
650
682
|
* Whether an element's non-empty content is only phrasing content (text,
|
|
651
|
-
* 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.
|
|
652
688
|
*
|
|
653
689
|
* @param {any} node
|
|
654
690
|
* @param {string} source
|
|
@@ -659,13 +695,15 @@ function isPhrasingContent(node, source) {
|
|
|
659
695
|
if (children.length === 0) {
|
|
660
696
|
return false;
|
|
661
697
|
}
|
|
698
|
+
const inlineElementsAttach = isSingleLineStartTag(node);
|
|
662
699
|
return children.every(
|
|
663
700
|
(/** @type {any} */ c) =>
|
|
664
701
|
c.type === 'text' ||
|
|
665
702
|
c.type === 'twig' ||
|
|
666
703
|
c.type === 'comment' ||
|
|
667
704
|
c.type === 'twigComment' ||
|
|
668
|
-
(c.type === 'twigBlock' && isInlineTwigBlock(c, source))
|
|
705
|
+
(c.type === 'twigBlock' && isInlineTwigBlock(c, source)) ||
|
|
706
|
+
(c.type === 'element' && inlineElementsAttach && INLINE_ELEMENTS.has(c.name)),
|
|
669
707
|
);
|
|
670
708
|
}
|
|
671
709
|
|
|
@@ -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 -->
|