feat: add standalone HTML support - #147
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 16 files
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
25cde7a to
341be1b
Compare
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
2 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/formats/html.rs">
<violation number="1" location="src/formats/html.rs:179">
P1: For HTML-style `<div/>` chains, this condition undercounts nesting because non-void self-closing flags are ignored by the HTML tree builder. Track those elements as open, while handling foreign-content self-closing elements separately, so pathological depth is rejected before DOM construction.</violation>
<violation number="2" location="src/formats/html.rs:222">
P2: When malformed HTML relies on HTML5 heading repair, the preflight rejects it as too deep even though html5ever would produce sibling headings. Add the heading implied-closing rule (and the other HTML5 tree-builder closures) before applying the depth limit.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
2 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/formats/detect.rs">
<violation number="1" location="src/formats/detect.rs:76">
P2: A UTF-16 doctype with more than 51 whitespace code units between `DOCTYPE` and `html` is not detected because the fixed prefix ends before the name. Scan the doctype marker state instead of truncating detection at 64 code units.</violation>
</file>
<file name="Cargo.toml">
<violation number="1" location="Cargo.toml:38">
P3: This change drops the trailing newline at the end of Cargo.toml (`strip = "symbols"` now ends the file with no newline). The target branch ends the file with a trailing newline. Restore the trailing newline to keep the file clean and avoid noisy diffs.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 16 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Cubic findings (PR 149 review run ed78d83a, shared HTML frontend):
1. The complexity preflight did not model HTML5's implicit <p> closure
on block-level start tags, so repeated <p><div> pairs accumulated
phantom nesting depth: documents html5ever repairs into shallow trees
were rejected and the depth accounting diverged from the DOM the
limits are meant to model. Close an innermost open <p> when a
block-level start tag arrives; deeper arrangements remain
over-counted, keeping the preflight fail-closed.
2. href="#" produced LinkTarget::Anchor(""), which the Markdown
renderer cannot resolve, so the link was dropped to plain text.
Preserve the empty fragment as a relative "#" URL instead.
Regressions: 200 <p><div> pairs now convert (depth 200 < limit), 300
pairs are still rejected before DOM construction, and a bare-hash link
renders as [top](#).
Cubic (PR 149, src/formats/html.rs:308) claimed dialog and summary do not implicitly close an open <p>; html5ever's in-body mode (tree_builder/rules.rs) proves they do. Keep both and complete the preflight list to the parser's full close_p_element_in_button_scope set: center, dir, listing, plaintext, and search were missing. table is retained: html5ever closes <p> for <table> outside quirks mode, and the preflight assumes standards mode.
|
@cubic-dev-ai ultrareview: focus on the HTML complexity preflight (implied-end-tag modeling for paragraphs, headings, anchors, lists), charset/detection handling, and the standalone HTML conversion path. |
@marcellmanfrin Couldn't start the ultrareview: PR author |
… stray list text Ultrareview findings (fork staging PR #8): - P1: after a foreign-content breakout (<svg><p>), the preflight kept the stale svg entry, so subsequent HTML self-closing tags were honored as foreign and skipped depth accounting; malformed input could force construction of an arbitrarily deep DOM before any limit fired. Pop through the nearest svg/math ancestor when an HTML breakout tag arrives, before classifying self-closing tags. - optgroup: model the HTML5 select insertion rule (close an open option and the enclosing optgroup). - table row groups: tbody/thead/tfoot starts close the previous row group. - headings: keep the parser-faithful single pop; html5ever pops only when the current node is already a heading (verified against html5ever 0.39's in-body arm), so headings separated by unclosed inlines genuinely nest and are now pinned by a preflight regression. - shared list parsing preserves visible text left directly under ul/ol by malformed markup instead of dropping it. - python: add "html" to the public Format alias in __init__.py (the private stub already had it). - README: list HTML in the overview and the detection bullet. - tests: drop the exact golden copy already recorded by the snapshot harness; add regressions for all behavioral changes.
Cubic finding (staging PR #10, src/formats/html.rs): the complexity preflight switched the tokenizer to RCDATA/RawText for title, textarea, style, xmp, iframe, noembed, noframes, and plaintext regardless of context. Inside foreign content those names are ordinary elements (svg title/desc/foreignObject are HTML integration points, so their children are parsed as markup), and the preflight therefore missed real element depth and node counts: malformed input such as <svg><title> followed by hundreds of unclosed <div> passed the pre-DOM limit while html5ever built the deep tree anyway. Decide the tokenizer state from the foreign-content context computed before the element is pushed; SVG script remains the one foreign case that switches the tokenizer. Factor the context scan out of html5_self_closing_is_honored into in_foreign_content. Regressions: <svg><title> + 300 unclosed <div> is rejected before DOM construction; an HTML <title> still swallows markup as raw text.
Cubic finding (staging PR #11, src/formats/html.rs): the preflight switched to ScriptData for every script start tag. html5ever 0.39 does not switch the tokenizer for scripts inside foreign content (its foreign_start_tag has no ScriptData transition), so markup after <svg><script> or <math><script> really is tokenized as elements while the preflight swallowed it as raw text, letting deeply nested input bypass the pre-DOM limits. Gate the ScriptData transition on HTML content like the other raw-text states. Regressions: <svg><script> followed by 300 unclosed <div> is rejected before DOM construction; an HTML <script> still swallows markup as script data.
…n roots Review findings (staging PR #11, src/formats/html.rs): - The preflight applied HTML implied-end-tag rules (p, li, option, tr, headings, anchors) inside foreign content too. There those names are ordinary elements, so applying the rules undercounted real nesting and deeply nested foreign markup could reach DOM construction. Skip close_implied_before_start when the stack is in foreign content. - On a foreign-content breakout, pop through the OUTERMOST svg/math root reachable without crossing an HTML integration point, matching html5ever's pop-until-HTML behavior. Truncating only the innermost root left an outer svg on the stack, so later HTML self-closing tags were still honored as foreign and bypassed the depth guard. - Pin the template behavior with a regression: a <body> inside <template> must not capture document conversion (html5ever keeps template contents out of the main tree, verified empirically). Regressions: 300 nested <option> inside <svg> are rejected before DOM construction; <svg><math><p> breakout followed by 300 <path/> is rejected before DOM construction; template body converts the real body.
Review findings (staging PR #11 and #10): - Void HTML names inside foreign content are ordinary foreign elements that html5ever pushes (input, param, source, ...); only void names in HTML content skip the stack, and they still run the implied-end-tag rules now, so <hr> closes an open <p> as the parser does. - Depth is counted the way the post-parse walk sees it: adapt_element treats body as depth 1 and html5ever auto-inserts html/body wrappers that may never appear as tokens. The preflight now computes the body-relative depth (last body token, else last html token, else the implicit wrapper), which removes both the off-by-one rejection of documents at exactly the limit and the undercount for documents without wrapper tokens. - End tags now respect scope: html5ever ignores an end tag whose element is not in scope, while the preflight used to truncate the stack at any matching name, popping real nesting and undercounting depth. Table family end tags use the narrower table scope. - Strengthen the repeated-anchors regression to require a successful conversion, not merely the absence of a depth error. Regressions: 255 nested divs at the exact boundary convert while 256 are rejected before DOM construction; 300 <p>x<hr> pairs convert; 300 <input> inside <svg> are rejected before DOM construction; an out-of-scope </div> above 300 nested spans no longer pops them.
Review findings (staging PRs #11 and #10): - Duplicate <html>/<body> start tags are ignored by html5ever; pushing them moved the body-relative depth baseline upward and undercounted every later descendant. Skip a wrapper token when that wrapper is already on the modeled stack. - End-tag scope rules are now tag-specific like the parser's: the table family uses table scope (html, table, template), </li> uses list-item scope (adds ol/ul/button), </p> uses button scope (adds button), and svg/math foreign roots end every HTML scope search. Previously an end tag crossing a template, ol/ul, button, or foreign root truncated the modeled stack that html5ever leaves intact, undercounting depth. - Refactor the repeated inline depth-error assertions in tests/html.rs to the existing assert_preflight_depth_limit helper. Regressions: a duplicate <body> no longer moves the baseline (260 divs rejected before DOM construction); </p> blocked by button scope keeps 150+150 nested divs counted; </table> blocked by a template keeps 260 nested spans counted.
- close_element: </body> and </html> never truncate the open-element stack; html5ever only switches insertion modes and later start tags keep nesting, so truncating undercounted real open depth. - duplicate html/body wrapper suppression now skips foreign content, where <html> is an ordinary foreign element, not a duplicate wrapper. - is_end_tag_scope_marker: drop "button" from li list-item scope markers per the HTML5 spec; </li> must still close the item past an open button. - refresh stale hr comment: void tags reach the implied-close hook via close_implied, so <hr> does close an open <p>. Adds regression tests for all three behavior fixes.
Preflight fidelity fixes for the HTML complexity sink (cubic round C): - close_element: any-other end tags now stop at html5ever's full special category (plus svg/math foreign roots), not just the 9 generic scope markers. Previously </em> below <em><div><span> truncated real nesting the parser keeps open, undercounting depth and defeating the reject-before-DOM guarantee. The div family, button, form, headings, and template keep their generic-scope rule; dd/dt use button scope and rt/rp/rb/rtc use ruby scope, matching the parser. - close_element: </body>/</html> only stay no-ops in HTML content; inside foreign content they are ordinary foreign elements whose end tags pop. - close_element: option/optgroup end tags only check the current node, like html5ever; deeper searches truncated real nesting. - close_implied_before_start: li/dd/dt walk from the innermost element and stop at any special element other than address/div/p; p closes only within button scope; rt/rp pop only the current node; option pops only the current node and optgroup additionally closes an innermost optgroup inside a select context. - close_implied_before_start: table-family start tags (caption/col/ colgroup/tbody/td/tfoot/th/thead/tr) outside a table are ignored entirely, matching body-context parse errors; pushing them let a later implied close truncate divs opened between stray cells (undercount). Inside a table the row/body-context clears are unchanged. Adds 8 regression tests (66 html integration tests total).
Cubic (run bad77c8a, conf6) proposed treating bare svg/math as pass-through in is_end_tag_scope_marker, aligning the markers with the in-body default_scope set. That proposal is rejected: while a bare svg/math root is reachable, html5ever resolves end tags in the foreign-content phase, whose walk stops at the first HTML element or integration point below the root and ignores the token — the same outcome as the modeled root stop. Integration points always sit below their root, so both marker sets stop identically; dropping the root stop instead would let scope-based end tags truncate through an open foreign root that the parser ignores, undercounting depth. Verified empirically: with svg/math made pass-through, this test fails (the deep foreign nesting passes preflight); with the root stop it rejects before DOM construction, matching the parser.
Cubic (run d03d9e02, tests/html.rs pin) proved — and html5ever 0.39 source confirms — that the previous root-stop model was wrong: - step_foreign (rules.rs 1652-1684): an end tag in foreign content walks the stack for a name match in ANY namespace and truncates there; on reaching an HTML element below the root without a match it REPROCESSES the token in the current insertion mode. - default_scope (tag_sets.rs): html_default_scope (incl. select) plus the MathML text and SVG HTML integration points; bare svg/math are NOT markers, so reprocessed scope searches pop through open foreign roots to an in-scope target (e.g. </p> under <p><svg><g> closes the p). - process_end_tag_in_body (mod.rs 1563): any-other end tags stop at special_tag (incl. isindex; integration points are not special). Changes: - close_element: unified walk without the svg/math root stop; body/html and option/optgroup use a foreign-region match (>= foreign_root_index) because their in-body rules never pop, while foreign-region matches do. - GENERIC_SCOPE_MARKERS: add select and the integration-point names (matched without namespaces; HTML title collision is harmless). - is_special_element: align exactly with html5ever special_tag. - pop_foreign_breakout refactored onto the new foreign_root_index helper. - Replace the wrong-premise pin test with two source-verified ones: scope end tags pop through foreign roots to their target (converts), and unmatched end tags still stop at special elements below roots (preflight rejects). All 68 html integration tests pass; fixtures byte-identical. Supersedes the round-C N2 rejection: the rejection was wrong about the mechanism (foreign-phase name-match pop is real); its undercount warning applies only to dropping the stop WITHOUT the region logic, which this commit implements properly.
Cubic round D (PR #10 run 10ff97ec, P2 conf8): html5ever ignores a stray frame start tag in body context and inserts-then-immediately-pops it in frameset context, so frames never nest in the real DOM. The preflight stacked every frame, falsely reporting max_xml_depth for framesets with more than 256 sibling frames. Treat frame like the void elements in HTML content (it still pushes inside foreign content, where it is an ordinary foreign element). Verified discriminating: without the fix, the new frameset test fails; with it, 70/70 html tests pass. Also pins two round-D rejections grounded in html5ever 0.39 source: - markup inside <select> really nests (html5ever has no in-select insertion mode; InBody inserts it), so the depth guard must fire — ignoring select content would undercount the real DOM. - the unconditional title/textarea raw-text switch cannot hide deep nesting: every mode that accepts those tags inserts them, and the frameset modes that ignore them also ignore all later start tags (comment only, no behavior change).
Cubic round E (PR #11 run e74d240e, P2 conf9): html5ever's InTable arms for caption, col, colgroup, tbody/tfoot/thead, and td/th/tr all begin with pop_until_current(table_scope) — stray foster-parented content above the table leaves the open-element stack. The preflight kept it, so repaired table markup with deep stray content accumulated phantom depth and could be falsely rejected at max_xml_depth. Model the clear (stopping at the innermost table/template/html, exactly the table_scope set); the phantom tbody/tr wrappers html5ever inserts afterwards remain unmodeled (bounded, documented deviation). The previous per-tag rposition truncation is subsumed by the clear and was removed — it could also reach below the current table for stale matches. Also strengthens the raw-text tests (P3 conf7): the script test moves into the body and asserts the script source is NOT rendered, so it actually distinguishes ScriptData swallowing from data-state parsing; the title test gains the same negative assertion. Regression test: table_family_starts_clear_stray_content_above_the_table (253 stray divs then tbody/tr/td must convert). Discrimination verified: without the clear the test fails; with it, 71/71 html tests pass.
Cubic round F (PR #11 run 7cf7afee, P2 conf9): when a block start tag arrived with an open <p> holding inline descendants, the preflight left the paragraph (and its descendants) on the modeled stack — html5ever closes any button-scope p first (close_p_element_in_button_scope, mod.rs), popping the descendants with it. The stale p was worse than an overcount: the next p start's button-scope walk would find it BELOW intervening blocks opened since (blockquote, etc.) and truncate through them, undercounting the nesting the parser really keeps open — <blockquote><p><b> repeated stays flat in the model while html5ever nests the blockquotes without bound, defeating the reject-before-DOM guarantee. Walk with the button-scope markers for every paragraph-closing block start (p included, so the duplicate p-start branch is removed). Regression test block_starts_close_paragraphs_with_inline_descendants: 255 iterations must reject at preflight depth. Discrimination verified against the exact previous state (innermost-only block close + p-start walk): the test fails there and passes with the fix. 72/72 html tests.
There was a problem hiding this comment.
3 issues found across 6 files (changes from recent commits).
Confidence score: 3/5
src/formats/html.rsdrops modeledtbody/trcontexts before<td>handling, allowing deeply nested tables to bypass the preflight depth limit and potentially fail during DOM construction — preserve implicit table contexts in the shadow stack.src/shared/html.rsemits separate paragraphs when visible list text is split by a non-rendering node, which loses whitespace and inline continuity in rendered output — accumulate adjacent direct text into one block.tests/html.rsdoes not actually verify RCDATA semantics for a non-foreign<title>becauseparseonly adapts<body>insrc/formats/html.rs; strengthen the test or adjust the conversion path so this behavior is covered.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/shared/html.rs">
<violation number="1" location="src/shared/html.rs:595">
P2: When visible list text is split by a non-rendering node, `text_blocks` renders each text node as a separate paragraph, losing inter-node whitespace and inline continuity. Accumulate adjacent direct text in one block context and flush it only at a real rendered block or list-item boundary.</violation>
</file>
<file name="src/formats/html.rs">
<violation number="1" location="src/formats/html.rs:653">
P2: Normal table markup discards the modeled `tbody`/`tr` depth at each `<td>`, so the preflight no longer enforces the depth limit before DOM construction. Preserve the implicit table contexts in the shadow stack, or otherwise account for their depth before accepting the input.</violation>
</file>
<file name="tests/html.rs">
<violation number="1" location="tests/html.rs:451">
P3: This test claims to verify that a non-foreign <title> keeps RCDATA semantics, but its assertions cannot detect that behavior. The conversion path only adapts the <body> element (src/formats/html.rs `parse` adapts `body` and passes it to `to_blocks`), so `<title>` content in `<head>` is never rendered to markdown. The `!markdown.contains("not markup")` assertion therefore passes whether the title is parsed as RCDATA text or as nested markup, and the `contains("ok")` assertion is unrelated. The test offers no regression protection for the RCDATA behavior it names. Mirror the sibling foreign-content test: nest many `<div>`s inside the title and assert the document still converts (if title were parsed as markup the preflight would reject at max_xml_depth, so this distinguishes the two cases).</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| let child = match child { | ||
| Node::Elem(child) => child, | ||
| Node::Text(text) => { | ||
| let text_blocks = self.text_blocks(text, delta); |
There was a problem hiding this comment.
P2: When visible list text is split by a non-rendering node, text_blocks renders each text node as a separate paragraph, losing inter-node whitespace and inline continuity. Accumulate adjacent direct text in one block context and flush it only at a real rendered block or list-item boundary.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/shared/html.rs, line 595:
<comment>When visible list text is split by a non-rendering node, `text_blocks` renders each text node as a separate paragraph, losing inter-node whitespace and inline continuity. Accumulate adjacent direct text in one block context and flush it only at a real rendered block or list-item boundary.</comment>
<file context>
@@ -558,13 +573,40 @@ impl Builder<'_> {
+ let child = match child {
+ Node::Elem(child) => child,
+ Node::Text(text) => {
+ let text_blocks = self.text_blocks(text, delta);
+ if text_blocks.is_empty() {
+ continue;
</file context>
| // html5ever inserts afterwards are not modeled (a bounded, | ||
| // documented deviation), and a template above the table stops the | ||
| // clear exactly as table_scope does. | ||
| if let Some(clear_to) = open |
There was a problem hiding this comment.
P2: Normal table markup discards the modeled tbody/tr depth at each <td>, so the preflight no longer enforces the depth limit before DOM construction. Preserve the implicit table contexts in the shadow stack, or otherwise account for their depth before accepting the input.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/formats/html.rs, line 653:
<comment>Normal table markup discards the modeled `tbody`/`tr` depth at each `<td>`, so the preflight no longer enforces the depth limit before DOM construction. Preserve the implicit table contexts in the shadow stack, or otherwise account for their depth before accepting the input.</comment>
<file context>
@@ -216,45 +365,322 @@ impl TokenSink for HtmlComplexitySink {
+ // html5ever inserts afterwards are not modeled (a bounded,
+ // documented deviation), and a template above the table stops the
+ // clear exactly as table_scope does.
+ if let Some(clear_to) = open
+ .iter()
+ .rposition(|candidate| matches!(candidate.as_ref(), "table" | "template" | "html"))
</file context>
| fn html_title_still_swallows_markup_as_raw_text() { | ||
| // The HTML (non-foreign) <title> keeps RCDATA semantics: nested markup is | ||
| // text, not elements, and the document converts normally. | ||
| let html = br#"<!doctype html><html><head><title><div>not markup</div></title></head><body><p>ok</p></body></html>"#; | ||
| let markdown = to_markdown_bytes(html, Some(Format::Html)).unwrap(); | ||
| assert!(markdown.contains("ok"), "got: {markdown:?}"); |
There was a problem hiding this comment.
P3: This test claims to verify that a non-foreign <title> keeps RCDATA semantics, but its assertions cannot detect that behavior. The conversion path only adapts the element (src/formats/html.rs parse adapts body and passes it to to_blocks), so <title> content in <head> is never rendered to markdown. The !markdown.contains("not markup") assertion therefore passes whether the title is parsed as RCDATA text or as nested markup, and the contains("ok") assertion is unrelated. The test offers no regression protection for the RCDATA behavior it names. Mirror the sibling foreign-content test: nest many <div>s inside the title and assert the document still converts (if title were parsed as markup the preflight would reject at max_xml_depth, so this distinguishes the two cases).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/html.rs, line 451:
<comment>This test claims to verify that a non-foreign <title> keeps RCDATA semantics, but its assertions cannot detect that behavior. The conversion path only adapts the <body> element (src/formats/html.rs `parse` adapts `body` and passes it to `to_blocks`), so `<title>` content in `<head>` is never rendered to markdown. The `!markdown.contains("not markup")` assertion therefore passes whether the title is parsed as RCDATA text or as nested markup, and the `contains("ok")` assertion is unrelated. The test offers no regression protection for the RCDATA behavior it names. Mirror the sibling foreign-content test: nest many `<div>`s inside the title and assert the document still converts (if title were parsed as markup the preflight would reject at max_xml_depth, so this distinguishes the two cases).</comment>
<file context>
@@ -374,3 +374,505 @@ fn bare_hash_link_is_preserved_as_relative_url() {
+}
+
+#[test]
+fn html_title_still_swallows_markup_as_raw_text() {
+ // The HTML (non-foreign) <title> keeps RCDATA semantics: nested markup is
+ // text, not elements, and the document converts normally.
</file context>
| fn html_title_still_swallows_markup_as_raw_text() { | |
| // The HTML (non-foreign) <title> keeps RCDATA semantics: nested markup is | |
| // text, not elements, and the document converts normally. | |
| let html = br#"<!doctype html><html><head><title><div>not markup</div></title></head><body><p>ok</p></body></html>"#; | |
| let markdown = to_markdown_bytes(html, Some(Format::Html)).unwrap(); | |
| assert!(markdown.contains("ok"), "got: {markdown:?}"); | |
| #[test] | |
| fn html_title_still_swallows_markup_as_raw_text() { | |
| // HTML <title> keeps RCDATA semantics: nested markup is text, so the | |
| // preflight must not count it and the document converts. | |
| let mut html = String::from("<!doctype html><html><head><title>"); | |
| for _ in 0..300 { | |
| html.push_str("<div>"); | |
| } | |
| html.push_str("</title></head><body><p>ok</p></body></html>"); | |
| let markdown = to_markdown_bytes(html.as_bytes(), Some(Format::Html)).unwrap(); | |
| assert!(markdown.contains("ok"), "got: {markdown:?}"); | |
| } | |
Scope
Adds standalone HTML support (
.html/.htm) for the HTML portion of #52. MHTML/MHT remains out of scope here and is handled separately in #149.Summary
Format::Html, extension mapping, UTF-8/UTF-16 content detection, charset sniffing, and tolerant HTML5 parsing viascraper/html5everReview follow-ups
The current head includes the earlier charset, detector-precedence, malformed-HTML repair, list, heading, foreign-content, and anchor-preflight fixes.
The latest Cubic findings discovered while reviewing #149 were reproduced against the previous #147 head and fixed here because they belong to the shared HTML frontend:
framesetdocuments no longer fail solely because they have no<body>; they convert to an empty document when there is no renderable body content<img src>values are preserved as image references, just like absolute and protocol-relative references; anydoc still does not fetch or load those resourcesTDD evidence
RED branch:
audit/html-cubic-frameset-relative-red, based on previous headc6b7bb18608d4bde426a71d9a5300070d8f653fc.GitHub Actions run
33300432395, job99227423622reproduced both findings:Malformed("HTML parser produced no body element")Validated functional SHA:
ce7948287c8b4a62666f851a5845beabb091ef38.The functional diff from the previous head is limited to
src/formats/html.rs,tests/html.rs, and the expected LibreOffice snapshot update.Fresh full validation
Temporary CI-only branch:
verify/html-cubic-frameset-relative-full-v2.CI-only commit:
56cf2e57fc02f17c20f1ede780524b99e4acaed2. Its sole parent is the validated functional SHAce7948287c8b4a62666f851a5845beabb091ef38; every job explicitly checks out that functional SHA detached.Authoritative GitHub Actions run:
33300809474— all 6 jobs passed.html,html_list,html_corpus, snapshots, and content-detection regressions: passed-D warnings, andcargo test --locked: passednpm ci, build, tests, and committed binding determinism: passednode --test wasm/test.mjs: passedsite-packagesmodule, and full unittest suite: passedReal validation fixture:
387c6f2d7223da224a8f55962b97eac947734c97fc84888e1e5619e93745837c32fca03d52dc62518bf4419e2a4b1be96160195cd99f613a2d3230f1bb1e817c6119ae4bb085acabe21149492020e159e431de4f7a169350b1b619f2add61c1a�,Ã, orÂmojibake markersThe current PR head is exactly the validated functional SHA above.