Skip to content

nickpetrovic/djangofmt

 
 

Repository files navigation

Djangofmt

Pypi Version License Supported Python Versions Actions status pre-commit.ci status CodSpeed Badge

A fast, HTML aware, Django template formatter, written in Rust.

Shows a bar chart with benchmark results.

Formatting 100k+ lines of HTML across 1.7k+ files from scratch.

Heavily rely on the awesome markup_fmt with some additions to support Django fully.

Installation

djangofmt is available on PyPI.

# With pip
pip install djangofmt

# With uv
uv tool install djangofmt@latest  # Install djangofmt globally.
uv add --dev djangofmt            # Or add djangofmt to your project.

# With pipx
pipx install djangofmt

As a pre-commit hook

See pre-commit for instructions

Sample .pre-commit-config.yaml:

- repo: https://github.com/UnknownPlatypus/djangofmt-pre-commit
  rev: v0.2.7
  hooks:
    - id: djangofmt

The separate repository enables installation without compiling the Rust code.

By default, the configuration uses pre-commit’s files option to detect all text files in directories named templates. If your templates are stored elsewhere, you can override this behavior by specifying the desired files in the hook configuration within your .pre-commit-config.yaml file.

Usage

djangofmt .                    # Format all files in the current directory (and any subdirectories).
djangofmt src/templates        # Format all template files in `src/templates`
djangofmt templates/base.html  # Format individual files

When given a directory, djangofmt recurses into it and formats all *.html, *.jinja, *.jinja2, and *.j2 files it finds. It also respects .gitignore files.

Looking for a check mode ?

djangofmt intentionally does not provide a built-in check functionality because CI is too late for a code formatter. We strongly recommend using pre-commit or any IDE "format on save" integration. That being said, you can emulate check capability by chaining with a git diff command like so:

djangofmt .
git diff --exit-code -- '*.html' || (echo "HTML templates are not formatted. Run 'djangofmt' to fix." && exit 1)

Configuration

Djangofmt can also be configured via a [tool.djangofmt] section in your pyproject.toml:

[tool.djangofmt]
line-length = 120
indent-width = 4
profile = "django"
custom-blocks = ["stage", "flatblock"]
html-void-self-closing = "never"
preserve-unquoted-attrs = false

Djangofmt looks for a pyproject.toml file by traversing directories upward from the current working directory. The first pyproject.toml found is used. If no file is found or the file doesn't contain a [tool.djangofmt] section, defaults are used.

Command-line arguments always take precedence over pyproject.toml settings.

Controlling the formatting

DjangoFmt gives users control over formatting in cases where static analysis struggles to determine the optimal approach.

Splitting an opening tag across multiple lines

You can control this formatting by choosing whether to insert a newline before the first attribute:

# Unchanged
<div class="flex" id="great" data-a>
  This is nice!
</div>

# Wrap on multiple lines
<div
-    class="flex" id="great" data-a>
+    class="flex"
+    id="great"
+    data-a
+>
    This is nice!
</div>

Class attribute formatting

The class attribute will be formatted as a space-separated sequence of strings, unless there are already newlines inside the attribute value.

This makes it possible to accommodate the 2 following use cases:

<div class="
  mt-8 p-8
  bg-indigo-600 hover:bg-indigo-700
  border border-transparent
  font-medium text-white
">
    Hello world
</div>

<div class="mt-8 p-8 bg-indigo-600 hover:bg-indigo-700 border border-transparent font-medium text-white">
    Hello world
</div>

See g-plane/markup_fmt#75 (comment) for the rationale.

Preserving unquoted attribute values

By default, djangofmt quotes all attribute values:

- <c-button editable=True count=42 />
+ <c-button editable="True" count="42" />

Enable preserve-unquoted-attrs to suppress this transformation and keep them unquoted. This is useful for frameworks like Django Cotton that use unquoted attribute values to pass non-string types (booleans, numbers, template variables).

Disabling formatting

To disable formatting for an entire file, add <!-- djangofmt:ignore --> at the very top of the file.

To disable formatting for a specific node, prefix it with the same comment:

<!-- djangofmt:ignore -->
<div   class="keep-this-unformatted"   >Content</div>
<div class="this-will-be-formatted">Content</div>

Known limitations

style attributes formatting

The style attribute will be formatted using a CSS formatter (Malva), but the output will always be on a single line.

Before:

<div class="flex flex-col items-center absolute z-10"
     style="top:60%;
            transform:translate(0,-50%)">
    Such a lovely day
</div>

After:

<div class="flex flex-col items-center absolute z-10"
     style="top:60%; transform:translate(0,-50%)">
    Such a lovely day
</div>

Conditional open/close tags

Djangofmt doesn't accept and will produce parsing errors for any syntax that could cut off HTML in obvious ways, e.g.:

{% if condition %}
    <div class="container">
{% endif %}
    Some content
{% if condition %}
    </div>
{% endif %}

This is generally discouraged and should be avoided because it's an easy way to create invalid HTML.

You can almost always write it another way that is much more readable. For example:

-<div {{ attr_name }}{% if not boolean_attr %}="{{ attr_value }}"{% endif %}></div>
+<div
+    {% if boolean_attr %}
+        {{ attr_name }}
+    {% else %}
+        {{ attr_name }}="{{ attr_value }}"
+    {% endif %}
+></div>

See upstream tracking issue: g-plane/markup_fmt#97

.svg files support

Djangofmt can format svg files too. It will behave exactly the same way as if they were html files.

There is a dedicated pre-commit for these:

- repo: https://github.com/UnknownPlatypus/djangofmt-pre-commit
  rev: v0.2.7
  hooks:
    - id: djangofmt-svg

Benchmarks

Here are the results benchmarking djangofmt against similar tools on 100k lines of HTML across 1.7k files.

Shows a bar chart with benchmark results.

Formatting 100k+ lines of HTML across 1.7k+ files from scratch.

This is important to note that only djlint covers the same scope in terms of formatting capabilities. djade only alter django templating, djhtml only fix indentation and prettier only understand html (and will break templates)

As always, these results should be taken with a grain of salt. Results on my machine will differ from yours, especially if you have many CPU cores because some tools take better advantage of parallelization than others.

But at least it was fun to build thanks to the wonderful hyperfine tool.

Benchmark details (2025-02-28)

This was run on my AMD Ryzen 9 7950X (32) @ 5.881GHz.

Tools versions:

  • djangofmt: v0.1.0
  • prettier: v3.5.2
  • djlint: v1.36.4
  • djade: v1.3.2
  • djhtml: v3.0.7
Benchmark 1: cat /tmp/test-files | xargs --max-procs=0 ../../target/release/djangofmt format --profile django --line-length 120 --quiet
  Time (mean ± σ):      19.8 ms ±   0.9 ms    [User: 179.6 ms, System: 73.7 ms]
  Range (min … max):    18.3 ms …  23.3 ms    73 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: cat /tmp/test-files | xargs --max-procs=0 djade --target-version 5.1
  Time (mean ± σ):      72.0 ms ±   1.0 ms    [User: 63.2 ms, System: 9.3 ms]
  Range (min … max):    70.5 ms …  73.4 ms    18 runs

Benchmark 3: cat /tmp/test-files | xargs --max-procs=0 djhtml
  Time (mean ± σ):      1.401 s ±  0.026 s    [User: 1.322 s, System: 0.079 s]
  Range (min … max):    1.373 s …  1.453 s    10 runs

Benchmark 4: cat /tmp/test-files | xargs --max-procs=0 djlint --reformat --profile=django --max-line-length 120
  Time (mean ± σ):      2.343 s ±  0.026 s    [User: 64.944 s, System: 1.176 s]
  Range (min … max):    2.297 s …  2.377 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 5: cat /tmp/test-files | xargs --max-procs=0 ./node_modules/.bin/prettier --ignore-unknown --write --print-width 120 --log-level silent
  Time (mean ± σ):      3.226 s ±  0.062 s    [User: 4.481 s, System: 0.261 s]
  Range (min … max):    3.092 s …  3.292 s    10 runs

  Warning: Ignoring non-zero exit code.

Summary
  cat /tmp/test-files | xargs --max-procs=0 ../../target/release/djangofmt format --profile django --line-length 120 --quiet ran
    3.63 ± 0.17 times faster than cat /tmp/test-files | xargs --max-procs=0 djade --target-version 5.1
   70.71 ± 3.45 times faster than cat /tmp/test-files | xargs --max-procs=0 djhtml
  118.28 ± 5.48 times faster than cat /tmp/test-files | xargs --max-procs=0 djlint --reformat --profile=django --max-line-length 120
  162.80 ± 7.96 times faster than cat /tmp/test-files | xargs --max-procs=0 ./node_modules/.bin/prettier --ignore-unknown --write --print-width 120 --log-level silent

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details on how to get started.

Shell Completions

You can generate shell completions for your preferred shell using the djangofmt completions command.

Usage: djangofmt completions <SHELL>

Arguments:
  <SHELL>
      The shell to generate the completions for
      [possible values: bash, elvish, fish, nushell, powershell, zsh]

About

A fast, HTML aware, Django template formatter, written in Rust.

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • HTML 58.3%
  • Rust 22.2%
  • Python 12.9%
  • Astro 3.4%
  • TypeScript 1.1%
  • Just 0.9%
  • Other 1.2%