Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DjHTML Version 3 #77

Merged
merged 45 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c2c706d
DjHTML Version 3
JaapJoris Feb 13, 2023
9ebcc42
Introduce CloseDouble token to fix problem with missing `;` in CSS
JaapJoris Feb 13, 2023
7b8cc45
Account for nested SCSS with missing semicolons in various indentatio…
JaapJoris Feb 14, 2023
8083d0a
Tidy up for next release
JaapJoris Feb 15, 2023
4c7b1f1
Add `{% placeholder %}` to ambiguous block tags
JaapJoris Feb 15, 2023
0ec8335
Upgrade Black to 23.1.0
JaapJoris Feb 16, 2023
1fd02b2
Correct handling of single-line JavaScript comments
JaapJoris Feb 16, 2023
bae7427
JavaScript multi-line variable assignment
JaapJoris Feb 16, 2023
ce41ad1
Fix AssertionError on empty files
JaapJoris Feb 16, 2023
a877f30
Better handling of multi-line CSS statements
JaapJoris Feb 18, 2023
79c61f9
Better handling of multi-line JS variable assignments
JaapJoris Feb 18, 2023
b3aaa16
Fix CSS multi-line with prior statements
JaapJoris Feb 18, 2023
bd324e6
Fix multi-line HTML elements with preceding elements
JaapJoris Feb 18, 2023
de41a06
Expect a space between CSS properties and values
JaapJoris Feb 18, 2023
86e32d0
Save the planet by not wasting resources to indent minified JavaScript
JaapJoris Feb 18, 2023
07791e5
Tabwidth guessing
JaapJoris Feb 18, 2023
528f78a
Support the JavaScript "spread" operator
JaapJoris Feb 18, 2023
1d71ec9
JavaScript multi-line ternary operator
JaapJoris Feb 18, 2023
e4951d8
Detect quoted strings with escaped quotes in DjJS mode
JaapJoris Feb 18, 2023
afb7e00
Improve JavaScript multi-line variable declaration
JaapJoris Feb 18, 2023
aa47f1c
Support single-line JavaScript if statements, under limited circumsta…
JaapJoris Feb 19, 2023
7d2d076
JavaScript multi-line variable assignment
JaapJoris Feb 19, 2023
be2dde0
Count tabs as 4 spaces
JaapJoris Feb 19, 2023
df3f493
Single-line JavaScript else, for and while statements
JaapJoris Feb 19, 2023
2443375
Skip empty lines when guessing tabwidth
JaapJoris Feb 19, 2023
225de11
Limit guesses to 2, 4, or 8
JaapJoris Feb 19, 2023
2bb321b
Fix off-by-one error
JaapJoris Feb 19, 2023
aaa328d
Fix false negative unittest
JaapJoris Feb 19, 2023
98dccf6
Fix template tags in multi-line HTML elements
JaapJoris Feb 19, 2023
dc16645
Allow single-quoted HTML attribute values
JaapJoris Feb 19, 2023
f6087f3
Less regexes
JaapJoris Feb 19, 2023
60f9c11
Improve guessing algorithm (thanks SJ!)
JaapJoris Feb 20, 2023
8c6af6d
Remove existing indentation from test suite before testing
JaapJoris Feb 20, 2023
5d14523
Omit empty lines from middleware
JaapJoris Feb 20, 2023
e73ff43
Cleanup help message and remove `--quiet` option
JaapJoris Feb 20, 2023
9f37df8
Update README with new examples
JaapJoris Feb 21, 2023
fe4dde3
Remove Django middleware
JaapJoris Feb 21, 2023
09b419a
Update regular expression to match quoted strings [wip]
JaapJoris Feb 21, 2023
92d68ae
Fix regular expression to match quoted strings
JaapJoris Feb 21, 2023
1c56e79
Remove superfluous backslashes
JaapJoris Feb 21, 2023
553e254
Use `!r` conversion when generating repr() strings in debug mode
JaapJoris Feb 21, 2023
05a2de2
Preserve original indentation of {# comment #} tags
JaapJoris Feb 21, 2023
a7f9e07
Simplify regular expressions
JaapJoris Feb 21, 2023
01d6fdc
JavaScript regular expression literals
JaapJoris Feb 21, 2023
8a89686
Compare actual with expected tokenization in unittests
JaapJoris Feb 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ on:
jobs:
tests:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04

strategy:
matrix:
python-version:
- 3.6
- 3.7
- 3.8
- 3.9
- '3.10'
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
Expand Down
157 changes: 95 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

***A pure-Python Django/Jinja template indenter without dependencies.***

DjHTML is a fully automatic template indenter that works with mixed
HTML/CSS/Javascript templates that contain
DjHTML indents mixed HTML/CSS/JavaScript templates that contain
[Django](https://docs.djangoproject.com/en/stable/ref/templates/language/)
or [Jinja](https://jinja.palletsprojects.com/templates/) template
tags. It works similar to other code-formatting tools such as
Expand All @@ -15,54 +14,77 @@ whitespace at the beginning of lines. It will not insert newlines or
other characters. The goal is to correctly indent already
well-structured templates, not to fix broken ones.

For example, consider the following incorrectly indented template:

### New! Multi-line HTML elements

As of version 3, DjHTML indents multi-line HTML elements and
multi-line attribute values like this:

```jinja
<blockquote cite="Guido Van Rossum"
style="font-style: italic;
{% if dark_mode %}
background: black;
{% endif %}
">
Don't you hate code that's not properly indented?
</blockquote>
```


### New! Multi-line CSS indentation

Multi-line CSS values are now continued at the same indentation level:

```jinja
<!doctype html>
<html>
<body>
{% block content %}
Hello, world!
{% endblock %}
<script>
$(function() {
console.log('Hi mom!');
});
</script>
</body>
</html>
<style>
@font-face {
font-family: Helvetica;
src: {% for format, filename in licensed_fonts %}
url('{% static filename %}') format('{{ format }}'),
{% endfor %}
url('Arial.woff2') format('woff2'),
url('Arial.woff') format('woff');
}
</style>
```

This is what it will look like after processing by DjHTML:

### New! Improved JavaScript indentation

Many new JavaScript indention rules have been added, such as the
indentation of method chaining:

```jinja
<!doctype html>
<html>
<body>
{% block content %}
Hello, world!
{% endblock %}
<script>
$(function() {
console.log('Hi mom!');
});
</script>
</body>
</html>
<script>
window.fetch('/test.html')
.then((html) => {
document.body.innerHTML = html;
{% block extra_statements %}
{% endblock %}
});
</script>
```


### New! Tabwidth guessing

Without the `-t` / `--tabwidth` argument, DjHTML no longer defaults to
a tabwidth of 4 but instead guesses the correct tabwidth.


## Installation

DjHTML is compatible with all operating systems supported by Python.
Install DjHTML with the following command:
DjHTML requires Python 3.8 or higher and is compatible with all
operating systems supported by Python. Install DjHTML with the
following command:

$ pip install djhtml

Note that [Windows still uses legacy code pages for the system
encoding](https://docs.python.org/3/using/windows.html#win-utf8-mode).
It is highly advised to set the environment variable `PYTHONUTF8` to
`1` to avoid issues with indenting UTF-8 files. You can do so with the
Note that
[Windows still uses legacy code pages](https://docs.python.org/3/using/windows.html#win-utf8-mode)
instead of UTF-8. It is highly advised to set the environment variable
`PYTHONUTF8` to `1` with the
[setx](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/setx)
command:

Expand All @@ -78,12 +100,14 @@ command:
reindented template.html
1 template has been reindented.

You can also run `djhtml .` to indent all HTML files beneath the
current directory.

An exit status of 0 means that everything went well, regardless of
whether any files were changed. When the option `-c` / `--check` is
used, the exit status is 1 when one or more files would have changed,
but no changes are actually made. The exit status of 123 means that
there was an error while indenting one or more files. All available
options are given by `djthml -h` / `djthml --help`.
but no changes are actually made. All available options are given by
`djthml -h` / `djthml --help`.


## `fmt:off` and `fmt:on`
Expand All @@ -92,13 +116,11 @@ You can exclude specific lines from being processed with the
`{# fmt:off #}` and `{# fmt:on #}` operators:

```jinja
<div class="
{# fmt:off #}
,-._|\
/ .\
\_,--._/
{# fmt:on #}
"/>
{# fmt:off #}
,-._|\
/ .\
\_,--._/
{# fmt:on #}
```

Contents inside `<pre> ... </pre>`, `<!-- ... --->`, `/* ... */`, and
Expand All @@ -124,7 +146,7 @@ The indenter operates in one of three different modes:

## pre-commit configuration

The best way to use DjHTML is as a [pre-commit](https://pre-commit.com/)
A great way to use DjHTML is as a [pre-commit](https://pre-commit.com/)
hook, so all your HTML, CSS and JavaScript files will automatically be
indented upon every commit.

Expand Down Expand Up @@ -203,25 +225,36 @@ happy, please do the following:

Your feedback for improving DjHTML is very welcome!


## Development

Use your preferred system for setting up a virtualenv, docker environment,
or whatever else, then run the following:
First of all, clone this repository:

```sh
python -m pip install -e '.[dev]'
pre-commit install --install-hooks
```
$ git clone https://github.com/rtts/djhtml
$ cd djhtml

Tests can then be run quickly in that environment:
Then, create a Python virtualenv and activate it:

```sh
python -m unittest discover -v
```
$ python -m venv ~/.virtualenvs/djhtml
$ . ~/.virtualenvs/djhtml/bin/activate

Or testing in all available supported environments and linting can be run
with [`nox`](https://nox.thea.codes):
Then, install the package in [development
mode](https://setuptools.pypa.io/en/latest/userguide/development_mode.html)
including the `dev` dependencies, and install the pre-commit hooks:

```sh
nox
```
$ python -m pip install -e '.[dev]'
$ pre-commit install --install-hooks

You can run the unittests with:

$ python -m unittest

Or use [`nox`](https://nox.thea.codes) to test all supported Python
interpreters:

$ nox

Finally, to get a little insight into the tokenization step of the
indenting algorithm, you can run DjHTML with the `-d` / `--debug`
argument. You will see a Python representation of the tokens that are
created.
51 changes: 41 additions & 10 deletions djhtml/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
standard output. Example usage:

$ djhtml - < input.html > output.html

Passing a directory name will recurse into the directory and format
all files with typical extensions. For more fine-grained control of
which files get processed, use external tools like find, xargs or
pre-commit.

"""

import sys
Expand Down Expand Up @@ -51,12 +57,25 @@ def main():
_error(e)
continue

# Guess tabwidth
if not options.tabwidth:
prev = 0
probabilities = [0] * 9
for line in source.splitlines():
if line and not line.isspace():
depth = _get_depth(line)
if abs(depth - prev) in [2, 4, 8]:
probabilities[abs(depth - prev)] += 1
prev = depth
guess = probabilities.index(max(probabilities))

# Indent input file
try:
if options.debug:
print(Mode(source).debug())
sys.exit()
result = Mode(source).indent(options.tabwidth)
result = Mode(source).indent(options.tabwidth or guess or 4)
except modes.MaxLineLengthExceeded:
problematic_files += 1
_error(f"Maximum line length exceeded in {filename}")
continue
except Exception:
_error(
f"Fatal error while processing {filename}\n\n"
Expand All @@ -66,17 +85,15 @@ def main():
)
raise

changed = _verify_changed(source, result)
if changed:
if changed := _verify_changed(source, result):
changed_files += 1
else:
unchanged_files += 1

# Write output file
if not options.check:
if filename == "-":
if not options.quiet:
print(result, end="")
print(result, end="")
elif changed:
try:
with open(filename, "w") as output_file:
Expand All @@ -102,6 +119,9 @@ def main():
f"{problematic_files} template{s} could not be processed due to an error."
)

if options.debug:
print(Mode(source).debug(), file=sys.stderr)

# Exit with appropriate exit status
if problematic_files:
sys.exit(123)
Expand Down Expand Up @@ -141,9 +161,20 @@ def _verify_changed(source, result):
return changed


def _get_depth(line):
count = 0
for char in line:
if char == " ":
count += 1
elif char == "\t":
count += 4
else:
break
return count


def _info(msg):
if not options.quiet:
print(msg, file=sys.stderr)
print(msg, file=sys.stderr)


def _error(msg):
Expand Down
Loading