From ccd2f840a90319a5e1db6d6ff49790c497e7890b Mon Sep 17 00:00:00 2001 From: rtritto Date: Thu, 4 Jun 2026 20:04:08 +0200 Subject: [PATCH 1/6] fix: unpaired apostrophe in JavaScript comments --- src/parse.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/parse.ts b/src/parse.ts index 091c22c..e4fb0be 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -109,7 +109,7 @@ export function parse(this: Eta, str: string): Array { ); const parseCloseReg = new RegExp( - "'|\"|`|\\/\\*|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")", + "'|\"|`|\\/\\*|\\/\\/|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")", "g", ); @@ -161,6 +161,13 @@ export function parse(this: Eta, str: string): Array { ParseErr("unclosed comment", str, closeTag.index); } parseCloseReg.lastIndex = commentCloseInd; + } else if (char === "//") { + const commentCloseInd = str.indexOf("\n", parseCloseReg.lastIndex); + if (commentCloseInd === -1) { + parseCloseReg.lastIndex = str.length; + } else { + parseCloseReg.lastIndex = commentCloseInd; + } } else if (char === "'") { singleQuoteReg.lastIndex = closeTag.index; From b335b277be8671e9e4c2a0ac708b1a6524f16378 Mon Sep 17 00:00:00 2001 From: rtritto Date: Thu, 4 Jun 2026 20:11:09 +0200 Subject: [PATCH 2/6] fix unpaired apostrophes within comments, and specific line terminators in JavaScript comments --- src/parse.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index e4fb0be..cb4f34d 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -162,11 +162,15 @@ export function parse(this: Eta, str: string): Array { } parseCloseReg.lastIndex = commentCloseInd; } else if (char === "//") { - const commentCloseInd = str.indexOf("\n", parseCloseReg.lastIndex); - if (commentCloseInd === -1) { - parseCloseReg.lastIndex = str.length; + const lineTerminatorRegex = /[\n\r\u2028\u2029]/g; + lineTerminatorRegex.lastIndex = parseCloseReg.lastIndex; + const match = lineTerminatorRegex.exec(str); + if (match) { + parseCloseReg.lastIndex = + match.index + + (match[0] === "\r" && str[match.index + 1] === "\n" ? 2 : 1); } else { - parseCloseReg.lastIndex = commentCloseInd; + parseCloseReg.lastIndex = str.length; } } else if (char === "'") { singleQuoteReg.lastIndex = closeTag.index; From 038fd66d022b9c7888cf9f74844e0ffde30edaa4 Mon Sep 17 00:00:00 2001 From: rtritto Date: Thu, 4 Jun 2026 20:24:48 +0200 Subject: [PATCH 3/6] hoisting the line-terminator regex outside the loop/function scope --- src/parse.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index cb4f34d..8da69ec 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -19,6 +19,8 @@ const singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g; const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g; +const lineTerminatorReg = /[\n\r\u2028\u2029]/g; + /** Escape special regular expression characters inside a string */ function escapeRegExp(string: string) { @@ -162,9 +164,8 @@ export function parse(this: Eta, str: string): Array { } parseCloseReg.lastIndex = commentCloseInd; } else if (char === "//") { - const lineTerminatorRegex = /[\n\r\u2028\u2029]/g; - lineTerminatorRegex.lastIndex = parseCloseReg.lastIndex; - const match = lineTerminatorRegex.exec(str); + lineTerminatorReg.lastIndex = parseCloseReg.lastIndex; + const match = lineTerminatorReg.exec(str); if (match) { parseCloseReg.lastIndex = match.index + From c29c0aca9a178504a4b61ace7365ec6e9244d18f Mon Sep 17 00:00:00 2001 From: rtritto Date: Thu, 4 Jun 2026 20:26:04 +0200 Subject: [PATCH 4/6] simplify the CRLF handling of the regex --- src/parse.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 8da69ec..8eecf22 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -19,7 +19,7 @@ const singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g; const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g; -const lineTerminatorReg = /[\n\r\u2028\u2029]/g; +const lineTerminatorReg = /(?:\r\n|[\n\r\u2028\u2029])/g; /** Escape special regular expression characters inside a string */ @@ -167,9 +167,7 @@ export function parse(this: Eta, str: string): Array { lineTerminatorReg.lastIndex = parseCloseReg.lastIndex; const match = lineTerminatorReg.exec(str); if (match) { - parseCloseReg.lastIndex = - match.index + - (match[0] === "\r" && str[match.index + 1] === "\n" ? 2 : 1); + parseCloseReg.lastIndex = match.index + match[0].length; } else { parseCloseReg.lastIndex = str.length; } From bc1e1d183b7e885d110c45d0bea588ea452a84b7 Mon Sep 17 00:00:00 2001 From: rtritto Date: Fri, 5 Jun 2026 12:10:29 +0200 Subject: [PATCH 5/6] add tests --- test/parse.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/parse.spec.ts b/test/parse.spec.ts index 2b8f757..ab2c212 100644 --- a/test/parse.spec.ts +++ b/test/parse.spec.ts @@ -34,6 +34,22 @@ describe("parse test", () => { ]); }); + it("works with unpaired apostrophe in single-line comment", () => { + const buff = eta.parse("hi <% // comment with unpaired apostrophe' \n %>"); + expect(buff).toEqual([ + "hi ", + { val: "// comment with unpaired apostrophe' \n ", t: "e" }, + ]); + }); + + it("works with unpaired apostrophe in multiline comment", () => { + const buff = eta.parse("hi <% /* comment with unpaired apostrophe' */ %>"); + expect(buff).toEqual([ + "hi ", + { val: "/* comment with unpaired apostrophe' */ ", t: "e" }, + ]); + }); + it("parses with simple template literal", () => { // biome-ignore lint/suspicious/noTemplateCurlyInString: intentional const buff = eta.parse("hi <%= `template %> ${value}` %>"); From 34d8d6b4c95b448318dd996da97824fb7b3102e5 Mon Sep 17 00:00:00 2001 From: rtritto Date: Tue, 14 Jul 2026 21:10:04 +0200 Subject: [PATCH 6/6] fix: handle unpaired apostrophes and improve closing tag parsing in comments --- src/parse.ts | 4 +++- test/parse.spec.ts | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 8eecf22..62fb5a9 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -111,7 +111,9 @@ export function parse(this: Eta, str: string): Array { ); const parseCloseReg = new RegExp( - "'|\"|`|\\/\\*|\\/\\/|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")", + "'|\"|`|\\/\\*|(\\s*(-|_)?" + + escapeRegExp(config.tags[1]) + + ")|\\/\\/", "g", ); diff --git a/test/parse.spec.ts b/test/parse.spec.ts index ab2c212..4de8e01 100644 --- a/test/parse.spec.ts +++ b/test/parse.spec.ts @@ -38,7 +38,7 @@ describe("parse test", () => { const buff = eta.parse("hi <% // comment with unpaired apostrophe' \n %>"); expect(buff).toEqual([ "hi ", - { val: "// comment with unpaired apostrophe' \n ", t: "e" }, + { val: "// comment with unpaired apostrophe' \n", t: "e" }, ]); }); @@ -46,7 +46,7 @@ describe("parse test", () => { const buff = eta.parse("hi <% /* comment with unpaired apostrophe' */ %>"); expect(buff).toEqual([ "hi ", - { val: "/* comment with unpaired apostrophe' */ ", t: "e" }, + { val: "/* comment with unpaired apostrophe' */", t: "e" }, ]); }); @@ -130,4 +130,32 @@ describe("parse test", () => { <%= /* %> ^`); }); + + it("handles alternative closing tags properly, rather than confusing them with comments", () => { + const originalEta = new Eta({ tags: ["{{", "//"] }); + const buff = originalEta.parse("{{= it.x//"); + expect(buff).toEqual([{ val: "it.x", t: "i" }]); + const originalEta2 = new Eta({ tags: ["{{", "//}}"] }); + const buff2 = originalEta2.parse("{{= it.x//}}"); + expect(buff2).toEqual([{ val: "it.x", t: "i" }]); + }); + + it("handles various line termination characters in single-line comments", () => { + const cases = ["\r", "\r\n", "\u2028", "\u2029", "\n"]; + for (const terminator of cases) { + const buff = eta.parse(`hi <% // comment ending with terminator${terminator} %>`); + expect(buff).toEqual([ + "hi ", + { val: `// comment ending with terminator${terminator}`, t: "e" }, + ]); + } + }); + + it("handles a sequence looking like close tag inside single line comment", () => { + const buff = eta.parse("hi <% // comment %> with close tag\n %>"); + expect(buff).toEqual([ + "hi ", + { val: "// comment %> with close tag\n", t: "e" }, + ]); + }); });