Skip to content

Not idempotent: nested multi-line tags oscillate between two indentations forever (no fixed point exists) #134

Description

@PetrDlouhy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions