Skip to content

Commit d4edefb

Browse files
authored
fix(template-compiler): escape attribute names (#4499)
1 parent ab432ba commit d4edefb

File tree

16 files changed

+476
-2
lines changed

16 files changed

+476
-2
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { createElement } from 'lwc';
2+
import { catchUnhandledRejectionsAndErrors } from 'test-utils';
3+
import BooleanValue from 'x/booleanValue';
4+
import StringValue from 'x/stringValue';
5+
6+
// Browsers treat attribute names containing the ` (backtick) character differently
7+
// depending on whether the HTML is parsed or you call `setAttribute` directly.
8+
//
9+
// Succeeds:
10+
//
11+
// elm.innerHTML = '<div a`b`c></div>'
12+
//
13+
// Fails with: Uncaught InvalidCharacterError: Failed to execute 'setAttribute' on 'Element': 'a`b`c' is not a valid attribute name.
14+
//
15+
// elm.setAttribute(theName, 'a`b`c')
16+
//
17+
// Since the static content optimization only uses the first pattern and non-optimized only uses the second,
18+
// one case will work whereas the other will throw an error.
19+
//
20+
// Since using backticks in attribute names is fairly useless, we do not attempt to smooth out this difference.
21+
22+
let caughtError;
23+
24+
catchUnhandledRejectionsAndErrors((error) => {
25+
caughtError = error;
26+
});
27+
28+
afterEach(() => {
29+
caughtError = undefined;
30+
});
31+
32+
const scenarios = [
33+
{
34+
name: 'boolean-true-value',
35+
expectedValue: '',
36+
Ctor: BooleanValue,
37+
tagName: 'x-boolean-value',
38+
},
39+
{
40+
name: 'string-value',
41+
expectedValue: 'yolo',
42+
Ctor: StringValue,
43+
tagName: 'x-string-value',
44+
},
45+
];
46+
47+
scenarios.forEach(({ name, expectedValue, Ctor, tagName }) => {
48+
describe(name, () => {
49+
it('should render attr names with proper escaping', async () => {
50+
const elm = createElement(tagName, { is: Ctor });
51+
document.body.appendChild(elm);
52+
53+
await Promise.resolve();
54+
if (process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION) {
55+
expect(elm.shadowRoot.children.length).toBe(0); // does not render
56+
expect(caughtError).not.toBeUndefined();
57+
expect(caughtError.message).toMatch(
58+
/Failed to execute 'setAttribute' on 'Element'|Invalid qualified name|String contains an invalid character|The string contains invalid characters/
59+
);
60+
} else {
61+
expect(elm.shadowRoot.children[0].getAttribute('a`b`c')).toBe(expectedValue);
62+
expect(caughtError).toBeUndefined();
63+
}
64+
});
65+
});
66+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div a`b`c></div>
3+
</template>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class extends LightningElement {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div a`b`c="yolo"></div>
3+
</template>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class extends LightningElement {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<template>
2+
<div a`b`c></div>
3+
<div a`b`c="yolo"></div>
4+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
{
2+
"root": {
3+
"type": "Root",
4+
"location": {
5+
"startLine": 1,
6+
"startColumn": 1,
7+
"endLine": 4,
8+
"endColumn": 12,
9+
"start": 0,
10+
"end": 73,
11+
"startTag": {
12+
"startLine": 1,
13+
"startColumn": 1,
14+
"endLine": 1,
15+
"endColumn": 11,
16+
"start": 0,
17+
"end": 10
18+
},
19+
"endTag": {
20+
"startLine": 4,
21+
"startColumn": 1,
22+
"endLine": 4,
23+
"endColumn": 12,
24+
"start": 62,
25+
"end": 73
26+
}
27+
},
28+
"directives": [],
29+
"children": [
30+
{
31+
"type": "Element",
32+
"name": "div",
33+
"namespace": "http://www.w3.org/1999/xhtml",
34+
"location": {
35+
"startLine": 2,
36+
"startColumn": 5,
37+
"endLine": 2,
38+
"endColumn": 22,
39+
"start": 15,
40+
"end": 32,
41+
"startTag": {
42+
"startLine": 2,
43+
"startColumn": 5,
44+
"endLine": 2,
45+
"endColumn": 16,
46+
"start": 15,
47+
"end": 26
48+
},
49+
"endTag": {
50+
"startLine": 2,
51+
"startColumn": 16,
52+
"endLine": 2,
53+
"endColumn": 22,
54+
"start": 26,
55+
"end": 32
56+
}
57+
},
58+
"attributes": [
59+
{
60+
"type": "Attribute",
61+
"name": "a`b`c",
62+
"value": {
63+
"type": "Literal",
64+
"value": true
65+
},
66+
"location": {
67+
"startLine": 2,
68+
"startColumn": 10,
69+
"endLine": 2,
70+
"endColumn": 15,
71+
"start": 20,
72+
"end": 25
73+
}
74+
}
75+
],
76+
"properties": [],
77+
"directives": [],
78+
"listeners": [],
79+
"children": []
80+
},
81+
{
82+
"type": "Element",
83+
"name": "div",
84+
"namespace": "http://www.w3.org/1999/xhtml",
85+
"location": {
86+
"startLine": 3,
87+
"startColumn": 5,
88+
"endLine": 3,
89+
"endColumn": 29,
90+
"start": 37,
91+
"end": 61,
92+
"startTag": {
93+
"startLine": 3,
94+
"startColumn": 5,
95+
"endLine": 3,
96+
"endColumn": 23,
97+
"start": 37,
98+
"end": 55
99+
},
100+
"endTag": {
101+
"startLine": 3,
102+
"startColumn": 23,
103+
"endLine": 3,
104+
"endColumn": 29,
105+
"start": 55,
106+
"end": 61
107+
}
108+
},
109+
"attributes": [
110+
{
111+
"type": "Attribute",
112+
"name": "a`b`c",
113+
"value": {
114+
"type": "Literal",
115+
"value": "yolo"
116+
},
117+
"location": {
118+
"startLine": 3,
119+
"startColumn": 10,
120+
"endLine": 3,
121+
"endColumn": 22,
122+
"start": 42,
123+
"end": 54
124+
}
125+
}
126+
],
127+
"properties": [],
128+
"directives": [],
129+
"listeners": [],
130+
"children": []
131+
}
132+
]
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"enableStaticContentOptimization": false
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import _implicitStylesheets from "./non-optimized.css";
2+
import _implicitScopedStylesheets from "./non-optimized.scoped.css?scoped=true";
3+
import { freezeTemplate, registerTemplate } from "lwc";
4+
const stc0 = {
5+
attrs: {
6+
"a`b`c": "",
7+
},
8+
key: 0,
9+
};
10+
const stc1 = {
11+
attrs: {
12+
"a`b`c": "yolo",
13+
},
14+
key: 1,
15+
};
16+
function tmpl($api, $cmp, $slotset, $ctx) {
17+
const { h: api_element } = $api;
18+
return [api_element("div", stc0), api_element("div", stc1)];
19+
/*LWC compiler vX.X.X*/
20+
}
21+
export default registerTemplate(tmpl);
22+
tmpl.stylesheets = [];
23+
tmpl.stylesheetToken = "lwc-71tdq7g4m0k";
24+
tmpl.legacyStylesheetToken = "x-non-optimized_non-optimized";
25+
if (_implicitStylesheets) {
26+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
27+
}
28+
if (_implicitScopedStylesheets) {
29+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
30+
}
31+
freezeTemplate(tmpl);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"warnings": [
3+
{
4+
"code": 1057,
5+
"message": "LWC1057: a`b`c is not valid attribute for div. For more information refer to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div",
6+
"level": 2,
7+
"location": {
8+
"line": 2,
9+
"column": 10,
10+
"start": 20,
11+
"length": 5
12+
}
13+
},
14+
{
15+
"code": 1057,
16+
"message": "LWC1057: a`b`c is not valid attribute for div. For more information refer to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div",
17+
"level": 2,
18+
"location": {
19+
"line": 3,
20+
"column": 10,
21+
"start": 42,
22+
"length": 12
23+
}
24+
}
25+
]
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<template>
2+
<div a`b`c></div>
3+
<div a`b`c="yolo"></div>
4+
</template>

0 commit comments

Comments
 (0)