Skip to content

refactor(md/parse): parse HTML into one single node#10848

Merged
ematipico merged 2 commits into
mainfrom
refactor/html-single-text
Jul 3, 2026
Merged

refactor(md/parse): parse HTML into one single node#10848
ematipico merged 2 commits into
mainfrom
refactor/html-single-text

Conversation

@ematipico

Copy link
Copy Markdown
Member

Summary

Implemented with a coding agent.

The HTML was being parsed as normal markdown text. This is incorrect because it prevents us from enabling embedding.

This PR changes its parsing by storing all content inside one single literal token. That's what we do in the HTML parser, so it's a solution that works for us.

It required some tuning. I left comments where it required an explanation.

Test Plan

Updated tests

Docs

N/A

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

✅ Organic activity

No automation signals detected in the analyzed events.

View full analysis →

This is an automated analysis by AgentScan

@ematipico
ematipico requested review from a team July 3, 2026 13:56
@github-actions github-actions Bot added A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools L-Markdown Language: Markdown labels Jul 3, 2026
@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5819bde

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 53419 53419 0
Passed 52138 52138 0
Failed 1239 1239 0
Panics 42 42 0
Coverage 97.60% 97.60% 0.00%

jsx/babel

Test result main count This PR count Difference
Total 38 38 0
Passed 37 37 0
Failed 1 1 0
Panics 0 0 0
Coverage 97.37% 97.37% 0.00%

markdown/commonmark

Test result main count This PR count Difference
Total 652 652 0
Passed 652 652 0
Failed 0 0 0
Panics 0 0 0
Coverage 100.00% 100.00% 0.00%

symbols/microsoft

Test result main count This PR count Difference
Total 5467 5467 0
Passed 1915 1915 0
Failed 3552 3552 0
Panics 0 0 0
Coverage 35.03% 35.03% 0.00%

ts/babel

Test result main count This PR count Difference
Total 658 658 0
Passed 574 574 0
Failed 84 84 0
Panics 0 0 0
Coverage 87.23% 87.23% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18876 18876 0
Passed 13010 13010 0
Failed 5865 5865 0
Panics 1 1 0
Coverage 68.92% 68.92% 0.00%

@codspeed-hq

codspeed-hq Bot commented Jul 3, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 18.13%

⚡ 4 improved benchmarks
✅ 24 untouched benchmarks
⏩ 228 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
spec/inline-html.md[uncached] 1.9 ms 1.6 ms +21.31%
spec/inline-html.md[cached] 1.9 ms 1.6 ms +20.9%
synthetic/inline-html.md[uncached] 1.3 ms 1.1 ms +15.44%
synthetic/inline-html.md[cached] 1.3 ms 1.1 ms +14.99%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing refactor/html-single-text (5819bde) with main (e324b89)

Open in CodSpeed

Footnotes

  1. 228 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds MdHtmlContent as a dedicated HTML block content node, with lexer/parser support to re-lex the block as a single MD_HTML_LITERAL, and updates HTML rendering and formatting to use that node directly. It also refactors markdown newline formatting to use TextPrintMode instead of a boolean removal flag, updates list-block call sites, and adjusts bullet-list newline handling around HTML and fenced-code blocks.

Possibly related PRs

  • biomejs/biome#9917: Closely related FormatMdNewline and block-list newline handling changes in the markdown formatter.
  • biomejs/biome#10383: Also changes ListBlockList newline and blank-line emission in bullet_list.rs.
  • biomejs/biome#10833: Also touches bullet-list break/newline handling around fenced blocks.

Suggested labels: L-HTML

Suggested reviewers: dyc3

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: parsing HTML into a single node for md/parse.
Description check ✅ Passed The description is directly related to the change and explains the HTML parsing refactor and motivation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/html-single-text

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_markdown_parser/src/syntax/html_block.rs (1)

337-347: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop on container-prefixed blank lines.

For type 6/7 blocks inside quotes/lists, a blank continuation line looks like NEWLINE + prefix + NEWLINE. After skip_container_prefixes(p), the parser can be sitting on that blank line’s NEWLINE, but the loop continues and absorbs following content into the HTML block.

Suggested fix
             if at_container_boundary(p) {
                 break;
             }
             skip_container_prefixes(p);
+            if p.at(NEWLINE) {
+                break;
+            }
             continue;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_markdown_parser/src/syntax/html_block.rs` around lines 337 -
347, The HTML block continuation logic in html_block should stop when a blank
continuation line is encountered after container prefixes, not keep consuming
the next line. In the loop around at_container_boundary and
skip_container_prefixes, detect the pattern where a NEWLINE is followed by a
container prefix and then another NEWLINE, and break before continuing so the
parser does not absorb following content into the block. Use the existing
helpers at_container_boundary(p), skip_container_prefixes(p), and the html block
parsing loop in the relevant block type handling to place the early exit in the
right spot.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/biome_markdown_parser/src/syntax/html_block.rs`:
- Around line 301-313: The HTML block tokenization in html_block.rs is currently
including container prefixes in MD_HTML_LITERAL, which causes raw quote/list
markers to be treated as HTML content. Update the html block parsing path around
p.lookahead, p.re_lex_html_content, and the MD_HTML_CONTENT/MD_HTML_LITERAL
construction so container prefixes remain separate CST nodes or are stripped
before storing the raw literal. Make sure MdHtmlContent.value_token() only
returns actual HTML source, not leading container prefix text.
- Around line 305-308: The lookahead in html_block parsing is mutating parser
state through advance_until_terminator/advance_until_blank_line, which can leak
virtual_line_start changes past the checkpoint. Update the logic around the
p.lookahead call in html_block.rs to keep the measuring pass side-effect free by
saving and restoring the relevant parser state (especially
state.virtual_line_start), or refactor the container-prefix scan used by
skip_container_prefixes to be pure.

---

Outside diff comments:
In `@crates/biome_markdown_parser/src/syntax/html_block.rs`:
- Around line 337-347: The HTML block continuation logic in html_block should
stop when a blank continuation line is encountered after container prefixes, not
keep consuming the next line. In the loop around at_container_boundary and
skip_container_prefixes, detect the pattern where a NEWLINE is followed by a
container prefix and then another NEWLINE, and break before continuing so the
parser does not absorb following content into the block. Use the existing
helpers at_container_boundary(p), skip_container_prefixes(p), and the html block
parsing loop in the relevant block type handling to place the early exit in the
right spot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 73075030-b646-4d0f-8b7e-71a84a86a880

📥 Commits

Reviewing files that changed from the base of the PR and between dbe05f4 and 6cffc5f.

⛔ Files ignored due to path filters (15)
  • crates/biome_markdown_factory/src/generated/node_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_factory/src/generated/syntax_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/code/additional-space.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/issue-17652.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/markdown/test-case.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-223.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-278.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-281.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_parser/tests/md_test_suite/ok/html_block.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_parser/tests/md_test_suite/ok/html_block_in_list.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_parser/tests/md_test_suite/ok/html_block_in_list_continuation.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_syntax/src/generated/kind.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_syntax/src/generated/macros.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_syntax/src/generated/nodes.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_syntax/src/generated/nodes_mut.rs is excluded by !**/generated/**, !**/generated/** and included by **
📒 Files selected for processing (14)
  • crates/biome_markdown_formatter/src/bullet_list.rs
  • crates/biome_markdown_formatter/src/generated.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/html_content.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/mod.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/newline.rs
  • crates/biome_markdown_formatter/src/markdown/lists/block_list.rs
  • crates/biome_markdown_parser/src/lexer/mod.rs
  • crates/biome_markdown_parser/src/parser.rs
  • crates/biome_markdown_parser/src/syntax/html_block.rs
  • crates/biome_markdown_parser/src/to_html.rs
  • crates/biome_markdown_parser/src/token_source.rs
  • crates/biome_markdown_syntax/src/block_ext.rs
  • xtask/codegen/markdown.ungram
  • xtask/codegen/src/markdown_kinds_src.rs

Comment thread crates/biome_markdown_parser/src/syntax/html_block.rs
Comment thread crates/biome_markdown_parser/src/syntax/html_block.rs
@ematipico
ematipico force-pushed the refactor/html-single-text branch from 6cffc5f to b80ff6c Compare July 3, 2026 14:12
@ematipico
ematipico force-pushed the refactor/html-single-text branch from b80ff6c to 5819bde Compare July 3, 2026 14:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
crates/biome_markdown_formatter/src/markdown/lists/block_list.rs (2)

460-486: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate lookahead helpers.

next_content_block_is_thematic_break and next_content_block_is_link_reference_definition are identical except for the final predicate. Consider merging into one generic helper taking a predicate closure.

♻️ Suggested consolidation
-fn next_content_block_is_thematic_break(
-    block_list: &MdBlockList,
-    start: usize,
-    content_count: usize,
-) -> bool {
-    block_list
-        .iter()
-        .enumerate()
-        .skip(start)
-        .take_while(|(index, _)| *index < content_count)
-        .find(|(_, block)| !block.is_newline())
-        .is_some_and(|(_, block)| block.is_thematic_break())
-}
-
-fn next_content_block_is_link_reference_definition(
-    block_list: &MdBlockList,
-    start: usize,
-    content_count: usize,
-) -> bool {
-    block_list
-        .iter()
-        .enumerate()
-        .skip(start)
-        .take_while(|(index, _)| *index < content_count)
-        .find(|(_, block)| !block.is_newline())
-        .is_some_and(|(_, block)| block.is_link_reference_definition())
-}
+fn next_content_block_matches(
+    block_list: &MdBlockList,
+    start: usize,
+    content_count: usize,
+    predicate: impl Fn(&AnyMdBlock) -> bool,
+) -> bool {
+    block_list
+        .iter()
+        .enumerate()
+        .skip(start)
+        .take_while(|(index, _)| *index < content_count)
+        .find(|(_, block)| !block.is_newline())
+        .is_some_and(|(_, block)| predicate(&block))
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_markdown_formatter/src/markdown/lists/block_list.rs` around
lines 460 - 486, The two lookahead helpers in block_list are duplicated except
for the final predicate, so consolidate them into a single reusable helper that
accepts a predicate closure. Refactor next_content_block_is_thematic_break and
next_content_block_is_link_reference_definition to delegate to the shared
helper, keeping the existing iteration logic over MdBlockList and preserving
their current behavior.

279-296: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated "collect and remove extra newlines" loop.

The pattern of pushing the current newline with print_mode: Remove then draining subsequent newline siblings the same way is duplicated between the link-reference-definition branch and the thematic-break branch. A small helper (e.g. drain_extra_newlines(&mut iter, &mut joiner, content_count)) would remove the repetition.

Also applies to: 301-318

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_markdown_formatter/src/markdown/lists/block_list.rs` around
lines 279 - 296, The newline-collection logic in the block list formatter is
duplicated across the link-reference-definition and thematic-break branches in
`block_list.rs`. Extract the repeated “emit current newline with
TextPrintMode::Remove, then drain consecutive MdNewline siblings while tracking
content_count” sequence into a small helper such as `drain_extra_newlines`, and
reuse it from the relevant match branches to keep `iter`, `joiner`, and
`content_count` handling consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/biome_markdown_formatter/src/markdown/lists/block_list.rs`:
- Around line 460-486: The two lookahead helpers in block_list are duplicated
except for the final predicate, so consolidate them into a single reusable
helper that accepts a predicate closure. Refactor
next_content_block_is_thematic_break and
next_content_block_is_link_reference_definition to delegate to the shared
helper, keeping the existing iteration logic over MdBlockList and preserving
their current behavior.
- Around line 279-296: The newline-collection logic in the block list formatter
is duplicated across the link-reference-definition and thematic-break branches
in `block_list.rs`. Extract the repeated “emit current newline with
TextPrintMode::Remove, then drain consecutive MdNewline siblings while tracking
content_count” sequence into a small helper such as `drain_extra_newlines`, and
reuse it from the relevant match branches to keep `iter`, `joiner`, and
`content_count` handling consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 68fbb845-4870-4760-8b4c-682fed1eceb8

📥 Commits

Reviewing files that changed from the base of the PR and between b80ff6c and 5819bde.

⛔ Files ignored due to path filters (15)
  • crates/biome_markdown_factory/src/generated/node_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_factory/src/generated/syntax_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/code/additional-space.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/issue-17652.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/markdown/test-case.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-223.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-278.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-281.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_parser/tests/md_test_suite/ok/html_block.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_parser/tests/md_test_suite/ok/html_block_in_list.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_parser/tests/md_test_suite/ok/html_block_in_list_continuation.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_syntax/src/generated/kind.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_syntax/src/generated/macros.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_syntax/src/generated/nodes.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_markdown_syntax/src/generated/nodes_mut.rs is excluded by !**/generated/**, !**/generated/** and included by **
📒 Files selected for processing (15)
  • crates/biome_markdown_formatter/src/bullet_list.rs
  • crates/biome_markdown_formatter/src/generated.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/html_content.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/mod.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/newline.rs
  • crates/biome_markdown_formatter/src/markdown/lists/block_list.rs
  • crates/biome_markdown_formatter/src/shared.rs
  • crates/biome_markdown_parser/src/lexer/mod.rs
  • crates/biome_markdown_parser/src/parser.rs
  • crates/biome_markdown_parser/src/syntax/html_block.rs
  • crates/biome_markdown_parser/src/to_html.rs
  • crates/biome_markdown_parser/src/token_source.rs
  • crates/biome_markdown_syntax/src/block_ext.rs
  • xtask/codegen/markdown.ungram
  • xtask/codegen/src/markdown_kinds_src.rs
🚧 Files skipped from review as they are similar to previous changes (13)
  • xtask/codegen/markdown.ungram
  • xtask/codegen/src/markdown_kinds_src.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/mod.rs
  • crates/biome_markdown_syntax/src/block_ext.rs
  • crates/biome_markdown_parser/src/parser.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/html_content.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/newline.rs
  • crates/biome_markdown_formatter/src/generated.rs
  • crates/biome_markdown_formatter/src/bullet_list.rs
  • crates/biome_markdown_parser/src/to_html.rs
  • crates/biome_markdown_parser/src/token_source.rs
  • crates/biome_markdown_parser/src/lexer/mod.rs
  • crates/biome_markdown_parser/src/syntax/html_block.rs

@ematipico
ematipico merged commit f852f65 into main Jul 3, 2026
30 checks passed
@ematipico
ematipico deleted the refactor/html-single-text branch July 3, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Formatter Area: formatter A-Parser Area: parser A-Tooling Area: internal tools L-Markdown Language: Markdown

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant