Skip to content

Render until the output stops changing - #597

Open
faresrafat3 wants to merge 1 commit into
hukkin:masterfrom
faresrafat3:mdformat-588-idempotent-wrap
Open

faresrafat3 wants to merge 1 commit into
hukkin:masterfrom
faresrafat3:mdformat-588-idempotent-wrap

Conversation

@faresrafat3

Copy link
Copy Markdown

Fixes #588.

Escaping can depend on where word wrapping split a paragraph, so a single
render pass may produce text that the next pass formats differently. With a
small --wrap, three passes are sometimes needed, but only two were
performed, so a file mdformat had just written failed --check and a second
run changed it again:

$ printf 'A * * b\n' > test.md
$ mdformat --wrap 5 test.md && cat test.md
A *
\* b
$ mdformat --check --wrap 5 test.md
Error: File "test.md" is not formatted.

Root cause

For that input the render passes are:

pass1: 'A * *\nb\n'
pass2: 'A *\n\\* b\n'
pass3: 'A \\*\n\\* b\n'   <- fixed point

Pass 1 breaks the line after A * *, so on pass 2 the second asterisk is at
the end of a text token and loses the whitespace neighbour that had kept it
unescaped. Pass 2 then escapes it, and pass 3 escapes the first one too
because it is now adjacent to the backslash. mdformat.text() only ran two
passes, so it returned the pass-2 output, which is not a fixed point.

Changes

_api.text() now renders repeatedly until the output is unchanged, with a
bound of 9 iterations so a pathological input cannot loop forever. This makes
"what mdformat writes" and "what --check accepts" the same thing.

Verification

The reported command sequence now settles on the first run and --check
passes:

$ printf 'A * * b\n' > test.md
$ mdformat --wrap 5 test.md && cat test.md
A \*
\* b
$ mdformat --check --wrap 5 test.md
$ echo $?
0

A regression test covers exactly this (test_check__after_format_with_small_wrap).

I measured how many passes the CommonMark spec fixtures actually need, to check
that the loop is not papering over a deeper problem:

wrap passes needed
keep 0 or 1 (no change)
no 0, 1, or 2
60 0, 1, or 2
5 0, 1, 2, or 3 (1 case)

So two passes cover everything except one case at --wrap=5, which is why the
bug only shows up at small wrap widths.

Full suite: 4283 passed, 5 skipped (4282 before, plus the new test).

Escaping can depend on where word wrapping split a paragraph, so a
single render pass may produce text that the next pass formats
differently. With a small --wrap, three passes are sometimes needed,
but only two were performed. The result was that a file mdformat had
just written failed --check, and a second mdformat run changed it again:

    $ printf 'A * * b\n' > test.md
    $ mdformat --wrap 5 test.md && cat test.md
    A *
    \* b
    $ mdformat --check --wrap 5 test.md
    Error: File "test.md" is not formatted.

Rendering now repeats until the output is unchanged (bounded, so a
pathological input cannot loop forever). Two passes still cover almost
every input; across the CommonMark spec fixtures with --wrap=5, four
cases need three.
Copilot AI balanced review requested due to automatic review settings September 30, 2026 22:49

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@faresrafat3

Copy link
Copy Markdown
Author

Hi @hukkin — following up on this PR. It fixes the fixed-point rendering issue where --check would fail on freshly-formatted files. All 4283 tests pass. Let me know if you'd like any changes or if there's anything else I can help with.

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.

mdformat --check fails on a file mdformat just formatted (small --wrap)

2 participants