Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g;

const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;

const lineTerminatorReg = /(?:\r\n|[\n\r\u2028\u2029])/g;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me think that we might be missing the U+2028 / U+2029 line terminators from the end-quote regex patterns (singleQuoteReg, doubleQuoteReg). I haven't explored this yet but would like to.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex itself seems correct to me. There might be a way to use the /.*/ pattern as an optimisation later; that should match on everything except for line terminators -- which are exactly the set of things this regex searches for.

The regex consumes CRLF when it is found, which is potentially helpful, because it'll move the search needle entirely past the linebreak to the following line's content.. although makes the code slightly less platform-agnostic.


/** Escape special regular expression characters inside a string */

function escapeRegExp(string: string) {
Expand Down Expand Up @@ -109,7 +111,9 @@ export function parse(this: Eta, str: string): Array<AstObject> {
);

const parseCloseReg = new RegExp(
"'|\"|`|\\/\\*|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")",
"'|\"|`|\\/\\*|(\\s*(-|_)?" +
escapeRegExp(config.tags[1]) +
")|\\/\\/",
"g",
);

Expand Down Expand Up @@ -161,6 +165,14 @@ export function parse(this: Eta, str: string): Array<AstObject> {
ParseErr("unclosed comment", str, closeTag.index);
}
parseCloseReg.lastIndex = commentCloseInd;
} else if (char === "//") {
lineTerminatorReg.lastIndex = parseCloseReg.lastIndex;
const match = lineTerminatorReg.exec(str);
if (match) {
parseCloseReg.lastIndex = match.index + match[0].length;
} else {
parseCloseReg.lastIndex = str.length;
}
Comment thread
rtritto marked this conversation as resolved.
} else if (char === "'") {
singleQuoteReg.lastIndex = closeTag.index;

Expand Down
44 changes: 44 additions & 0 deletions test/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more request for a test case: please could we add a (negative) test case where a single-line comment extends all the way to the end of the template?

In other words, something like:

const buff = eta.parse("hi <% // comment with unpaired apostrophe'");

This should raise an unclosed tag parse error exception.

The reason I'm asking: I think it might be possible to break out of one of the loops as a potential optimisation, at a later date. But let's add test coverage on the scenario to confirm the behaviour, before doing that.

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' */ %>");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rtritto your fix works for double-quotes in addition to apostrophes, and I think that might be worth demonstrating too.

Another set of scenarios I'll look into is cases where there are mixed // and /* comments, including interleaving of those (for example -- start /* // middle */ end).

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}` %>");
Expand Down Expand Up @@ -114,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" },
]);
});
});