mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-08-04 15:18:20 +00:00
wip
This commit is contained in:
parent
fc4ba3f8d8
commit
ff0e90b9fc
4 changed files with 121 additions and 45 deletions
|
@ -9,12 +9,13 @@ from django.core.exceptions import SuspiciousFileOperation
|
|||
from django.template import TemplateDoesNotExist, Origin
|
||||
from django.utils._os import safe_join
|
||||
from django.template import Template
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
|
||||
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
||||
from bs4.formatter import HTMLFormatter
|
||||
|
||||
from django_cotton.utils import CottonHTMLTreeBuilder
|
||||
|
||||
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||
|
||||
|
||||
|
@ -112,7 +113,6 @@ class CottonCompiler:
|
|||
def process(self, content, template_name):
|
||||
content = self._replace_syntax_with_placeholders(content)
|
||||
content = self._compile_cotton_to_django(content, template_name)
|
||||
content = self._fix_bs4_attribute_empty_attribute_behaviour(content)
|
||||
content = self._replace_placeholders_with_syntax(content)
|
||||
content = self._remove_duplicate_attribute_markers(content)
|
||||
|
||||
|
@ -163,11 +163,7 @@ class CottonCompiler:
|
|||
|
||||
def _compile_cotton_to_django(self, html_content, template_name):
|
||||
"""Convert cotton <c-* syntax to {%."""
|
||||
soup = BeautifulSoup(
|
||||
html_content,
|
||||
"html.parser",
|
||||
on_duplicate_attribute=self.handle_duplicate_attributes,
|
||||
)
|
||||
soup = get_bs4_instance(html_content)
|
||||
|
||||
# check if soup contains a 'c-vars' tag
|
||||
if cvars_el := soup.find("c-vars"):
|
||||
|
@ -202,13 +198,6 @@ class CottonCompiler:
|
|||
def _remove_duplicate_attribute_markers(self, content):
|
||||
return re.sub(r"__COTTON_DUPE_ATTR__[0-9A-F]{5}", "", content, flags=re.IGNORECASE)
|
||||
|
||||
def _fix_bs4_attribute_empty_attribute_behaviour(self, contents):
|
||||
"""Bs4 adds ="" to valueless attribute-like parts in HTML tags that causes issues when we want to manipulate
|
||||
django expressions."""
|
||||
contents = contents.replace('=""', "")
|
||||
|
||||
return contents
|
||||
|
||||
def _wrap_with_cotton_vars_frame(self, soup, cvars_el):
|
||||
"""If the user has defined a <c-vars> tag, wrap content with {% cotton_vars_frame %} to be able to create and
|
||||
govern vars and attributes. To be able to defined new vars within a component and also have them available in the
|
||||
|
@ -246,11 +235,7 @@ class CottonCompiler:
|
|||
)
|
||||
|
||||
# Since we can't replace the soup object itself, we create new soup instead
|
||||
new_soup = BeautifulSoup(
|
||||
wrapped_content,
|
||||
"html.parser",
|
||||
on_duplicate_attribute=self.handle_duplicate_attributes,
|
||||
)
|
||||
new_soup = get_bs4_instance(wrapped_content)
|
||||
|
||||
return new_soup
|
||||
|
||||
|
@ -271,6 +256,11 @@ class CottonCompiler:
|
|||
|
||||
# Build the attributes
|
||||
for key, value in tag.attrs.items():
|
||||
# value might be None
|
||||
if value is None:
|
||||
opening_tag += f" {key}"
|
||||
continue
|
||||
|
||||
# BS4 stores class values as a list, so we need to join them back into a string
|
||||
if key == "class":
|
||||
value = " ".join(value)
|
||||
|
@ -291,10 +281,8 @@ class CottonCompiler:
|
|||
component_tag += f"{{% cotton_slot {key} {component_key} expression_attr %}}{value}{{% end_cotton_slot %}}"
|
||||
|
||||
if tag.contents:
|
||||
tag_soup = BeautifulSoup(
|
||||
tag_soup = get_bs4_instance(
|
||||
tag.decode_contents(formatter=UnsortedAttributes()),
|
||||
"html.parser",
|
||||
on_duplicate_attribute=self.handle_duplicate_attributes,
|
||||
)
|
||||
self._transform_components(tag_soup, component_key)
|
||||
component_tag += str(
|
||||
|
@ -304,11 +292,7 @@ class CottonCompiler:
|
|||
component_tag += "{% end_cotton_component %}"
|
||||
|
||||
# Replace the original tag with the compiled django syntax
|
||||
new_soup = BeautifulSoup(
|
||||
component_tag,
|
||||
"html.parser",
|
||||
on_duplicate_attribute=self.handle_duplicate_attributes,
|
||||
)
|
||||
new_soup = get_bs4_instance(component_tag)
|
||||
tag.replace_with(new_soup)
|
||||
|
||||
return soup
|
||||
|
@ -319,24 +303,15 @@ class CottonCompiler:
|
|||
inner_html = "".join(str(content) for content in slot_tag.contents)
|
||||
|
||||
# Check and process any components in the slot content
|
||||
slot_soup = BeautifulSoup(
|
||||
inner_html,
|
||||
"html.parser",
|
||||
on_duplicate_attribute=self.handle_duplicate_attributes,
|
||||
)
|
||||
slot_soup = get_bs4_instance(inner_html)
|
||||
self._transform_components(slot_soup, component_key)
|
||||
|
||||
cotton_slot_tag = f"{{% cotton_slot {slot_name} {component_key} %}}{str(slot_soup.encode(formatter=UnsortedAttributes()).decode('utf-8'))}{{% end_cotton_slot %}}"
|
||||
|
||||
slot_tag.replace_with(
|
||||
BeautifulSoup(
|
||||
cotton_slot_tag,
|
||||
"html.parser",
|
||||
on_duplicate_attribute=self.handle_duplicate_attributes,
|
||||
)
|
||||
)
|
||||
slot_tag.replace_with(get_bs4_instance(cotton_slot_tag))
|
||||
|
||||
@staticmethod
|
||||
|
||||
def get_bs4_instance(content):
|
||||
def handle_duplicate_attributes(tag_attrs, key, value):
|
||||
"""BS4 cleans html and removes duplicate attributes. This would be fine if our target was html, but actually
|
||||
we're targeting Django Template Language. This contains expressions to govern content including attributes of
|
||||
|
@ -346,11 +321,19 @@ class CottonCompiler:
|
|||
|
||||
The solution here is to make duplicate attribute keys unique across that tag so BS4 will not attempt to merge or
|
||||
replace existing. Then in post processing we'll remove the unique mask.
|
||||
|
||||
Todo - This could be simplified with a custom formatter
|
||||
"""
|
||||
key_id = "".join(random.choice("0123456789ABCDEF") for i in range(5))
|
||||
key = f"{key}__COTTON_DUPE_ATTR__{key_id}"
|
||||
tag_attrs[key] = value
|
||||
|
||||
return BeautifulSoup(
|
||||
content,
|
||||
"html.parser",
|
||||
builder=CottonHTMLTreeBuilder(on_duplicate_attribute=handle_duplicate_attributes),
|
||||
)
|
||||
|
||||
|
||||
class CottonTemplateCacheHandler:
|
||||
"""This mimics the simple template caching mechanism in Django's cached.Loader. Django's cached.Loader is a bit
|
||||
|
|
|
@ -30,7 +30,7 @@ def cotton_component(parser, token):
|
|||
except ValueError:
|
||||
# No value provided, assume boolean attribute
|
||||
key = bit
|
||||
value = ""
|
||||
value = True
|
||||
|
||||
kwargs[key] = value
|
||||
|
||||
|
@ -92,14 +92,12 @@ class CottonComponentNode(Node):
|
|||
|
||||
for key, value in self.kwargs.items():
|
||||
# strip single or double quotes only if both sides have them
|
||||
if value and value[0] == value[-1] and value[0] in ('"', "'"):
|
||||
if isinstance(value, str) and value[0] == value[-1] and value[0] in ('"', "'"):
|
||||
value = value[1:-1]
|
||||
|
||||
if key.startswith(":"):
|
||||
key = key[1:]
|
||||
attrs[key] = self._process_dynamic_attribute(value, context)
|
||||
elif value == "":
|
||||
attrs[key] = True
|
||||
else:
|
||||
attrs[key] = value
|
||||
|
||||
|
@ -116,7 +114,7 @@ class CottonComponentNode(Node):
|
|||
pass
|
||||
|
||||
# Boolean attribute?
|
||||
if value == "":
|
||||
if value is True:
|
||||
return True
|
||||
|
||||
# Could be a string literal but process any template strings first to handle intermingled expressions
|
||||
|
|
|
@ -323,6 +323,34 @@ class InlineTestCase(CottonInlineTestCase):
|
|||
response = self.client.get("/view/")
|
||||
self.assertContains(response, "It's True")
|
||||
|
||||
def test_empty_strings_are_not_considered_booleans(self):
|
||||
self.create_template(
|
||||
"cotton/empty_string_attrs.html",
|
||||
"""
|
||||
{% if something1 == "" %}
|
||||
I am string
|
||||
{% endif %}
|
||||
|
||||
{% if something2 is True %}
|
||||
I am boolean
|
||||
{% endif %}
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"empty_string_attrs_view.html",
|
||||
"""
|
||||
<c-empty-string-attrs something1="" something2 />
|
||||
""",
|
||||
"view/",
|
||||
)
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, "I am string")
|
||||
self.assertContains(response, "I am boolean")
|
||||
|
||||
|
||||
class CottonTestCase(TestCase):
|
||||
def test_parent_component_is_rendered(self):
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import ast
|
||||
|
||||
from bs4.builder._htmlparser import BeautifulSoupHTMLParser, HTMLParserTreeBuilder
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
def eval_string(value):
|
||||
"""
|
||||
|
@ -16,3 +19,67 @@ def ensure_quoted(value):
|
|||
return value
|
||||
else:
|
||||
return f'"{value}"'
|
||||
|
||||
|
||||
class CottonHTMLParser(BeautifulSoupHTMLParser):
|
||||
def __init__(self, tree_builder, soup, on_duplicate_attribute):
|
||||
# Initialize the parent class (HTMLParser) without additional arguments
|
||||
HTMLParser.__init__(self)
|
||||
self._first_processing_instruction = None
|
||||
self.tree_builder = tree_builder
|
||||
self.soup = soup
|
||||
self._root_tag = None # Initialize _root_tag
|
||||
self.already_closed_empty_element = [] # Initialize this list
|
||||
self.on_duplicate_attribute = (
|
||||
on_duplicate_attribute # You can set this according to your needs
|
||||
)
|
||||
self.IGNORE = "ignore"
|
||||
self.REPLACE = "replace"
|
||||
|
||||
def handle_starttag(self, name, attrs, handle_empty_element=True):
|
||||
"""Handle an opening tag, e.g. '<tag>'"""
|
||||
attr_dict = {}
|
||||
for key, value in attrs:
|
||||
# START COTTON EDIT
|
||||
# In cotton we want to preserve the intended value of
|
||||
# the attribute from the developer so that we can differentiate
|
||||
# boolean attributes and simply empty ones:
|
||||
# if value is None:
|
||||
# value = ''
|
||||
# / END COTTON EDIT
|
||||
|
||||
if key in attr_dict:
|
||||
on_dupe = self.on_duplicate_attribute
|
||||
if on_dupe == self.IGNORE:
|
||||
pass
|
||||
elif on_dupe in (None, self.REPLACE):
|
||||
attr_dict[key] = value
|
||||
else:
|
||||
on_dupe(attr_dict, key, value)
|
||||
else:
|
||||
attr_dict[key] = value
|
||||
sourceline, sourcepos = self.getpos()
|
||||
tag = self.soup.handle_starttag(
|
||||
name, None, None, attr_dict, sourceline=sourceline, sourcepos=sourcepos
|
||||
)
|
||||
if tag and tag.is_empty_element and handle_empty_element:
|
||||
self.handle_endtag(name, check_already_closed=False)
|
||||
self.already_closed_empty_element.append(name)
|
||||
|
||||
if self._root_tag is None:
|
||||
self._root_tag_encountered(name)
|
||||
|
||||
def _root_tag_encountered(self, name):
|
||||
pass
|
||||
|
||||
|
||||
class CottonHTMLTreeBuilder(HTMLParserTreeBuilder):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.handle_duplicate_attributes = kwargs.get("on_duplicate_attribute", None)
|
||||
self.parser_class = CottonHTMLParser
|
||||
|
||||
def feed(self, markup):
|
||||
parser = self.parser_class(self, self.soup, self.handle_duplicate_attributes)
|
||||
parser.feed(markup)
|
||||
parser.close()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue