Skip to content

Commit

Permalink
feat(formatter): will now attempt to fix case on known html tags by d…
Browse files Browse the repository at this point in the history
…efault, can be disabled

closes #589
  • Loading branch information
christopherpickering committed Apr 13, 2023
1 parent b4ef599 commit fb1f6eb
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 51 deletions.
19 changes: 19 additions & 0 deletions docs/src/_data/configuration.json
Expand Up @@ -435,5 +435,24 @@
"value": "\"files\": [\n \"index.html\"\n }"
}
]
},
{
"name": "ignore_case",
"tags": ["formatter"],
"description": {
"en": "Do not attempt to fix the case of known html tags.",
"ru": "Не пытайтесь исправить регистр известных html-тегов.",
"fr": "N'essayez pas de corriger la casse des balises html connues."
},
"usage": [
{
"name": "pyproject.toml",
"value": "[tool.djlint]\nignore-case=true\n"
},
{
"name": ".djlintrc",
"value": "\"ignore_case\": true"
}
]
}
]
2 changes: 2 additions & 0 deletions docs/src/docs/getting-started.md
Expand Up @@ -33,6 +33,7 @@ Options:
--version Show the version and exit.
-e, --extension TEXT File extension to check [default: html]
-i, --ignore TEXT Codes to ignore. ex: "H014,H017"
--include TEXT Codes to include. ex: "H014,H017"
--reformat Reformat the file(s).
--check Check formatting on the file(s).
--indent INTEGER Indent spacing. [default: 4]
Expand All @@ -52,6 +53,7 @@ Options:
--configuration PATH Path to global configuration file in .djlintrc format
--statistics Count the number of occurrences of each
error/warning code.
--ignore-case Do not fix case on known html tags.
-h, --help Show this message and exit.
```
Expand Down
2 changes: 2 additions & 0 deletions docs/src/fr/docs/getting-started.md
Expand Up @@ -33,6 +33,7 @@ Options:
--version Show the version and exit.
-e, --extension TEXT File extension to check [default: html]
-i, --ignore TEXT Codes to ignore. ex: "H014,H017"
--include TEXT Codes to include. ex: "H014,H017"
--reformat Reformat the file(s).
--check Check formatting on the file(s).
--indent INTEGER Indent spacing. [default: 4]
Expand All @@ -52,6 +53,7 @@ Options:
--configuration PATH Path to global configuration file in .djlintrc format
--statistics Count the number of occurrences of each
error/warning code.
--ignore-case Do not fix case on known html tags.
-h, --help Show this message and exit.
```
Expand Down
2 changes: 2 additions & 0 deletions docs/src/ru/docs/getting-started.md
Expand Up @@ -33,6 +33,7 @@ Options:
--version Show the version and exit.
-e, --extension TEXT File extension to check [default: html]
-i, --ignore TEXT Codes to ignore. ex: "H014,H017"
--include TEXT Codes to include. ex: "H014,H017"
--reformat Reformat the file(s).
--check Check formatting on the file(s).
--indent INTEGER Indent spacing. [default: 4]
Expand All @@ -52,6 +53,7 @@ Options:
--configuration PATH Path to global configuration file in .djlintrc format
--statistics Count the number of occurrences of each
error/warning code.
--ignore-case Do not fix case on known html tags.
-h, --help Show this message and exit.
```
Expand Down
7 changes: 7 additions & 0 deletions src/djlint/__init__.py
Expand Up @@ -134,6 +134,11 @@
help='Codes to include. ex: "H014,H017"',
show_default=False,
)
@click.option(
"--ignore-case",
is_flag=True,
help="Do not fix case on known html tags.",
)
@colorama_text(autoreset=True)
def main(
src: List[str],
Expand All @@ -155,6 +160,7 @@ def main(
configuration: Optional[str],
statistics: bool,
include: str,
ignore_case: bool,
) -> None:
"""djLint · HTML template linter and formatter."""
config = Config(
Expand All @@ -177,6 +183,7 @@ def main(
configuration=configuration,
statistics=statistics,
include=include,
ignore_case=ignore_case,
)

temp_file = None
Expand Down
42 changes: 31 additions & 11 deletions src/djlint/formatter/compress.py
Expand Up @@ -4,6 +4,7 @@
"""

import regex as re
from HtmlTagNames import html_tag_names

from ..helpers import child_of_ignored_block
from ..settings import Config
Expand All @@ -12,6 +13,11 @@
def compress_html(html: str, config: Config) -> str:
"""Compress html."""

def _fix_case(tag):
if config.ignore_case is False and tag.lower() in html_tag_names:
return tag.lower()
return tag

def _flatten_attributes(match: re.Match) -> str:
"""Flatten multiline attributes back to one line.
Expand All @@ -28,65 +34,79 @@ def _flatten_attributes(match: re.Match) -> str:
close = match.group(3) if "/" not in match.group(3) else f" {match.group(3)}"

# pylint: disable=C0209
return "{} {}{}".format(
match.group(1),
return "<{} {}{}".format(
_fix_case(match.group(1)),
" ".join(x.strip() for x in match.group(2).strip().splitlines()),
close,
)

# put attributes on one line
html = re.sub(
re.compile(
rf"(<(?:{config.indent_html_tags}))\s((?:\s*?(?:\"[^\"]*\"|'[^']*'|{{{{(?:(?!}}}}).)*}}}}|{{%(?:(?!%}}).)*%}}|[^'\">{{}}\/\s]))+)\s*?(/?>)",
rf"<({config.indent_html_tags})\s((?:\s*?(?:\"[^\"]*\"|'[^']*'|{{{{(?:(?!}}}}).)*}}}}|{{%(?:(?!%}}).)*%}}|[^'\">{{}}\/\s]))+)\s*?(/?>)",
flags=re.IGNORECASE | re.MULTILINE | re.VERBOSE,
),
_flatten_attributes,
html,
)

def _closing_clean_space(match):
return f"</{_fix_case(match.group(1))}>"

# put closing tags back on one line
# <a ...
# >
html = re.sub(
re.compile(
rf"(</(?:{config.indent_html_tags}))\s*?(>)",
rf"</({config.indent_html_tags})\s*?>",
flags=re.IGNORECASE | re.MULTILINE | re.VERBOSE,
),
r"\1\2",
_closing_clean_space,
html,
)

def _emtpy_clean_space(match):
return f"<{_fix_case(match.group(1))}>"

# remove extra space from empty tags
# <a >
# ^
html = re.sub(
re.compile(
rf"(<(?:{config.indent_html_tags}))\s*?(>)",
rf"<({config.indent_html_tags})\s*?>",
flags=re.IGNORECASE | re.MULTILINE | re.VERBOSE,
),
r"\1\2",
_emtpy_clean_space,
html,
)

def _void_clean_space(match):
return f"<{_fix_case(match.group(1))} />"

# ensure space before closing tag
# <a />
# ^
html = re.sub(
re.compile(
rf"(<(?:{config.indent_html_tags}))\s*?(/>)",
rf"<({config.indent_html_tags})\s*?/>",
flags=re.IGNORECASE | re.MULTILINE | re.VERBOSE,
),
r"\1 \2",
_void_clean_space,
html,
)

def _doctype_clean_space(match):
if config.ignore_case is False:
return f"<!DOCTYPE {match.group(2)}>"
return f"<!{match.group(1)} {match.group(2)}>"

Check warning on line 101 in src/djlint/formatter/compress.py

View check run for this annotation

Codecov / codecov/patch

src/djlint/formatter/compress.py#L101

Added line #L101 was not covered by tests

# cleanup whitespace in doctype
html = re.sub(
re.compile(
r"(<!(?:doctype))\s((?:\s*?(?:\"[^\"]*\"|'[^']*'|{{(?:(?!}}).)*}}|{%(?:(?!%}).)*%}|[^'\">{}\/\s]))+)\s*?(>)",
r"<!(doctype)\s((?:\s*?(?:\"[^\"]*\"|'[^']*'|{{(?:(?!}}).)*}}|{%(?:(?!%}).)*%}|[^'\">{}\/\s]))+)\s*?>",
flags=re.IGNORECASE | re.MULTILINE | re.VERBOSE,
),
r"\1 \2\3",
_doctype_clean_space,
html,
)

Expand Down
5 changes: 5 additions & 0 deletions src/djlint/settings.py
Expand Up @@ -219,6 +219,7 @@ def __init__(
configuration: Optional[str] = None,
statistics: bool = False,
include: Optional[str] = None,
ignore_case: bool = False,
):
self.reformat = reformat
self.check = check
Expand Down Expand Up @@ -273,6 +274,10 @@ def __init__(

self.format_css: bool = format_css or djlint_settings.get("format_css", False)

self.ignore_case: bool = ignore_case or djlint_settings.get(
"ignore_case", False
)

# ignore is based on input and also profile
self.ignore: str = str(ignore or djlint_settings.get("ignore", ""))
self.include: str = str(include or djlint_settings.get("include", ""))
Expand Down
24 changes: 12 additions & 12 deletions tests/test_html/test_basics.py
Expand Up @@ -46,7 +46,7 @@
"</html>\n"
),
(
"<!doctype html>\n"
"<!DOCTYPE html>\n"
"<html>\n"
" <head></head>\n"
" <body></body>\n"
Expand Down Expand Up @@ -315,7 +315,7 @@
"</html>\n"
),
(
"<!doctype html>\n"
"<!DOCTYPE html>\n"
'<html class="no-js" lang="">\n'
" <head>\n"
' <meta charset="utf-8">\n'
Expand Down Expand Up @@ -575,7 +575,7 @@
" <foo:bar>\n"
" <div>looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog block</div>\n"
" <div>block</div>\n"
" <DIV>BLOCK</DIV>\n"
" <div>BLOCK</div>\n"
" <div>block</div>\n"
" <div>block</div>\n"
" <div>block</div>\n"
Expand Down Expand Up @@ -633,7 +633,7 @@
" <foo:div>\n"
" <div>looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog block</div>\n"
" <div>block</div>\n"
" <DIV>BLOCK</DIV>\n"
" <div>BLOCK</div>\n"
" <div>block</div>\n"
" <div>block</div>\n"
" <div>block</div>\n"
Expand Down Expand Up @@ -691,7 +691,7 @@
" <foo:span>\n"
" <div>looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog block</div>\n"
" <div>block</div>\n"
" <DIV>BLOCK</DIV>\n"
" <div>BLOCK</div>\n"
" <div>block</div>\n"
" <div>\n"
" block\n"
Expand Down Expand Up @@ -757,9 +757,9 @@
" <div>\n"
" block\n"
" </div>\n"
" <DIV>\n"
" <div>\n"
" BLOCK\n"
" </DIV>\n"
" </div>\n"
" <div>\n"
" block\n"
" </div>\n"
Expand Down Expand Up @@ -827,9 +827,9 @@
" <div>\n"
" block\n"
" </div>\n"
" <DIV>\n"
" <div>\n"
" BLOCK\n"
" </DIV>\n"
" </div>\n"
" <div>\n"
" block\n"
" </div>\n"
Expand Down Expand Up @@ -902,10 +902,10 @@
"<!-- script like -->\n"
"<with:colon>\n"
"<style>.a{color:#f00}</style>\n"
"<SCRIPT>\n"
"<script>\n"
" const func = function() { console.log('Hello, there');}\n"
"</SCRIPT>\n"
"<STYLE>.A{COLOR:#F00}</STYLE>\n"
"</script>\n"
"<style>.A{COLOR:#F00}</style>\n"
"<html:script>\n"
" const func = function() { console.log('Hello, there');}\n"
"</html:script>\n"
Expand Down
45 changes: 32 additions & 13 deletions tests/test_html/test_case.py
Expand Up @@ -5,6 +5,7 @@
import pytest

from src.djlint.reformat import formatter
from src.djlint.settings import Config
from tests.conftest import printer

test_data = [
Expand All @@ -29,25 +30,25 @@
),
(
"<!DOCTYPE html>\n"
'<HTML CLASS="no-js mY-ClAsS">\n'
" <HEAD>\n"
' <META CHARSET="utf-8">\n'
" <TITLE>My tITlE</TITLE>\n"
' <META NAME="description" content="My CoNtEnT">\n'
" </HEAD>\n"
'<html CLASS="no-js mY-ClAsS">\n'
" <head>\n"
' <meta CHARSET="utf-8">\n'
" <title>My tITlE</title>\n"
' <meta NAME="description" content="My CoNtEnT">\n'
" </head>\n"
" <body>\n"
" <P>\n"
" <p>\n"
" Hello world!\n"
" <BR>\n"
" <br>\n"
" This is HTML5 Boilerplate.\n"
" </P>\n"
" <SCRIPT>\n"
" </p>\n"
" <script>\n"
" window.ga = function () { ga.q.push(arguments) }; ga.q = []; ga.l = +new Date;\n"
" ga('create', 'UA-XXXXX-Y', 'auto'); ga('send', 'pageview')\n"
" </SCRIPT>\n"
' <SCRIPT src="https://www.google-analytics.com/analytics.js" ASYNC DEFER></SCRIPT>\n'
" </script>\n"
' <script src="https://www.google-analytics.com/analytics.js" ASYNC DEFER></script>\n'
" </body>\n"
"</HTML>\n"
"</html>\n"
),
id="case",
)
Expand All @@ -60,3 +61,21 @@ def test_base(source, expected, basic_config):

printer(expected, source, output)
assert expected == output


test_data_two = [
pytest.param(
("<dIV></Div>\n" "<bR>\n" "<Br />\n" "<MeTa class='asdf' />\n"),
("<dIV></Div>\n" "<bR>\n" "<Br />\n" "<MeTa class='asdf' />\n"),
id="preserve_case",
)
]


@pytest.mark.parametrize(("source", "expected"), test_data_two)
def test_base_two(source, expected):
config = Config("dummy/source.html", ignore_case=True)
output = formatter(config, source)

printer(expected, source, output)
assert expected == output
4 changes: 2 additions & 2 deletions tests/test_html/test_doctype_declarations.py
Expand Up @@ -10,12 +10,12 @@
test_data = [
pytest.param(
("<!DocType htMl>"),
("<!DocType htMl>\n"),
("<!DOCTYPE htMl>\n"),
id="case",
),
pytest.param(
("<!DocType htMl >"),
("<!DocType htMl>\n"),
("<!DOCTYPE htMl>\n"),
id="case_2",
),
pytest.param(
Expand Down

0 comments on commit fb1f6eb

Please sign in to comment.