@mrhenry/prettier-twig 0.1.4 → 0.1.5

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 (2026-09-25)
4
+
5
+ * Preserve the `<html>` tag's line layout: keep it on a single line when the source did (even with several attributes), and format it multi-line when the source split it
6
+
3
7
  ## 0.1.3 (2026-09-24)
4
8
 
5
9
  * Normalize twig structures inside attribute values (`{ w:1256 , q: 45 }` becomes `{ w: 1256, q: 45 }`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrhenry/prettier-twig",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
package/src/printer.js CHANGED
@@ -609,6 +609,28 @@ function isSingleLineStartTag(node) {
609
609
  return realAttrs.length <= 1 && !hasConditional && !hasComplexValue;
610
610
  }
611
611
 
612
+ /**
613
+ * Whether an `<html>` start tag must stay on a single line because the source
614
+ * wrote it that way. Unlike other elements, `html` may carry several attributes
615
+ * on one line; a tag the author split across lines is formatted multi-line
616
+ * instead. Conditional attributes are excluded because their blocks always
617
+ * expand.
618
+ *
619
+ * @param {any} node
620
+ * @param {string} source
621
+ * @returns {boolean}
622
+ */
623
+ function keepsHtmlTagOnOneLine(node, source) {
624
+ if (node.name !== 'html') {
625
+ return false;
626
+ }
627
+ const attrs = node.attrs ?? [];
628
+ const hasConditional = attrs.some(
629
+ (/** @type {any} */ a) => a.type === 'twig' || a.type === 'twigBlock',
630
+ );
631
+ return !hasConditional && !/[\r\n]/.test(source.slice(node.rawStart, node.startTagEnd));
632
+ }
633
+
612
634
  /**
613
635
  * Prints an element's start tag.
614
636
  *
@@ -619,9 +641,9 @@ function isSingleLineStartTag(node) {
619
641
  */
620
642
  function printOpenTag(node, attrDocs, source) {
621
643
  const forcedSingleLine = FORCE_SINGLE_LINE.has(node.name);
622
- const singleLineTag = forcedSingleLine || isSingleLineStartTag(node);
644
+ const singleLineTag = forcedSingleLine || keepsHtmlTagOnOneLine(node, source) || isSingleLineStartTag(node);
623
645
  return forcedSingleLine
624
- ? // `html`, `link` and `meta` are never broken across lines
646
+ ? // `link` and `meta` are never broken across lines
625
647
  collapseWhitespace(source.slice(node.rawStart, node.startTagEnd)).replace(
626
648
  /\s*\/?>$/,
627
649
  node.selfClosing ? ' />' : '>',
@@ -783,7 +805,7 @@ function printInlineText(inner) {
783
805
  }
784
806
 
785
807
  /** Elements whose start tag must always stay on a single line. */
786
- const FORCE_SINGLE_LINE = new Set(['html', 'link', 'meta']);
808
+ const FORCE_SINGLE_LINE = new Set(['link', 'meta']);
787
809
 
788
810
  /** Void elements that never have an end tag. */
789
811
  const VOID_ELEMENTS = new Set([
@@ -126,6 +126,84 @@ test('keeps html, link and meta tags on a single line', async () => {
126
126
  }
127
127
  });
128
128
 
129
+ test('preserves the html tag line layout', async () => {
130
+ {
131
+ // a simple twig value in a single attribute stays on one line
132
+ const out = await fmt(`<html lang="{{ currentSite.language }}">
133
+ <head></head>
134
+ </html>`);
135
+ assert.equal(
136
+ out,
137
+ `<html lang="{{ currentSite.language }}">
138
+ <head></head>
139
+ </html>
140
+ `,
141
+ );
142
+ }
143
+
144
+ {
145
+ // several attributes written on one line stay on one line
146
+ const out = await fmt(`<html lang="en" class="no-js" data-x="1">
147
+ <head></head>
148
+ </html>`);
149
+ assert.equal(
150
+ out,
151
+ `<html lang="en" class="no-js" data-x="1">
152
+ <head></head>
153
+ </html>
154
+ `,
155
+ );
156
+ }
157
+
158
+ {
159
+ // a tag the author split across lines is formatted multi-line
160
+ const out = await fmt(`<html
161
+ lang="en"
162
+ class="no-js"
163
+ data-x="1"
164
+ >
165
+ <head></head>
166
+ </html>`);
167
+ assert.equal(
168
+ out,
169
+ `<html
170
+ lang="en"
171
+ class="no-js"
172
+ data-x="1"
173
+ >
174
+ <head></head>
175
+ </html>
176
+ `,
177
+ );
178
+ }
179
+
180
+ {
181
+ // conditional attributes force the multi-line layout
182
+ const out = await fmt(`<html
183
+ lang="en"
184
+ class="no-js"
185
+ {% if current %}
186
+ data-current
187
+ {% endif %}
188
+ >
189
+ <head></head>
190
+ </html>`);
191
+ assert.equal(
192
+ out,
193
+ `<html
194
+ lang="en"
195
+ class="no-js"
196
+ {% if current %}
197
+ data-current
198
+ {% endif %}
199
+ >
200
+ <head></head>
201
+ </html>
202
+ `,
203
+ );
204
+ }
205
+ });
206
+
129
207
  test('does not close open-ended partials', async () => {
130
208
  {
131
209
  // a partial that opens the document, completed by another template