Skip to content

feat: add MHTML support - #149

Open
marcellmanfrin wants to merge 26 commits into
firecrawl:mainfrom
marcellmanfrin:feature/mhtml-support-pr
Open

feat: add MHTML support#149
marcellmanfrin wants to merge 26 commits into
firecrawl:mainfrom
marcellmanfrin:feature/mhtml-support-pr

Conversation

@marcellmanfrin

@marcellmanfrin marcellmanfrin commented Aug 28, 2026

Copy link
Copy Markdown

Scope

Adds MHTML/MHT parsing and conversion, completing the MHTML portion of #52.

This head is explicitly reconciled with standalone HTML PR #147 head c6b7bb18608d4bde426a71d9a5300070d8f653fc.

The reconciled functional commit 32c647468193fbff6e4d21b07ab8395295e79e75 is a direct child of that #147 head. Its tree is 48566f9d8eee0e9836c7b48a05ea05c827a1a1ec, exactly the same tree as the previously validated/reviewed #149 head 17fe665d8f921bb1a881bb33a5ec4b87ceb43dd5. The reconciliation therefore changes ancestry only; it does not change the MHTML/HTML code content.

Summary

  • adds Format::Mhtml with .mhtml / .mht support
  • conservatively detects Chrome/Blink-style multipart/related archives without classifying ordinary multipart email as MHTML
  • decodes quoted-printable/base64 and honors MIME charset and start
  • resolves Content-ID, Content-Location, relative URLs, <base href>, and case-insensitive cid: references
  • preserves linked/inline CSS document order and embedded image assets
  • preflights decoded MHTML HTML roots before constructing the MHTML DOM

#147 reconciliation and realistic corpus

The branches were reconciled semantically rather than by blindly taking either side's HTML implementation. The current #149 commit is now directly based on the validated #147 head, while retaining the exact content tree of the previous #149 head. This proves the latest #147 anchor-recovery change was already present semantically in #149 and no MHTML-specific behavior was lost during the ancestry rewrite.

The #149 tree includes the shared HTML list fixes from #147, its controlled/LibreOffice HTML corpus, HTML list/corpus regressions and snapshots, while preserving the MHTML-specific MIME, charset, resource-resolution, CSS, and asset behavior.

tests/mhtml_corpus.rs reuses the #147 fixtures. It validates byte-for-byte standalone HTML vs MHTML invariance where MIME should not change semantics, base64 UTF-8 roots, realistic linked CSS and embedded images, deliberate relative-image resolution through MHTML Content-Location, and conservative handling of ordinary MIME messages.

Review follow-up fixes

The current head includes the earlier review fixes plus the latest Cubic follow-up:

  • bare relative references no longer match a MIME part solely because its Content-ID has the same bare text; Content-ID resources require cid: semantics
  • fragments on a part's Content-Location are removed from the resource-index key, matching fragment-stripped lookup behavior
  • HTML complexity preflight models HTML5 repair of repeated unclosed <a> start tags without accumulating false nesting depth
  • anchor recovery removes only the prior active anchor from the preflight stack instead of truncating intervening structural elements, preserving eager structural-depth accounting
  • review-specific regressions were moved into the discoverable feature suites: HTML cases in tests/html.rs, MHTML resource-resolution cases in tests/mhtml.rs; tests/review_followup.rs was removed

The earlier resource-safety fix remains: decoded MHTML HTML roots run preflight_html_complexity before the MHTML DOM is materialized.

The previous Cubic review of the identical content tree completed with 0 new issues in the latest delta. Because the #147 reconciliation rewrote branch ancestry, Cubic intentionally skipped its automatic review of the rewritten head. A manual re-review was triggered for exact head 32c647468193fbff6e4d21b07ab8395295e79e75 (check 99186037778); it has reviewed all 4 files and is currently completing its final review processing.

A separate P3 performance/maintainability thread about multiple HTML passes remains intentionally unchanged after audit: the bounded charset sniff, streaming pre-DOM complexity preflight, and actual HTML5 tree construction serve distinct purposes. Removing the full-document preflight would weaken pre-DOM resource enforcement; combining it with HTML5 tree construction would require a substantially larger parser/tree-sink refactor.

Fresh GitHub Actions validation

Validated functional SHA: 32c647468193fbff6e4d21b07ab8395295e79e75.

Parent HTML SHA: c6b7bb18608d4bde426a71d9a5300070d8f653fc (current validated head of #147).

Temporary CI-only branch: verify/mhtml-reconciled-c6b7bb1-full.
Temporary CI-only commit used to launch the authoritative gate: 156d6d858e1d5349e8716e278b329830187a071f. This commit is not part of the PR code; its sole parent is the validated functional SHA above, and the workflow explicitly checks out that SHA detached before testing.

Authoritative verification: run 33284379175, job 99184901026 — success.

  • HTML and MHTML regressions: passed (html, html_corpus, html_list, mhtml, mhtml_charset, mhtml_corpus)
  • Rust cargo fmt --all -- --check: passed
  • Rust workspace/all-targets/all-features Clippy with -D warnings: passed
  • Rust cargo test --locked: passed
  • Node npm ci, build, tests, and generated binding determinism: passed
  • WASM wasm32 Clippy, wasm-pack 0.15.0 release build, runtime tests: passed
  • Python locked Maturin release wheel, installed compiled site-packages module, full unittest suite: passed
  • real MHTML fixture validation: passed
  • exact candidate identity after all builds/tests: passed

The current PR head is exactly the validated functional SHA above.

Real MHTML validation

The authoritative run reconstructed and SHA-256-verified the original 505,883-byte SEI/TRE-PB Chrome/Blink MHTML fixture and converted it directly.

  • input SHA-256: 387c6f2d7223da224a8f55962b97eac947734c97fc84888e1e5619e93745837c
  • detected directly as mhtml
  • direct conversion without manual HTML extraction
  • expected Portuguese TRE-PB/ETP content present
  • no , Ã, or  mojibake markers
  • output size: 78,154 bytes
  • output SHA-256: f6f75cec7bc06afe038bd89cd7967e5af358ec3562553f5bfc5c47c0299eb80c

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 21 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread src/formats/html.rs
Comment thread src/formats/html.rs Outdated
Comment thread src/formats/html.rs Outdated
Comment thread src/formats/html.rs
Comment thread src/formats/detect.rs Outdated
Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/mhtml.rs Outdated
Comment thread src/lib.rs
Comment thread tests/mhtml.rs Outdated
@marcellmanfrin
marcellmanfrin force-pushed the feature/mhtml-support-pr branch from af6e811 to 049b63a Compare August 28, 2026 19:36
@marcellmanfrin

Copy link
Copy Markdown
Author

@cubic-dev-ai review this pull request

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 28, 2026

Copy link
Copy Markdown

@cubic-dev-ai review this pull request

@marcellmanfrin I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 21 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread src/formats/detect.rs
Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/html.rs Outdated
Comment thread src/formats/html.rs
Comment thread tests/mhtml.rs
Comment thread src/formats/html.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue 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/mhtml.rs">

<violation number="1" location="src/formats/mhtml.rs:152">
P3: The new `html_resource_base` re-parses the entire root HTML with `Html::parse_document`, and `collect_stylesheets_in_order` parses the same string again immediately after (plus a third parse inside `parse_text_with_context`). Parsing the document twice for resource-base extraction adds a redundant full-DOM-build of a potentially large HTML root. Parse the document once in `parse()` and pass the parsed root (or the extracted base) into both helpers to avoid the duplicate parse.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/mhtml.rs Outdated
@marcellmanfrin

Copy link
Copy Markdown
Author

@cubic-dev-ai Please re-review the two still-open older threads on this PR against the current head "f23fdfdddf6ec2de475c4727bb2455ec8b11a956":

  1. "src/lib.rs" — ordinary "multipart/related; type="text/html"" email being misdetected as MHTML (review comment 3882775384). The current implementation requires archive-specific evidence, and the regression "ordinary_related_html_email_is_not_mhtml" passes.

  2. "src/formats/detect.rs" — PDF-vs-HTML marker precedence (review comment 3883736268). The current detector behavior is covered by the passing regressions "pdf_header_before_html_marker_remains_pdf" and "html_prefix_wins_over_embedded_pdf_marker".

The current head has passed the complete AnyDoc validation gate, and the real 505,883-byte Chrome/Blink MHTML fixture was also converted directly and validated successfully.

Please verify whether these two findings still apply to the current head and resolve/update the old threads accordingly if they are now addressed.

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

@cubic-dev-ai Please re-review the two still-open older threads on this PR against the current head "f23fdfdddf6ec2de475c4727bb2455ec8b11a956":

  1. "src/lib.rs" — ordinary "multipart/related; type="text/html"" email being misdetected as MHTML (review comment 3882775384). The current implementation requires archive-specific evidence, and the regression "ordinary_related_html_email_is_not_mhtml" passes.

  2. "src/formats/detect.rs" — PDF-vs-HTML marker precedence (review comment 3883736268). The current detector behavior is covered by the passing regressions "pdf_header_before_html_marker_remains_pdf" and "html_prefix_wins_over_embedded_pdf_marker".
    ...

@marcellmanfrin I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

4 issues found across 21 files

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/mhtml.rs">

<violation number="1" location="src/formats/mhtml.rs:158">
P2: When an MHTML document contains `<base href="">`, relative embedded resources stop resolving against the root `Content-Location`. Treat an empty resolved base as absent so the root location remains the fallback.</violation>

<violation number="2" location="src/formats/mhtml.rs:418">
P2: When an embedded image or stylesheet URL includes a fragment, the lookup retains the fragment even though MIME parts are keyed by the underlying resource URL. Strip fragments for embedded-resource matching while preserving them for external image URLs.</violation>

<violation number="3" location="src/formats/mhtml.rs:424">
P2: When an MHTML root has no usable URL base, protocol-relative images are dropped because `is_absolute_uri` rejects `//host/path`. Preserve network-path references as external images, matching the standalone HTML frontend.</violation>
</file>

<file name="src/formats/html.rs">

<violation number="1" location="src/formats/html.rs:246">
P2: Alternating 257 heading tags triggers `max_xml_depth` before HTML5 parsing can repair them, because `close_implied_before_start` does not close prior `h1`–`h6` elements. Track heading tags as mutually implied-closing elements so malformed HTML is not rejected solely by the preflight stack approximation.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/mhtml.rs
Comment thread src/formats/html.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 21 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/mhtml.rs
Comment thread src/formats/html.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 21 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/mhtml.rs Outdated
Comment thread src/formats/html.rs
Comment thread src/formats/mhtml.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread tests/review_followup.rs Outdated
Comment thread src/formats/html.rs
@marcellmanfrin
marcellmanfrin force-pushed the feature/mhtml-support-pr branch from 17fe665 to 32c6474 Compare August 30, 2026 01:02
@marcellmanfrin

Copy link
Copy Markdown
Author

@cubic-dev-ai Please start a fresh AI code review of the current rewritten head 32c647468193fbff6e4d21b07ab8395295e79e75. The branch rewrite only reconciles ancestry with #147 head c6b7bb18608d4bde426a71d9a5300070d8f653fc; the candidate tree is exactly identical to the previously reviewed 17fe665d8f921bb1a881bb33a5ec4b87ceb43dd5 tree. A fresh full GitHub Actions gate also passed on the exact functional SHA via run 33284379175. Please review the current head after the rewrite.

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 30, 2026

Copy link
Copy Markdown

@cubic-dev-ai Please start a fresh AI code review of the current rewritten head 32c647468193fbff6e4d21b07ab8395295e79e75. The branch rewrite only reconciles ancestry with #147 head c6b7bb18608d4bde426a71d9a5300070d8f653fc; the candidate tree is exactly identical to the previously reviewed 17fe665d8f921bb1a881bb33a5ec4b87ceb43dd5 tree. A fresh full GitHub Actions gate also passed on the exact functional SHA via run 33284379175. Please review the current head after the rewrite.

@marcellmanfrin I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 38 files

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:64">
P2: Valid frameset HTML has no `<body>`, so this standalone parser returns `Malformed` for a supported HTML5 document. Handle a frameset/no-body tree as an empty convertible document or convert its frame sources instead of unconditionally erroring.</violation>

<violation number="2" location="src/formats/html.rs:493">
P2: When standalone HTML uses a relative `<img src>` without alt text, `StandaloneCtx::image_source` drops the image instead of preserving its relative URL. Return a URL-bearing image source for nonempty relative references so Markdown retains the image.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/html.rs
}
}

let body = root

@cubic-dev-ai cubic-dev-ai Bot Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Valid frameset HTML has no <body>, so this standalone parser returns Malformed for a supported HTML5 document. Handle a frameset/no-body tree as an empty convertible document or convert its frame sources instead of unconditionally erroring.

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 64:

<comment>Valid frameset HTML has no `<body>`, so this standalone parser returns `Malformed` for a supported HTML5 document. Handle a frameset/no-body tree as an empty convertible document or convert its frame sources instead of unconditionally erroring.</comment>

<file context>
@@ -0,0 +1,530 @@
+        }
+    }
+
+    let body = root
+        .descendent_elements()
+        .find(|e| e.value().name() == "body")
</file context>
Fix with cubic

Comment thread src/formats/html.rs
if src.is_empty() {
return Ok(None);
}
Ok((is_absolute_uri(src) || src.starts_with("//"))

@cubic-dev-ai cubic-dev-ai Bot Aug 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When standalone HTML uses a relative <img src> without alt text, StandaloneCtx::image_source drops the image instead of preserving its relative URL. Return a URL-bearing image source for nonempty relative references so Markdown retains the image.

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 493:

<comment>When standalone HTML uses a relative `<img src>` without alt text, `StandaloneCtx::image_source` drops the image instead of preserving its relative URL. Return a URL-bearing image source for nonempty relative references so Markdown retains the image.</comment>

<file context>
@@ -0,0 +1,530 @@
+        if src.is_empty() {
+            return Ok(None);
+        }
+        Ok((is_absolute_uri(src) || src.starts_with("//"))
+            .then(|| ImageSource::External(src.to_owned())))
+    }
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant