Skip to content

Embedded HTML Formatting (markup_fmt integration) - #797

Open
chandlerc wants to merge 4 commits into
rvben:mainfrom
chandlerc:html-fmt
Open

Embedded HTML Formatting (markup_fmt integration)#797
chandlerc wants to merge 4 commits into
rvben:mainfrom
chandlerc:html-fmt

Conversation

@chandlerc

@chandlerc chandlerc commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR: Embedded HTML Formatting (markup_fmt integration)

Adds HTML formatting to rumdl via markup_fmt. Supports HTML blocks, JS/TS in <script> tags (via dprint), and Markdown inside HTML comments.

This is a draft/starting point for discussion.

Changes

1. Engine Integration (MD089 / embedded-html-fmt)

  • Integrated as standard rule MD089.
    • Supports inline disable comments (<!-- rumdl-disable MD089 -->).
    • Supports per-file-ignores.
    • Works with LSP (diagnostics and quick-fixes).
  • Uses lazy rule loading (via Mutex<Option<Vec<Box<dyn Rule>>>>) to avoid recursion when formatting Markdown inside HTML comments.

2. Configuration (.rumdl.toml)

New [html] and [html.script] sections:

[html]
enabled = true
print-width = 80
indent-width = 2
use-tabs = false
quotes = "double" # "double" | "single"
format-comments-as-markdown = false

[html.script]
enabled = false
semi-colons = "always" # "always" | "prefer" | "asi"
quote-style = "prefer-double" # "always-double" | "always-single" | "prefer-double" | "prefer-single"

3. Robustness Fixes (from review)

  • UTF-8 Safety: Fixed panic in LineIndex::byte_to_line_col under multi-byte UTF-8 by clamping offsets to char boundaries.
  • Indentation Stripping: Fixed panic in strip_common_indent on mixed indentation (Unicode whitespace).
  • Performance: Cached dprint typescript configuration outside the script formatting loop.

4. Commits

  • feat(config): add [html] and [html.script] configuration sections
  • feat(html-fmt): implement and integrate embedded HTML formatting check (refactored to MD089 in working copy)
  • docs(html-fmt): add user guide and configuration details for HTML formatting
  • feat(html-fmt): add format-comments-as-markdown option

Feedback Requested

  1. Rule ID/Name: Is MD089 / embedded-html-fmt okay? (Updated from MD088 as it was reserved).
  2. Config Layout: Should these options stay under [html] or move to [MD089]?
  3. Comment Formatting: Is the recursive Markdown formatting in comments useful?
  4. Dependencies: Added markup_fmt and dprint-plugin-typescript (gated under html-fmt feature).

@chandlerc
chandlerc force-pushed the html-fmt branch 2 times, most recently from 0298dfe to 31e8f1c Compare August 5, 2026 08:17
Introduce `HtmlConfig` and `ScriptConfig` settings to control embedded
HTML block formatting.

- Added configuration structures with kebab-case serialization.
- Implemented sourced provenance mapping and merge behavior for
  per-directory configuration inheritance.
- Added `html-fmt` feature dependency gates for `markup_fmt` and
  `dprint-plugin-typescript`.
- Updated intelligent configuration merge unit tests.
- Regenerated JSON schema.

Assisted-by: Antigravity with Gemini
Introduce the embedded HTML block formatting checker and integrate it
into the single-file lint loop and the LSP workspace diagnostics.

- Added `LineIndex::byte_to_line_col` range utility to map global byte
  offsets to 1-indexed (line, character-column) document coordinates.
- Added `check_embedded_html_blocks` which parses Markdown files for
  `Tag::HtmlBlock` events and validates them against `markup_fmt`.
- Added script formatting callback mapping to `dprint-plugin-typescript`
  for script tags.
- Added JSX component tag skipping in MDX.
- Integrated the check under the `html-fmt` feature gate.

Assisted-by: Antigravity with Gemini
…matting

Create `docs/embedded-html-formatting.md` describing how HTML block
formatting and script tag formatting work inside Markdown documents.
Update the global settings reference and configuration file indexes to
point to it.

Assisted-by: Antigravity with Gemini
Assisted-by: Antigravity with Gemini
@chandlerc

Copy link
Copy Markdown
Contributor Author

Just wanted to check in to see if there was any discussions we could kick off on the overall approach, or if it would help to talk through the use case more, etc. I understand this is a pretty big new feature, so just trying to understand the best path forward.

@rvben

rvben commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Thanks for the ping, and apologies for the wait. I have been partly away recently, and this is a big enough change that I did not want to answer it off the cuff, so it took longer than my usual turnaround. You clearly put real work into this, so here is a proper answer.

The short version, up front rather than buried at the end: I am not going to take the formatter integration. Below is why, and there are two pieces of this branch I do want.

The blocking problem: an external formatter cannot see containers

calculate_fix slices &ctx.content[start..end] (md089_embedded_html_fmt.rs:301) and hands those bytes to markup_fmt. That range comes from pulldown-cmark's HTML block, which covers the raw source, container prefixes included. markup_fmt has no way to know that a > or a two-space list indent is Markdown structure, so it reindents them as HTML text.

On c0b5a151, a bare rumdl fmt (no config, no --enable, since MD089 is default-on in the branch):

> <div class="note">
> <p>Some text inside a blockquote.</p>
> </div>

becomes

> <div class="note">
  > <p>Some text inside a blockquote.</p>
  >
</div>

</div> has left the blockquote. The list-item case loses its indent the same way, with the closing tag coming back at column 0 outside the item. Released 0.2.52 leaves both inputs untouched; I checked with a third file it must rewrite, as a control.

I do not think this is something to patch, though: it follows from handing a raw slice to an outside transformer. rumdl has one rule that formats a block correctly inside containers, MD060 for tables, and it gets there by stripping the container prefix, transforming, and re-adding it (md060_table_format.rs:766-793 and 961-975). The same approach would be needed here, and for HTML that means reconstructing prefixes across a formatter that is free to add, remove and rewrap lines. That is a much harder contract than MD060's.

Being default-on sharpens it: a plain rumdl check fires MD089 and prints "Run rumdl fmt to automatically fix" on the very lines where MD033 is telling the user to delete the HTML.

What would remain even if that were fixed

  • Adopting a formatter adopts its style opinions in a domain rumdl cannot arbitrate. CommonMark says nothing about how HTML inside Markdown should be laid out, so every disagreement becomes a rumdl issue with no principled basis to resolve it.
  • A markup_fmt patch bump would silently reformat everyone's HTML, changing rumdl fmt output outside rumdl's own semver.
  • It fights the tools already doing this over the same bytes. prettier and dprint both format embedded HTML in Markdown, and MD076 and MD031 conflict when list item starts with a code block #787 was exactly that shape.

On cost, so you have the real numbers rather than a vague concern: +59 crates, about +20s clean build, +4.15 MiB binary. Effectively all of it is dprint-plugin-typescript for the <script> half (114 crates and 46s measured on its own); markup_fmt alone is 14 crates and 6s. I mention it mainly so you are not left guessing what the cost was, and so you do not spend time trimming it on my account: what decides this is the container problem above, not the dependency weight.

What I do want from this branch

The infrastructure half is a genuine improvement and I would review it as its own PR expecting to take it: dropping the hand-rolled detect_html_blocks (heading_detection.rs:324) in favour of pulldown-cmark's own block ranges carried on LintContext, and the html_block_range_at lookup.

MD033's calculate_fix moving from the line-level in_html_block flag to byte-precise containment via is_nested_in_html_block is the piece that interests me most, because the flag genuinely cannot distinguish a tag that is the HTML block from one nested inside it. That is also why I would want it separately with its own tests rather than riding along with a new rule: it changes a fix path that deletes content.

A counter-proposal

An MD089 that normalizes only the indentation of HTML block lines: opt-in, no new dependency.

The appeal is that an indentation-only transform can carry a machine-checkable invariant in its fix path, which a general formatter never can. MD013's reflow does exactly that: preserves_content (src/utils/text_reflow.rs:974, called from reflow_line at :961) hands back the original line untouched when the transform would not preserve it.

I should be straight that the hard half is making that argument at the Markdown level rather than the HTML one. My first instinct was "HTML collapses whitespace, so leading whitespace cannot change rendering", and that is wrong: indentation is load-bearing in CommonMark. These three lines render as HTML at column 0, and as escaped literal text inside a <pre><code> when each is indented four spaces:

<div>
text
</div>

And de-indenting a closing tag out of a list item is the same corruption the current branch produces. So the invariant has to be closer to: container prefix untouched, no line's indent taken below what its container requires or up to the indented-code threshold, line count unchanged, no blank/non-blank flips, everything after the indent byte-identical, and preformatted content skipped outright. src/utils/html_block.rs:11 lists CommonMark's raw-text elements (pre, script, style, textarea) as a starting point for that last one, though it is about parsing rather than about what renders whitespace, so an explicit white-space style would want skipping too.

Bounded, but not trivial. If it is interesting it is yours to write and I will review it promptly. If it is not what you were after, I understand, and I would still like the HTML block range refactor as its own PR.

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.

2 participants