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

Handle newlines in template arguments #13

Merged
merged 5 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion src/dj_angles/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class Attributes(Sequence):
template_tag_args: str
"""The original attributes as an unparsed string."""

def __init__(self, template_tag_args: str):
def __init__(self, template_tag_args: str = ""):
self._attributes: list[Attribute] = []

self.template_tag_args = template_tag_args
Expand All @@ -74,6 +74,15 @@ def parse(self):
self._attributes.append(attribute)
attribute_keys.add(attribute.key)

def has(self, name: str) -> bool:
"""Whether or not an there is an :obj:`~dj_angles.attributes.Attribute` by name.

Args:
param name: The name of the attribute.
"""

return self.get(name) is not None

def get(self, name: str) -> Optional[Attribute]:
"""Get an :obj:`~dj_angles.attributes.Attribute` by name. Returns `None` if the attribute is missing.

Expand Down
8 changes: 4 additions & 4 deletions src/dj_angles/mappers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
from dj_angles.mappers.thirdparty import map_bird

__all__ = [
"map_include",
"default_mapper",
"map_angles_include",
"map_autoescape",
"map_bird",
"map_block",
"map_css",
"map_endblock",
"map_extends",
"map_image",
"default_mapper",
"map_angles_include",
"map_bird",
"map_include",
]
2 changes: 1 addition & 1 deletion src/dj_angles/mappers/angles.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def default_mapper(tag: "Tag") -> str:

django_template_tag = map_include(tag)

if tag.is_end and tag.is_shadow or (tag.start_tag and tag.start_tag.is_shadow):
if (tag.is_end and tag.is_shadow) or (tag.start_tag and tag.start_tag.is_shadow):
django_template_tag = f"</template>{django_template_tag}"

return django_template_tag
Expand Down
3 changes: 3 additions & 0 deletions src/dj_angles/regex_replacer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dj_angles.exceptions import InvalidEndTagError
from dj_angles.mappers.mapper import get_tag_map
from dj_angles.settings import get_setting, get_tag_regex
from dj_angles.strings import replace_newlines
from dj_angles.tags import Tag


Expand All @@ -32,7 +33,9 @@ def get_replacements(html: str, *, raise_for_missing_start_tag: bool = True) ->
for match in re.finditer(tag_regex, html):
tag_html = html[match.start() : match.end()].strip()
tag_name = match.group("tag_name").strip()

template_tag_args = match.group("template_tag_args").strip()
template_tag_args = replace_newlines(template_tag_args, " ")

if (map_explicit_tags_only or tag_map.get(None) is None) and tag_name.lower() not in tag_map:
continue
Expand Down
2 changes: 1 addition & 1 deletion src/dj_angles/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ def get_tag_regex():
def _compile_regex(_tag_regex):
"""Silly internal function to cache the compiled regex."""

return re.compile(_tag_regex)
return re.compile(_tag_regex, re.DOTALL)

return _compile_regex(tag_regex)
14 changes: 14 additions & 0 deletions src/dj_angles/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,17 @@ def dequotify(s: str) -> str:
return s[1:-1]

return s


def replace_newlines(s: str, replacement: str = "") -> str:
"""Replaces newlines with the given replacement string.

Args:
param s: The string to replace newlines in.
param replacement: The string to replace the newlines with.

Returns:
A new string with the newlines replaced.
"""

return s.replace("\r\n", replacement).replace("\n", replacement).replace("\r", replacement)
15 changes: 10 additions & 5 deletions src/dj_angles/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from collections import deque


SHADOW_ATTRIBUTE_KEY = "shadow"


class Tag:
"""Encapsulates metadata and functionality for a tag that will be processed by `dj-angles`."""

Expand Down Expand Up @@ -54,17 +57,16 @@ def __init__(
self.tag_name = tag_name

self._template_tag_args = template_tag_args
self.attributes = Attributes()
self.parse_attributes()

if self.tag_name.endswith("!"):
self.tag_name = self.tag_name[:-1]
self.is_shadow = True
else:
shadow_attribute = self.attributes.get("shadow")

if shadow_attribute:
elif self.attributes:
if self.attributes.has(SHADOW_ATTRIBUTE_KEY):
self.is_shadow = True
self.attributes.remove(shadow_attribute.key)
self.attributes.remove(SHADOW_ATTRIBUTE_KEY)

if get_setting("lower_case_tag", default=False) is True:
self.tag_name = self.tag_name.lower()
Expand All @@ -90,6 +92,9 @@ def parse_attributes(self):

self.attributes = Attributes(self._template_tag_args)

if self.is_shadow and self.attributes.has(SHADOW_ATTRIBUTE_KEY):
self.attributes.remove(SHADOW_ATTRIBUTE_KEY)

def get_django_template_tag(self, slots: Optional[list[tuple[str, Element]]] = None) -> str:
"""Generate the Django template tag.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_short_include_shadow():
assert actual == expected


def test_short_include_self_closing_shadow():
def test_short_include_self_closing_shadow_bang():
expected = "<dj-partial><template shadowrootmode='open'>{% include 'partial.html' %}</template></dj-partial>"

template = "<dj-partial! />"
Expand All @@ -130,6 +130,15 @@ def test_short_include_self_closing_shadow():
assert actual == expected


def test_short_include_self_closing_shadow():
expected = "<dj-partial><template shadowrootmode='open'>{% include 'partial.html' %}</template></dj-partial>"

template = "<dj-include template='partial.html' shadow />"
actual = replace_django_template_tags(template)

assert actual == expected


def test_include_no_extension():
expected = "<dj-partial>{% include 'partial.html' %}</dj-partial>"

Expand Down Expand Up @@ -202,6 +211,16 @@ def test_include_template_extension_self_closing():
assert actual == expected


def test_include_arguments_with_newlines():
expected = "<dj-partial>{% include 'partial.html' with blob=True stuff=True %}</dj-partial>"

template = """<dj-partial with blob=True
stuff=True></dj-partial>"""
actual = replace_django_template_tags(template)

assert actual == expected


def test_invalid_tag():
with pytest.raises(InvalidEndTagError) as e:
replace_django_template_tags("""
Expand Down Expand Up @@ -307,3 +326,23 @@ def test_short_include_underscore_in_subdirectory():
actual = replace_django_template_tags(template)

assert actual == expected


def test_with():
expected = '<dj-www-components-include>{% include "www/components/include.html" with request=request only %}\
</dj-www-components-include>'

template = '<dj-include src="www/components/include.html" with request=request only></dj-include>'
actual = replace_django_template_tags(template)

assert actual == expected


def test_with_only():
expected = '<dj-www-components-include>{% include "www/components/include.html" with request=request %}\
</dj-www-components-include>'

template = '<dj-include src="www/components/include.html" with request=request></dj-include>'
actual = replace_django_template_tags(template)

assert actual == expected
Empty file.
15 changes: 15 additions & 0 deletions tests/dj_angles/strings/test_replace_newlines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dj_angles.strings import replace_newlines


def test_replace_newlines():
assert replace_newlines("\r\n") == ""
assert replace_newlines("\n") == ""
assert replace_newlines("\r") == ""
assert replace_newlines("\r\n\r\n\n\n") == ""


def test_replace_newlines_with_string():
assert replace_newlines("\r\n", "c") == "c"
assert replace_newlines("\n", "c") == "c"
assert replace_newlines("\r", "c") == "c"
assert replace_newlines("\r\n\r\n\n\n", "c") == "cccc"
Loading