@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrhenry/prettier-twig",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
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
- /** Phrasing (inline) elements that may share a line with neighbouring text. */
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
- 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em',
138
- 'i', 'img', 'kbd', 'mark', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small',
139
- 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',
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
- * Prints an element's start tag.
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
- * @param {any[]} attrDocs
528
- * @param {string} source
529
- * @returns {any} A Prettier doc.
549
+ * @returns {boolean}
530
550
  */
531
- function printOpenTag(node, attrDocs, source) {
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 || (realAttrs.length <= 1 && !hasConditional && !hasComplexValue);
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 and inline twig blocks).
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 -->