Summary
For certain well-formed HTML, djhtml never converges. It flips the indentation
of a nested multi-line open tag back and forth between two states on every run,
indefinitely — a deterministic 2-cycle.
Worse than "runs twice": neither state is accepted by djhtml -c. Both
states report would have reindented, so there is no content you can commit
that passes the check. In a pre-commit/CI setup the gate fails permanently, and
an autoformatting bot ping-pongs commits.
System information
- djhtml 3.0.11 (installed from PyPI into a clean virtualenv)
- Also reproduces on 3.0.10
- Python 3.12.3, Linux 6.8
- Default settings — no arguments, no config file, run in an empty directory
Minimal reproduction
repro.html — plain HTML, no Django/Jinja template tags involved:
<form action="/x"
method="post">
<button type="submit"
class="btn">Pay</button>
</form>
$ md5sum repro.html
9744086f3c8484895d859bc02aa3ba77 repro.html
$ djhtml repro.html && md5sum repro.html
reindented repro.html
1 template has been reindented.
45937171220590e694a12aa2949f16a3 repro.html
$ djhtml repro.html && md5sum repro.html
reindented repro.html
1 template has been reindented.
9744086f3c8484895d859bc02aa3ba77 repro.html # <- back to the original
$ djhtml repro.html && md5sum repro.html
reindented repro.html
1 template has been reindented.
45937171220590e694a12aa2949f16a3 repro.html
The checksums alternate A, B, A, B, … — verified over 6+ iterations. The diff
of a single run:
<form action="/x"
method="post">
- <button type="submit"
- class="btn">Pay</button>
+ <button type="submit"
+ class="btn">Pay</button>
</form>
No fixed point exists
Both members of the cycle fail the check:
$ djhtml -c state_A.html
would have reindented state_A.html
1 template would have been reindented. # exit 1
$ djhtml -c state_B.html
would have reindented state_B.html
1 template would have been reindented. # exit 1
So djhtml -c cannot be satisfied for this file by any amount of reformatting.
What triggers it
Both the parent and the child must have multi-line open tags (a single-line
child is stable). Whether it oscillates then depends on the column at which the
parent's attributes continue — i.e. on the length of the parent tag name.
Sweeping parent tag-name length with the child at indent 4:
| parent tag length |
attribute continuation column |
result |
| 1–3 |
3–5 |
stable |
| 4 |
6 |
oscillates |
| 5–7 |
7–9 |
stable |
| 8 |
10 |
oscillates |
| 9 |
11 |
stable |
| 10 |
12 |
oscillates |
| 11–12 |
13–14 |
stable |
In practice this means common 4-letter tags — <form>, <span>, <main> —
oscillate, while <div> does not, which is why it can hide for a long time and
then appear the moment a template is wrapped in a <form>.
Related, but not duplicates
I could not find an open or closed issue about non-idempotency / oscillation on
well-formed input.
Impact for us
This is the sole remaining reason one of our templates has to stay on the
formatter's exclusion list: after djLint formats it into exactly this nested
multi-line shape, our djhtml pre-commit gate passes or fails depending on run
parity.
Summary
For certain well-formed HTML,
djhtmlnever converges. It flips the indentationof a nested multi-line open tag back and forth between two states on every run,
indefinitely — a deterministic 2-cycle.
Worse than "runs twice": neither state is accepted by
djhtml -c. Bothstates report
would have reindented, so there is no content you can committhat passes the check. In a pre-commit/CI setup the gate fails permanently, and
an autoformatting bot ping-pongs commits.
System information
Minimal reproduction
repro.html— plain HTML, no Django/Jinja template tags involved:The checksums alternate
A, B, A, B, …— verified over 6+ iterations. The diffof a single run:
<form action="/x" method="post"> - <button type="submit" - class="btn">Pay</button> + <button type="submit" + class="btn">Pay</button> </form>No fixed point exists
Both members of the cycle fail the check:
So
djhtml -ccannot be satisfied for this file by any amount of reformatting.What triggers it
Both the parent and the child must have multi-line open tags (a single-line
child is stable). Whether it oscillates then depends on the column at which the
parent's attributes continue — i.e. on the length of the parent tag name.
Sweeping parent tag-name length with the child at indent 4:
In practice this means common 4-letter tags —
<form>,<span>,<main>—oscillate, while
<div>does not, which is why it can hide for a long time andthen appear the moment a template is wrapped in a
<form>.Related, but not duplicates
runs, closed) — also multi-pass instability, but it is caused by malformed
HTML (an unclosed quote) and it does converge after N passes. This report is
well-formed HTML that never converges.
area, different request.
I could not find an open or closed issue about non-idempotency / oscillation on
well-formed input.
Impact for us
This is the sole remaining reason one of our templates has to stay on the
formatter's exclusion list: after djLint formats it into exactly this nested
multi-line shape, our djhtml pre-commit gate passes or fails depending on run
parity.