diff --git a/docs/djlint/changelog.rst b/docs/djlint/changelog.rst
index cf5abd4..7f7b189 100644
--- a/docs/djlint/changelog.rst
+++ b/docs/djlint/changelog.rst
@@ -8,13 +8,14 @@ Next Release
- Added rule H022 to find http links
- Added rule H023 to find entity references
- Added rule H024 to find type on scripts and styles
+- Added rule H025 to check for orphan tags. Thanks to https://stackoverflow.com/a/1736801/10265880
- Improved attribute formatting
- Updated ``blank_line_after_tag`` option to add newline regardless of location
- Fixed django ``trans`` tag formatting
- Added formatting for inline styles
- Added formatting for template conditions inside attributes
- Added srcset as possible url location in linter rules
-
+- Special thanks to `jayvdb `_
0.5.3
-----
- Change stdout for ``--reformat/check`` options to only print new html when using stdin as the input
diff --git a/src/djlint/lint.py b/src/djlint/lint.py
index 6c17fd3..32f6001 100644
--- a/src/djlint/lint.py
+++ b/src/djlint/lint.py
@@ -1,4 +1,5 @@
"""Djlint html linter."""
+import copy
from pathlib import Path
from typing import Dict, List
@@ -69,20 +70,59 @@ def lint_file(config: Config, this_file: Path) -> Dict:
for pattern in rule["patterns"]:
- for match in re.finditer(
- re.compile(pattern, flags=build_flags(rule.get("flags", "re.DOTALL"))),
- html,
- ):
+ # rule H025 is a special case where the output must be an even num.
+ if rule["name"] == "H025":
+ open_tags = []
- if _should_ignore(config, html, match) is False:
- errors[file_name].append(
- {
- "code": rule["name"],
- "line": get_line(match.start(), line_ends),
- "match": match.group().strip()[:20],
- "message": rule["message"],
- }
- )
+ for match in re.finditer(
+ re.compile(
+ pattern, flags=build_flags(rule.get("flags", "re.DOTALL"))
+ ),
+ html,
+ ):
+ # close tags should equal open tags
+ if match.group(1).split(" ")[0][0] != "/":
+ open_tags.append(match)
+ else:
+ for i, tag in enumerate(copy.deepcopy(open_tags)):
+ if (
+ tag.group(1).split(" ")[0]
+ == match.group(1).split(" ")[0][1:]
+ ):
+ open_tags.pop(i)
+ break
+ else:
+ # there was no open tag matching the close tag
+ open_tags.append(match)
+
+ for match in open_tags:
+ if _should_ignore(config, html, match) is False:
+ errors[file_name].append(
+ {
+ "code": rule["name"],
+ "line": get_line(match.start(), line_ends),
+ "match": match.group().strip()[:20],
+ "message": rule["message"],
+ }
+ )
+ else:
+
+ for match in re.finditer(
+ re.compile(
+ pattern, flags=build_flags(rule.get("flags", "re.DOTALL"))
+ ),
+ html,
+ ):
+
+ if _should_ignore(config, html, match) is False:
+ errors[file_name].append(
+ {
+ "code": rule["name"],
+ "line": get_line(match.start(), line_ends),
+ "match": match.group().strip()[:20],
+ "message": rule["message"],
+ }
+ )
# remove duplicate matches
for file_name, error_dict in errors.items():
diff --git a/src/djlint/rules.yaml b/src/djlint/rules.yaml
index c331e60..e8764ef 100644
--- a/src/djlint/rules.yaml
+++ b/src/djlint/rules.yaml
@@ -299,3 +299,8 @@
flags: re.I
patterns:
- <(?:script|style)[^>]*?type=["|'][^>]*?>
+- rule:
+ name: H025
+ message: Tag seems to be an orphan.
+ patterns:
+ - <((?:\"[^\"<>]*\"['\"]*|'[^'<>]*'['\"]*|[^'\"<>])+)(?
diff --git a/tests/test_linter.py b/tests/test_linter.py
index a29119c..25c2966 100644
--- a/tests/test_linter.py
+++ b/tests/test_linter.py
@@ -7,7 +7,7 @@ run::
# for a single test
- pytest tests/test_linter.py::test_H021 --cov=src/djlint --cov-branch \
+ pytest tests/test_linter.py::test_H012 --cov=src/djlint --cov-branch \
--cov-report xml:coverage.xml --cov-report term-missing
"""
@@ -152,6 +152,7 @@ def test_H012(runner: CliRunner, tmp_file: TextIO) -> None:
b"{% if activity.reporting_groups|length <= 0 %}
{% trans 'General' %}
{% endif %}
",
)
result = runner.invoke(djlint, [tmp_file.name])
+ print(result.output)
assert result.exit_code == 0
assert "H012 1:" not in result.output
@@ -203,7 +204,7 @@ def test_H017(runner: CliRunner, tmp_file: TextIO) -> None:
assert "H017 1:" in result.output
# test colgroup tag
- write_to_file(tmp_file.name, b"")
+ write_to_file(tmp_file.name, b"")
result = runner.invoke(djlint, [tmp_file.name])
assert result.exit_code == 0
assert "H017 1:" not in result.output
@@ -212,7 +213,7 @@ def test_H017(runner: CliRunner, tmp_file: TextIO) -> None:
def test_DJ018(runner: CliRunner, tmp_file: TextIO) -> None:
write_to_file(
tmp_file.name,
- b'\n