mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-08-04 07:08:21 +00:00
removed cotton copy
This commit is contained in:
parent
419292664c
commit
c3431c6241
40 changed files with 0 additions and 1234 deletions
|
@ -1,304 +0,0 @@
|
||||||
import warnings
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.template.loaders.base import Loader as BaseLoader
|
|
||||||
from django.core.exceptions import SuspiciousFileOperation
|
|
||||||
from django.template import TemplateDoesNotExist
|
|
||||||
from bs4.formatter import HTMLFormatter
|
|
||||||
from django.utils._os import safe_join
|
|
||||||
from django.template import Template
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.template import Origin
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
|
||||||
|
|
||||||
|
|
||||||
class Loader(BaseLoader):
|
|
||||||
is_usable = True
|
|
||||||
|
|
||||||
def __init__(self, engine, dirs=None):
|
|
||||||
super().__init__(engine)
|
|
||||||
self.cache_handler = CottonTemplateCacheHandler()
|
|
||||||
self.template_processor = CottonTemplateProcessor()
|
|
||||||
self.dirs = dirs
|
|
||||||
|
|
||||||
def get_contents(self, origin):
|
|
||||||
# check if file exists, whilst getting the mtime for cache key
|
|
||||||
try:
|
|
||||||
mtime = os.path.getmtime(origin.name)
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise TemplateDoesNotExist(origin)
|
|
||||||
|
|
||||||
cache_key = self.cache_handler.get_cache_key(origin.template_name, mtime)
|
|
||||||
cached_content = self.cache_handler.get_cached_template(cache_key)
|
|
||||||
|
|
||||||
if cached_content is not None:
|
|
||||||
return cached_content
|
|
||||||
|
|
||||||
template_string = self._get_template_string(origin.name)
|
|
||||||
compiled_template = self.template_processor.process(
|
|
||||||
template_string, origin.template_name
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cache_handler.cache_template(cache_key, compiled_template)
|
|
||||||
|
|
||||||
return compiled_template
|
|
||||||
|
|
||||||
def get_template_from_string(self, template_string):
|
|
||||||
"""Create and return a Template object from a string. Used primarily for testing."""
|
|
||||||
return Template(template_string, engine=self.engine)
|
|
||||||
|
|
||||||
def _get_template_string(self, template_name):
|
|
||||||
try:
|
|
||||||
with open(template_name, "r") as f:
|
|
||||||
return f.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise TemplateDoesNotExist(template_name)
|
|
||||||
|
|
||||||
def get_dirs(self):
|
|
||||||
return self.dirs if self.dirs is not None else self.engine.dirs
|
|
||||||
|
|
||||||
def get_template_sources(self, template_name):
|
|
||||||
"""Return an Origin object pointing to an absolute path in each directory
|
|
||||||
in template_dirs. For security reasons, if a path doesn't lie inside
|
|
||||||
one of the template_dirs it is excluded from the result set."""
|
|
||||||
for template_dir in self.get_dirs():
|
|
||||||
try:
|
|
||||||
name = safe_join(template_dir, template_name)
|
|
||||||
except SuspiciousFileOperation:
|
|
||||||
# The joined path was located outside of this template_dir
|
|
||||||
# (it might be inside another one, so this isn't fatal).
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield Origin(
|
|
||||||
name=name,
|
|
||||||
template_name=template_name,
|
|
||||||
loader=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UnsortedAttributes(HTMLFormatter):
|
|
||||||
"""This keeps BS4 from re-ordering attributes"""
|
|
||||||
|
|
||||||
def attributes(self, tag):
|
|
||||||
for k, v in tag.attrs.items():
|
|
||||||
yield k, v
|
|
||||||
|
|
||||||
|
|
||||||
class CottonTemplateProcessor:
|
|
||||||
DJANGO_SYNTAX_PLACEHOLDER_PREFIX = "__django_syntax__"
|
|
||||||
COTTON_VERBATIM_PATTERN = re.compile(
|
|
||||||
r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}", re.DOTALL
|
|
||||||
)
|
|
||||||
DJANGO_TAG_PATTERN = re.compile(r"\{%.*?%\}")
|
|
||||||
DJANGO_VAR_PATTERN = re.compile(r"\{\{.*?\}\}")
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.django_syntax_placeholders = []
|
|
||||||
|
|
||||||
def process(self, content, component_key):
|
|
||||||
content = self._replace_syntax_with_placeholders(content)
|
|
||||||
content = self._compile_cotton_to_django(content, component_key)
|
|
||||||
content = self._replace_placeholders_with_syntax(content)
|
|
||||||
return self._revert_bs4_attribute_empty_attribute_fixing(content)
|
|
||||||
|
|
||||||
def _replace_syntax_with_placeholders(self, content):
|
|
||||||
"""# replace {% ... %} and {{ ... }} with placeholders so they dont get touched
|
|
||||||
or encoded by bs4. Store them to later switch them back in after transformation.
|
|
||||||
"""
|
|
||||||
self.django_syntax_placeholders = []
|
|
||||||
|
|
||||||
def replace_pattern(pattern, replacement_func):
|
|
||||||
return pattern.sub(replacement_func, content)
|
|
||||||
|
|
||||||
def replace_cotton_verbatim(match):
|
|
||||||
inner_content = match.group(1)
|
|
||||||
self.django_syntax_placeholders.append(inner_content)
|
|
||||||
return f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
|
|
||||||
|
|
||||||
def replace_django_syntax(match):
|
|
||||||
self.django_syntax_placeholders.append(match.group(0))
|
|
||||||
return f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
|
|
||||||
|
|
||||||
# Replace cotton_verbatim blocks
|
|
||||||
content = replace_pattern(self.COTTON_VERBATIM_PATTERN, replace_cotton_verbatim)
|
|
||||||
|
|
||||||
# Replace {% ... %}
|
|
||||||
content = replace_pattern(self.DJANGO_TAG_PATTERN, replace_django_syntax)
|
|
||||||
|
|
||||||
# Replace {{ ... }}
|
|
||||||
content = replace_pattern(self.DJANGO_VAR_PATTERN, replace_django_syntax)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
def _compile_cotton_to_django(self, html_content, component_key):
|
|
||||||
"""Convert cotton <c-* syntax to {%."""
|
|
||||||
soup = BeautifulSoup(html_content, "html.parser")
|
|
||||||
|
|
||||||
# check if soup contains a 'c-vars' tag
|
|
||||||
if cvars_el := soup.find("c-vars"):
|
|
||||||
soup = self._wrap_with_cotton_vars_frame(soup, cvars_el)
|
|
||||||
|
|
||||||
self._transform_components(soup, component_key)
|
|
||||||
|
|
||||||
return str(soup.encode(formatter=UnsortedAttributes()).decode("utf-8"))
|
|
||||||
|
|
||||||
def _replace_placeholders_with_syntax(self, content):
|
|
||||||
"""After modifying the content, replace the placeholders with the django template tags and variables."""
|
|
||||||
for i, placeholder in enumerate(self.django_syntax_placeholders, 1):
|
|
||||||
content = content.replace(
|
|
||||||
f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{i}__", placeholder
|
|
||||||
)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
def _revert_bs4_attribute_empty_attribute_fixing(self, contents):
|
|
||||||
"""
|
|
||||||
Removes empty attribute values added by BeautifulSoup to Django template tags.
|
|
||||||
|
|
||||||
BeautifulSoup adds ="" to empty attribute-like parts in HTML-like nodes.
|
|
||||||
This method removes these additions for Django template tags.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- <div {{ something }}=""> becomes <div {{ something }}>
|
|
||||||
- <div {% something %}=""> becomes <div {% something %}>
|
|
||||||
"""
|
|
||||||
# Remove ="" after Django variable tags
|
|
||||||
contents = contents.replace('}}=""', "}}")
|
|
||||||
|
|
||||||
# Remove ="" after Django template tags
|
|
||||||
contents = contents.replace('%}=""', "%}")
|
|
||||||
|
|
||||||
return contents
|
|
||||||
|
|
||||||
def _wrap_with_cotton_vars_frame(self, soup, cvars_el):
|
|
||||||
"""Wrap content with {% cotton_vars_frame %} to be able to govern vars and attributes. In order to recognise
|
|
||||||
vars defined in a component and also have them available in the same component's context, we wrap the entire
|
|
||||||
contents in another component: cotton_vars_frame."""
|
|
||||||
|
|
||||||
vars_with_defaults = []
|
|
||||||
for var, value in cvars_el.attrs.items():
|
|
||||||
# Attributes in context at this point will already have been formatted in _component to be accessible, so in order to cascade match the style.
|
|
||||||
accessible_var = var.replace("-", "_")
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
vars_with_defaults.append(f"{var}={accessible_var}")
|
|
||||||
elif var.startswith(":"):
|
|
||||||
# If ':' is present, the user wants to parse a literal string as the default value,
|
|
||||||
# i.e. "['a', 'b']", "{'a': 'b'}", "True", "False", "None" or "1".
|
|
||||||
var = var[1:] # Remove the ':' prefix
|
|
||||||
accessible_var = accessible_var[1:] # Remove the ':' prefix
|
|
||||||
vars_with_defaults.append(
|
|
||||||
f'{var}={accessible_var}|eval_default:"{value}"'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Assuming value is already a string that represents the default value
|
|
||||||
vars_with_defaults.append(f'{var}={accessible_var}|default:"{value}"')
|
|
||||||
|
|
||||||
cvars_el.decompose()
|
|
||||||
|
|
||||||
# Construct the {% with %} opening tag
|
|
||||||
opening = "{% cotton_vars_frame " + " ".join(vars_with_defaults) + " %}"
|
|
||||||
closing = "{% endcotton_vars_frame %}"
|
|
||||||
|
|
||||||
# Convert the remaining soup back to a string and wrap it within {% with %} block
|
|
||||||
wrapped_content = (
|
|
||||||
opening
|
|
||||||
+ str(soup.encode(formatter=UnsortedAttributes()).decode("utf-8")).strip()
|
|
||||||
+ closing
|
|
||||||
)
|
|
||||||
|
|
||||||
# Since we can't replace the soup object itself, we create new soup instead
|
|
||||||
new_soup = BeautifulSoup(wrapped_content, "html.parser")
|
|
||||||
|
|
||||||
return new_soup
|
|
||||||
|
|
||||||
def _transform_components(self, soup, parent_key):
|
|
||||||
"""Replace <c-[component path]> tags with the {% cotton_component %} template tag"""
|
|
||||||
for tag in soup.find_all(re.compile("^c-"), recursive=True):
|
|
||||||
if tag.name == "c-slot":
|
|
||||||
self._transform_named_slot(tag, parent_key)
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
component_key = tag.name[2:]
|
|
||||||
component_path = component_key.replace(".", "/").replace("-", "_")
|
|
||||||
opening_tag = f"{{% cotton_component {'{}/{}.html'.format(settings.COTTON_DIR if hasattr(settings, 'COTTON_DIR') else 'cotton', component_path)} {component_key} "
|
|
||||||
|
|
||||||
# Store attributes that contain template expressions, they are when we use '{{' or '{%' in the value of an attribute
|
|
||||||
expression_attrs = []
|
|
||||||
|
|
||||||
# Build the attributes
|
|
||||||
for key, value in tag.attrs.items():
|
|
||||||
# BS4 stores class values as a list, so we need to join them back into a string
|
|
||||||
if key == "class":
|
|
||||||
value = " ".join(value)
|
|
||||||
|
|
||||||
# Django templates tags cannot have {{ or {% expressions in their attribute values
|
|
||||||
# Neither can they have new lines, let's treat them both as "expression attrs"
|
|
||||||
if self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX in value or "\n" in value:
|
|
||||||
expression_attrs.append((key, value))
|
|
||||||
continue
|
|
||||||
|
|
||||||
opening_tag += ' {}="{}"'.format(key, value)
|
|
||||||
opening_tag += " %}"
|
|
||||||
|
|
||||||
component_tag = opening_tag
|
|
||||||
|
|
||||||
if expression_attrs:
|
|
||||||
for key, value in expression_attrs:
|
|
||||||
component_tag += f"{{% cotton_slot {key} {component_key} expression_attr %}}{value}{{% end_cotton_slot %}}"
|
|
||||||
|
|
||||||
if tag.contents:
|
|
||||||
tag_soup = BeautifulSoup(tag.decode_contents(), "html.parser")
|
|
||||||
self._transform_components(tag_soup, component_key)
|
|
||||||
component_tag += str(
|
|
||||||
tag_soup.encode(formatter=UnsortedAttributes()).decode("utf-8")
|
|
||||||
)
|
|
||||||
|
|
||||||
component_tag += "{% end_cotton_component %}"
|
|
||||||
|
|
||||||
# Replace the original tag with the compiled django syntax
|
|
||||||
new_soup = BeautifulSoup(component_tag, "html.parser")
|
|
||||||
tag.replace_with(new_soup)
|
|
||||||
|
|
||||||
return soup
|
|
||||||
|
|
||||||
def _transform_named_slot(self, slot_tag, component_key):
|
|
||||||
"""Compile <c-slot> to {% cotton_slot %}"""
|
|
||||||
slot_name = slot_tag.get("name", "").strip()
|
|
||||||
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")
|
|
||||||
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"))
|
|
||||||
|
|
||||||
|
|
||||||
class CottonTemplateCacheHandler:
|
|
||||||
"""Handles caching of cotton templates so the html parsing is only done on first load of each view or component."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.enabled = getattr(settings, "COTTON_TEMPLATE_CACHING_ENABLED", True)
|
|
||||||
|
|
||||||
def get_cache_key(self, template_name, mtime):
|
|
||||||
template_hash = hashlib.sha256(template_name.encode()).hexdigest()
|
|
||||||
return f"cotton_cache_{template_hash}_{mtime}"
|
|
||||||
|
|
||||||
def get_cached_template(self, cache_key):
|
|
||||||
if not self.enabled:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return cache.get(cache_key)
|
|
||||||
|
|
||||||
def cache_template(self, cache_key, content, timeout=None):
|
|
||||||
if self.enabled:
|
|
||||||
cache.set(cache_key, content, timeout=timeout)
|
|
|
@ -1,3 +0,0 @@
|
||||||
<c-merges-attributes class="extra-class" silica:model="test" another="test">
|
|
||||||
ss
|
|
||||||
</c-merges-attributes>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<c-receives-attributes attribute_1="hello" and-another="woo1" thirdForLuck="yes">
|
|
||||||
ss
|
|
||||||
</c-receives-attributes>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<c-parent>
|
|
||||||
<c-child>d</c-child>
|
|
||||||
</c-parent>
|
|
|
@ -1 +0,0 @@
|
||||||
<div class="i-am-child"></div>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<div>
|
|
||||||
Header:
|
|
||||||
{{ header }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
Content:
|
|
||||||
{{ slot }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
{% if none is None %}
|
|
||||||
<p>none is None</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if number == 1 %}
|
|
||||||
<p>number is 1</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if boolean_true is True %}
|
|
||||||
<p>boolean_true is True</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if boolean_false is False %}
|
|
||||||
<p>boolean_false is False</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if dict.key == 'value' %}
|
|
||||||
<p>dict.key is 'value'</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if list.0 == 1 %}
|
|
||||||
<p>list.0 is 1</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if listdict.0.key == 'value' %}
|
|
||||||
<p>listdict.0.key is 'value'</p>
|
|
||||||
{% endif %}
|
|
|
@ -1,29 +0,0 @@
|
||||||
<c-vars :none="None" :number="1" :boolean_true="True" :boolean_false="False" :dict="{'key': 'value'}" :list="[1, 2, 3]" :listdict="[{'key': 'value'}]" />
|
|
||||||
|
|
||||||
{% if none is None %}
|
|
||||||
<p>none is None</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if number == 1 %}
|
|
||||||
<p>number is 1</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if boolean_true is True %}
|
|
||||||
<p>boolean_true is True</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if boolean_false is False %}
|
|
||||||
<p>boolean_false is False</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if dict.key == 'value' %}
|
|
||||||
<p>dict.key is 'value'</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if list.0 == 1 %}
|
|
||||||
<p>list.0 is 1</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if listdict.0.key == 'value' %}
|
|
||||||
<p>listdict.0.key is 'value'</p>
|
|
||||||
{% endif %}
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div {{ attrs_dict|merge:'class:form-group another-class-with:colon' }}>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div>
|
|
||||||
{{ name }}
|
|
||||||
</div>
|
|
|
@ -1,4 +0,0 @@
|
||||||
Attribute 1 says: '{{ attr1 }}'
|
|
||||||
Attribute 2 says: '{{ attr2 }}'
|
|
||||||
Attribute 3 says: '{{ attr3 }}'
|
|
||||||
attrs tag is: '{{ attrs }}'
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="i-am-parent">
|
|
||||||
{{slot}}
|
|
||||||
</div>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div {{ attrs }}>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<c-vars var1 default_var="default var" />
|
|
||||||
|
|
||||||
<p>slot: '{{ slot }}'</p>
|
|
||||||
|
|
||||||
<p>attr1: '{{ attr1 }}'</p>
|
|
||||||
<p>attr2: '{{ attr2 }}'</p>
|
|
||||||
|
|
||||||
<p>var1: '{{ var1 }}'</p>
|
|
||||||
<p>default_var: '{{ default_var }}'</p>
|
|
||||||
|
|
||||||
<p>named_slot: '{{ named_slot }}'</p>
|
|
||||||
|
|
||||||
<p>attrs: '{{ attrs }}'</p>
|
|
|
@ -1,3 +0,0 @@
|
||||||
{% if attr1 is True %}
|
|
||||||
It's True
|
|
||||||
{% endif %}
|
|
|
@ -1 +0,0 @@
|
||||||
<div class="{% if 1 < 2 %} some-class {% endif %}">Hello, World!</div>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<c-eval-attributes-test-component
|
|
||||||
:none="None"
|
|
||||||
:number="1"
|
|
||||||
:boolean_true="True"
|
|
||||||
:boolean_false="False"
|
|
||||||
:dict="{'key': 'value'}"
|
|
||||||
:list="[1, 2, 3]"
|
|
||||||
:listdict="[{'key': 'value'}]"
|
|
||||||
/>
|
|
|
@ -1 +0,0 @@
|
||||||
<c-eval-vars-test-component />
|
|
|
@ -1,3 +0,0 @@
|
||||||
<c-parent>
|
|
||||||
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
|
|
||||||
</c-parent>
|
|
|
@ -1,7 +0,0 @@
|
||||||
{% for item in items %}
|
|
||||||
<c-named-slot-component>
|
|
||||||
<c-slot name="name">
|
|
||||||
item name: {{ item.name }}
|
|
||||||
</c-slot>
|
|
||||||
</c-named-slot-component>
|
|
||||||
{% endfor %}
|
|
|
@ -1,12 +0,0 @@
|
||||||
<c-vars test="world" name="Will"></c-vars>
|
|
||||||
|
|
||||||
<!-- create different types of native tag examples -->
|
|
||||||
|
|
||||||
<c-native-tags-in-attributes
|
|
||||||
attr1="Hello {{ name }}"
|
|
||||||
attr2="{{ test|default:"none" }}"
|
|
||||||
attr3="{% if 1 == 1 %}cowabonga!{% endif %}"
|
|
||||||
normal="normal"
|
|
||||||
>
|
|
||||||
<c-slot name="named">test</c-slot>
|
|
||||||
</c-native-tags-in-attributes>
|
|
|
@ -1 +0,0 @@
|
||||||
<c-parent></c-parent>
|
|
|
@ -1,4 +0,0 @@
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
|
|
||||||
<c-parent/>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<c-test-component var1="string with space" attr1="I have spaces">
|
|
||||||
<c-slot name="named_slot">
|
|
||||||
named_slot with spaces
|
|
||||||
</c-slot>
|
|
||||||
</c-test-component>
|
|
|
@ -1 +0,0 @@
|
||||||
<c-valueless-attribute-test-component attr1 />
|
|
|
@ -1,2 +0,0 @@
|
||||||
<c-test-component attr1="variable" :attr2="variable">
|
|
||||||
</c-test-component>
|
|
|
@ -1,115 +0,0 @@
|
||||||
import ast
|
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
from django import template
|
|
||||||
from django.template import Node
|
|
||||||
from django.template.loader import get_template
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from django_cotton.utils import ensure_quoted
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
|
||||||
def get_cached_template(template_name):
|
|
||||||
return get_template(template_name)
|
|
||||||
|
|
||||||
|
|
||||||
def render_template(template_name, context):
|
|
||||||
template = get_cached_template(template_name)
|
|
||||||
return template.render(context)
|
|
||||||
|
|
||||||
|
|
||||||
def cotton_component(parser, token):
|
|
||||||
"""
|
|
||||||
Template tag to render a cotton component with dynamic attributes.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
{% cotton_component 'template_path' 'component_key' key1="value1" :key2="dynamic_value" %}
|
|
||||||
"""
|
|
||||||
bits = token.split_contents()
|
|
||||||
template_path = bits[1]
|
|
||||||
component_key = bits[2]
|
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
for bit in bits[3:]:
|
|
||||||
key, value = bit.split("=")
|
|
||||||
kwargs[key] = value
|
|
||||||
|
|
||||||
nodelist = parser.parse(("end_cotton_component",))
|
|
||||||
parser.delete_first_token()
|
|
||||||
|
|
||||||
return CottonComponentNode(nodelist, template_path, component_key, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CottonComponentNode(Node):
|
|
||||||
def __init__(self, nodelist, template_path, component_key, kwargs):
|
|
||||||
self.nodelist = nodelist
|
|
||||||
self.template_path = template_path
|
|
||||||
self.component_key = component_key
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
local_context = context.flatten()
|
|
||||||
attrs = {}
|
|
||||||
|
|
||||||
for key, value in self.kwargs.items():
|
|
||||||
value = value.strip("'\"")
|
|
||||||
|
|
||||||
if key.startswith(":"):
|
|
||||||
key = key[1:]
|
|
||||||
attrs[key] = self.process_dynamic_attribute(value, context)
|
|
||||||
elif value == "":
|
|
||||||
attrs[key] = True
|
|
||||||
else:
|
|
||||||
attrs[key] = value
|
|
||||||
|
|
||||||
# Add the remainder as the default slot
|
|
||||||
local_context["slot"] = self.nodelist.render(context)
|
|
||||||
|
|
||||||
# Merge slots and attributes into the local context
|
|
||||||
all_slots = context.get("cotton_slots", {})
|
|
||||||
component_slots = all_slots.get(self.component_key, {})
|
|
||||||
local_context.update(component_slots)
|
|
||||||
|
|
||||||
# We need to check if any dynamic attributes are present in the component slots and move them over to attrs
|
|
||||||
if "ctn_template_expression_attrs" in component_slots:
|
|
||||||
for expression_attr in component_slots["ctn_template_expression_attrs"]:
|
|
||||||
attrs[expression_attr] = component_slots[expression_attr]
|
|
||||||
|
|
||||||
# Build attrs string before formatting any '-' to '_' in attr names
|
|
||||||
attrs_string = " ".join(
|
|
||||||
f"{key}={ensure_quoted(value)}" for key, value in attrs.items()
|
|
||||||
)
|
|
||||||
local_context["attrs"] = mark_safe(attrs_string)
|
|
||||||
|
|
||||||
# Make the attrs available in the context for the vars frame, also before formatting the attr names
|
|
||||||
local_context["attrs_dict"] = attrs
|
|
||||||
|
|
||||||
# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
|
|
||||||
attrs = {key.replace("-", "_"): value for key, value in attrs.items()}
|
|
||||||
local_context.update(attrs)
|
|
||||||
|
|
||||||
# Reset the component's slots in context to prevent bleeding into sibling components
|
|
||||||
all_slots[self.component_key] = {}
|
|
||||||
|
|
||||||
return render_template(self.template_path, local_context)
|
|
||||||
|
|
||||||
def process_dynamic_attribute(self, value, context):
|
|
||||||
"""
|
|
||||||
Process a dynamic attribute, resolving template variables and evaluating literals.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return template.Variable(value).resolve(context)
|
|
||||||
except template.VariableDoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Check for boolean attribute
|
|
||||||
if value == "":
|
|
||||||
return True
|
|
||||||
|
|
||||||
# It's not a template var or boolean attribute,
|
|
||||||
# attempt to evaluate literal string or pass back raw value
|
|
||||||
try:
|
|
||||||
return ast.literal_eval(value)
|
|
||||||
except (ValueError, SyntaxError):
|
|
||||||
return value
|
|
|
@ -1,57 +0,0 @@
|
||||||
from django import template
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
|
|
||||||
def cotton_slot(parser, token):
|
|
||||||
"""
|
|
||||||
Template tag to render a cotton slot.
|
|
||||||
is_expression_attr: bool whether the attribute is dynamic or not.
|
|
||||||
dynamic attributes are from the use of native tags {{ {% in the component attribute. We put them through as a named
|
|
||||||
slot so they can be rendered and provided as a template variable
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
tag_name, slot_name, component_key, *optional = token.split_contents()
|
|
||||||
is_expression_attr = optional[0] if optional else None
|
|
||||||
except ValueError:
|
|
||||||
raise template.TemplateSyntaxError("incomplete c-slot %r" % token.contents)
|
|
||||||
|
|
||||||
nodelist = parser.parse(("end_cotton_slot",))
|
|
||||||
parser.delete_first_token()
|
|
||||||
return CottonSlotNode(slot_name, nodelist, component_key, is_expression_attr)
|
|
||||||
|
|
||||||
|
|
||||||
class CottonSlotNode(template.Node):
|
|
||||||
def __init__(self, slot_name, nodelist, component_key, is_expression_attr):
|
|
||||||
self.slot_name = slot_name
|
|
||||||
self.nodelist = nodelist
|
|
||||||
self.component_key = component_key
|
|
||||||
self.is_expression_attr = is_expression_attr
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
# Add the rendered content to the context.
|
|
||||||
if "cotton_slots" not in context:
|
|
||||||
context.update({"cotton_slots": {}})
|
|
||||||
|
|
||||||
output = self.nodelist.render(context)
|
|
||||||
|
|
||||||
# Store the slot data in a component-namespaced dictionary
|
|
||||||
if self.component_key not in context["cotton_slots"]:
|
|
||||||
context["cotton_slots"][self.component_key] = {}
|
|
||||||
|
|
||||||
context["cotton_slots"][self.component_key][self.slot_name] = mark_safe(output)
|
|
||||||
|
|
||||||
# If the slot is a dynamic attribute, we record it so it can be transferred to attrs in the component
|
|
||||||
if self.is_expression_attr:
|
|
||||||
if (
|
|
||||||
"ctn_template_expression_attrs"
|
|
||||||
not in context["cotton_slots"][self.component_key]
|
|
||||||
):
|
|
||||||
context["cotton_slots"][self.component_key][
|
|
||||||
"ctn_template_expression_attrs"
|
|
||||||
] = []
|
|
||||||
|
|
||||||
context["cotton_slots"][self.component_key][
|
|
||||||
"ctn_template_expression_attrs"
|
|
||||||
].append(self.slot_name)
|
|
||||||
|
|
||||||
return ""
|
|
|
@ -1,65 +0,0 @@
|
||||||
from django import template
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from django_cotton.utils import ensure_quoted
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
def cotton_vars_frame(parser, token):
|
|
||||||
"""The job of the vars frame is:
|
|
||||||
1. to filter out attributes declared as vars inside {{ attrs }} string.
|
|
||||||
2. to provide default values to attributes.
|
|
||||||
Because we're effecting variables inside the same component, which is not possible usually, we we wrap
|
|
||||||
the vars frame around the contents of the component so we can govern the attributes and vars that are available.
|
|
||||||
"""
|
|
||||||
bits = token.split_contents()[1:] # Skip the tag name
|
|
||||||
|
|
||||||
# We dont use token_kwargs because it doesn't allow for hyphens in key names, i.e. x-data=""
|
|
||||||
tag_kwargs = {}
|
|
||||||
for bit in bits:
|
|
||||||
key, value = bit.split("=")
|
|
||||||
tag_kwargs[key] = parser.compile_filter(value)
|
|
||||||
|
|
||||||
nodelist = parser.parse(("endcotton_vars_frame",))
|
|
||||||
parser.delete_first_token()
|
|
||||||
return CottonVarsFrameNode(nodelist, tag_kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CottonVarsFrameNode(template.Node):
|
|
||||||
def __init__(self, nodelist, kwargs):
|
|
||||||
self.nodelist = nodelist
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
# Assume 'attrs' are passed from the parent and are available in the context
|
|
||||||
component_attrs = context.get("attrs_dict", {})
|
|
||||||
|
|
||||||
# Initialize vars based on the frame's kwargs and parent attrs
|
|
||||||
vars = {}
|
|
||||||
for key, value in self.kwargs.items():
|
|
||||||
# Check if the var exists in component attrs; if so, use it, otherwise use the resolved default
|
|
||||||
if key in component_attrs:
|
|
||||||
vars[key] = component_attrs[key]
|
|
||||||
else:
|
|
||||||
# Attempt to resolve each kwarg value (which may include template variables)
|
|
||||||
resolved_value = value.resolve(context)
|
|
||||||
vars[key] = resolved_value
|
|
||||||
|
|
||||||
# Overwrite 'attrs' in the local context by excluding keys that are identified as vars
|
|
||||||
attrs_without_vars = {k: v for k, v in component_attrs.items() if k not in vars}
|
|
||||||
|
|
||||||
# Provide all of the attrs as a string to pass to the component before any '-' to '_' replacing
|
|
||||||
attrs = " ".join(
|
|
||||||
f"{key}={ensure_quoted(value)}" for key, value in attrs_without_vars.items()
|
|
||||||
)
|
|
||||||
context["attrs"] = mark_safe(attrs)
|
|
||||||
|
|
||||||
context["attrs_dict"] = attrs_without_vars
|
|
||||||
|
|
||||||
# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
|
|
||||||
formatted_vars = {key.replace("-", "_"): value for key, value in vars.items()}
|
|
||||||
|
|
||||||
context.update(formatted_vars)
|
|
||||||
|
|
||||||
return self.nodelist.render(context)
|
|
|
@ -1,30 +0,0 @@
|
||||||
from django import template
|
|
||||||
from django.utils.html import format_html_join
|
|
||||||
|
|
||||||
from django_cotton.templatetags._component import cotton_component
|
|
||||||
from django_cotton.templatetags._slot import cotton_slot
|
|
||||||
from django_cotton.templatetags._vars_frame import cotton_vars_frame
|
|
||||||
from django_cotton.utils import eval_string
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
register.tag("cotton_component", cotton_component)
|
|
||||||
register.tag("cotton_slot", cotton_slot)
|
|
||||||
register.tag("cotton_vars_frame", cotton_vars_frame)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def merge(attrs, args):
|
|
||||||
# attrs is expected to be a dictionary of existing attributes
|
|
||||||
# args is a string of additional attributes to merge, e.g., "class:extra-class"
|
|
||||||
for arg in args.split(","):
|
|
||||||
key, value = arg.split(":", 1)
|
|
||||||
if key in attrs:
|
|
||||||
attrs[key] = value + " " + attrs[key]
|
|
||||||
else:
|
|
||||||
attrs[key] = value
|
|
||||||
return format_html_join(" ", '{0}="{1}"', attrs.items())
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def eval_default(value, arg):
|
|
||||||
return value or eval_string(arg)
|
|
|
@ -1,78 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.urls import path
|
|
||||||
from django.test import override_settings
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
from django.conf import settings
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicURLModule:
|
|
||||||
def __init__(self):
|
|
||||||
self.urlpatterns = []
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
return self.urlpatterns
|
|
||||||
|
|
||||||
|
|
||||||
class CottonInlineTestCase(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
super().setUpClass()
|
|
||||||
|
|
||||||
# Set tmp dir and register a url module for our tmp files
|
|
||||||
cls.temp_dir = tempfile.mkdtemp()
|
|
||||||
cls.url_module = DynamicURLModule()
|
|
||||||
cls.url_module_name = f"dynamic_urls_{cls.__name__}"
|
|
||||||
sys.modules[cls.url_module_name] = cls.url_module
|
|
||||||
|
|
||||||
# Register our temp directory as a TEMPLATES path
|
|
||||||
cls.new_templates_setting = settings.TEMPLATES.copy()
|
|
||||||
cls.new_templates_setting[0]["DIRS"] = [
|
|
||||||
cls.temp_dir
|
|
||||||
] + cls.new_templates_setting[0]["DIRS"]
|
|
||||||
|
|
||||||
# Apply the setting
|
|
||||||
cls.templates_override = override_settings(TEMPLATES=cls.new_templates_setting)
|
|
||||||
cls.templates_override.enable()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
"""Remove temporary directory and clean up modules"""
|
|
||||||
cls.templates_override.disable()
|
|
||||||
shutil.rmtree(cls.temp_dir, ignore_errors=True)
|
|
||||||
del sys.modules[cls.url_module_name]
|
|
||||||
super().tearDownClass()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Clear cache between tests so that we can use the same file names for simplicity"""
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
def create_template(self, name, content):
|
|
||||||
"""Create a template file in the temporary directory and return the path"""
|
|
||||||
path = os.path.join(self.temp_dir, name)
|
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
||||||
with open(path, "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def make_view(self, template_name):
|
|
||||||
"""Make a view that renders the given template"""
|
|
||||||
return TemplateView.as_view(template_name=template_name)
|
|
||||||
|
|
||||||
def register_url(self, url, view):
|
|
||||||
"""Register a URL pattern and returns path"""
|
|
||||||
url_pattern = path(url, view)
|
|
||||||
self.url_module.urlpatterns.append(url_pattern)
|
|
||||||
return url_pattern
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.url_module.urlpatterns = []
|
|
||||||
|
|
||||||
def get_url_conf(self):
|
|
||||||
return self.url_module_name
|
|
|
@ -1,276 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from django_cotton.tests.inline_test_case import CottonInlineTestCase
|
|
||||||
from django_cotton.tests.utils import get_compiled, get_rendered
|
|
||||||
|
|
||||||
|
|
||||||
class InlineTestCase(CottonInlineTestCase):
|
|
||||||
def test_component_is_rendered(self):
|
|
||||||
self.create_template(
|
|
||||||
"cotton/component.html",
|
|
||||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.create_template(
|
|
||||||
"view.html",
|
|
||||||
"""<c-component>Hello, World!</c-component>""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register Url
|
|
||||||
self.register_url("view/", self.make_view("view.html"))
|
|
||||||
|
|
||||||
# Override URLconf
|
|
||||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
|
||||||
response = self.client.get("/view/")
|
|
||||||
self.assertContains(response, '<div class="i-am-component">')
|
|
||||||
self.assertContains(response, "Hello, World!")
|
|
||||||
|
|
||||||
def test_new_lines_in_attributes_are_preserved(self):
|
|
||||||
self.create_template(
|
|
||||||
"cotton/preserved.html",
|
|
||||||
"""<div {{ attrs }}>{{ slot }}</div>""",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.create_template(
|
|
||||||
"preserved_view.html",
|
|
||||||
"""
|
|
||||||
<c-preserved x-data="{
|
|
||||||
attr1: 'im an attr',
|
|
||||||
var1: 'im a var',
|
|
||||||
method() {
|
|
||||||
return 'im a method';
|
|
||||||
}
|
|
||||||
}" />
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register Url
|
|
||||||
self.register_url("view/", self.make_view("preserved_view.html"))
|
|
||||||
|
|
||||||
# Override URLconf
|
|
||||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
|
||||||
response = self.client.get("/view/")
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
"""{
|
|
||||||
attr1: 'im an attr',
|
|
||||||
var1: 'im a var',
|
|
||||||
method() {
|
|
||||||
return 'im a method';
|
|
||||||
}
|
|
||||||
}"""
|
|
||||||
in response.content.decode()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_attribute_names_on_component_containing_hyphens_are_converted_to_underscores(
|
|
||||||
self,
|
|
||||||
):
|
|
||||||
self.create_template(
|
|
||||||
"cotton/hyphens.html",
|
|
||||||
"""
|
|
||||||
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.create_template(
|
|
||||||
"hyphens_view.html",
|
|
||||||
"""
|
|
||||||
<c-hyphens x-data="{}" x-init="do_something()" />
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register Url
|
|
||||||
self.register_url("view/", self.make_view("hyphens_view.html"))
|
|
||||||
|
|
||||||
# Override URLconf
|
|
||||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
|
||||||
response = self.client.get("/view/")
|
|
||||||
|
|
||||||
self.assertContains(response, 'x-data="{}" x-init="do_something()"')
|
|
||||||
|
|
||||||
def test_attribute_names_on_cvars_containing_hyphens_are_converted_to_underscores(
|
|
||||||
self,
|
|
||||||
):
|
|
||||||
self.create_template(
|
|
||||||
"cotton/cvar_hyphens.html",
|
|
||||||
"""
|
|
||||||
<c-vars x-data="{}" x-init="do_something()" />
|
|
||||||
|
|
||||||
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.create_template(
|
|
||||||
"cvar_hyphens_view.html",
|
|
||||||
"""
|
|
||||||
<c-cvar-hyphens />
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register Url
|
|
||||||
self.register_url("view/", self.make_view("cvar_hyphens_view.html"))
|
|
||||||
|
|
||||||
# Override URLconf
|
|
||||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
|
||||||
response = self.client.get("/view/")
|
|
||||||
|
|
||||||
self.assertContains(response, 'x-data="{}" x-init="do_something()"')
|
|
||||||
|
|
||||||
def test_cotton_directory_can_be_configured(self):
|
|
||||||
custom_dir = "components"
|
|
||||||
|
|
||||||
self.create_template(
|
|
||||||
f"{custom_dir}/component.html",
|
|
||||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.create_template(
|
|
||||||
"view.html",
|
|
||||||
"""<c-component>Hello, World!</c-component>""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register Url
|
|
||||||
self.register_url("view/", self.make_view("view.html"))
|
|
||||||
|
|
||||||
# Override URLconf
|
|
||||||
with self.settings(ROOT_URLCONF=self.get_url_conf(), COTTON_DIR=custom_dir):
|
|
||||||
response = self.client.get("/view/")
|
|
||||||
self.assertContains(response, '<div class="i-am-component">')
|
|
||||||
self.assertContains(response, "Hello, World!")
|
|
||||||
|
|
||||||
|
|
||||||
class CottonTestCase(TestCase):
|
|
||||||
def test_parent_component_is_rendered(self):
|
|
||||||
response = self.client.get("/parent")
|
|
||||||
self.assertContains(response, '<div class="i-am-parent">')
|
|
||||||
|
|
||||||
def test_child_is_rendered(self):
|
|
||||||
response = self.client.get("/child")
|
|
||||||
self.assertContains(response, '<div class="i-am-parent">')
|
|
||||||
self.assertContains(response, '<div class="i-am-child">')
|
|
||||||
|
|
||||||
def test_self_closing_is_rendered(self):
|
|
||||||
response = self.client.get("/self-closing")
|
|
||||||
self.assertContains(response, '<div class="i-am-parent">')
|
|
||||||
|
|
||||||
def test_named_slots_correctly_display_in_loop(self):
|
|
||||||
response = self.client.get("/named-slot-in-loop")
|
|
||||||
self.assertContains(response, "item name: Item 1")
|
|
||||||
self.assertContains(response, "item name: Item 2")
|
|
||||||
self.assertContains(response, "item name: Item 3")
|
|
||||||
|
|
||||||
def test_attribute_passing(self):
|
|
||||||
response = self.client.get("/attribute-passing")
|
|
||||||
self.assertContains(
|
|
||||||
response, '<div attribute_1="hello" and-another="woo1" thirdforluck="yes">'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_attribute_merging(self):
|
|
||||||
response = self.client.get("/attribute-merging")
|
|
||||||
self.assertContains(
|
|
||||||
response, 'class="form-group another-class-with:colon extra-class"'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_django_syntax_decoding(self):
|
|
||||||
response = self.client.get("/django-syntax-decoding")
|
|
||||||
self.assertContains(response, "some-class")
|
|
||||||
|
|
||||||
def test_vars_are_converted_to_vars_frame_tags(self):
|
|
||||||
compiled = get_compiled(
|
|
||||||
"""
|
|
||||||
<c-vars var1="string with space" />
|
|
||||||
|
|
||||||
content
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
compiled,
|
|
||||||
"""{% cotton_vars_frame var1=var1|default:"string with space" %}content{% endcotton_vars_frame %}""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_attrs_do_not_contain_vars(self):
|
|
||||||
response = self.client.get("/vars-test")
|
|
||||||
self.assertContains(response, "attr1: 'im an attr'")
|
|
||||||
self.assertContains(response, "var1: 'im a var'")
|
|
||||||
self.assertContains(response, """attrs: 'attr1="im an attr"'""")
|
|
||||||
|
|
||||||
def test_strings_with_spaces_can_be_passed(self):
|
|
||||||
response = self.client.get("/string-with-spaces")
|
|
||||||
self.assertContains(response, "attr1: 'I have spaces'")
|
|
||||||
self.assertContains(response, "var1: 'string with space'")
|
|
||||||
self.assertContains(response, "default_var: 'default var'")
|
|
||||||
self.assertContains(response, "named_slot: '")
|
|
||||||
self.assertContains(response, "named_slot with spaces")
|
|
||||||
self.assertContains(response, """attrs: 'attr1="I have spaces"'""")
|
|
||||||
|
|
||||||
def test_named_slots_dont_bleed_into_sibling_components(self):
|
|
||||||
html = """
|
|
||||||
<c-test-component>
|
|
||||||
component1
|
|
||||||
<c-slot name="named_slot">named slot 1</c-slot>
|
|
||||||
</c-test-component>
|
|
||||||
<c-test-component>
|
|
||||||
component2
|
|
||||||
</c-test-component>
|
|
||||||
"""
|
|
||||||
|
|
||||||
rendered = get_rendered(html)
|
|
||||||
|
|
||||||
self.assertTrue("named_slot: 'named slot 1'" in rendered)
|
|
||||||
self.assertTrue("named_slot: ''" in rendered)
|
|
||||||
|
|
||||||
def test_template_variables_are_not_parsed(self):
|
|
||||||
html = """
|
|
||||||
<c-test-component attr1="variable" :attr2="variable">
|
|
||||||
<c-slot name="named_slot">
|
|
||||||
<a href="#" silica:click.prevent="variable = 'lineage'">test</a>
|
|
||||||
</c-slot>
|
|
||||||
</c-test-component>
|
|
||||||
"""
|
|
||||||
|
|
||||||
rendered = get_rendered(html, {"variable": 1})
|
|
||||||
|
|
||||||
self.assertTrue("attr1: 'variable'" in rendered)
|
|
||||||
self.assertTrue("attr2: '1'" in rendered)
|
|
||||||
|
|
||||||
def test_valueless_attributes_are_process_as_true(self):
|
|
||||||
response = self.client.get("/test/valueless-attributes")
|
|
||||||
|
|
||||||
self.assertContains(response, "It's True")
|
|
||||||
|
|
||||||
def test_component_attributes_can_converted_to_python_types(self):
|
|
||||||
response = self.client.get("/test/eval-attributes")
|
|
||||||
|
|
||||||
self.assertContains(response, "none is None")
|
|
||||||
self.assertContains(response, "number is 1")
|
|
||||||
self.assertContains(response, "boolean_true is True")
|
|
||||||
self.assertContains(response, "boolean_false is False")
|
|
||||||
self.assertContains(response, "list.0 is 1")
|
|
||||||
self.assertContains(response, "dict.key is 'value'")
|
|
||||||
self.assertContains(response, "listdict.0.key is 'value'")
|
|
||||||
|
|
||||||
def test_cvars_can_be_converted_to_python_types(self):
|
|
||||||
response = self.client.get("/test/eval-vars")
|
|
||||||
|
|
||||||
self.assertContains(response, "none is None")
|
|
||||||
self.assertContains(response, "number is 1")
|
|
||||||
self.assertContains(response, "boolean_true is True")
|
|
||||||
self.assertContains(response, "boolean_false is False")
|
|
||||||
self.assertContains(response, "list.0 is 1")
|
|
||||||
self.assertContains(response, "dict.key is 'value'")
|
|
||||||
self.assertContains(response, "listdict.0.key is 'value'")
|
|
||||||
|
|
||||||
def test_attributes_can_contain_django_native_tags(self):
|
|
||||||
response = self.client.get("/test/native-tags-in-attributes")
|
|
||||||
|
|
||||||
self.assertContains(response, "Attribute 1 says: 'Hello Will'")
|
|
||||||
self.assertContains(response, "Attribute 2 says: 'world'")
|
|
||||||
self.assertContains(response, "Attribute 3 says: 'cowabonga!'")
|
|
||||||
|
|
||||||
self.assertContains(
|
|
||||||
response,
|
|
||||||
"""attrs tag is: 'normal="normal" attr1="Hello Will" attr2="world" attr3="cowabonga!"'""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: implement inline test asset creation, i.e. store_template("native-tags-in-attributes", """)
|
|
|
@ -1,18 +0,0 @@
|
||||||
from django.template import Context, Template
|
|
||||||
|
|
||||||
from django_cotton.cotton_loader import Loader as CottonLoader
|
|
||||||
|
|
||||||
|
|
||||||
def get_compiled(template_string):
|
|
||||||
return CottonLoader(engine=None).template_processor.process(
|
|
||||||
template_string, "test_key"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_rendered(template_string, context: dict = None):
|
|
||||||
if context is None:
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
compiled_string = get_compiled(template_string)
|
|
||||||
|
|
||||||
return Template(compiled_string).render(Context(context))
|
|
|
@ -1,52 +0,0 @@
|
||||||
from django.views.generic import TemplateView
|
|
||||||
from . import views
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
app_name = "django_cotton"
|
|
||||||
|
|
||||||
|
|
||||||
class NamedSlotInLoop(TemplateView):
|
|
||||||
template_name = "named_slot_in_loop.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return {
|
|
||||||
"items": [
|
|
||||||
{"name": "Item 1"},
|
|
||||||
{"name": "Item 2"},
|
|
||||||
{"name": "Item 3"},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("parent", TemplateView.as_view(template_name="parent_test.html")),
|
|
||||||
path("child", TemplateView.as_view(template_name="child_test.html")),
|
|
||||||
path(
|
|
||||||
"self-closing",
|
|
||||||
TemplateView.as_view(template_name="self_closing_test.html"),
|
|
||||||
),
|
|
||||||
path("include", TemplateView.as_view(template_name="cotton_include.html")),
|
|
||||||
path("playground", TemplateView.as_view(template_name="playground.html")),
|
|
||||||
path("tag", TemplateView.as_view(template_name="tag.html")),
|
|
||||||
path("named-slot-in-loop", NamedSlotInLoop.as_view()),
|
|
||||||
path("test/compiled-cotton", views.compiled_cotton_test_view),
|
|
||||||
path("test/cotton", views.cotton_test_view),
|
|
||||||
path("test/native-extends", views.native_extends_test_view),
|
|
||||||
path("test/native-include", views.native_include_test_view),
|
|
||||||
path("test/valueless-attributes", views.valueless_attributes_test_view),
|
|
||||||
path("attribute-merging", views.attribute_merging_test_view),
|
|
||||||
path("attribute-passing", views.attribute_passing_test_view),
|
|
||||||
path("django-syntax-decoding", views.django_syntax_decoding_test_view),
|
|
||||||
path(
|
|
||||||
"string-with-spaces",
|
|
||||||
TemplateView.as_view(template_name="string_with_spaces.html"),
|
|
||||||
),
|
|
||||||
path("vars-test", TemplateView.as_view(template_name="vars_test.html")),
|
|
||||||
path("variable-parsing", views.variable_parsing_test_view),
|
|
||||||
path("test/eval-vars", views.eval_vars_test_view),
|
|
||||||
path("test/eval-attributes", views.eval_attributes_test_view),
|
|
||||||
path(
|
|
||||||
"test/native-tags-in-attributes",
|
|
||||||
TemplateView.as_view(template_name="native_tags_in_attributes_view.html"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
import ast
|
|
||||||
|
|
||||||
|
|
||||||
def eval_string(value):
|
|
||||||
"""
|
|
||||||
Evaluate a string representation of a constant, list, or dictionary to the actual Python object.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return ast.literal_eval(value)
|
|
||||||
except (ValueError, SyntaxError):
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_quoted(value):
|
|
||||||
if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return f'"{value}"'
|
|
|
@ -1,50 +0,0 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# benchmark tests
|
|
||||||
|
|
||||||
|
|
||||||
def compiled_cotton_test_view(request):
|
|
||||||
return render(request, "compiled_cotton_test.html")
|
|
||||||
|
|
||||||
|
|
||||||
def cotton_test_view(request):
|
|
||||||
return render(request, "cotton_test.html")
|
|
||||||
|
|
||||||
|
|
||||||
def native_extends_test_view(request):
|
|
||||||
return render(request, "native_extends_test.html")
|
|
||||||
|
|
||||||
|
|
||||||
def native_include_test_view(request):
|
|
||||||
return render(request, "native_include_test.html")
|
|
||||||
|
|
||||||
|
|
||||||
# Django tests
|
|
||||||
|
|
||||||
|
|
||||||
def attribute_merging_test_view(request):
|
|
||||||
return render(request, "attribute_merging_test.html")
|
|
||||||
|
|
||||||
|
|
||||||
def attribute_passing_test_view(request):
|
|
||||||
return render(request, "attribute_passing_test.html")
|
|
||||||
|
|
||||||
|
|
||||||
def django_syntax_decoding_test_view(request):
|
|
||||||
return render(request, "django_syntax_decoding_test.html")
|
|
||||||
|
|
||||||
|
|
||||||
def variable_parsing_test_view(request):
|
|
||||||
return render(request, "variable_parsing_test.html", {"variable": "some-class"})
|
|
||||||
|
|
||||||
|
|
||||||
def valueless_attributes_test_view(request):
|
|
||||||
return render(request, "valueless_attributes_test_view.html")
|
|
||||||
|
|
||||||
|
|
||||||
def eval_vars_test_view(request):
|
|
||||||
return render(request, "eval_vars_test_view.html")
|
|
||||||
|
|
||||||
|
|
||||||
def eval_attributes_test_view(request):
|
|
||||||
return render(request, "eval_attributes_test_view.html")
|
|
|
@ -1,16 +0,0 @@
|
||||||
"""
|
|
||||||
WSGI config for django_cotton project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
Loading…
Add table
Add a link
Reference in a new issue