Skip to content

Commit a62ac60

Browse files
committed
fix: improve non standard template literal processing
1 parent 132211e commit a62ac60

File tree

12 files changed

+216
-89
lines changed

12 files changed

+216
-89
lines changed

.changeset/modern-feet-move.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@marko/compat-v4": patch
3+
"@marko/compat-utils": patch
4+
"marko-widgets": patch
5+
---
6+
7+
Improve handling of non standard template literals.

packages/compat-v4/src/migrate/non-standard-template-literals/index.ts

+119-46
Original file line numberDiff line numberDiff line change
@@ -45,62 +45,94 @@ export function migrateNonStandardTemplateLiterals(path: t.NodePath<t.Node>) {
4545
}
4646

4747
function StringLiteral(string: t.NodePath<t.StringLiteral>) {
48-
const templateLiteral = parseNonStandardTemplateLiteral(string);
49-
if (templateLiteral) {
50-
if (
51-
templateLiteral.expressions.length === 1 &&
52-
templateLiteral.quasis.length === 2 &&
53-
templateLiteral.quasis[0].value.raw === "" &&
54-
templateLiteral.quasis[1].value.raw === ""
55-
) {
56-
diagnosticDeprecate(string, {
57-
label: "Non-standard template literals are deprecated.",
58-
fix() {
59-
string.replaceWith(templateLiteral.expressions[0]);
60-
},
61-
});
62-
} else if (templateLiteral.expressions.every(isNotNullish)) {
63-
diagnosticDeprecate(string, {
64-
label: "Non-standard template literals are deprecated.",
65-
fix() {
66-
string.replaceWith(templateLiteral);
67-
},
68-
});
48+
const { file } = string.hub;
49+
const replacement = parseAllNonStandardTemplateLiterals(file, string.node);
50+
if (replacement) {
51+
if (t.isTemplateLiteral(replacement)) {
52+
if (replacement.expressions.every(isNotNullish)) {
53+
diagnosticDeprecate(string, {
54+
label: "Non-standard template literals are deprecated.",
55+
fix() {
56+
string.replaceWith(replacement);
57+
},
58+
});
59+
} else {
60+
diagnosticDeprecate(string, {
61+
label: "Non-standard template literals are deprecated.",
62+
fix: {
63+
type: "confirm",
64+
message:
65+
"Are the interpolated values guaranteed to not be null or undefined?",
66+
apply(confirm) {
67+
if (confirm) {
68+
string.replaceWith(replacement);
69+
} else {
70+
string.replaceWith(
71+
t.templateLiteral(
72+
replacement.quasis,
73+
replacement.expressions.map((expr) => {
74+
return isNotNullish(expr)
75+
? expr
76+
: castNullishToString(file, expr as t.Expression);
77+
}),
78+
),
79+
);
80+
}
81+
},
82+
},
83+
});
84+
}
6985
} else {
7086
diagnosticDeprecate(string, {
7187
label: "Non-standard template literals are deprecated.",
72-
fix: {
73-
type: "confirm",
74-
message:
75-
"Are the interpolated values guaranteed to not be null or undefined?",
76-
apply(confirm) {
77-
if (confirm) {
78-
string.replaceWith(templateLiteral);
79-
} else {
80-
string.replaceWith(
81-
t.templateLiteral(
82-
templateLiteral.quasis,
83-
templateLiteral.expressions.map((expr) => {
84-
return isNotNullish(expr)
85-
? expr
86-
: castNullishToString(string, expr as t.Expression);
87-
}),
88-
),
89-
);
90-
}
91-
},
88+
fix() {
89+
string.replaceWith(replacement);
9290
},
9391
});
9492
}
9593
}
9694
}
9795

98-
function castNullishToString(string: t.NodePath, expression: t.Expression) {
99-
let nullishHelper = nullishHelpers.get(string.hub);
96+
function parseAllNonStandardTemplateLiterals(
97+
file: t.BabelFile,
98+
node: t.StringLiteral,
99+
) {
100+
const templateLiteral = parseNonStandardTemplateLiteral(file, node);
101+
if (templateLiteral) {
102+
for (let i = templateLiteral.expressions.length; i--; ) {
103+
traverseWithParent(
104+
file,
105+
templateLiteral.expressions[i],
106+
templateLiteral,
107+
i,
108+
replaceNestedNonStandardTemplateLiteral,
109+
);
110+
}
111+
112+
return isSingleExpressionTemplateLiteral(templateLiteral)
113+
? templateLiteral.expressions[0]
114+
: templateLiteral;
115+
}
116+
}
117+
118+
function replaceNestedNonStandardTemplateLiteral(
119+
file: t.BabelFile,
120+
node: t.Node,
121+
parent: t.Node,
122+
key: string | number,
123+
) {
124+
if (node.type === "StringLiteral") {
125+
(parent as any)[key] =
126+
parseAllNonStandardTemplateLiterals(file, node) || node;
127+
}
128+
}
129+
130+
function castNullishToString(file: t.BabelFile, expression: t.Expression) {
131+
let nullishHelper = nullishHelpers.get(file.hub);
100132
if (!nullishHelper) {
101-
nullishHelper = string.scope.generateUidIdentifier("toString");
102-
nullishHelpers.set(string.hub, nullishHelper);
103-
string.hub.file.path.unshiftContainer(
133+
nullishHelper = file.path.scope.generateUidIdentifier("toString");
134+
nullishHelpers.set(file.hub, nullishHelper);
135+
file.path.unshiftContainer(
104136
"body",
105137
t.markoScriptlet(
106138
[
@@ -129,6 +161,15 @@ function castNullishToString(string: t.NodePath, expression: t.Expression) {
129161
return t.callExpression(nullishHelper, [expression]);
130162
}
131163

164+
function isSingleExpressionTemplateLiteral(templateLiteral: t.TemplateLiteral) {
165+
return (
166+
templateLiteral.expressions.length === 1 &&
167+
templateLiteral.quasis.length === 2 &&
168+
templateLiteral.quasis[0].value.raw === "" &&
169+
templateLiteral.quasis[1].value.raw === ""
170+
);
171+
}
172+
132173
function isNotNullish(node: t.Node): boolean {
133174
switch (node.type) {
134175
case "ArrayExpression":
@@ -162,3 +203,35 @@ function isNotNullish(node: t.Node): boolean {
162203
return false;
163204
}
164205
}
206+
207+
function traverseWithParent(
208+
file: t.BabelFile,
209+
node: t.Node | null | undefined,
210+
parent: t.Node,
211+
key: string | number,
212+
enter: (
213+
file: t.BabelFile,
214+
node: t.Node,
215+
parent: t.Node,
216+
key: string | number,
217+
) => void,
218+
): void {
219+
if (!node) return;
220+
221+
const keys = (t as any).VISITOR_KEYS[node.type];
222+
if (!keys) return;
223+
224+
enter(file, node, parent, key);
225+
226+
for (const key of keys) {
227+
const value: t.Node | undefined | null = (node as any)[key];
228+
229+
if (Array.isArray(value)) {
230+
for (let i = 0; i < value.length; i++) {
231+
traverseWithParent(file, value[i], value, i, enter);
232+
}
233+
} else {
234+
traverseWithParent(file, value, parent, key, enter);
235+
}
236+
}
237+
}

packages/compat-v4/src/migrate/non-standard-template-literals/parse.ts

+37-23
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ const enum CODE {
1717
OPEN_PAREN = 40,
1818
OPEN_SQUARE_BRACKET = 91,
1919
SINGLE_QUOTE = 39,
20+
EXCLAMATION = 33,
2021
}
2122

2223
export function parseNonStandardTemplateLiteral(
23-
string: t.NodePath<t.StringLiteral>,
24+
file: t.BabelFile,
25+
string: t.StringLiteral,
2426
) {
25-
const { file } = string.hub;
26-
const { extra } = string.node;
27+
const { extra } = string;
2728
let value = extra?.raw as string | undefined;
2829
if (typeof value !== "string") return;
2930
value = value.slice(1, -1);
3031
const { length } = value;
31-
const nodeStart = getStart(file, string.node);
32+
const nodeStart = getStart(file, string);
3233
const valueStart = nodeStart == null ? null : nodeStart + 1;
3334
let elements: undefined | t.TemplateElement[];
3435
let expressions: undefined | t.Expression[];
@@ -39,9 +40,16 @@ export function parseNonStandardTemplateLiteral(
3940
case CODE.BACK_SLASH:
4041
i++;
4142
break;
42-
case CODE.DOLLAR_SIGN:
43-
if (value.charCodeAt(i + 1) === CODE.OPEN_CURLY_BRACE) {
44-
const bracketStart = i + 2;
43+
case CODE.DOLLAR_SIGN: {
44+
const bracketOffset =
45+
value.charCodeAt(i + 1) === CODE.EXCLAMATION &&
46+
value.charCodeAt(i + 2) === CODE.OPEN_CURLY_BRACE
47+
? 3
48+
: value.charCodeAt(i + 1) === CODE.OPEN_CURLY_BRACE
49+
? 2
50+
: 0;
51+
if (bracketOffset) {
52+
const bracketStart = i + bracketOffset;
4553
const bracketEnd = skipBracketed(
4654
value,
4755
bracketStart,
@@ -56,25 +64,31 @@ export function parseNonStandardTemplateLiteral(
5664

5765
i = bracketEnd - 1;
5866
lastEndBracket = bracketEnd;
59-
const expr =
60-
valueStart != null
61-
? parseExpression(
62-
file,
63-
value.slice(bracketStart, i),
64-
valueStart + bracketStart,
65-
valueStart + i,
66-
)
67-
: parseExpression(file, value.slice(bracketStart, i));
68-
69-
if (elements) {
70-
elements.push(el);
71-
expressions!.push(expr);
72-
} else {
73-
elements = [el];
74-
expressions = [expr];
67+
68+
try {
69+
const expr =
70+
valueStart != null
71+
? parseExpression(
72+
file,
73+
value.slice(bracketStart, i),
74+
valueStart + bracketStart,
75+
valueStart + i,
76+
)
77+
: parseExpression(file, value.slice(bracketStart, i));
78+
79+
if (elements) {
80+
elements.push(el);
81+
expressions!.push(expr);
82+
} else {
83+
elements = [el];
84+
expressions = [expr];
85+
}
86+
} catch {
87+
return; // bail if we couldn't process the expression.
7588
}
7689
}
7790
break;
91+
}
7892
}
7993
}
8094

tests/fixtures-class/non-standard-template-literals/__snapshots__/auto-migrate.expected/template.marko

+5-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ $ const d = "d";
4040
<div id="j">
4141
${'\${abc}'}
4242
</div>
43-
<div ...a id="c"/>
44-
<div ...d id="c"/>
43+
<div ...a id="k"/>
44+
<div ...d id="l"/>
45+
<div id="m">
46+
${a}
47+
</div>
4548
$ const handler = console.log;
4649
<button onClick(handler)/>

tests/fixtures-class/non-standard-template-literals/__snapshots__/dom.expected.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@
5555
${abc}
5656
</div>
5757
<div
58-
id="c"
58+
id="k"
5959
/>
6060
<div
6161
d=""
62-
id="c"
62+
id="l"
6363
/>
64+
<div
65+
id="m"
66+
>
67+
1
68+
</div>
6469
<button />
6570
```

tests/fixtures-class/non-standard-template-literals/__snapshots__/dom.expected/template.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,18 @@ _marko_template._ = _marko_renderer(function (input, out, _componentDef, _compon
7676
out.t('\${abc}', _component);
7777
out.ee();
7878
out.e("div", _marko_merge_attrs(a, {
79-
"id": "c"
79+
"id": "k"
8080
}), "11", _component, 0, 4);
8181
out.e("div", _marko_merge_attrs(d, {
82-
"id": "c"
82+
"id": "l"
8383
}), "12", _component, 0, 4);
84+
out.be("div", {
85+
"id": "m"
86+
}, "13", _component, null, 1);
87+
out.t(a, _component);
88+
out.ee();
8489
const handler = console.log;
85-
out.e("button", null, "13", _component, 0, 0, {
90+
out.e("button", null, "14", _component, 0, 0, {
8691
"onclick": _componentDef.d("click", handler, false)
8792
});
8893
}, {

tests/fixtures-class/non-standard-template-literals/__snapshots__/html.expected.md

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Write
2-
<!--M#s0-0--><div id="1" data-other=2></div><div id=a>${STATIC}</div><div id=b>${SCRIPLET}</div><div id=c>1</div><div id=d>abc}</div><div id=e>abc}</div><div id=f>abc}</div><div id=g>abcd}ef</div><div id=h>abc3</div><div id=i>abcdef</div><div id=j>${abc}</div><div id=c></div><div id=c d></div><button></button><!--M/--><script>$MC=(window.$MC||[]).concat({"w":[["s0-0",0,{"renderBody":null},{"f":1,"r":null}]],"t":["<fixture-dir>/template.marko"]})</script>
2+
<!--M#s0-0--><div id="1" data-other=2></div><div id=a>${STATIC}</div><div id=b>${SCRIPLET}</div><div id=c>1</div><div id=d>abc}</div><div id=e>abc}</div><div id=f>abc}</div><div id=g>abcd}ef</div><div id=h>abc3</div><div id=i>abcdef</div><div id=j>${abc}</div><div id=k></div><div id=l d></div><div id=m>1</div><button></button><!--M/--><script>$MC=(window.$MC||[]).concat({"w":[["s0-0",0,{"renderBody":null},{"f":1,"r":null}]],"t":["<fixture-dir>/template.marko"]})</script>
33

44
# Render
55
```html
@@ -58,11 +58,16 @@
5858
${abc}
5959
</div>
6060
<div
61-
id="c"
61+
id="k"
6262
/>
6363
<div
6464
d=""
65-
id="c"
65+
id="l"
6666
/>
67+
<div
68+
id="m"
69+
>
70+
1
71+
</div>
6772
<button />
6873
```

tests/fixtures-class/non-standard-template-literals/__snapshots__/html.expected/template.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,14 @@ _marko_template._ = _marko_renderer(function (input, out, _componentDef, _compon
5353
out.w("${abc}");
5454
out.w("</div>");
5555
out.w(`<div${_marko_merge_attrs(a, {
56-
"id": "c"
56+
"id": "k"
5757
})}></div>`);
5858
out.w(`<div${_marko_merge_attrs(d, {
59-
"id": "c"
59+
"id": "l"
6060
})}></div>`);
61+
out.w("<div id=m>");
62+
out.w(_marko_escapeXml(a));
63+
out.w("</div>");
6164
const handler = console.log;
6265
out.w("<button></button>");
6366
}, {

0 commit comments

Comments
 (0)