Skip to content

Commit fb312f9

Browse files
committed
improve docs & code comments
1 parent 919439f commit fb312f9

11 files changed

Lines changed: 55 additions & 91 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323

2424
## 🤔 For What?
2525

26-
Every language in your stack has a formatter and a linter. HTML templates are the exception. Generic HTML tools can't parse `{% %}` and `{{ }}`, and template engines don't care what the markup around them looks like. Templates end up in a tooling blind spot: drifting indentation, mismatched tags and inconsistent spacing that survive every code review.
26+
Every language in your stack has a formatter and a linter. HTML templates are the exception. Generic HTML tools can't parse `{% %}` and `{{ }}`, and template engines don't care what the markup around them looks like. So indentation drifts, tags go unclosed and spacing goes wherever, and none of it gets caught in review.
2727

28-
djLint covers that blind spot. It understands HTML _and_ the template syntax inside it, with profiles for Django, Jinja, Twig, Nunjucks, Handlebars, Liquid, Go templates and more.
28+
djLint reads HTML _and_ the template syntax inside it, with profiles for Django, Jinja, Twig, Nunjucks, Handlebars, Liquid, Go templates and more.
2929

3030
Take a template only its author could love:
3131

@@ -78,9 +78,9 @@ One `djlint --reformat --single-attribute-per-line` later:
7878

7979
One command rebuilt the indentation, fixed the tag case, split the long tag into one attribute per line, normalized the template tags and collapsed stray blank lines.
8080

81-
And the linter catches what formatting can't fix: orphan tags, missing `alt` attributes, hard-coded URLs and dozens of other checks.
81+
The linter catches what formatting can't fix: orphan tags, missing `alt` attributes, hard-coded URLs and dozens of other checks.
8282

83-
**[Try it on your own templates in the online playground](https://djlint.com/demo/)**
83+
You can [try it on your own templates in the online playground](https://djlint.com/demo/).
8484

8585
## ✨ How?
8686

@@ -189,7 +189,7 @@ uv sync
189189
uv run pytest
190190
```
191191

192-
## 🏃 Other Tools Of Note
192+
## 🏃 Other tools of note
193193

194194
- [djade](https://github.com/adamchainz/djade) A fast Django template formatter that formats template syntax whilst leaving HTML as-is, and applies fixes for older Django versions.
195195
- [djangofmt](https://github.com/UnknownPlatypus/djangofmt) A fast, HTML-aware Django/Jinja template formatter written in Rust that formats HTML and template syntax together.

docs/src/docs/getting-started.md

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,13 @@ echo "<div></div>" | djlint - --stdin-filename templates/index.html
9898
| `1` | djLint found linting errors, or files that need reformatting. `--warn` reports these as warnings and exits `0` instead. |
9999
| `2` | djLint did not check what you asked it to: the paths matched no files, the command line or config was invalid, or djLint failed. |
100100

101-
Only code `1` means "djLint looked at your templates and did not like what it
102-
found". Code `2` always means the run itself did not deliver, so a pipeline can
103-
treat the two differently instead of guessing.
104-
105-
That covers the "djLint checked nothing" case - a wrong path, an `--extension`
106-
that no longer matches, or templates that have moved - a run that would
107-
otherwise pass silently while checking none of your templates.
108-
109-
Files that djLint _did_ find and then skipped on purpose - through `exclude`,
110-
`extend_exclude`, `use_gitignore` or `require_pragma` - are not an error. That
111-
run exits `0`, because the configuration did exactly what it was told to. This
112-
is what lets `exclude` work under pre-commit, which passes the names of your
113-
staged files whether or not you want djLint to look at them.
114-
115-
If a path that legitimately has no templates is normal for your pipeline, turn
116-
code `2` off with `allow_empty_input`:
101+
Only code `1` means djLint looked at your templates and did not like what it found. Code `2` always means the run itself did not deliver, so a pipeline can treat the two differently instead of guessing.
102+
103+
Code `2` is what catches a run that checked nothing at all: a wrong path, an `--extension` that no longer matches, or templates that have moved. Without it such a run passes silently, having looked at none of your templates.
104+
105+
Files that djLint _did_ find and then skipped on purpose, through `exclude`, `extend_exclude`, `use_gitignore` or `require_pragma`, are not an error. That run exits `0`, because the configuration did exactly what it was told to. It is also what lets `exclude` work under pre-commit, which passes the names of your staged files whether or not you want djLint to look at them.
106+
107+
If a path that legitimately has no templates is normal for your pipeline, turn code `2` off with `allow_empty_input`:
117108

118109
```bash
119110
djlint /path/to/templates --lint --allow-empty-input

docs/src/docs/linter.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,7 @@ Do:
879879

880880
`Avoid use of <br> tags.`
881881

882-
`<br>` encodes presentation in markup: using it for spacing or to fake paragraphs breaks text reflow at narrow widths and degrades accessibility, since screen readers announce forced breaks instead of a natural pause between blocks. Separate thoughts belong in separate block elements, and vertical spacing belongs to CSS margins. Note that `<br>` is legitimate where the line break is part of the content itself (postal addresses, poems, lyrics), and this rule cannot tell those apart from presentational use: it flags every `<br>`. Leave it disabled if your templates render such content.
882+
`<br>` encodes presentation in markup: using it for spacing or to fake paragraphs breaks text reflow at narrow widths and degrades accessibility, since screen readers announce forced breaks instead of a natural pause between blocks. Use separate block elements for separate thoughts, and CSS margins for the spacing between them. `<br>` is legitimate where the line break is part of the content itself (postal addresses, poems, lyrics), but this rule cannot tell those apart from presentational use and flags every `<br>`, so leave it disabled if your templates render such content.
883883

884884
Off by default; enable with `--include=H036`.
885885

@@ -1047,9 +1047,7 @@ Please include a test to validate the rule.
10471047

10481048
## Custom Rules
10491049

1050-
You can add custom rules just for your project by creating a `.djlint_rules.yaml` alongside
1051-
your `pyproject.toml`. Rules can be added to this files and djLint will pick them up.
1052-
A rules file in another location can be given with the `--rules` CLI option.
1050+
You can add custom rules just for your project by creating a `.djlint_rules.yaml` alongside your `pyproject.toml`. Rules can be added to this files and djLint will pick them up. A rules file in another location can be given with the `--rules` CLI option.
10531051

10541052
### Pattern Rules
10551053

@@ -1075,20 +1073,16 @@ You can add rules that import and execute a custom python function:
10751073
python_module: your_package.your_module
10761074
```
10771075
1078-
The specified `python_module` must contain a `run()` function that will be executed on
1079-
every checked file. It must accept the following arguments:
1076+
The specified `python_module` must contain a `run()` function that will be executed on every checked file. It must accept the following arguments:
10801077

10811078
::: content
10821079

1083-
- `rule`: The dict that represent your rule in `.djlint_rules.yaml`. You will typically
1084-
use this variable to access the rule name and message.
1080+
- `rule`: The dict that represent your rule in `.djlint_rules.yaml`. You will typically use this variable to access the rule name and message.
10851081
- `config`: The DJLint configuration object.
10861082
- `html`: The full html content of the file.
10871083
- `filepath`: Path to the file that we are currently checking.
1088-
- `line_ends`: List of line `start` and `end` character position that you can use with
1089-
`djlint.lint.get_line()` to get line numbers from a character position. See the example.
1090-
- `*args, **kwargs`: We might add other arguments in the future, so you should include
1091-
those two arguments to reduce the risk of failure on djLint upgrade.
1084+
- `line_ends`: List of line `start` and `end` character position that you can use with `djlint.lint.get_line()` to get line numbers from a character position. See the example.
1085+
- `*args, **kwargs`: We might add other arguments in the future, so you should include those two arguments to reduce the risk of failure on djLint upgrade.
10921086
:::
10931087

10941088
It must return a list of dict, one for each errors, with the following keys:

src/djlint/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@
2727
def _fail_with_usage_code(func: Callable[..., None]) -> Callable[..., None]:
2828
"""Keep djLint failing apart from djLint finding things.
2929
30-
An unhandled error used to reach the interpreter and exit 1, the same
31-
code as "found lint errors" or "would reformat", so a pipeline could
32-
not tell a crash from a normal failing run - a template djLint cannot
33-
read looked exactly like a template it disliked. Failures now exit 2,
30+
Left to reach the interpreter, an unhandled error exits 1, the same code
31+
as "found lint errors" or "would reformat", and a pipeline cannot tell a
32+
crash from a normal failing run: a template djLint cannot read looks
33+
exactly like a template it disliked. So failures exit 2 instead,
3434
alongside the usage errors click already exits 2 for. The traceback is
3535
still printed, so bug reports lose nothing.
3636
"""
@@ -49,7 +49,7 @@ def wrapper(*args: object, **kwargs: object) -> None:
4949
echo(
5050
style(
5151
"djLint failed and did not finish checking. This is not a"
52-
" clean run - report unexpected failures at"
52+
" clean run. Please report unexpected failures at"
5353
" https://github.com/djlint/djLint/issues",
5454
fg="red",
5555
bold=True,

src/djlint/formatter/attributes.py

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,12 @@ def has_unquoted_template_expression(attribute_group: str) -> bool:
6464
def count_object_properties(config: Config, value: str) -> int:
6565
"""Count the number of properties in a JSON/JS object."""
6666
try:
67-
# Try parsing as JSON first
6867
data = json.loads(value)
6968
return len(data)
7069
except json.JSONDecodeError:
71-
# For JS objects, count property-like patterns
72-
# Simple heuristic: count comma-separated properties
73-
cleaned = config.format_attribute_js_json_string_pattern.sub(
74-
"", value
75-
) # Remove strings
70+
# not json, so fall back to counting comma separated properties.
71+
# strings go first, or a comma inside one counts as a separator.
72+
cleaned = config.format_attribute_js_json_string_pattern.sub("", value)
7673
return len(
7774
config.format_attribute_js_json_property_pattern.findall(cleaned)
7875
)
@@ -97,25 +94,21 @@ def format_json_with_indent(
9794
except json.JSONDecodeError:
9895
return value
9996
else:
100-
# Use indent_size from config for JSON formatting
10197
indent_size = config.js_config.get("indent_size", 4)
10298
formatted = json.dumps(data, indent=indent_size)
103-
# Add base_indent to each line (except first)
99+
# json.dumps indents from column 0, so the attribute's own indentation
100+
# goes in front of every line but the first, which follows the quote.
101+
# the closing brace sits one level back from the properties.
104102
lines = formatted.split("\n")
105103
if len(lines) > 1:
106104
indented_lines = [lines[0]]
107105
for i, line in enumerate(lines[1:], 1):
108-
if i == len(lines) - 1: # Last line (closing brace)
109-
# Indent closing brace indent_size spaces less than properties
110-
# Content lines get: base_indent + json_indent_size
111-
# Closing should get: base_indent + json_indent_size - indent_size
106+
if i == len(lines) - 1:
112107
content_indent = base_indent + (" " * indent_size)
113108
if len(content_indent) >= indent_size:
114-
closing_indent = content_indent[
115-
:-indent_size
116-
] # Remove indent_size spaces from content indent
109+
closing_indent = content_indent[:-indent_size]
117110
else:
118-
closing_indent = base_indent # Fallback to base_indent
111+
closing_indent = base_indent
119112
indented_lines.append(closing_indent + line)
120113
else:
121114
indented_lines.append(base_indent + line)
@@ -131,48 +124,38 @@ def format_js_with_indent(config: Config, value: str, base_indent: str) -> str:
131124
)
132125

133126
try:
134-
# Use the same JS config as the main JS formatter
127+
# the same config the main js formatter uses, except that the
128+
# attribute's indentation is added below, so jsbeautifier adds none.
135129
js_config = dict(config.js_config)
136-
js_config["indent_level"] = 0 # No extra indentation from jsbeautifier
130+
js_config["indent_level"] = 0
137131

138132
opts = BeautifierOptions(js_config)
139133
formatted: str = jsbeautifier.beautify(value, opts)
140134
except ValueError:
141135
return value
142136
else:
143-
# Add base_indent to each line while preserving relative indentation
137+
# keep jsbeautifier's relative indentation and put the attribute's own
138+
# in front of it. the first line follows the quote, so it takes none.
144139
lines = formatted.split("\n")
145140
if len(lines) > 1:
146-
indented_lines = [
147-
lines[0].strip()
148-
] # Remove jsbeautifier's indentation from first line
141+
indented_lines = [lines[0].strip()]
149142

150143
for i, line in enumerate(lines[1:], 1):
151-
if not line.strip(): # Handle empty lines
144+
if not line.strip():
152145
indented_lines.append("")
153146
continue
154147

155-
# Preserve the original jsbeautifier indentation structure
156148
line_indent = len(line) - len(line.lstrip())
157149

158-
# Check if this is an object (starts and ends with braces)
159150
is_object = value.strip().startswith(
160151
"{"
161152
) and value.strip().endswith("}")
162153

163-
if (
164-
is_object and i == len(lines) - 1
165-
): # Last line of object (closing brace)
166-
# Indent closing brace 2 spaces less than properties for objects
167-
# jsbeautifier already provides the correct line_indent difference
168-
# Content lines get: base_indent + (" " * line_indent_for_content) (e.g., 14 + 2 = 16)
169-
# Closing line gets: base_indent + (" " * line_indent_for_closing) (e.g., 14 + 0 = 14)
170-
# This naturally creates the 2-space difference we want
154+
if is_object and i == len(lines) - 1:
171155
indented_lines.append(
172156
base_indent + (" " * line_indent) + line.strip()
173157
)
174158
else:
175-
# For general JS code or object properties, use full base_indent + jsbeautifier indent
176159
indented_lines.append(
177160
base_indent + (" " * line_indent) + line.strip()
178161
)
@@ -434,18 +417,17 @@ def format_attributes(config: Config, html: str, token: TagToken) -> str:
434417
and VERBATIM_ATTRIBUTE_NEWLINE not in attrib_value
435418
and config.format_attribute_js_json_pattern.match(attrib_name)
436419
):
437-
# Check if it's an object or general JavaScript code
438420
if config.format_attribute_js_json_object_pattern.match(
439421
attrib_value
440422
):
441-
# Skip objects with fewer than minimum properties
423+
# an object short enough to read on one line is left there.
442424
if (
443425
count_object_properties(config, attrib_value)
444426
>= config.format_attribute_js_json_min_props
445427
):
446-
# Format JSON objects first, then JavaScript objects
428+
# the value starts after the attribute name and its quote,
429+
# so that is where its content lines up.
447430
if is_json_object(attrib_value):
448-
# Calculate proper base indentation for JSON content
449431
json_base_indent = (
450432
spacing
451433
+ (quote_length + len(attrib_name or "")) * " "
@@ -454,18 +436,15 @@ def format_attributes(config: Config, html: str, token: TagToken) -> str:
454436
config, attrib_value, json_base_indent
455437
)
456438
else:
457-
# Calculate proper base indentation for JavaScript objects
458439
js_base_indent = (
459440
spacing
460441
+ (quote_length + len(attrib_name or "")) * " "
461442
)
462-
# Format JavaScript objects
463443
attrib_value = format_js_with_indent(
464444
config, attrib_value, js_base_indent
465445
)
466446
else:
467-
# Format general JavaScript code (non-objects)
468-
# Calculate base indentation for general JS code
447+
# not an object, so js code with no property count to check.
469448
js_code_base_indent = (
470449
spacing + (quote_length + len(attrib_name or "")) * " "
471450
)

src/djlint/formatter/class_attributes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
# attribute values that hold a token list or css, where a line break is
4040
# only formatting. everything else is kept as written: a value reaches the
41-
# page as it stands, so joining its lines changes the document - a `title`
41+
# page as it stands, so joining its lines changes the document. a `title`
4242
# tooltip loses a line, hyperscript ("_") loses a command separator, and a
4343
# `data-` value read by script comes back different.
4444
_JOINABLE_ATTRIBUTE_NAMES: Final = frozenset({
@@ -48,8 +48,8 @@
4848
"srcset",
4949
"style",
5050
})
51-
# of those, the ones whose edges mean nothing at all - a token list and
52-
# css - so joining their lines can close up rather than leave a space
51+
# of those, the ones whose edges mean nothing at all (a token list and
52+
# css), so joining their lines can close up rather than leave a space
5353
# behind. the rest are laid out by their own rules, which decide that.
5454
_TRIMMED_ATTRIBUTE_NAMES: Final = frozenset({"class", "style"})
5555

src/djlint/formatter/condense.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,8 @@ def _rendered_whitespace(text: str, left: str, right: str) -> str:
308308
309309
Css collapses each run of space, tab and line break to one space, then
310310
drops that space where it falls against a line edge or against other
311-
collapsible whitespace - the neighbour renders it instead. Whatever is
312-
left over is layout this formatter owns rather than content, so it
311+
collapsible whitespace, since the neighbour renders it instead. Whatever
312+
is left over is layout this formatter owns rather than content, so it
313313
goes. Other whitespace (e.g. u+2005) is never collapsed or dropped.
314314
315315
`left` and `right` are the single characters the whitespace sits

src/djlint/formatter/expand.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,8 @@ def touches_rendered_content(index: int, *, back: bool) -> bool:
367367
"""Whether rendered content runs right up to this position.
368368
369369
Looks through tags that lay out nothing of their own, and through
370-
the inside edge of a box - whitespace there is the edge of that
371-
box's own content, so what lies beyond it is what would be parted.
370+
the inside edge of a box, where whitespace is the edge of that box's
371+
own content, so what lies beyond it is what would be parted.
372372
"""
373373
while True:
374374
char = html[index - 1 : index] if back else html[index : index + 1]

0 commit comments

Comments
 (0)