diff --git a/.changeset/big-horses-sniff.md b/.changeset/big-horses-sniff.md new file mode 100644 index 0000000000..bd6af5c8a5 --- /dev/null +++ b/.changeset/big-horses-sniff.md @@ -0,0 +1,5 @@ +--- +"@marko/runtime-tags": patch +--- + +Escape grave (`) characters in template literals diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/.name-cache.json new file mode 100644 index 0000000000..db413faf9a --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/.name-cache.json @@ -0,0 +1,5 @@ +{ + "vars": { + "props": {} + } +} diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/csr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/csr-sanitized.expected.md new file mode 100644 index 0000000000..84ef69bd22 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/csr-sanitized.expected.md @@ -0,0 +1,12 @@ +# Render +```html +
+ 1` + + child`"' + + + ${value} + +
+``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/csr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/csr.expected.md new file mode 100644 index 0000000000..25559b6e00 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/csr.expected.md @@ -0,0 +1,17 @@ +# Render +```html +
+ 1` + + child`"' + + + ${value} + +
+``` + +# Mutations +``` +INSERT div +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/tags/child.js b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/tags/child.js new file mode 100644 index 0000000000..64a9513ca1 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/tags/child.js @@ -0,0 +1,6 @@ +export const $template = "child`\"'${value}"; +export const $walks = /* over(2) */"c"; +export const $setup = () => {}; +const value = "No!!"; +import * as _ from "@marko/runtime-tags/debug/dom"; +export default /* @__PURE__ */_._template("__tests__/tags/child.marko", $template, $walks, $setup); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/template.hydrate.js new file mode 100644 index 0000000000..3eb9cd79d3 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/template.hydrate.js @@ -0,0 +1 @@ +// size: 0 diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/template.js new file mode 100644 index 0000000000..00b85ed7be --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/dom.expected/template.js @@ -0,0 +1,10 @@ +export const $template = `
\` ${_child_template}
`; +export const $walks = /* next(1), replace, over(2), beginChild, _child_walks, endChild, out(1) */`D%c/${_child_walks}&l`; +import * as _ from "@marko/runtime-tags/debug/dom"; +import { $setup as _child, $template as _child_template, $walks as _child_walks } from "./tags/child.marko"; +const $count = /* @__PURE__ */_._let("count/2", ($scope, count) => _._text($scope["#text/0"], count)); +export function $setup($scope) { + _child($scope["#childScope/1"]); + $count($scope, 1); +} +export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/html.expected/tags/child.js b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/html.expected/tags/child.js new file mode 100644 index 0000000000..3246883e18 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/html.expected/tags/child.js @@ -0,0 +1,6 @@ +const value = "No!!"; +import * as _ from "@marko/runtime-tags/debug/html"; +export default _._template("__tests__/tags/child.marko", input => { + const $scope0_id = _._scope_id(); + _._html("child`\"'${value}"); +}); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/html.expected/template.js new file mode 100644 index 0000000000..4cca5189e6 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/html.expected/template.js @@ -0,0 +1,11 @@ +import * as _ from "@marko/runtime-tags/debug/html"; +import _child from "./tags/child.marko"; +export default _._template("__tests__/template.marko", input => { + const $scope0_id = _._scope_id(); + let count = 1; + _._html(`
${_._escape(count)}${_._el_resume($scope0_id, "#text/0")}\` `); + _child({}); + _._html("
"); + _._scope($scope0_id, {}, "__tests__/template.marko", 0); + _._resume_branch($scope0_id); +}); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/resume-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/resume-sanitized.expected.md new file mode 100644 index 0000000000..84ef69bd22 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/resume-sanitized.expected.md @@ -0,0 +1,12 @@ +# Render +```html +
+ 1` + + child`"' + + + ${value} + +
+``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/resume.expected.md new file mode 100644 index 0000000000..fd9dc2207f --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/resume.expected.md @@ -0,0 +1,24 @@ +# Render +```html + + + +
+ 1 + + ` + + child`"' + + + ${value} + +
+ + + +``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/ssr-sanitized.expected.md new file mode 100644 index 0000000000..0dc90cc6d8 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/ssr-sanitized.expected.md @@ -0,0 +1,12 @@ +# Render End +```html +
+ 1` + + child`"' + + + ${value} + +
+``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/ssr.expected.md new file mode 100644 index 0000000000..f6e0ffab56 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/__snapshots__/ssr.expected.md @@ -0,0 +1,46 @@ +# Write +```html +
1` child`"'${value}
+``` + +# Render End +```html + + + +
+ 1 + + ` + + child`"' + + + ${value} + +
+ + + +``` + +# Mutations +``` +INSERT html +INSERT html/head +INSERT html/body +INSERT html/body/div +INSERT html/body/div/#text0 +INSERT html/body/div/#comment +INSERT html/body/div/#text1 +INSERT html/body/div/span0 +INSERT html/body/div/span0/#text +INSERT html/body/div/span1 +INSERT html/body/div/span1/#text +INSERT html/body/script +INSERT html/body/script/#text +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/tags/child.marko b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/tags/child.marko new file mode 100644 index 0000000000..ee7a31f4de --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/tags/child.marko @@ -0,0 +1,4 @@ +static const value = "No!!" +child`"' +\${value} + \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/template.marko b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/template.marko new file mode 100644 index 0000000000..526b6906b4 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/escape-html-strings/template.marko @@ -0,0 +1,5 @@ + +
+ ${count}` + +
diff --git a/packages/runtime-tags/src/translator/core/html-comment.ts b/packages/runtime-tags/src/translator/core/html-comment.ts index a9f7e69f11..9a1bc6c7b7 100644 --- a/packages/runtime-tags/src/translator/core/html-comment.ts +++ b/packages/runtime-tags/src/translator/core/html-comment.ts @@ -8,6 +8,7 @@ import { } from "@marko/compiler/babel-utils"; import { WalkCode } from "../../common/types"; +import { bodyToTextLiteral } from "../util/body-to-text-literal"; import { generateUidIdentifier } from "../util/generate-uid"; import isInvokedFunction from "../util/is-invoked-function"; import { isOutputHTML } from "../util/marko-config"; @@ -180,23 +181,11 @@ export default { } } } else { - const templateQuasis: t.TemplateElement[] = []; - const templateExpressions: t.Expression[] = []; - let currentQuasi = ""; - for (const child of tag.node.body.body) { - if (t.isMarkoText(child)) { - currentQuasi += child.value; - } else if (t.isMarkoPlaceholder(child)) { - templateQuasis.push(t.templateElement({ raw: currentQuasi })); - templateExpressions.push(child.value); - currentQuasi = ""; - } - } + const textLiteral = bodyToTextLiteral(tag.node.body); - if (templateExpressions.length === 0) { - write`${currentQuasi}`; + if (t.isStringLiteral(textLiteral)) { + write`${textLiteral}`; } else { - templateQuasis.push(t.templateElement({ raw: currentQuasi })); addStatement( "render", getSection(tag), @@ -209,7 +198,7 @@ export default { getScopeAccessorLiteral(nodeBinding!), true, ), - t.templateLiteral(templateQuasis, templateExpressions), + textLiteral, ), ), ); diff --git a/packages/runtime-tags/src/translator/core/html-script.ts b/packages/runtime-tags/src/translator/core/html-script.ts index cc92e853ef..a17e66ee83 100644 --- a/packages/runtime-tags/src/translator/core/html-script.ts +++ b/packages/runtime-tags/src/translator/core/html-script.ts @@ -12,6 +12,7 @@ import { import { getEventHandlerName, isEventHandler } from "../../common/helpers"; import { WalkCode } from "../../common/types"; +import { bodyToTextLiteral } from "../util/body-to-text-literal"; import evaluate from "../util/evaluate"; import { generateUidIdentifier } from "../util/generate-uid"; import isInvokedFunction from "../util/is-invoked-function"; @@ -405,29 +406,15 @@ export default { } } } else { - const templateQuasis: t.TemplateElement[] = []; - const templateExpressions: t.Expression[] = []; - let currentQuasi = ""; - let referencePlaceholder: t.MarkoPlaceholder | undefined; - for (const child of tag.node.body.body) { - if (t.isMarkoText(child)) { - currentQuasi += child.value; - } else if (t.isMarkoPlaceholder(child)) { - referencePlaceholder ||= child; - templateQuasis.push(t.templateElement({ raw: currentQuasi })); - templateExpressions.push(child.value); - currentQuasi = ""; - } - } + const textLiteral = bodyToTextLiteral(tag.node.body); - if (!referencePlaceholder) { - write`${currentQuasi}`; + if (t.isStringLiteral(textLiteral)) { + write`${textLiteral.value}`; } else { - templateQuasis.push(t.templateElement({ raw: currentQuasi })); addStatement( "render", getSection(tag), - referencePlaceholder.value.extra?.referencedBindings, + textLiteral.extra?.referencedBindings, t.expressionStatement( callRuntime( "_text_content", @@ -436,7 +423,7 @@ export default { getScopeAccessorLiteral(nodeBinding!), true, ), - t.templateLiteral(templateQuasis, templateExpressions), + textLiteral, ), ), ); diff --git a/packages/runtime-tags/src/translator/core/html-style.ts b/packages/runtime-tags/src/translator/core/html-style.ts index adf25c7980..45f1e2bc04 100644 --- a/packages/runtime-tags/src/translator/core/html-style.ts +++ b/packages/runtime-tags/src/translator/core/html-style.ts @@ -12,6 +12,7 @@ import { import { getEventHandlerName, isEventHandler } from "../../common/helpers"; import { WalkCode } from "../../common/types"; +import { bodyToTextLiteral } from "../util/body-to-text-literal"; import evaluate from "../util/evaluate"; import { generateUidIdentifier } from "../util/generate-uid"; import isInvokedFunction from "../util/is-invoked-function"; @@ -405,29 +406,15 @@ export default { } } } else { - const templateQuasis: t.TemplateElement[] = []; - const templateExpressions: t.Expression[] = []; - let currentQuasi = ""; - let referencePlaceholder: t.MarkoPlaceholder | undefined; - for (const child of tag.node.body.body) { - if (t.isMarkoText(child)) { - currentQuasi += child.value; - } else if (t.isMarkoPlaceholder(child)) { - referencePlaceholder ||= child; - templateQuasis.push(t.templateElement({ raw: currentQuasi })); - templateExpressions.push(child.value); - currentQuasi = ""; - } - } + const textLiteral = bodyToTextLiteral(tag.node.body); - if (!referencePlaceholder) { - write`${currentQuasi}`; + if (t.isStringLiteral(textLiteral)) { + write`${textLiteral}`; } else { - templateQuasis.push(t.templateElement({ raw: currentQuasi })); addStatement( "render", getSection(tag), - referencePlaceholder.value.extra?.referencedBindings, + textLiteral.extra?.referencedBindings, t.expressionStatement( callRuntime( "_text_content", @@ -436,7 +423,7 @@ export default { getScopeAccessorLiteral(nodeBinding!), true, ), - t.templateLiteral(templateQuasis, templateExpressions), + textLiteral, ), ), ); diff --git a/packages/runtime-tags/src/translator/util/body-to-text-literal.ts b/packages/runtime-tags/src/translator/util/body-to-text-literal.ts new file mode 100644 index 0000000000..90c8ed133a --- /dev/null +++ b/packages/runtime-tags/src/translator/util/body-to-text-literal.ts @@ -0,0 +1,35 @@ +import { types as t } from "@marko/compiler"; + +export function bodyToTextLiteral(body: t.MarkoTagBody) { + const templateQuasis: t.TemplateElement[] = []; + const templateExpressions: t.Expression[] = []; + let currentQuasi = ""; + let placeholderExtra: t.MarkoPlaceholder["extra"]; + for (const child of body.body) { + if (t.isMarkoText(child)) { + currentQuasi += child.value; + } else if (t.isMarkoPlaceholder(child)) { + placeholderExtra ||= child.value.extra; + templateQuasis.push(templateElement(currentQuasi, false)); + templateExpressions.push(child.value); + currentQuasi = ""; + } + } + if (templateExpressions.length) { + templateQuasis.push(templateElement(currentQuasi, true)); + const literal = t.templateLiteral(templateQuasis, templateExpressions); + literal.extra = placeholderExtra; + return literal; + } + return t.stringLiteral(currentQuasi); +} + +function templateElement(value: string, tail: boolean) { + return t.templateElement( + { + raw: value.replace(/`/g, "\\`"), + cooked: value, + }, + tail, + ); +} diff --git a/packages/runtime-tags/src/translator/util/normalize-string-expression.ts b/packages/runtime-tags/src/translator/util/normalize-string-expression.ts index 852809b733..c0df1e7ec2 100644 --- a/packages/runtime-tags/src/translator/util/normalize-string-expression.ts +++ b/packages/runtime-tags/src/translator/util/normalize-string-expression.ts @@ -44,7 +44,7 @@ export default function normalizeStringExpression( strs.push(curStr); return t.templateLiteral( - strs.map((raw) => t.templateElement({ raw })), + strs.map((raw) => t.templateElement({ raw: raw.replace(/`/g, "\\`") })), exprs, ); } else if (curStr) {