diff --git a/src/parse.js b/src/parse.js
index 2654df8..3125858 100644
--- a/src/parse.js
+++ b/src/parse.js
@@ -361,10 +361,16 @@ function toPlaceholderJS(source, parseResults) {
parts.push(source.slice(cursor, start));
+ // Blank out backticks and dollar signs instead of backslash-escaping
+ // them: escaping grows the content, and once the growth exceeds the
+ // padding slack the placeholder no longer lines up with the original
+ // region — matchPlaceholder then rejects it and the raw placeholder
+ // leaks into the AST (ember-tooling/ember-eslint-parser#230). The
+ // content is discarded when the Glimmer AST is spliced in, so only
+ // its length and line structure matter.
const content = source
.slice(pr.contentRange.startUtf16Codepoint, pr.contentRange.endUtf16Codepoint)
- .replace(/`/g, "\\`")
- .replace(/\$/g, "\\$");
+ .replace(/[`$]/g, " ");
if (pr.type === "class-member") {
const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
diff --git a/tests/parse.test.js b/tests/parse.test.js
index c04e210..38d76a8 100644
--- a/tests/parse.test.js
+++ b/tests/parse.test.js
@@ -149,4 +149,65 @@ class B extends Component {
const templates = findAllNodes(ast, "GlimmerTemplate");
expect(templates.length).toBe(2);
});
+
+ // ember-tooling/ember-eslint-parser#230: backticks/dollars in template
+ // content used to be backslash-escaped in the placeholder JS, growing it
+ // past the original region once the escapes exceeded the padding slack
+ // (11 chars for class members, 19 for expressions). The end-range check
+ // in matchPlaceholder then failed and the raw placeholder StaticBlock/
+ // TemplateLiteral leaked into the AST with every later offset shifted.
+ describe("placeholder-hostile template content (` and $)", () => {
+ function expectSingleTemplate(source, expected) {
+ const ast = parse(source);
+ const template = findNode(ast, "GlimmerTemplate");
+ expect(template).toBeTruthy();
+ expect(source.substring(template.start, template.end)).toBe(expected);
+ expect(findNode(ast, "StaticBlock")).toBeNull();
+ expect(findNode(ast, "TemplateLiteral")).toBeNull();
+ }
+
+ it("class body template with many backticks in a comment", () => {
+ const tpl = `
+ {{! \`asd\` \`qwe\` \`zxc\` \`undefined\` \`asd\` }}
+ {{! \`@foo\` }}
+ 123
+ `;
+ const source = `export default class MyComponent extends Component {
+ ${tpl}
+}`;
+ const ast = parse(source);
+
+ const classDecl = findNode(ast, "ClassDeclaration");
+ expect(classDecl.body.body.length).toBe(1);
+ expect(classDecl.body.body[0].type).toBe("GlimmerTemplate");
+ expect(source.substring(classDecl.body.body[0].start, classDecl.body.body[0].end)).toBe(tpl);
+ });
+
+ it("class body template with many dollar signs", () => {
+ const tpl = `{{! ${"$".repeat(30)} }}`;
+ const source = `export default class MyComponent extends Component {
+ ${tpl}
+}`;
+ expectSingleTemplate(source, tpl);
+ });
+
+ it("expression template with many backticks", () => {
+ const tpl = `{{! ${"`x` ".repeat(10)}}}`;
+ const source = `const x = ${tpl};`;
+ expectSingleTemplate(source, tpl);
+ });
+
+ it("keeps offsets accurate for code after a backtick-heavy template", () => {
+ const tpl = `{{! ${"`".repeat(30)} }}`;
+ const source = `export default class MyComponent extends Component {
+ ${tpl}
+}
+const after = 1;`;
+ const ast = parse(source);
+
+ const decl = findNode(ast, "VariableDeclaration");
+ expect(decl).toBeTruthy();
+ expect(source.substring(decl.start, decl.end)).toBe("const after = 1;");
+ });
+ });
});