feat(html): in-select-in-table closes select on table tags (#83)

This commit is contained in:
Mihaela Mihaljevic 2026-06-09 06:37:35 +02:00
parent 7e56265ed2
commit a5b032e9dc
4 changed files with 24 additions and 2 deletions

View File

@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- HTML5 select content model (#83, stage 10): a `<select>` now follows the in-select rules. `<optgroup>` closes an open `<option>`, a nested `<select>` closes the open one rather than nesting, and an `<input>`/`<keygen>`/`<textarea>` closes the select and is placed after it.
- HTML5 select content model (#83, stage 10): a `<select>` now follows the in-select rules. `<optgroup>` closes an open `<option>`, a nested `<select>` closes the open one rather than nesting, and an `<input>`/`<keygen>`/`<textarea>` closes the select and is placed after it. In the "in select in table" case, a table-structural tag (`caption`/`table`/`tbody`/`tfoot`/`thead`/`tr`/`td`/`th`) closes a select that is inside a table before being processed.
- HTML5 frameset documents (#83, stage 8): a `<frameset>` after the head now produces a frameset document whose `frameset` element replaces the body (`<html><head></head><frameset>...`), with nested framesets and `<noframes>` handled, and `<frame>` is now a void element (it no longer wrongly nests). An ordinary document still produces a body.
- HTML5 integration points (#83, stage 7): HTML content inside an SVG integration point (`foreignObject`, `desc`, `title`) is now parsed in the HTML namespace rather than SVG, so `<svg><foreignObject><div>` puts the `div` (and its descendants) back in HTML; re-entering `<svg>` inside an integration point switches back to the SVG namespace.
- HTML5 table foster-parenting (#83, stage 6): stray flow content inside a table is now moved out rather than nested in the table structure, matching the HTML standard. A non-table element or character data appearing while a `table`/`tbody`/`thead`/`tfoot`/`tr` is the open node is inserted immediately before the table (`<table><b>x</table>` -> `<b>x</b><table></table>`), while well-formed cell content is untouched.

View File

@ -89,7 +89,7 @@ flowchart TB
PureXML is a working, dependency-free XML library today: parse, emit, validate,
query, and stream documents on macOS, Linux, Windows, and WASM. The test suite
currently runs **749 tests in 117 suites** (`swift test`).
currently runs **750 tests in 117 suites** (`swift test`).
### Shipped (libxml2-aligned surface)

View File

@ -227,11 +227,27 @@ final class HTMLDocument {
/// or an input/keygen/textarea.
private static let closesSelect: Set<String> = ["select", "input", "keygen", "textarea"]
/// Table-structural tags that close a select which is inside a table (the "in
/// select in table" rule).
private static let closesSelectInTable: Set<String> = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
/// The open-stack index of a `<select>` that an opening table tag should close
/// (a select inside a table), or nil when there is none.
private func selectInTableToClose(for name: String) -> Int? {
guard Self.closesSelectInTable.contains(name),
let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1,
openBody[..<selectIndex].contains(where: { tagName($0) == "table" }) else { return nil }
return selectIndex
}
private func bodyOpen(_ name: String, _ attributes: [(String, String)], selfClosing: Bool) {
if Self.closesSelect.contains(name), let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1 {
openBody.removeLast(openBody.count - selectIndex)
if name == "select" { return } // a nested select just closes the open one
}
if let selectIndex = selectInTableToClose(for: name) {
openBody.removeLast(openBody.count - selectIndex)
}
if let closes = PureXML.HTML.Elements.impliedClose[name] {
while let top = openBody.last, top !== bodyRoot, closes.contains(tagName(top)) {
openBody.removeLast()

View File

@ -29,4 +29,10 @@ struct HTMLSelectTests {
func test_inputClosesSelect() {
#expect(body("<select><option>a<input></select>") == "<select><option>a</option></select><input>")
}
@Test("A table cell closes a select that is inside a table")
func test_selectInTable() {
let expected = "<table><tbody><tr><td>x<select><option>a</option></select></td><td>y</td></tr></tbody></table>"
#expect(body("<table><tr><td>x<select><option>a<td>y</table>") == expected)
}
}