From 907a1bfb9cb533cf2315fca12a29bca2af8452a5 Mon Sep 17 00:00:00 2001 From: Christopher Pickering Date: Thu, 3 Nov 2022 11:15:35 +0100 Subject: [PATCH] fix(attributes): prevented attribute indentation for url type attributes Also fixed attribute quoting issues closes #320, closes #86, closes #195 --- src/djlint/formatter/attributes.py | 238 +++++------------- src/djlint/settings.py | 23 +- .../html-one.html | 11 +- tests/test_html/test_attributes.py | 23 +- 4 files changed, 106 insertions(+), 189 deletions(-) diff --git a/src/djlint/formatter/attributes.py b/src/djlint/formatter/attributes.py index 415267034..1a4f52786 100644 --- a/src/djlint/formatter/attributes.py +++ b/src/djlint/formatter/attributes.py @@ -8,19 +8,13 @@ from ..settings import Config -def format_template_tags(config: Config, attributes: str) -> str: +def format_template_tags(config: Config, attributes: str, spacing: int) -> str: """Format template tags in attributes.""" # find break tags, add breaks + indent # find unindent lines and move back # put short stuff back on one line - leading_space = "" - if re.search(r"^[ ]+", attributes.splitlines()[0], re.MULTILINE): - leading_space = re.search( - r"^[ ]+", attributes.splitlines()[0], re.MULTILINE - ).group() - - def add_indentation(config: Config, attributes: str) -> str: + def add_indentation(config: Config, attributes: str, spacing: int) -> str: """Indent template tags. |
str: | ^----^ base indent | """ - attr_name = ( - list( - re.finditer( - re.compile(r"^<\w+\b\s*", re.M), attributes.splitlines()[0].strip() - ) - ) - )[-1] - - start_test_list = list( - re.finditer( - re.compile( - r"^.*?(?=" + config.template_indent + r")", re.I | re.X | re.M - ), - attributes.splitlines()[0].strip(), - ) - ) + list( - re.finditer( - re.compile(r"^<\w+\b\s*[^\"']+?[\"']", re.M), - attributes.splitlines()[0].strip(), - ) - ) - - start_test = start_test_list[-1] if start_test_list else None - - base_indent = len(attr_name.group()) - indent = 0 indented = "" - indent_adder = 0 - - # if the "start test" open is actually closed, then ignore the indent. - if not re.findall( - re.compile(r"[\"']$", re.M), attributes.splitlines()[0].strip() - ): - indent_adder = len(start_test.group()) - base_indent if start_test else 0 + indent_adder = spacing or 0 for line_number, line in enumerate(attributes.splitlines()): + # when checking for template tag, use "match" to force start of line check. if re.match( re.compile(config.template_unindent, re.I | re.X), line.strip() ): - indent = indent - 1 tmp = (indent * config.indent) + (indent_adder * " ") + line.strip() - # if we are leaving an indented group, then remove the indent_adder elif re.match( re.compile(config.tag_unindent_line, re.I | re.X), line.strip() ): + # if we are leaving an indented group, then remove the indent_adder tmp = ( max(indent - 1, 0) * config.indent + indent_adder * " " + line.strip() ) - # for open tags, search, but then check that they are not closed. elif re.search( re.compile(config.template_indent, re.I | re.X), line.strip() ) and not re.search( re.compile(config.template_unindent, re.I | re.X), line.strip() ): + # for open tags, search, but then check that they are not closed. tmp = (indent * config.indent) + (indent_adder * " ") + line.strip() indent = indent + 1 @@ -102,97 +64,28 @@ def add_indentation(config: Config, attributes: str) -> str: if line_number == 0: # don't touch first line - indented += f"{leading_space}{line.strip()}" + indented += line.strip() else: - # if changing indent level and not the first item on the line, then - # check if base indent is changed. - # match must start at first of string - start_test = list( - re.finditer(re.compile(r"^(\w+?=[\"'])", re.M), line.strip()) - ) + list( - re.finditer( - re.compile( - r"^(.+?)" + config.template_indent, re.I | re.X | re.M - ), - line.strip(), - ) - ) - - if start_test: - indent_adder = len(start_test[-1].group(1)) - ( - base_indent if line_number == 0 else 0 - ) - - base_indent_space = base_indent * " " - if tmp.strip() != "": - indented += f"\n{leading_space}{base_indent_space}{tmp}" - - end_text = re.findall(re.compile(r"[\"']$", re.M), line.strip()) - - if end_text: - indent_adder = 0 + indented += f"\n{tmp}" return indented - def add_break( - config: Config, attributes: str, pattern: str, match: re.Match - ) -> str: + def add_break(pattern: str, match: re.Match) -> str: """Make a decision if a break should be added.""" - # check if we are inside an attribute. - inside_attribute = any( - x.start() <= match.start() and match.end() <= x.end() - for x in re.finditer( - re.compile( - r"[a-zA-Z-_]+[ ]*?=[ ]*?([\"'])([^\1]*?" - + config.template_if_for_pattern - + r"[^\1]*?)\1", - re.I | re.M | re.X | re.DOTALL, - ), - attributes, - ) - ) - - if inside_attribute: - attr_name = list( - re.finditer( - re.compile(r"^.+?\w+[ ]*?=[ ]*?[\"|']", re.M), - attributes[: match.start()], - ) - )[-1] - else: - # if we don't know where we are, then return what we started with. - if not re.findall( - re.compile(r"^<\w+[^=\"']\s*", re.M), attributes[: match.start()] - ): - return match.group() - - attr_name = list( - re.finditer( - re.compile(r"^<\w+[^=\"']\s*", re.M), attributes[: match.start()] - ) - )[-1] - if pattern == "before": - # but don't add break if we are the first thing in an attribute. - if attr_name.end() == match.start(): - return match.group() - return f"\n{match.group()}" - # but don't add a break if the next char closes the attr. - if re.match(r"\s*?[\"|'|>]", match.group(2)): - return match.group(1) + match.group(2) - return f"{match.group(1)}\n{match.group(2).strip()}" break_char = config.break_before - func = partial(add_break, config, attributes, "before") + func = partial(add_break, "before") + attributes = re.sub( re.compile( break_char - + r"\K((?:{%|{{\#)[ ]*?(?:" + + r".\K((?:{%|{{\#)[ ]*?(?:" + config.break_template_tags + ")[^}]+?[%|}]})", flags=re.IGNORECASE | re.MULTILINE | re.VERBOSE, @@ -201,7 +94,7 @@ def add_break( attributes, ) - func = partial(add_break, config, attributes, "after") + func = partial(add_break, "after") # break after attributes = re.sub( re.compile( @@ -213,34 +106,11 @@ def add_break( func, attributes, ) - - attributes = add_indentation(config, attributes) + attributes = add_indentation(config, attributes, spacing) return attributes -def format_style(match: re.match) -> str: - """Format inline styles.""" - tag = match.group(2) - - quote = match.group(3) - - # if the style attrib is following the tag name - leading_stuff = ( - match.group(1) - if not bool(re.match(r"^\s+$", match.group(1), re.MULTILINE)) - else len(match.group(1)) * " " - ) - - spacing = "\n" + len(match.group(1)) * " " + len(tag) * " " + len(quote) * " " - - styles = (spacing).join( - [x.strip() + ";" for x in match.group(4).split(";") if x.strip()] - ) - - return f"{leading_stuff}{tag}{quote}{styles}{quote}" - - def format_attributes(config: Config, html: str, match: re.match) -> str: """Spread long attributes over multiple lines.""" # check that we are not inside an ignored block @@ -254,38 +124,68 @@ def format_attributes(config: Config, html: str, match: re.match) -> str: tag = match.group(2) + " " - spacing = "\n" + leading_space + len(tag) * " " + spacing = leading_space + len(tag) * " " + + attributes = [] # format attributes as groups - attributes = (spacing).join( - [ - x.group() - for x in re.finditer( - config.attribute_pattern, match.group(3).strip(), re.VERBOSE + for attr_grp in re.finditer( + config.attribute_pattern, match.group(3).strip(), re.VERBOSE + ): + + attrib_name = attr_grp.group(1) + is_quoted = attr_grp.group(2) and attr_grp.group(2)[0] in ["'", '"'] + quote = attr_grp.group(2)[0] if is_quoted else '"' + attrib_value = attr_grp.group(2).strip("\"'") if attr_grp.group(2) else None + standalone = attr_grp.group(3) + + quote_length = 1 + + if attrib_name and attrib_value: + # for the equals sign + quote_length += 1 + + # format style attribute + if attrib_name and attrib_name.lower() == "style": + if config.format_attribute_template_tags: + join_space = "\n" + spacing + else: + join_space = ( + "\n" + spacing + int(quote_length + len(attrib_name or "")) * " " + ) + attrib_value = (";" + join_space).join( + [value.strip() for value in attrib_value.split(";") if value.strip()] ) - ] - ) - close = match.group(4) + # format template stuff + if config.format_attribute_template_tags: + if attrib_value and attrib_name not in config.ignored_attributes: + attrib_value = format_template_tags( + config, + attrib_value, + int(len(spacing) + len(attrib_name or "") + quote_length), + ) - attributes = f"{leading_space}{tag}{attributes}{close}" + if standalone: + standalone = format_template_tags( + config, standalone, int(len(spacing) + len(attrib_name or "")) + ) - # format template tags - if config.format_attribute_template_tags: - attributes = format_template_tags(config, attributes) + if attrib_name and attrib_value or is_quoted: + attrib_value = attrib_value or "" + attributes.append(f"{attrib_name}={quote}{attrib_value}{quote}") + else: + attributes.append( + (attrib_name or "") + (attrib_value or "") + (standalone or "") + ) - # format styles - func = partial(format_style) - attributes = re.sub( - re.compile( - config.attribute_style_pattern, - re.VERBOSE | re.IGNORECASE | re.M, - ), - func, - attributes, - ) + attribute_string = ("\n" + spacing).join([x for x in attributes if x]) + + close = match.group(4) + + attribute_string = f"{leading_space}{tag}{attribute_string}{close}" # clean trailing spaces added by breaks - attributes = "\n".join([x.rstrip() for x in attributes.splitlines()]) + attribute_string = "\n".join([x.rstrip() for x in attribute_string.splitlines()]) - return f"{attributes}" + return f"{attribute_string}" diff --git a/src/djlint/settings.py b/src/djlint/settings.py index 24b386430..00afb4afa 100644 --- a/src/djlint/settings.py +++ b/src/djlint/settings.py @@ -505,11 +505,11 @@ def __init__( self.attribute_pattern: str = ( rf""" (?: - (?: + ( (?:\w|-|\.)+ | required | checked ) # attribute name (?: [ ]*?=[ ]*? # followed by "=" - (?: + ( \"[^\"]*? # double quoted attribute (?: {self.template_if_for_pattern} # if or for loop @@ -526,21 +526,32 @@ def __init__( | [^'] # anything else )*? \' # closing quote - | (?:\w|-)+ # or a non-quoted value + | (?:\w|-)+ # or a non-quoted string value + | {{{{.*?}}}} # a non-quoted template var + | {{%[^}}]*?%}} # a non-quoted template tag + | {self.template_if_for_pattern} # a non-quoted if statement ) )? # attribute value ) - | {self.template_if_for_pattern} + | ({self.template_if_for_pattern} """ + r""" | {{.*?}} - | {%.*?%} + | {%.*?%}) """ ) self.attribute_style_pattern: str = r"^(.*?)(style=)([\"|'])(([^\"']+?;)+?)\3" - + self.ignored_attributes = [ + "href", + "action", + "data-url", + "src", + "url", + "srcset", + "data-src", + ] self.start_template_tags: str = ( r""" if diff --git a/tests/test_config/test_format_attribute_template_tags/html-one.html b/tests/test_config/test_format_attribute_template_tags/html-one.html index dcdc8f422..739369374 100644 --- a/tests/test_config/test_format_attribute_template_tags/html-one.html +++ b/tests/test_config/test_format_attribute_template_tags/html-one.html @@ -4,11 +4,7 @@ {% else %} that is long stuff asdf and more even {% endif %}"/> -report image - + object-id="{{ report.report_id }}" + href="{% if %}{% endif %}"> + diff --git a/tests/test_html/test_attributes.py b/tests/test_html/test_attributes.py index 16a47ce79..0dcc8c021 100644 --- a/tests/test_html/test_attributes.py +++ b/tests/test_html/test_attributes.py @@ -46,7 +46,9 @@ def test_long_attributes(runner: CliRunner, tmp_file: TextIO) -> None: class="class one class two" disabled="true" value="something pretty long goes here" - style="width:100px;cursor: text;border:1px solid pink" + style="width:100px; + cursor: text; + border:1px solid pink" required="true"/> """ ) @@ -62,7 +64,7 @@ def test_long_attributes(runner: CliRunner, tmp_file: TextIO) -> None: style="margin-left: 90px; display: contents; font-weight: bold; - font-size: 1.5rem;"> + font-size: 1.5rem"> """, ) @@ -76,7 +78,7 @@ def test_long_attributes(runner: CliRunner, tmp_file: TextIO) -> None:
@@ -85,13 +87,18 @@ def test_long_attributes(runner: CliRunner, tmp_file: TextIO) -> None: ) assert output.exit_code == 0 - # attributes with space around = are not brocken + # attributes with space around = are not broken + # https://github.com/Riverside-Healthcare/djLint/issues/317 + # https://github.com/Riverside-Healthcare/djLint/issues/330 output = reformat( tmp_file, runner, b"""Test\n""", ) - assert output.exit_code == 0 + assert ( + output.text + == """Test\n""" + ) def test_ignored_attributes(runner: CliRunner, tmp_file: TextIO) -> None: @@ -156,7 +163,7 @@ def test_boolean_attributes(runner: CliRunner, tmp_file: TextIO) -> None: """, ) assert output.exit_code == 1 - print(output.text) + assert ( output.text == """ """ )