From 4b65e7c7333df4530b84a08b3dd3ccaf3adaaaaf Mon Sep 17 00:00:00 2001 From: vimzh Date: Sun, 22 Mar 2026 14:34:39 +0530 Subject: [PATCH 1/2] fix: table section tags should implicitly close other table sections Opening a , , or should auto-close any currently open table section, including the row and cell elements on top of it. Previously was missing from the close set, and had no entry at all, so a after (or after ) would nest incorrectly instead of being siblings under . Expanded tableSectionTags to include tr, td, and th so the top-of-stack close loop can peel off row/cell elements to reach the section beneath. Ref: https://html.spec.whatwg.org/multipage/syntax.html#optional-tags --- src/Parser.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ src/Parser.ts | 3 ++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Parser.spec.ts b/src/Parser.spec.ts index fe3fb89b..98aa955c 100644 --- a/src/Parser.spec.ts +++ b/src/Parser.spec.ts @@ -179,6 +179,43 @@ describe("API", () => { p.write("<__proto__>"); }); + it("should implicitly close table sections when another section opens", () => { + const onopentagname = vi.fn(); + const onclosetag = vi.fn(); + + // must auto-close + new Parser({ onopentagname, onclosetag }).end( + "
F
B
", + ); + + expect(onclosetag).toHaveBeenCalledWith("tfoot", true); + const tfootClose = onclosetag.mock.calls.findIndex( + ([name]: [string]) => name === "tfoot", + ); + const tbodyOpen = onopentagname.mock.calls.findIndex( + ([name]: [string]) => name === "tbody", + ); + expect(tfootClose).toBeLessThan(tbodyOpen); + }); + + it("should implicitly close when opens", () => { + const onopentagname = vi.fn(); + const onclosetag = vi.fn(); + + new Parser({ onopentagname, onclosetag }).end( + "
B
H
", + ); + + expect(onclosetag).toHaveBeenCalledWith("tbody", true); + const tbodyClose = onclosetag.mock.calls.findIndex( + ([name]: [string]) => name === "tbody", + ); + const theadOpen = onopentagname.mock.calls.findIndex( + ([name]: [string]) => name === "thead", + ); + expect(tbodyClose).toBeLessThan(theadOpen); + }); + it("should support custom tokenizer", () => { class CustomTokenizer extends Tokenizer {} diff --git a/src/Parser.ts b/src/Parser.ts index 800ca0e7..725a5df4 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -13,7 +13,7 @@ const formTags = new Set([ ]); const pTag = new Set(["p"]); const headingTags = new Set(["h1", "h2", "h3", "h4", "h5", "h6", "p"]); -const tableSectionTags = new Set(["thead", "tbody"]); +const tableSectionTags = new Set(["thead", "tbody", "tfoot", "tr", "td", "th"]); const ddtTags = new Set(["dd", "dt"]); const rtpTags = new Set(["rt", "rp"]); @@ -64,6 +64,7 @@ const openImpliesClose = new Map>([ ["ul", pTag], ["rt", rtpTags], ["rp", rtpTags], + ["thead", tableSectionTags], ["tbody", tableSectionTags], ["tfoot", tableSectionTags], ]); From ae2de9198828b3989229e4f4311cec173845d110 Mon Sep 17 00:00:00 2001 From: vimzh Date: Wed, 25 Mar 2026 01:18:28 +0530 Subject: [PATCH 2/2] fix: remove tuple type annotations from mock.calls destructuring The `: [string]` annotations are incompatible with vitest's `any[][]` typing for mock.calls, causing the TypeScript lint check to fail. --- src/Parser.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Parser.spec.ts b/src/Parser.spec.ts index 30daaafa..6d315ce3 100644 --- a/src/Parser.spec.ts +++ b/src/Parser.spec.ts @@ -190,10 +190,10 @@ describe("API", () => { expect(onclosetag).toHaveBeenCalledWith("tfoot", true); const tfootClose = onclosetag.mock.calls.findIndex( - ([name]: [string]) => name === "tfoot", + ([name]) => name === "tfoot", ); const tbodyOpen = onopentagname.mock.calls.findIndex( - ([name]: [string]) => name === "tbody", + ([name]) => name === "tbody", ); expect(tfootClose).toBeLessThan(tbodyOpen); }); @@ -208,10 +208,10 @@ describe("API", () => { expect(onclosetag).toHaveBeenCalledWith("tbody", true); const tbodyClose = onclosetag.mock.calls.findIndex( - ([name]: [string]) => name === "tbody", + ([name]) => name === "tbody", ); const theadOpen = onopentagname.mock.calls.findIndex( - ([name]: [string]) => name === "thead", + ([name]) => name === "thead", ); expect(tbodyClose).toBeLessThan(theadOpen); }); @@ -227,10 +227,10 @@ describe("API", () => { // must auto-close , making them siblings per the HTML spec expect(onclosetag).toHaveBeenCalledWith("td", true); const tdClose = onclosetag.mock.calls.findIndex( - ([name]: [string]) => name === "td", + ([name]) => name === "td", ); const thOpen = onopentagname.mock.calls.findIndex( - ([name]: [string]) => name === "th", + ([name]) => name === "th", ); expect(tdClose).toBeLessThan(thOpen); });