Skip to content

Commit 4b65e7c

Browse files
committed
fix: table section tags should implicitly close other table sections
Opening a <thead>, <tbody>, or <tfoot> should auto-close any currently open table section, including the row and cell elements on top of it. Previously <tfoot> was missing from the close set, and <thead> had no entry at all, so a <tbody> after <tfoot> (or <thead> after <tbody>) would nest incorrectly instead of being siblings under <table>. 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
1 parent 4ed6b05 commit 4b65e7c

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

src/Parser.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,43 @@ describe("API", () => {
179179
p.write("<__proto__>");
180180
});
181181

182+
it("should implicitly close table sections when another section opens", () => {
183+
const onopentagname = vi.fn();
184+
const onclosetag = vi.fn();
185+
186+
// <tbody> must auto-close <tfoot>
187+
new Parser({ onopentagname, onclosetag }).end(
188+
"<table><tfoot><tr><td>F<tbody><tr><td>B</table>",
189+
);
190+
191+
expect(onclosetag).toHaveBeenCalledWith("tfoot", true);
192+
const tfootClose = onclosetag.mock.calls.findIndex(
193+
([name]: [string]) => name === "tfoot",
194+
);
195+
const tbodyOpen = onopentagname.mock.calls.findIndex(
196+
([name]: [string]) => name === "tbody",
197+
);
198+
expect(tfootClose).toBeLessThan(tbodyOpen);
199+
});
200+
201+
it("should implicitly close <tbody> when <thead> opens", () => {
202+
const onopentagname = vi.fn();
203+
const onclosetag = vi.fn();
204+
205+
new Parser({ onopentagname, onclosetag }).end(
206+
"<table><tbody><tr><td>B<thead><tr><th>H</table>",
207+
);
208+
209+
expect(onclosetag).toHaveBeenCalledWith("tbody", true);
210+
const tbodyClose = onclosetag.mock.calls.findIndex(
211+
([name]: [string]) => name === "tbody",
212+
);
213+
const theadOpen = onopentagname.mock.calls.findIndex(
214+
([name]: [string]) => name === "thead",
215+
);
216+
expect(tbodyClose).toBeLessThan(theadOpen);
217+
});
218+
182219
it("should support custom tokenizer", () => {
183220
class CustomTokenizer extends Tokenizer {}
184221

src/Parser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const formTags = new Set([
1313
]);
1414
const pTag = new Set(["p"]);
1515
const headingTags = new Set(["h1", "h2", "h3", "h4", "h5", "h6", "p"]);
16-
const tableSectionTags = new Set(["thead", "tbody"]);
16+
const tableSectionTags = new Set(["thead", "tbody", "tfoot", "tr", "td", "th"]);
1717
const ddtTags = new Set(["dd", "dt"]);
1818
const rtpTags = new Set(["rt", "rp"]);
1919

@@ -64,6 +64,7 @@ const openImpliesClose = new Map<string, Set<string>>([
6464
["ul", pTag],
6565
["rt", rtpTags],
6666
["rp", rtpTags],
67+
["thead", tableSectionTags],
6768
["tbody", tableSectionTags],
6869
["tfoot", tableSectionTags],
6970
]);

0 commit comments

Comments
 (0)