diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d80350..c1f874a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4cd384..8b0fc01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/README.md b/README.md index e6daacd..1e5f84c 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 +
+ Don't you hate code that's not properly indented? ++``` + + +### New! Multi-line CSS indentation + +Multi-line CSS values are now continued at the same indentation level: ```jinja - - - - {% block content %} - Hello, world! - {% endblock %} - - - + ``` -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 - - - - {% block content %} - Hello, world! - {% endblock %} - - - + ``` +### 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: @@ -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` @@ -92,13 +116,11 @@ You can exclude specific lines from being processed with the `{# fmt:off #}` and `{# fmt:on #}` operators: ```jinja - +{# fmt:off #} + ,-._|\ + / .\ + \_,--._/ +{# fmt:on #} ``` Contents inside `
...`, ``, `/* ... */`, and @@ -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. @@ -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. diff --git a/djhtml/__main__.py b/djhtml/__main__.py index 5368a55..7e062e8 100644 --- a/djhtml/__main__.py +++ b/djhtml/__main__.py @@ -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 @@ -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" @@ -66,8 +85,7 @@ def main(): ) raise - changed = _verify_changed(source, result) - if changed: + if changed := _verify_changed(source, result): changed_files += 1 else: unchanged_files += 1 @@ -75,8 +93,7 @@ def main(): # 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: @@ -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) @@ -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): diff --git a/djhtml/lines.py b/djhtml/lines.py index 6314562..03f8153 100644 --- a/djhtml/lines.py +++ b/djhtml/lines.py @@ -2,57 +2,69 @@ class Line: """ A single output line not including the final newline. - The behavior regarding final newlines has changed between DjHTML - v1.4.14 and v1.5.0. It used to always append the final newline, - but now this will only happen when the source file already - contains a final newline. - - See https://github.com/rtts/djhtml/issues/56 for the discussion - that led to this change. - """ - def __init__(self, nr=1): - self.nr = nr - self.tokens = [] - self.level = 0 + def __init__(self, tokens=None, level=0, offset=0, ignore=False): + """ + Lines are currently never instantiated with arguments, but + that doesn't mean they can't. + + """ + self.tokens = tokens or [] + self.level = level + self.offset = offset + self.ignore = ignore def append(self, token): """ - Append tokens to the line. + Append token to line. """ - token.line_nr = self.nr self.tokens.append(token) @property def text(self): """ - The unindented text of this line without leading/trailing spaces. + The text of this line including the original + leading/trailing spaces. + + """ + return "".join([token.text for token in self.tokens]) + + @property + def indents(self): + """ + Whether this line has more opening than closing tokens. """ - return "".join([str(token) for token in self.tokens]).strip() + return len([token for token in self.tokens if token.indents]) > len( + [token for token in self.tokens if token.dedents] + ) def indent(self, tabwidth): """ - The final, indented text of this line. Make sure to set the level - and optionally offset before calling this method. + The final, indented text of this line. """ - if self.tokens: - if self.tokens[0].ignore: - return "".join([str(token) for token in self.tokens]) - elif self.text: - offset = self.tokens[0].offset * tabwidth - spaces = tabwidth * self.level + offset - return " " * spaces + self.text + if self.ignore: + return self.text + if text := self.text.strip(): + return " " * (tabwidth * self.level + self.offset) + text return "" - def __repr__(self): - return repr(self.tokens) + def __len__(self): + """ + The length of the line (so far), excluding the whitespace + at the beginning. Be careful calling len() because it might + result in trailing spaces being counted that will be removed + by indent(). - def __bool__(self): - return bool(self.tokens and self.text) + """ + return len(self.text.lstrip()) - def __next__(self): - return Line(nr=self.nr + 1) + def __repr__(self): + kwargs = "" + for attr in ["level", "offset", "ignore"]: + if value := getattr(self, attr): + kwargs += f", {attr}={value!r}" + return f"Line({self.tokens!r}{kwargs})" diff --git a/djhtml/modes.py b/djhtml/modes.py index d032a21..e2c8630 100644 --- a/djhtml/modes.py +++ b/djhtml/modes.py @@ -4,43 +4,30 @@ from .tokens import Token -class DjTXT: +class BaseMode: """ - Mode for indenting text files that contain Django/Jinja template tags. - Also serves as the base class for the other modes: - - - DjHTML - - DjCSS - - DjJS + Base class for the different modes. """ - RAW_TOKENS = [ - r"\n", - r"{%[-\+]?.*?[-\+]?%}", - r"{#.*?#}", - r"{#", - ] - OPENING_AND_CLOSING_TAGS = [ - "elif", - "else", - "empty", - "plural", - ] - AMBIGUOUS_BLOCK_TAGS = { - # token_name: (regex_if_block, regex_if_not_block) - "set": (None, " = "), - "video": (" as ", None), - } - FMT_ON = r"{# fmt:on #}" - FMT_OFF = r"{# fmt:off #}" - OPENING_TAG = r"{%[-\+]? *(\w+).*?[-\+]?%}" + MAX_LINE_LENGTH = 10_000 + + def __init__(self, source=None, return_mode=None): + """ + Instantiate with source text before calling indent(), or + with the return_mode when invoked from within another mode. + + """ + assert type(self) is not BaseMode + assert source is not None or return_mode - def __init__(self, source="", return_mode=None): self.source = source self.return_mode = return_mode or self self.token_re = compile_re(self.RAW_TOKENS) + # To keep track of the current offsets. + self.offsets = dict(relative=0, absolute=0) + def indent(self, tabwidth): """ Return the indented text as a single string. @@ -50,78 +37,18 @@ def indent(self, tabwidth): self.parse() return "\n".join([line.indent(tabwidth) for line in self.lines]) - def parse(self): - """ - You found the top-secret indenting algorithm! - - """ - stack = [] - for line in self.lines: - first_token = True - for token in line.tokens: - opening_token = None - - # When a dedenting token is found, match it with the - # token at the top of the stack. - if token.dedents: - try: - if ( - stack[-1].kind == token.kind - and stack[-1].is_hard == token.is_hard - ): - opening_token = stack.pop() - elif token.kind == "django": - opening_token = stack.pop() - while opening_token.kind != token.kind: - opening_token = stack.pop() - elif first_token: - # This closing token could not be matched. - # Instead of erroring out, set the line level - # to what it would have been with a - # regular text token. - - # If there is any OpenHard token in the - # set and current token is CloseHard then - # let's move back to OpenHard. - if token.is_hard and any( - t.is_hard and t.indents for t in stack - ): - s = stack.pop() - while not s.is_hard or not s.indents: - s = stack.pop() - - line.level = stack[-1].level + 1 - except IndexError: - line.level = 0 - - # If this dedenting token is the first in line, - # set the line level to the line level of the - # corresponding opening token. - if first_token and opening_token: - line.level = opening_token.level - - # If the first token is not a dedenting token, the - # line level will be one higher than that of the token - # at the top of the stack. - elif first_token: - line.level = stack[-1].level + 1 if stack else 0 - - # Push indenting tokens onto the stack. Note that some - # tokens can be both indenting and dedenting (e.g., - # ``{% else %}``), hence the if instead of elif. - if token.indents: - token.level = opening_token.level if opening_token else line.level - stack.append(token) - - # Subsequent tokens have no effect on the line level - # (but tokens with only spaces don't count). - if token.text.strip(): - first_token = False - def tokenize(self): """ Split the source text into tokens and place them on lines. + How text is split into raw tokens is defined by the regexes + of each mode. For each raw token, the create_token() method of + the mode is called. + + Some tokens, such a ", - r"[\{\(\)\}]", + r"[{()}]", r"/\*", + r'".+"', + r"'.+'", + r"[\w-]+: ", + r";", ] - def create_token(self, raw_token, src): - kind = "css" - self.next_mode = self + def create_token(self, raw_token, src, line): + mode = self if raw_token in "{(": - return Token.Open(raw_token, kind) - if raw_token in "})": - return Token.Close(raw_token, kind) - if raw_token == "/*": - self.next_mode = Comment(r"\*/", self, kind) - return Token.Open(raw_token, kind) - if raw_token == "": - self.next_mode = self.return_mode - return Token.Close(raw_token, "html") - - return super().create_token(raw_token, src) + token = Token.Open(raw_token, mode=DjCSS, **self.offsets) + if raw_token == "{": + self.offsets["absolute"] = 0 + elif raw_token in "})": + if raw_token == "}": + self.offsets["absolute"] = 0 + token = Token.Close(raw_token, mode=DjCSS, **self.offsets) + elif raw_token.endswith(": "): + token = Token.Text(raw_token, mode=DjCSS, **self.offsets) + self.offsets["absolute"] = len(line) + len(raw_token) + elif raw_token == ";": + self.offsets["absolute"] = 0 + token = Token.Text(raw_token, mode=DjCSS, **self.offsets) + elif raw_token == "/*": + token, mode = Token.Open(raw_token, mode=DjCSS, ignore=True), Comment( + r"\*/", mode=DjCSS, return_mode=self + ) + elif raw_token == "": + token, mode = ( + Token.Close(raw_token, mode=self.return_mode.__class__), + self.return_mode, + ) + else: + token, mode = super().create_token(raw_token, src, line) + + return token, mode class DjJS(DjTXT): @@ -321,61 +366,122 @@ class DjJS(DjTXT): """ RAW_TOKENS = DjTXT.RAW_TOKENS + [ - r"", - r'".*?"', - r"'.*?'", - r"`.*?`", - r"`", - r"[\{\[\(\)\]\}]", r"//.*", r"/\*", + r"[$\w-]+:", + r'"(?:\\.|[^\\"])*"', # "string" + r"'(?:\\.|[^\\'])*'", # 'string' + r"`(?:\\.|[^\\`])*`", # `string` + r"/(?:\\.|[^\\/\n])*/", # /string/ + r"[{[()\]}]", + r"var ", + r"let ", + r"const ", + r"if(?= *\()", + r"else(?= *\n)", + r"for(?= *\()", + r"while(?= *\()", + r"", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.hard_indents = 0 - self.opened_brackets = 0 - - def create_token(self, raw_token, src): - kind = "javascript" - self.next_mode = self - - if raw_token.strip() == "switch": - self.hard_indents = self.hard_indents + 1 - - if self.hard_indents: - if raw_token == "{": - self.opened_brackets += 1 - if self.opened_brackets == self.hard_indents: - return Token.OpenHard(raw_token, kind) - elif raw_token == "}": - if self.opened_brackets == self.hard_indents: - self.opened_brackets -= 1 - self.hard_indents -= 1 - return Token.CloseHard(raw_token, kind) - self.opened_brackets -= 1 + self.previous_offsets = [] + self.haskell = False + self.haskell_re = re.compile(r"^ *, ([$\w-]+ *=|[$\w-]+;?)") + self.variable_re = re.compile(r"^ *([$\w-]+ *=|[$\w-]+;?)") + self.previous_line_ended_with_comma = False + def create_token(self, raw_token, src, line): + mode = self + persist_relative_offset = False + + # Reset absolute offset in almost all cases + if ( + not line + and not self.haskell_re.match(raw_token) + and not ( + self.variable_re.match(raw_token) + and self.previous_line_ended_with_comma + ) + ): + self.offsets["absolute"] = 0 + + # Opening and closing tokens if raw_token in "{[(": - return Token.Open(raw_token, kind) - if raw_token in ")]}": - return Token.Close(raw_token, kind) - if raw_token == "`": - self.next_mode = Comment("`", self, kind) - return Token.Open(raw_token, kind) - if raw_token == "/*": - self.next_mode = Comment(r"\*/", self, kind) - return Token.Open(raw_token, kind) - if raw_token.lstrip().startswith("."): - return Token.Text(raw_token, offset=1) - if raw_token.lstrip().startswith("case "): - return Token.OpenAndClose(raw_token, kind) - if raw_token.lstrip().startswith("default:"): - return Token.OpenAndClose(raw_token, kind) - if raw_token == "": - self.next_mode = self.return_mode - return Token.Close(raw_token, "html") - - return super().create_token(raw_token, src) + self.previous_offsets.append(self.offsets.copy()) + self.offsets = dict(relative=0, absolute=0) + token = Token.Open(raw_token, mode=DjJS) + elif raw_token in ")]}": + if self.previous_offsets: + self.offsets = self.previous_offsets.pop() + if raw_token == ")": + persist_relative_offset = True + token = Token.Close(raw_token, mode=DjJS) + elif raw_token == "/*": + token, mode = Token.Open(raw_token, mode=DjJS, ignore=True), Comment( + r"\*/", mode=DjJS, return_mode=self + ) + elif raw_token.startswith("//"): + token = Token.Text(raw_token, mode=DjJS, ignore=True) + + # "Double" tokens + elif not line and raw_token.lstrip().startswith("case "): + token = Token.OpenDouble(raw_token, mode=DjJS) + return token, mode + elif raw_token.rstrip().endswith("default:"): + token = Token.OpenDouble(raw_token, mode=DjJS) + return token, mode + + # Tokens that mess with relative offset + elif raw_token.lstrip().startswith("..."): + token = Token.Text(raw_token, mode=DjJS) + elif not line and raw_token.lstrip().startswith((".", ": ", "? ")): + token = Token.Text(raw_token, mode=DjJS, relative=1) + elif not line and raw_token in ["if", "else", "for", "while"]: + token = Token.Text(raw_token, mode=DjJS, **self.offsets) + self.offsets["relative"] += 1 + persist_relative_offset = True + elif raw_token.rstrip().endswith(("=", ":")): + token = Token.Text(raw_token, mode=DjJS, **self.offsets) + if not line.indents: + self.offsets["relative"] = 1 + persist_relative_offset = True + + # Tokens that mess with absolute offset + elif raw_token in ["var ", "let ", "const "]: + token = Token.Text(raw_token, mode=DjJS) + self.offsets["absolute"] = len(line) + len(raw_token) + elif ( + not line + and not self.haskell + and not self.previous_line_ended_with_comma + and self.haskell_re.match(raw_token) + ): + self.haskell = True + self.offsets["absolute"] -= 2 + token = Token.Text(raw_token, mode=DjJS, **self.offsets) + + # Get out of this mess! + elif raw_token == "": + token, mode = ( + Token.Close(raw_token, mode=self.return_mode.__class__), + self.return_mode, + ) + else: + token, mode = super().create_token(raw_token, src, line) + + # Reset relative offset in almost all cases + if not persist_relative_offset and raw_token.strip(): + self.offsets["relative"] = 0 + + # Remember whether the line ended with a comma + if raw_token.rstrip().endswith(","): + self.previous_line_ended_with_comma = True + else: + self.previous_line_ended_with_comma = False + + return token, mode # The following are "special" modes with different constructors. @@ -387,18 +493,16 @@ class Comment(DjTXT): """ - def __init__(self, endtag, return_mode, kind): + def __init__(self, endtag, *, mode, return_mode): self.endtag = endtag + self.mode = mode self.return_mode = return_mode - self.kind = kind self.token_re = compile_re([r"\n", endtag]) - def create_token(self, raw_token, src): - self.next_mode = self + def create_token(self, raw_token, src, line): if re.match(self.endtag, raw_token): - self.next_mode = self.return_mode - return Token.Close(raw_token, self.kind) - return Token.Ignore(raw_token) + return Token.Close(raw_token, mode=self.mode, ignore=True), self.return_mode + return Token.Text(raw_token, mode=Comment, ignore=True), self class InsideHTMLTag(DjTXT): @@ -407,43 +511,68 @@ class InsideHTMLTag(DjTXT): """ - RAW_TOKENS = DjTXT.RAW_TOKENS + [r"/?>", r'"'] + RAW_TOKENS = DjTXT.RAW_TOKENS + [r"/?>", r"[^ ='\">/]+=", r'"', r"'"] - def __init__(self, tagname, return_mode): + def __init__(self, tagname, line, return_mode): self.tagname = tagname self.return_mode = return_mode self.token_re = compile_re(self.RAW_TOKENS) self.inside_attr = False + self.offsets = dict( + relative=-1 if line.indents else 0, absolute=len(line) + len(tagname) + 2 + ) - def create_token(self, raw_token, src): - kind = "html" - self.next_mode = self + # Pff... + self.additional_offset = -len(tagname) - 1 - if raw_token == '"': + def create_token(self, raw_token, src, line): + mode = self + + if not line: + self.additional_offset = 0 + self.additional_offset += len(raw_token) + + if "text/template" in raw_token: + self.tagname = "" + + if raw_token in ['"', "'"]: if self.inside_attr: - self.inside_attr = False - return Token.Close(raw_token, kind) - self.inside_attr = True - return Token.Open(raw_token, kind) + token = Token.Text(raw_token, mode=InsideHTMLTag, **self.offsets) + if self.inside_attr == raw_token: + self.inside_attr = False + token.absolute = self.offsets["absolute"] - 1 + self.offsets["absolute"] = self.previous_offset + else: + self.inside_attr = raw_token + self.previous_offset = self.offsets["absolute"] + self.offsets["absolute"] += self.additional_offset + token = Token.Text(raw_token, mode=InsideHTMLTag, **self.offsets) elif not self.inside_attr and raw_token == "/>": - self.next_mode = self.return_mode - return Token.Close(raw_token, kind) + token, mode = Token.Text(raw_token, mode=DjHTML), self.return_mode elif not self.inside_attr and raw_token == ">": if self.tagname.lower() in DjHTML.IGNORE_TAGS: - self.next_mode = self.return_mode - return Token.Close(raw_token, kind) + token, mode = Token.Text(raw_token, mode=DjHTML), self.return_mode + elif self.tagname == "style": + token, mode = Token.Open(raw_token, mode=DjHTML), DjCSS( + return_mode=self.return_mode + ) + elif self.tagname == "script": + token, mode = Token.Open(raw_token, mode=DjHTML), DjJS( + return_mode=self.return_mode + ) else: - if self.tagname == "style": - self.next_mode = DjCSS(return_mode=self.return_mode) - elif self.tagname == "script": - self.next_mode = DjJS(return_mode=self.return_mode) - else: - self.next_mode = self.return_mode - return Token.OpenAndClose(raw_token, kind) - elif "text/template" in raw_token: - self.tagname = "" + token, mode = ( + Token.Open(raw_token, mode=DjHTML), + self.return_mode, + ) + else: + token, mode = super().create_token(raw_token, src, line) + + return token, mode + - return super().create_token(raw_token, src) +class MaxLineLengthExceeded(Exception): + pass def compile_re(raw_tokens): diff --git a/djhtml/options.py b/djhtml/options.py index 9fee2b1..3d05b85 100644 --- a/djhtml/options.py +++ b/djhtml/options.py @@ -3,19 +3,38 @@ import options print(f"Tabwidth is {options.tabwidth}") + """ import argparse import sys +from importlib.metadata import version parser = argparse.ArgumentParser( description=( - "DjHTML is a fully automatic template indenter that works with mixed" - " HTML/CSS/Javascript templates that contain Django or Jinja template" - " tags. It works similar to other code-formatting tools such as Black and" - " interoperates nicely with pre-commit. Full documentation can be found at" - " https://github.com/rtts/djhtml" + """ +DjHTML indents mixed HTML/CSS/JavaScript templates that +contain Django or Jinja template tags. It works similar to +other code-formatting tools such as Black and interoperates +nicely with pre-commit.""" ), + epilog="Full documentation at https://github.com/rtts/djhtml", + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, +) +parser.add_argument( + "-h", + "--help", + dest="show_help", + action="store_true", + help="show this help message and exit", +) +parser.add_argument( + "-v", + "--version", + dest="show_version", + action="store_true", + help="show version number and exit", ) parser.add_argument( "-c", @@ -23,20 +42,19 @@ action="store_true", help="check indentation without modifying files", ) -parser.add_argument("-q", "--quiet", action="store_true", help="be quiet") parser.add_argument( "-t", "--tabwidth", metavar="N", type=int, - default=4, - help="tabwidth (default is 4)", + default=0, + help="tabwidth (the default is to guess)", ) parser.add_argument( "input_filenames", - metavar="filename", - nargs="+", - help="input filenames (either paths or directories)", + metavar="SOURCE", + nargs="*", + help="file or directory name(s) to indent", ) parser.add_argument("-d", "--debug", action="store_true", help=argparse.SUPPRESS) parser.add_argument("-i", "--in-place", action="store_true", help=argparse.SUPPRESS) @@ -45,7 +63,13 @@ self = sys.modules[__name__] args = parser.parse_args(namespace=self) -if self.in_place: +if show_version: + print(version("djhtml")) + sys.exit() +elif show_help or not input_filenames: + parser.print_help() + sys.exit() +elif in_place: sys.exit( """ You have called DjHTML with the -i or --in-place argument which diff --git a/djhtml/tokens.py b/djhtml/tokens.py index 308ef49..831fe7a 100644 --- a/djhtml/tokens.py +++ b/djhtml/tokens.py @@ -8,37 +8,50 @@ class _Base: indents = False dedents = False ignore = False - is_hard = False - - def __init__(self, text, kind="", offset=0): + is_double = False + + def __init__( + self, text, *, mode, level=0, relative=0, absolute=0, ignore=False + ): + """ + Tokens must have a text and a mode class. The level + represents the line level of opening tokens and is set + afterwards by the parser. This violates the principle of + encapsulation, but makes sense because the line level can + only be determined after the tokenization is complete. + + """ self.text = text - self.kind = kind - self.offset = offset - - def __str__(self): - return self.text + self.mode = mode + self.level = level + self.relative = relative + self.absolute = absolute + self.ignore = ignore def __repr__(self): - return f"({self.__class__.__name__}:{repr(self.text)})" + kwargs = f", mode={self.mode.__name__}" + for attr in ["level", "relative", "absolute", "ignore"]: + if value := getattr(self, attr): + kwargs += f", {attr}={value!r}" + return f"{self.__class__.__name__}({self.text!r}{kwargs})" class Text(_Base): pass - class Ignore(_Base): - ignore = True - class Open(_Base): indents = True - class OpenHard(Open): - is_hard = True + class OpenDouble(_Base): + indents = True + is_double = True class Close(_Base): dedents = True - class CloseHard(Close): - is_hard = True + class CloseDouble(_Base): + dedents = True + is_double = True - class OpenAndClose(_Base): + class CloseAndOpen(_Base): indents = True dedents = True diff --git a/noxfile.py b/noxfile.py index 0ff90eb..13b1406 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,6 @@ import nox -@nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]) +@nox.session(python=["3.8", "3.9", "3.10", "3.11"]) def tests(session): - session.run("python", "-m", "unittest", "discover", "-v") + session.run("python", "-m", "unittest") diff --git a/setup.cfg b/setup.cfg index ca589da..fb2d9c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = djhtml -version = 2.0.0 +version = 3.0.0 description = Django/Jinja template indenter long_description = file: README.md long_description_content_type = text/markdown @@ -11,15 +11,13 @@ license = GPLv3+ classifiers = Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Operating System :: OS Independent -python_requires = '>=3.6' +python_requires = '>=3.8' [options] packages = find: @@ -27,9 +25,6 @@ packages = find: [options.extras_require] dev = nox - isort - black - flake8 pre-commit [options.packages.find] @@ -45,6 +40,8 @@ console_scripts = [flake8] max-line-length = 88 extend-ignore = E203 +per-file-ignores = + djhtml/options.py: F821 [isort] profile = black diff --git a/tests/generate_tokens.py b/tests/generate_tokens.py new file mode 100755 index 0000000..5ffb11f --- /dev/null +++ b/tests/generate_tokens.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +""" +Generate *.tokens files from *.html files in the suite directory. + +""" + +from pathlib import Path + +from djhtml.modes import DjHTML + +DIR = Path(__file__).parent / "suite" + +for filename in DIR.iterdir(): + if filename.suffix == ".html": + with open(DIR / filename) as html: + with open(DIR / (filename.stem + ".tokens"), "w") as f: + f.write(DjHTML(html.read()).debug()) diff --git a/tests/suite/css.html b/tests/suite/css.html new file mode 100644 index 0000000..477eb95 --- /dev/null +++ b/tests/suite/css.html @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/suite/css.in b/tests/suite/css.in deleted file mode 100644 index 4ee1693..0000000 --- a/tests/suite/css.in +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - diff --git a/tests/suite/css.out b/tests/suite/css.out deleted file mode 100644 index 359adf0..0000000 --- a/tests/suite/css.out +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - diff --git a/tests/suite/css.tokens b/tests/suite/css.tokens new file mode 100644 index 0000000..12da6cb --- /dev/null +++ b/tests/suite/css.tokens @@ -0,0 +1,190 @@ +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' a ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('text-decoration: ', mode=DjCSS), Text('none', mode=DjCSS, absolute=17), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' a ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('text-decoration: ', mode=DjCSS), Text('none', mode=DjCSS, absolute=17), Text(';', mode=DjCSS)], level=2) +Line([Text(' &:hover ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('text-decoration: ', mode=DjCSS), Text('underline', mode=DjCSS, absolute=17), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' ', mode=DjCSS), Open('/*', mode=DjCSS, level=1, ignore=True)], level=1, ignore=True) +Line([Text(' * Hi mom!', mode=Comment, ignore=True)], level=2, ignore=True) +Line([Text(' *', mode=Comment, ignore=True)], level=2, ignore=True) +Line([Text(' ', mode=Comment, ignore=True), Close('*/', mode=DjCSS, ignore=True)], level=1, ignore=True) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' #gradient ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('background: ', mode=DjCSS), Text('linear-gradient', mode=DjCSS, absolute=12), Open('(', mode=DjCSS, level=2, absolute=12)], level=2) +Line([Text(' to top left,', mode=DjCSS, absolute=12)], level=3, offset=12) +Line([Text(' rgba', mode=DjCSS, absolute=12), Open('(', mode=DjCSS, level=3, absolute=12), Text('204,0,0,1', mode=DjCSS, absolute=12), Close(')', mode=DjCSS, absolute=12), Text(',', mode=DjCSS, absolute=12)], level=3, offset=12) +Line([Text(' rgba', mode=DjCSS, absolute=12), Open('(', mode=DjCSS, level=3, absolute=12), Text('204,0,0,0', mode=DjCSS, absolute=12), Close(')', mode=DjCSS, absolute=12)], level=3, offset=12) +Line([Text(' ', mode=DjCSS, absolute=12), Close(')', mode=DjCSS, absolute=12), Text(';', mode=DjCSS)], level=2, offset=12) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' @font-face ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('font-family: ', mode=DjCSS), Text("'Open Sans'", mode=DjCSS, absolute=13), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Text('font-style: ', mode=DjCSS), Text('normal', mode=DjCSS, absolute=12), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Text('font-weight: ', mode=DjCSS), Text('normal', mode=DjCSS, absolute=13), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Text('src: ', mode=DjCSS), Text('url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=2, absolute=5), Text("'Open_Sans.woff2') format('woff2'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(',', mode=DjCSS, absolute=5)], level=2) +Line([Text(' url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=2, absolute=5), Text("'Open_Sans.woff') format('woff'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(',', mode=DjCSS, absolute=5)], level=2, offset=5) +Line([Text(' url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=2, absolute=5), Text("'Open_Sans.eot?#iefix') format('embedded-opentype'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(',', mode=DjCSS, absolute=5)], level=2, offset=5) +Line([Text(' url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=2, absolute=5), Text("'Open_Sans.svg#OpenSans') format('svg'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(',', mode=DjCSS, absolute=5)], level=2, offset=5) +Line([Text(' url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=2, absolute=5), Text("'Open_Sans.ttf') format('truetype'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(';', mode=DjCSS)], level=2, offset=5) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' @font-face ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('font-family: ', mode=DjCSS), Text('Helvetica', mode=DjCSS, absolute=13), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Text('src: ', mode=DjCSS), Open('{% for format, filename in licensed_fonts %}', mode=DjTXT, level=2, absolute=5)], level=2) +Line([Text(' url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=3, absolute=5), Text("'{% static filename %}') format('{{ format }}'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(',', mode=DjCSS, absolute=5)], level=3, offset=5) +Line([Text(' ', mode=DjCSS, absolute=5), Close('{% endfor %}', mode=DjTXT, absolute=5)], level=2, offset=5) +Line([Text(' url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=2, absolute=5), Text("'Arial.woff2') format('woff2'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(',', mode=DjCSS, absolute=5)], level=2, offset=5) +Line([Text(' url', mode=DjCSS, absolute=5), Open('(', mode=DjCSS, level=2, absolute=5), Text("'Arial.woff') format('woff'", mode=DjCSS, absolute=5), Close(')', mode=DjCSS, absolute=5), Text(';', mode=DjCSS)], level=2, offset=5) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' @media', mode=DjCSS), Open('(', mode=DjCSS, level=1), Text('max-width: ', mode=DjCSS), Text('800px', mode=DjCSS, absolute=18), Close(')', mode=DjCSS, absolute=18), Text(' ', mode=DjCSS, absolute=18), Open('{', mode=DjCSS, level=1, absolute=18)], level=1) +Line([Text(' body ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('margin-top: ', mode=DjCSS), Text('1em', mode=DjCSS, absolute=12), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Text('margin-bottom: ', mode=DjCSS), Text('1em', mode=DjCSS, absolute=15), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' p ', mode=DjCSS), Open('{', mode=DjCSS, level=1), Text(' ', mode=DjCSS), Text('align: ', mode=DjCSS), Text('left ', mode=DjCSS, absolute=11), Close('}', mode=DjCSS)], level=1) +Line([Text(' h1 ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('font-weight: ', mode=DjCSS), Text('bold', mode=DjCSS, absolute=13), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Text('font-size: ', mode=DjCSS), Text('huge', mode=DjCSS, absolute=11)], level=2) +Line([Text(' ', mode=DjCSS, absolute=11), Close('}', mode=DjCSS)], level=1) +Line([Text(' footnote ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('dontread: ', mode=DjCSS), Text('true !important', mode=DjCSS, absolute=10)], level=2) +Line([Text(' ', mode=DjCSS, absolute=10), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' article ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' p ', mode=DjCSS), Open('{', mode=DjCSS, level=2), Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('#999', mode=DjCSS, absolute=11), Text(';', mode=DjCSS), Text(' ', mode=DjCSS), Text('align: ', mode=DjCSS), Text('left ', mode=DjCSS, absolute=24), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Text(' article ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' p ', mode=DjCSS), Open('{', mode=DjCSS, level=2), Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('#999', mode=DjCSS, absolute=11), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Text('align: ', mode=DjCSS), Text('left ', mode=DjCSS, absolute=7), Close('}', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Text(' article ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' p ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('#999', mode=DjCSS, absolute=7), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Text('align: ', mode=DjCSS), Text('left ', mode=DjCSS, absolute=7), Close('}', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Text(' article ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' p ', mode=DjCSS), Open('{', mode=DjCSS, level=2), Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('#999', mode=DjCSS, absolute=11), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Text('align: ', mode=DjCSS), Text('left', mode=DjCSS, absolute=7)], level=3) +Line([Text(' ', mode=DjCSS, absolute=7), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Text(' article ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' p ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('#999', mode=DjCSS, absolute=7), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Text('align: ', mode=DjCSS), Text('left', mode=DjCSS, absolute=7)], level=3) +Line([Text(' ', mode=DjCSS, absolute=7), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Text(' article ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' p', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('#999', mode=DjCSS, absolute=7), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Text('align: ', mode=DjCSS), Text('left', mode=DjCSS, absolute=7), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' blockquote:before ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('content: ', mode=DjCSS), Text('"text-align: right"', mode=DjCSS, absolute=9), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Text(' @media', mode=DjCSS), Open('(', mode=DjCSS, level=1), Text('text-align: ', mode=DjCSS), Text('right', mode=DjCSS, absolute=19), Close(')', mode=DjCSS, absolute=19), Text(' ', mode=DjCSS, absolute=19), Open('{', mode=DjCSS, level=1, absolute=19)], level=1) +Line([Text(' a:hover, a:active ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' text-decoration:none', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=2) +Line([Text(' p::first-line ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('red', mode=DjCSS, absolute=7), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' a:hover, a:active ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('foo: ', mode=DjCSS), Text('bar,', mode=DjCSS, absolute=5)], level=2) +Line([Text(' baz', mode=DjCSS, absolute=5), Text(';', mode=DjCSS)], level=2, offset=5) +Line([]) +Line([Text(' a:hover, a:active ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('foo: ', mode=DjCSS), Text('bar', mode=DjCSS, absolute=5)], level=3) +Line([Text(' baz', mode=DjCSS, absolute=5)], level=3, offset=5) +Line([Text(' ', mode=DjCSS, absolute=5), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' body ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('font-family: ', mode=DjCSS), Text('Arial,', mode=DjCSS, absolute=13)], level=2) +Line([Text(' Helvetica', mode=DjCSS, absolute=13), Text(';', mode=DjCSS)], level=2, offset=13) +Line([Text(' a:link,', mode=DjCSS)], level=2) +Line([Text(' a:hover,', mode=DjCSS)], level=2) +Line([Text(' a:active ', mode=DjCSS), Open('{', mode=DjCSS, level=2)], level=2) +Line([Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('red', mode=DjCSS, absolute=7), Text(';', mode=DjCSS)], level=3) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([]) +Line([Text(' ', mode=DjCSS), Open('/*', mode=DjCSS, level=1, ignore=True), Text(' Since DjCSS expects a space between the property and the value,', mode=Comment, ignore=True)], level=1, ignore=True) +Line([Text(' the following is currently broken ', mode=Comment, ignore=True), Close('*/', mode=DjCSS, ignore=True)], level=2, ignore=True) +Line([Text(' body ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' font-family:Arial,', mode=DjCSS)], level=2) +Line([Text(' Helvetica', mode=DjCSS), Text(';', mode=DjCSS)], level=2) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) +Line([]) +Line([Open('', mode=DjHTML, ignore=True)], ignore=True) +Line([Text('<', mode=DjHTML), Text('style', mode=InsideHTMLTag, absolute=7), Open('>', mode=DjHTML)]) +Line([Text(' body ', mode=DjCSS), Open('{', mode=DjCSS, level=1)], level=1) +Line([Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('green', mode=DjCSS, absolute=7), Text(';', mode=DjCSS), Text(' ', mode=DjCSS), Text('font-family: ', mode=DjCSS), Text('Arial,', mode=DjCSS, absolute=27)], level=2) +Line([Text(' Helvetica', mode=DjCSS, absolute=27), Text(';', mode=DjCSS)], level=2, offset=27) +Line([]) +Line([Text(' ', mode=DjCSS), Open('/*', mode=DjCSS, level=2, ignore=True), Text(' Spaces within lines are always preserved ', mode=Comment, ignore=True), Close('*/', mode=DjCSS, ignore=True)], level=2, ignore=True) +Line([Text(' ', mode=DjCSS), Text('color: ', mode=DjCSS), Text('red', mode=DjCSS, absolute=7), Text(';', mode=DjCSS), Text(' ', mode=DjCSS), Text('font-family: ', mode=DjCSS), Text('Arial,', mode=DjCSS, absolute=27)], level=2) +Line([Text(' Helvetica', mode=DjCSS, absolute=27), Text(';', mode=DjCSS)], level=2, offset=27) +Line([Text(' ', mode=DjCSS), Close('}', mode=DjCSS)], level=1) +Line([Close('', mode=DjHTML)]) +Line([]) \ No newline at end of file diff --git a/tests/suite/django.out b/tests/suite/django.html similarity index 68% rename from tests/suite/django.out rename to tests/suite/django.html index faf9e9b..f31857f 100644 --- a/tests/suite/django.out +++ b/tests/suite/django.html @@ -1,3 +1,13 @@ + + + + {% load static %} {% block content %} @@ -110,3 +120,46 @@
+
++ {# + #} + {##} + + + +
+ Don't you hate code that's not properly indented? ++ + + + diff --git a/tests/suite/html.in b/tests/suite/html.in deleted file mode 100644 index 2188436..0000000 --- a/tests/suite/html.in +++ /dev/null @@ -1,173 +0,0 @@ - - - - -
-
-