Skip to content

Commit dbaa710

Browse files
fix(language-plugin-pug): handle backtick attributes containing both quote types (#5970)
1 parent 33b81d5 commit dbaa710

File tree

2 files changed

+128
-4
lines changed

2 files changed

+128
-4
lines changed

packages/language-plugin-pug/lib/baseParse.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,21 @@ export function baseParse(pugCode: string) {
239239
getDocOffset(attrToken.loc.start.line, attrToken.loc.start.column),
240240
getDocOffset(attrToken.loc.end.line, attrToken.loc.end.column),
241241
);
242-
if (typeof attrToken.val === 'string' && attrText.indexOf('=') >= 0) {
242+
if (typeof attrToken.val === 'string' && attrText.includes('=')) {
243243
let valText = attrToken.val;
244244
if (valText.startsWith('`') && valText.endsWith('`')) {
245-
if (valText.indexOf('"') === -1) {
246-
valText = `"${valText.slice(1, -1)}"`;
245+
const innerContent = valText.slice(1, -1);
246+
if (!innerContent.includes('"')) {
247+
valText = `"${innerContent}"`;
248+
}
249+
else if (!innerContent.includes("'")) {
250+
valText = `'${innerContent}'`;
247251
}
248252
else {
249-
valText = `'${valText.slice(1, -1)}'`;
253+
// Both quote types present: convert inner single quotes to double quotes
254+
// This allows using single quotes as the outer delimiter
255+
// JavaScript accepts both 'str' and "str" for string literals
256+
valText = `'${innerContent.replace(/'/g, '"')}'`;
250257
}
251258
}
252259
valText = valText.replace(/ \\\n/g, '//\n');
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as CompilerDOM from '@vue/compiler-dom';
2+
import { describe, expect, it } from 'vitest';
3+
import { baseParse } from '../lib/baseParse';
4+
5+
describe('baseParse', () => {
6+
describe('multiline class binding with backticks', () => {
7+
it('should parse multiline :class attribute with backtick template string', () => {
8+
const pugCode = `span(
9+
v-tooltip="{ text: 'Hello' }"
10+
@click="onClick('item')"
11+
:class=\`[
12+
"my-component__element",
13+
"my-component__modifier",
14+
{
15+
"my-component__element--active my-component__element--highlighted": (isActive === 'yes')
16+
}
17+
]\`
18+
data-testid="my-element"
19+
)`;
20+
21+
const result = baseParse(pugCode);
22+
23+
expect(result.error).toBeUndefined();
24+
expect(result.htmlCode).toContain('<span');
25+
expect(result.htmlCode).toContain(':class=');
26+
});
27+
28+
it('should correctly convert backtick to double quotes when no double quotes inside', () => {
29+
const pugCode = `div(:class=\`['foo', 'bar']\`)`;
30+
31+
const result = baseParse(pugCode);
32+
33+
expect(result.error).toBeUndefined();
34+
expect(result.htmlCode).toContain(":class=\"['foo', 'bar']\"");
35+
});
36+
37+
it('should correctly convert backtick to single quotes when double quotes inside', () => {
38+
const pugCode = `div(:class=\`["foo", "bar"]\`)`;
39+
40+
const result = baseParse(pugCode);
41+
42+
expect(result.error).toBeUndefined();
43+
expect(result.htmlCode).toContain(':class=\'["foo", "bar"]\'');
44+
});
45+
46+
it('should handle multiline backtick values with newlines', () => {
47+
const pugCode = `div(
48+
:class=\`[
49+
"foo",
50+
"bar"
51+
]\`
52+
)`;
53+
54+
const result = baseParse(pugCode);
55+
56+
expect(result.error).toBeUndefined();
57+
});
58+
59+
it('should produce valid HTML that Vue compiler can parse when both quote types present', () => {
60+
const pugCode = `span(
61+
v-tooltip="{ text: 'Hello' }"
62+
@click="onClick('item')"
63+
:class=\`[
64+
"my-component__element",
65+
"my-component__modifier",
66+
{
67+
"my-component__element--active my-component__element--highlighted": (state.type === 'active')
68+
}
69+
]\`
70+
data-testid="my-element"
71+
)`;
72+
73+
const result = baseParse(pugCode);
74+
expect(result.error).toBeUndefined();
75+
76+
const errors: any[] = [];
77+
CompilerDOM.parse(result.htmlCode, {
78+
onError(error) {
79+
errors.push(error);
80+
},
81+
});
82+
83+
expect(errors).toHaveLength(0);
84+
});
85+
86+
it('should convert inner single quotes to double quotes when both quote types present', () => {
87+
const pugCode = `div(:class=\`["foo", 'bar']\`)`;
88+
const result = baseParse(pugCode);
89+
90+
expect(result.error).toBeUndefined();
91+
// Inner single quotes are converted to double quotes, outer delimiter becomes single quote
92+
expect(result.htmlCode).toContain(':class=\'["foo", "bar"]\'');
93+
});
94+
95+
it('should produce valid JavaScript expression when both quote types present', () => {
96+
const pugCode = `div(:class=\`["foo", 'bar']\`)`;
97+
const result = baseParse(pugCode);
98+
99+
const errors: any[] = [];
100+
const ast = CompilerDOM.parse(result.htmlCode, {
101+
onError(error) {
102+
errors.push(error);
103+
},
104+
});
105+
106+
expect(errors).toHaveLength(0);
107+
108+
const div = ast.children[0] as CompilerDOM.ElementNode;
109+
const classDir = div.props.find((p): p is CompilerDOM.DirectiveNode =>
110+
p.type === CompilerDOM.NodeTypes.DIRECTIVE && p.name === 'bind'
111+
);
112+
113+
// Single quotes in the original are converted to double quotes
114+
expect((classDir?.exp as CompilerDOM.SimpleExpressionNode)?.content).toBe('["foo", "bar"]');
115+
});
116+
});
117+
});

0 commit comments

Comments
 (0)