Skip to content

Commit af16880

Browse files
Don't break liquid tags inside attributes when sorting classes (#143)
* Revert "Revert "Fix Liquid `capture` sorting (#135)" (#140)" This reverts commit 2aabc3c. * Fix off-by-1 error when String nodes don’t have quotes * Refactor * Add tests * Simplify test cases * Update changelog
1 parent 70ea7aa commit af16880

File tree

5 files changed

+102
-62
lines changed

5 files changed

+102
-62
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Don't break liquid tags inside attributes when sorting classes ([#143](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/143))
1113

1214
## [0.2.6] - 2023-03-29
1315

src/index.js

+61-27
Original file line numberDiff line numberDiff line change
@@ -379,55 +379,89 @@ function transformLiquid(ast, { env }) {
379379
: node.name === 'class'
380380
}
381381

382-
function sortAttribute(attr, path) {
382+
/**
383+
* @param {string} str
384+
*/
385+
function hasSurroundingQuotes(str) {
386+
let start = str[0]
387+
let end = str[str.length - 1]
388+
389+
return start === end && (start === '"' || start === "'" || start === "`")
390+
}
391+
392+
/** @type {{type: string, source: string}[]} */
393+
let sources = []
394+
395+
/** @type {{pos: {start: number, end: number}, value: string}[]} */
396+
let changes = []
397+
398+
function sortAttribute(attr) {
383399
visit(attr.value, {
384400
TextNode(node) {
385401
node.value = sortClasses(node.value, { env });
386-
387-
let source = node.source.slice(0, node.position.start) + node.value + node.source.slice(node.position.end)
388-
path.forEach(node => (node.source = source))
402+
changes.push({
403+
pos: node.position,
404+
value: node.value,
405+
})
389406
},
390407

391408
String(node) {
392-
node.value = sortClasses(node.value, { env });
409+
let pos = { ...node.position }
410+
411+
// We have to offset the position ONLY when quotes are part of the String node
412+
// This is because `value` does NOT include quotes
413+
if (hasSurroundingQuotes(node.source.slice(pos.start, pos.end))) {
414+
pos.start += 1;
415+
pos.end -= 1;
416+
}
393417

394-
// String position includes the quotes even if the value doesn't
395-
// Hence the +1 and -1 when slicing
396-
let source = node.source.slice(0, node.position.start+1) + node.value + node.source.slice(node.position.end-1)
397-
path.forEach(node => (node.source = source))
418+
node.value = sortClasses(node.value, { env })
419+
changes.push({
420+
pos,
421+
value: node.value,
422+
})
398423
},
399424
})
400425
}
401426

402427
visit(ast, {
403-
LiquidTag(node, _parent, _key, _index, meta) {
404-
meta.path = [...meta.path ?? [], node];
428+
LiquidTag(node) {
429+
sources.push(node)
405430
},
406431

407-
HtmlElement(node, _parent, _key, _index, meta) {
408-
meta.path = [...meta.path ?? [], node];
432+
HtmlElement(node) {
433+
sources.push(node)
409434
},
410435

411-
AttrSingleQuoted(node, _parent, _key, _index, meta) {
412-
if (!isClassAttr(node)) {
413-
return;
436+
AttrSingleQuoted(node) {
437+
if (isClassAttr(node)) {
438+
sources.push(node)
439+
sortAttribute(node)
414440
}
415-
416-
meta.path = [...meta.path ?? [], node];
417-
418-
sortAttribute(node, meta.path)
419441
},
420442

421-
AttrDoubleQuoted(node, _parent, _key, _index, meta) {
422-
if (!isClassAttr(node)) {
423-
return;
443+
AttrDoubleQuoted(node) {
444+
if (isClassAttr(node)) {
445+
sources.push(node)
446+
sortAttribute(node)
424447
}
425-
426-
meta.path = [...meta.path ?? [], node];
427-
428-
sortAttribute(node, meta.path)
429448
},
430449
});
450+
451+
// Sort so all changes occur in order
452+
changes = changes.sort((a, b) => {
453+
return a.start - b.start
454+
|| a.end - b.end
455+
})
456+
457+
for (let change of changes) {
458+
for (let node of sources) {
459+
node.source =
460+
node.source.slice(0, change.pos.start) +
461+
change.value +
462+
node.source.slice(change.pos.end)
463+
}
464+
}
431465
}
432466

433467
function sortStringLiteral(node, { env }) {

tests/plugins.test.js

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const prettier = require('prettier')
22
const path = require('path')
3+
const { t, yes } = require('./utils')
34

45
function format(str, options = {}) {
56
return prettier
@@ -245,18 +246,18 @@ let tests = [
245246
],
246247
tests: {
247248
'liquid-html': [
248-
[
249-
`<a class="sm:p-0 p-4" href="https://www.example.com">Example</a>`,
250-
`<a class='p-4 sm:p-0' href='https://www.example.com'>Example</a>`,
251-
],
252-
[
253-
`{% if state == true %}\n <a class="{{ "sm:p-0 p-4" | escape }}" href="https://www.example.com">Example</a>\n{% endif %}`,
254-
`{% if state == true %}\n <a class='{{ "p-4 sm:p-0" | escape }}' href='https://www.example.com'>Example</a>\n{% endif %}`,
255-
],
256-
[
257-
`{%- capture class_ordering -%}<div class="sm:p-0 p-4"></div>{%- endcapture -%}`,
258-
`{%- capture class_ordering -%}<div class="p-4 sm:p-0"></div>{%- endcapture -%}`,
259-
],
249+
t`<a class='${yes}' href='https://www.example.com'>Example</a>`,
250+
t`{% if state == true %}\n <a class='{{ "${yes}" | escape }}' href='https://www.example.com'>Example</a>\n{% endif %}`,
251+
t`{%- capture class_ordering -%}<div class="${yes}"></div>{%- endcapture -%}`,
252+
t`{%- capture class_ordering -%}<div class="foo1 ${yes}"></div><div class="foo2 ${yes}"></div>{%- endcapture -%}`,
253+
t`{%- capture class_ordering -%}<div class="foo1 ${yes}"><div class="foo2 ${yes}"></div></div>{%- endcapture -%}`,
254+
t`<p class='${yes} {{ some.prop | prepend: 'is-' }} '></p>`,
255+
t`<div class='${yes} {% render 'some-snippet', settings: section.settings %}'></div>`,
256+
t`<div class='${yes} {{ foo }}'></div>`,
257+
t`<div class='${yes} {% render 'foo' %}'></div>`,
258+
t`<div class='${yes} {% render 'foo', bar: true %}'></div>`,
259+
t`<div class='${yes} {% include 'foo' %}'></div>`,
260+
t`<div class='${yes} {% include 'foo', bar: true %}'></div>`,
260261
],
261262
}
262263
},

tests/test.js

+1-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const prettier = require('prettier')
22
const path = require('path')
33
const { execSync } = require('child_process')
4+
const { t, yes, no } = require('./utils')
45

56
function format(str, options = {}) {
67
options.plugins = options.plugins ?? [
@@ -38,28 +39,6 @@ function formatFixture(name) {
3839
.trim()
3940
}
4041

41-
let yes = '__YES__'
42-
let no = '__NO__'
43-
let testClassName = 'sm:p-0 p-0'
44-
let testClassNameSorted = 'p-0 sm:p-0'
45-
46-
function t(strings, ...values) {
47-
let input = ''
48-
strings.forEach((string, i) => {
49-
input += string + (values[i] ? testClassName : '')
50-
})
51-
52-
let output = ''
53-
strings.forEach((string, i) => {
54-
let value = values[i] || ''
55-
if (value === yes) value = testClassNameSorted
56-
else if (value === no) value = testClassName
57-
output += string + value
58-
})
59-
60-
return [input, output]
61-
}
62-
6342
let html = [
6443
t`<div class="${yes}"></div>`,
6544
t`<!-- <div class="${no}"></div> -->`,

tests/utils.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
let testClassName = 'sm:p-0 p-0'
2+
let testClassNameSorted = 'p-0 sm:p-0'
3+
let yes = '__YES__'
4+
let no = '__NO__'
5+
6+
module.exports.yes = yes
7+
module.exports.no = no
8+
9+
module.exports.t = function t(strings, ...values) {
10+
let input = ''
11+
strings.forEach((string, i) => {
12+
input += string + (values[i] ? testClassName : '')
13+
})
14+
15+
let output = ''
16+
strings.forEach((string, i) => {
17+
let value = values[i] || ''
18+
if (value === yes) value = testClassNameSorted
19+
else if (value === no) value = testClassName
20+
output += string + value
21+
})
22+
23+
return [input, output]
24+
}

0 commit comments

Comments
 (0)