Skip to content

feat(md/fmt): links, codeblocks#9699

Merged
ematipico merged 3 commits intomainfrom
feat/md-format-code-blocks
Mar 29, 2026
Merged

feat(md/fmt): links, codeblocks#9699
ematipico merged 3 commits intomainfrom
feat/md-format-code-blocks

Conversation

@ematipico
Copy link
Copy Markdown
Member

Summary

This PR:

  • implements formatting of code blocks
  • implements formatting of inline links
  • removes unused nodes
  • changes back MdFormatContext to Markdown*, because that's how our codegen is wired
  • Trimming has changed now. We now have a TextPrintMode which holds different semantics of how whitespaces should be handled. For example, for links we use different kinds of trimming. For code blocks, use clean

I designed the architecture around trimming and print mode, as well as the code blocks. Tests and implementations were tweaked and fixed using a coding agent.

Test Plan

Added new tests. Made sure we had small regressions around prettier snapshots.

Docs

N/A

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 29, 2026

⚠️ No Changeset found

Latest commit: ffb5a53

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 added A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools L-Markdown Language: Markdown labels Mar 29, 2026
@ematipico ematipico requested review from a team March 29, 2026 17:42
@github-actions
Copy link
Copy Markdown
Contributor

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 53139 53139 0
Passed 51919 51919 0
Failed 1178 1178 0
Panics 42 42 0
Coverage 97.70% 97.70% 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 5466 5466 0
Passed 1915 1915 0
Failed 3551 3551 0
Panics 0 0 0
Coverage 35.03% 35.03% 0.00%

ts/babel

Test result main count This PR count Difference
Total 640 640 0
Passed 569 569 0
Failed 71 71 0
Panics 0 0 0
Coverage 88.91% 88.91% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18875 18875 0
Passed 13013 13013 0
Failed 5861 5861 0
Panics 1 1 0
Coverage 68.94% 68.94% 0.00%

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 79e09b84-5e44-487f-8b4b-66dd4a2e658a

📥 Commits

Reviewing files that changed from the base of the PR and between a795ea8 and ffb5a53.

⛔ Files ignored due to path filters (21)
  • 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/markdown/fenced_code_block.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block_info_string.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/markdown/hard_line.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/markdown/inline_links.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/broken-plugins/missing-comments.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/code/backtick.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/multiparser-js/meta-in-code-block.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-104.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-106.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-110.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-111.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-113.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-476.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-91.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-94.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 (43)
  • crates/biome_markdown_formatter/src/comments.rs
  • crates/biome_markdown_formatter/src/context.rs
  • crates/biome_markdown_formatter/src/cst.rs
  • crates/biome_markdown_formatter/src/generated.rs
  • crates/biome_markdown_formatter/src/lib.rs
  • crates/biome_markdown_formatter/src/markdown/any/block.rs
  • crates/biome_markdown_formatter/src/markdown/any/bullet_list_member.rs
  • crates/biome_markdown_formatter/src/markdown/any/code_block.rs
  • crates/biome_markdown_formatter/src/markdown/any/container_block.rs
  • crates/biome_markdown_formatter/src/markdown/any/inline.rs
  • crates/biome_markdown_formatter/src/markdown/any/leaf_block.rs
  • crates/biome_markdown_formatter/src/markdown/any/thematic_break_part.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/hard_line.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/header.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/inline_link.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/link_block.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/link_title.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/mod.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/paragraph.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/textual.rs
  • crates/biome_markdown_formatter/src/markdown/lists/block_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/bullet_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/code_name_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/hash_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/indent_token_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/inline_item_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/quote_indent_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/thematic_break_part_list.rs
  • crates/biome_markdown_formatter/src/prelude.rs
  • crates/biome_markdown_formatter/src/shared.rs
  • crates/biome_markdown_formatter/src/trivia.rs
  • crates/biome_markdown_formatter/src/verbatim.rs
  • crates/biome_markdown_formatter/tests/language.rs
  • crates/biome_markdown_formatter/tests/quick_test.rs
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block.md
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block_info_string.md
  • crates/biome_markdown_formatter/tests/specs/markdown/hard_line.md
  • crates/biome_markdown_formatter/tests/specs/markdown/inline_links.md
  • crates/biome_markdown_parser/src/to_html.rs
  • crates/biome_markdown_syntax/src/text_ext.rs
  • xtask/codegen/markdown.ungram
  • xtask/codegen/src/markdown_kinds_src.rs
💤 Files with no reviewable changes (4)
  • crates/biome_markdown_formatter/src/markdown/auxiliary/mod.rs
  • xtask/codegen/src/markdown_kinds_src.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/link_block.rs
  • xtask/codegen/markdown.ungram
✅ Files skipped from review due to trivial changes (14)
  • crates/biome_markdown_formatter/src/markdown/lists/block_list.rs
  • crates/biome_markdown_formatter/tests/specs/markdown/hard_line.md
  • crates/biome_markdown_formatter/src/markdown/any/block.rs
  • crates/biome_markdown_formatter/src/markdown/lists/thematic_break_part_list.rs
  • crates/biome_markdown_formatter/src/comments.rs
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block_info_string.md
  • crates/biome_markdown_formatter/src/markdown/auxiliary/header.rs
  • crates/biome_markdown_formatter/src/markdown/any/inline.rs
  • crates/biome_markdown_formatter/src/verbatim.rs
  • crates/biome_markdown_formatter/tests/specs/markdown/inline_links.md
  • crates/biome_markdown_formatter/src/markdown/any/bullet_list_member.rs
  • crates/biome_markdown_formatter/tests/language.rs
  • crates/biome_markdown_formatter/src/trivia.rs
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block.md
🚧 Files skipped from review as they are similar to previous changes (21)
  • crates/biome_markdown_formatter/src/markdown/lists/bullet_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/quote_indent_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/indent_token_list.rs
  • crates/biome_markdown_formatter/src/markdown/any/container_block.rs
  • crates/biome_markdown_parser/src/to_html.rs
  • crates/biome_markdown_formatter/src/markdown/lists/hash_list.rs
  • crates/biome_markdown_formatter/src/markdown/any/code_block.rs
  • crates/biome_markdown_formatter/src/markdown/any/thematic_break_part.rs
  • crates/biome_markdown_formatter/src/prelude.rs
  • crates/biome_markdown_formatter/tests/quick_test.rs
  • crates/biome_markdown_formatter/src/markdown/any/leaf_block.rs
  • crates/biome_markdown_formatter/src/markdown/lists/code_name_list.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/link_title.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/inline_link.rs
  • crates/biome_markdown_syntax/src/text_ext.rs
  • crates/biome_markdown_formatter/src/context.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/paragraph.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/textual.rs
  • crates/biome_markdown_formatter/src/lib.rs
  • crates/biome_markdown_formatter/src/markdown/lists/inline_item_list.rs
  • crates/biome_markdown_formatter/src/generated.rs

Walkthrough

This PR renames the markdown formatter context from MdFormatContext to MarkdownFormatContext across the crate (imports, trait impls, re-exports and generated bindings). It introduces TextPrintMode and TrimMode and threads them into paragraph, textual, inline-item-list and hard-line formatting logic. Fenced-code-block formatting was rewritten to normalise fence delimiters. The legacy MdLinkBlock non‑terminal and its formatter were removed from the grammar and codegen. Several markdown test fixtures were added and MdTextual::is_empty semantics were adjusted to treat newlines as empty.

Possibly related PRs

  • biomejs/biome PR 9331 — Modifies the same formatter-context types and many FormatRule/AsFormat/IntoFormat bindings, showing a strong code-level overlap.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarises the main changes: implementing formatting for Markdown code blocks and inline links, which are the primary features added in this PR.
Description check ✅ Passed The description is directly related to the changeset, explaining the key modifications including code block/link formatting, unused node removal, context renaming, and the new TextPrintMode architecture for trimming.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/md-format-code-blocks

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/biome_markdown_formatter/src/markdown/auxiliary/paragraph.rs (1)

37-40: Refresh the option docs.

Line 38 still describes the old boolean flag, but trim_mode now carries the full TextPrintMode enum. Tiny mismatch, easy future head-scratcher.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_markdown_formatter/src/markdown/auxiliary/paragraph.rs` around
lines 37 - 40, The doc comment for FormatMdParagraphOptions::trim_mode is
outdated (mentions a boolean) — update the field doc to describe that trim_mode
is a TextPrintMode enum controlling how paragraph start is trimmed (e.g.,
None/TrimStart/Full or whatever variants exist), clarifying expected behavior
for each relevant TextPrintMode variant; ensure the comment sits directly above
pub trim_mode: TextPrintMode so future readers of FormatMdParagraphOptions and
users of trim_mode understand the enum semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_markdown_formatter/src/markdown/auxiliary/hard_line.rs`:
- Around line 14-16: The code currently only returns early for pristine print
mode, but when print mode is Trim(TrimMode::All) we must also avoid emitting the
hard line; in the method that does the early-return (the branch that calls
format_verbatim_node(node.syntax()).fmt(f)), add a special-case check for
self.print_mode being Trim(TrimMode::All) (or equivalent enum variant) and
return before calling hard_line_break(); keep the existing pristine check and
ensure the new branch runs the same early return path so Trim(TrimMode::All)
drops leading/trailing MdHardLine without writing the newline (affecting the
same function that calls hard_line_break(), format_verbatim_node, and fmt(f)).

In `@crates/biome_markdown_formatter/src/markdown/auxiliary/textual.rs`:
- Around line 20-37: The clean-mode branch currently calls trim_matches on the
whole token, removing indentation on non-empty lines; instead, read the original
string from value_token.text(), split it into lines preserving newline
separators, and for each line only strip whitespace if the line is entirely
whitespace (i.e., line.trim().is_empty()), otherwise keep the line unchanged;
then rejoin lines (preserving original newlines) into cleaned and use
format_replaced(&value_token, &text(cleaned,
value_token.text_trimmed_range().start())) so print_mode.is_clean(),
value_token, format_replaced, text(), and value_token.text_trimmed_range() are
used as in the diff.

---

Nitpick comments:
In `@crates/biome_markdown_formatter/src/markdown/auxiliary/paragraph.rs`:
- Around line 37-40: The doc comment for FormatMdParagraphOptions::trim_mode is
outdated (mentions a boolean) — update the field doc to describe that trim_mode
is a TextPrintMode enum controlling how paragraph start is trimmed (e.g.,
None/TrimStart/Full or whatever variants exist), clarifying expected behavior
for each relevant TextPrintMode variant; ensure the comment sits directly above
pub trim_mode: TextPrintMode so future readers of FormatMdParagraphOptions and
users of trim_mode understand the enum semantics.
🪄 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: a06cd915-e6dc-4f57-b273-8055415a6716

📥 Commits

Reviewing files that changed from the base of the PR and between 88c15cb and 3acd38d.

⛔ Files ignored due to path filters (21)
  • 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/markdown/fenced_code_block.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block_info_string.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/markdown/hard_line.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/markdown/inline_links.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/broken-plugins/missing-comments.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/code/backtick.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/multiparser-js/meta-in-code-block.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-104.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-106.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-110.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-111.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-113.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-476.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-91.md.snap is excluded by !**/*.snap and included by **
  • crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-94.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 (42)
  • crates/biome_markdown_formatter/src/comments.rs
  • crates/biome_markdown_formatter/src/context.rs
  • crates/biome_markdown_formatter/src/cst.rs
  • crates/biome_markdown_formatter/src/generated.rs
  • crates/biome_markdown_formatter/src/lib.rs
  • crates/biome_markdown_formatter/src/markdown/any/block.rs
  • crates/biome_markdown_formatter/src/markdown/any/bullet_list_member.rs
  • crates/biome_markdown_formatter/src/markdown/any/code_block.rs
  • crates/biome_markdown_formatter/src/markdown/any/container_block.rs
  • crates/biome_markdown_formatter/src/markdown/any/inline.rs
  • crates/biome_markdown_formatter/src/markdown/any/leaf_block.rs
  • crates/biome_markdown_formatter/src/markdown/any/thematic_break_part.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/hard_line.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/header.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/inline_link.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/link_block.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/link_title.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/mod.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/paragraph.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/textual.rs
  • crates/biome_markdown_formatter/src/markdown/lists/block_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/bullet_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/code_name_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/hash_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/indent_token_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/inline_item_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/quote_indent_list.rs
  • crates/biome_markdown_formatter/src/markdown/lists/thematic_break_part_list.rs
  • crates/biome_markdown_formatter/src/prelude.rs
  • crates/biome_markdown_formatter/src/shared.rs
  • crates/biome_markdown_formatter/src/trivia.rs
  • crates/biome_markdown_formatter/src/verbatim.rs
  • crates/biome_markdown_formatter/tests/language.rs
  • crates/biome_markdown_formatter/tests/quick_test.rs
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block.md
  • crates/biome_markdown_formatter/tests/specs/markdown/fenced_code_block_info_string.md
  • crates/biome_markdown_formatter/tests/specs/markdown/hard_line.md
  • crates/biome_markdown_formatter/tests/specs/markdown/inline_links.md
  • crates/biome_markdown_syntax/src/text_ext.rs
  • xtask/codegen/markdown.ungram
  • xtask/codegen/src/markdown_kinds_src.rs
💤 Files with no reviewable changes (4)
  • crates/biome_markdown_formatter/src/markdown/auxiliary/mod.rs
  • xtask/codegen/src/markdown_kinds_src.rs
  • crates/biome_markdown_formatter/src/markdown/auxiliary/link_block.rs
  • xtask/codegen/markdown.ungram

Comment thread crates/biome_markdown_formatter/src/markdown/auxiliary/hard_line.rs
Comment on lines +20 to +37
} else if self.print_mode.is_clean() {
// Clean mode: strip spaces/tabs but preserve newlines.
// Used for code block content where trailing whitespace on empty
// lines should be removed but newlines must be kept.
let cleaned = value_token
.text()
.trim_matches(|c: char| c == ' ' || c == '\t');
if cleaned == value_token.text() {
write!(f, [value_token.format()])
} else {
write!(
f,
[format_replaced(
&value_token,
&text(cleaned, value_token.text_trimmed_range().start())
)]
)
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Documented Clean-mode contract:"
sed -n '7,23p' crates/biome_markdown_formatter/src/shared.rs

echo
echo "Current Clean-mode implementation:"
sed -n '20,37p' crates/biome_markdown_formatter/src/markdown/auxiliary/textual.rs

echo
python - <<'PY'
samples = [
    "    function f() {}",
    "\t\tlet x = 1;",
    "   \n",
]
for sample in samples:
    cleaned = sample.strip(" \t")
    print(f"{sample!r} -> {cleaned!r}")
PY

Repository: biomejs/biome

Length of output: 1516


Clean mode strips indentation from all lines, not just empty ones.

The current implementation uses trim_matches(|c| c == ' ' || c == '\t'), which removes spaces and tabs from both ends of the entire token. This violates the documented contract in crates/biome_markdown_formatter/src/shared.rs, which explicitly states that indentation should be preserved on non-empty code lines.

For example:

  • function f() {} becomes function f() {} (indentation lost)
  • \t\tlet x = 1; becomes let x = 1; (indentation lost)

Only whitespace-only lines should have their trailing/leading spaces removed. This requires a line-by-line approach rather than a token-wide trim.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_markdown_formatter/src/markdown/auxiliary/textual.rs` around
lines 20 - 37, The clean-mode branch currently calls trim_matches on the whole
token, removing indentation on non-empty lines; instead, read the original
string from value_token.text(), split it into lines preserving newline
separators, and for each line only strip whitespace if the line is entirely
whitespace (i.e., line.trim().is_empty()), otherwise keep the line unchanged;
then rejoin lines (preserving original newlines) into cleaned and use
format_replaced(&value_token, &text(cleaned,
value_token.text_trimmed_range().start())) so print_mode.is_clean(),
value_token, format_replaced, text(), and value_token.text_trimmed_range() are
used as in the diff.

@ematipico ematipico force-pushed the feat/md-format-code-blocks branch from a795ea8 to ffb5a53 Compare March 29, 2026 17:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

106-112: Inconsistent closure passing style.

Line 106 passes the closure by reference (&is_content), whilst line 112 passes it by value (is_content). Both work because the closure is Copy, but the inconsistency is a minor readability concern.

✨ Suggested fix for consistency
-        let first_content = items.iter().position(&is_content);
+        let first_content = items.iter().position(|item| is_content(item));

         // Find the first non-empty item from the right.
         let last_content = items
             .iter()
             .rev()
-            .position(is_content)
+            .position(|item| is_content(item))
             .map(|pos| items.len() - 1 - pos);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_markdown_formatter/src/markdown/lists/inline_item_list.rs`
around lines 106 - 112, The two calls to position are inconsistent:
first_content uses items.iter().position(&is_content) while last_content uses
items.iter().rev().position(is_content); make them consistent by passing the
closure the same way in both places (e.g., change the first call to
items.iter().position(is_content) or change the second to
.position(&is_content)). Update the call site(s) in the inline_item_list logic
so both uses of is_content use the same closure passing style.
crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs (1)

68-90: longest_fence_char_sequence only inspects MdTextual items.

Per MdInlineItemList's definition, content can include other inline variants (links, emphasis, etc.). However, fence characters inside structured inline elements wouldn't confuse the CommonMark parser at the block level, so this is likely intentional and correct.

Worth a brief comment for future maintainers, but not blocking.

📝 Optional documentation improvement
 /// Find the longest consecutive run of `fence_char` in the code block's content.
+/// Only inspects raw textual nodes; structured inline elements (links, etc.)
+/// don't affect block-level fence parsing.
 fn longest_fence_char_sequence(node: &MdFencedCodeBlock, fence_char: char) -> usize {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs`
around lines 68 - 90, The function longest_fence_char_sequence currently only
iterates MdTextual items which is intentional because MdInlineItemList may
contain structured inlines (links, emphasis, etc.) and fence characters inside
those won't affect block-level fence parsing; add a concise comment above
longest_fence_char_sequence referencing MdInlineItemList and MdTextual to
explain why only textual tokens are checked and that structured inline elements
cannot form valid fence sequences at the block level, so no further traversal is
required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs`:
- Around line 20-23: The code assumes l_fence.text() is non-empty and directly
indexes fence_text.as_bytes()[0], which can panic; update the fenced code block
handling to defensively check fence_text.is_empty() after obtaining fence_text
from l_fence (and after l_fence? unwrap), and if empty return an appropriate
error or early return (propagate Err) instead of indexing; then safely extract
the fence character (e.g., use fence_text.chars().next().unwrap() or similar
only after the emptiness check). Ensure you reference and update the logic
around l_fence, fence_text and fence_char in fenced_code_block.rs to avoid any
direct indexing on an empty slice.
- Around line 54-62: r_fence extraction can fail and currently no closing fence
is emitted, producing invalid Markdown; add an else branch for the if let
Ok(r_fence) = r_fence that writes a synthesized closing fence so output remains
well-formed. Specifically, after the existing Ok branch that calls write!(f,
[format_replaced(...)]), implement an else that calls write!(f, [...]) to emit a
closing fence derived from normalized_fence (use the same text/formatter helpers
— text and format_replaced or text(&normalized_fence, ...) — to produce the
closing fence string) so a closing fence is always written even when r_fence is
Err.

---

Nitpick comments:
In `@crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs`:
- Around line 68-90: The function longest_fence_char_sequence currently only
iterates MdTextual items which is intentional because MdInlineItemList may
contain structured inlines (links, emphasis, etc.) and fence characters inside
those won't affect block-level fence parsing; add a concise comment above
longest_fence_char_sequence referencing MdInlineItemList and MdTextual to
explain why only textual tokens are checked and that structured inline elements
cannot form valid fence sequences at the block level, so no further traversal is
required.

In `@crates/biome_markdown_formatter/src/markdown/lists/inline_item_list.rs`:
- Around line 106-112: The two calls to position are inconsistent: first_content
uses items.iter().position(&is_content) while last_content uses
items.iter().rev().position(is_content); make them consistent by passing the
closure the same way in both places (e.g., change the first call to
items.iter().position(is_content) or change the second to
.position(&is_content)). Update the call site(s) in the inline_item_list logic
so both uses of is_content use the same closure passing style.
🪄 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: 9d52a6cb-36cd-4c23-acad-7f95789693c3

📥 Commits

Reviewing files that changed from the base of the PR and between 3acd38d and a795ea8.

📒 Files selected for processing (3)
  • crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs
  • crates/biome_markdown_formatter/src/markdown/lists/inline_item_list.rs
  • crates/biome_markdown_parser/src/to_html.rs

Comment on lines +20 to +23
let l_fence = l_fence?;
let fence_text = l_fence.text();
// SAFETY: fence_text has at least one character.
let fence_char = fence_text.as_bytes()[0] as char;
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.

⚠️ Potential issue | 🟡 Minor

Potential panic if fence token is empty.

The SAFETY comment asserts the fence has at least one character, but there's no runtime check. If the parser ever produces an empty l_fence token (malformed input, future parser changes), this will panic.

🛡️ Suggested defensive check
         let l_fence = l_fence?;
         let fence_text = l_fence.text();
-        // SAFETY: fence_text has at least one character.
-        let fence_char = fence_text.as_bytes()[0] as char;
+        // Fence must contain at least one ` or ~.
+        let fence_char = fence_text
+            .chars()
+            .next()
+            .unwrap_or('`');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs`
around lines 20 - 23, The code assumes l_fence.text() is non-empty and directly
indexes fence_text.as_bytes()[0], which can panic; update the fenced code block
handling to defensively check fence_text.is_empty() after obtaining fence_text
from l_fence (and after l_fence? unwrap), and if empty return an appropriate
error or early return (propagate Err) instead of indexing; then safely extract
the fence character (e.g., use fence_text.chars().next().unwrap() or similar
only after the emptiness check). Ensure you reference and update the logic
around l_fence, fence_text and fence_char in fenced_code_block.rs to avoid any
direct indexing on an empty slice.

Comment on lines +54 to +62
if let Ok(r_fence) = r_fence {
write!(
f,
[format_replaced(
&r_fence,
&text(&normalized_fence, r_fence.text_trimmed_range().start())
)]
)?;
}
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.

⚠️ Potential issue | 🟠 Major

Missing closing fence when r_fence is Err.

If r_fence fails to extract, no closing fence is emitted. This could produce invalid Markdown output. Consider emitting a synthesised closing fence to maintain well-formedness.

🔧 Suggested fix to always emit a closing fence
-        if let Ok(r_fence) = r_fence {
+        if let Ok(r_fence) = &r_fence {
             write!(
                 f,
                 [format_replaced(
-                    &r_fence,
+                    r_fence,
                     &text(&normalized_fence, r_fence.text_trimmed_range().start())
                 )]
             )?;
+        } else {
+            // Synthesise a closing fence if the original is missing/malformed.
+            write!(f, [dynamic_text(&normalized_fence, TextSize::default())])?;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_markdown_formatter/src/markdown/auxiliary/fenced_code_block.rs`
around lines 54 - 62, r_fence extraction can fail and currently no closing fence
is emitted, producing invalid Markdown; add an else branch for the if let
Ok(r_fence) = r_fence that writes a synthesized closing fence so output remains
well-formed. Specifically, after the existing Ok branch that calls write!(f,
[format_replaced(...)]), implement an else that calls write!(f, [...]) to emit a
closing fence derived from normalized_fence (use the same text/formatter helpers
— text and format_replaced or text(&normalized_fence, ...) — to produce the
closing fence string) so a closing fence is always written even when r_fence is
Err.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 29, 2026

Merging this PR will create unknown performance changes

🆕 28 new benchmarks
⏩ 228 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 real/blog-post.md[cached] N/A 2.6 ms N/A
🆕 spec/autolinks.md[cached] N/A 1.4 ms N/A
🆕 real/readme-style.md[cached] N/A 3 ms N/A
🆕 real/blog-post.md[uncached] N/A 2.6 ms N/A
🆕 synthetic/emphasis-heavy.md[uncached] N/A 2.2 ms N/A
🆕 real/readme-style.md[uncached] N/A 3 ms N/A
🆕 spec/autolinks.md[uncached] N/A 1.4 ms N/A
🆕 synthetic/blockquotes-nested.md[cached] N/A 2.7 ms N/A
🆕 spec/lists.md[uncached] N/A 4.8 ms N/A
🆕 synthetic/blockquotes-nested.md[uncached] N/A 2.8 ms N/A
🆕 synthetic/inline-html.md[cached] N/A 1.3 ms N/A
🆕 synthetic/emphasis-heavy.md[cached] N/A 2.2 ms N/A
🆕 synthetic/long-paragraphs.md[uncached] N/A 2 ms N/A
🆕 spec/blockquotes.md[cached] N/A 1.1 ms N/A
🆕 synthetic/long-paragraphs.md[cached] N/A 2 ms N/A
🆕 spec/blockquotes.md[uncached] N/A 1.1 ms N/A
🆕 synthetic/nested-lists.md[cached] N/A 4.3 ms N/A
🆕 spec/emphasis.md[cached] N/A 2.8 ms N/A
🆕 synthetic/links-and-images.md[cached] N/A 3.6 ms N/A
🆕 synthetic/inline-html.md[uncached] N/A 1.3 ms N/A
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.


Comparing feat/md-format-code-blocks (ffb5a53) with main (63fcbb3)2

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.

  2. No successful run was found on main (88c15cb) during the generation of this report, so 63fcbb3 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

2 participants