From afa0f1275e738de56aad39873aabf2c1b844affc Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Thu, 26 Sep 2024 17:49:44 +0100 Subject: [PATCH 1/6] new compilation --- django_cotton/cotton_loader.py | 104 ++++++++++++++++++++++++- django_cotton/tests/test_attributes.py | 15 +++- django_cotton/tests/test_compiler.py | 41 ++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 django_cotton/tests/test_compiler.py diff --git a/django_cotton/cotton_loader.py b/django_cotton/cotton_loader.py index 05ccd2e..6e1bf51 100755 --- a/django_cotton/cotton_loader.py +++ b/django_cotton/cotton_loader.py @@ -91,6 +91,108 @@ def get_template_sources(self, template_name): ) +from typing import List, Tuple + + +class Tag: + def __init__( + self, html: str, tag_name: str, attrs: str, is_closing: bool, is_self_closing: bool + ): + self.html = html + self.tag_name = tag_name + self.attrs = attrs + self.is_closing = is_closing + self.is_self_closing = is_self_closing + + def get_template_tag(self) -> str: + if self.tag_name == "c-vars": + return f"{{% cvars{self.attrs} %}}{{% endcvars %}}" + elif self.tag_name == "c-slot": + if self.is_closing: + return "{% endslot %}" + else: + # Extract the name attribute + name_match = re.search(r'name\s*=\s*["\'](.*?)["\']', self.attrs) + if not name_match: + raise ValueError(f"c-slot tag must have a name attribute: {self.html}") + slot_name = name_match.group(1) + return f"{{% slot {slot_name} %}}" + elif self.tag_name.startswith("c-"): + component_name = self.tag_name[2:] + if self.is_closing: + return "{% endc %}" + elif self.is_self_closing: + return f"{{% c {component_name}{self.attrs} %}}{{% endc %}}" + else: + return f"{{% c {component_name}{self.attrs} %}}" + else: + return self.html # This case should never be reached with the current regex + + +class CottonCompiler: + def __init__(self): + self.tag_pattern = re.compile(r"<(/?)c-([^\s>]+)(\s[^>]*?)?(/?)>") + self.comment_pattern = re.compile( + r"({%\s*comment\s*%}.*?{%\s*endcomment\s*%}|{#.*?#})", re.DOTALL + ) + + def exclude_comments(self, html: str) -> Tuple[str, List[Tuple[str, str]]]: + comments = [] + + def replace_comment(match): + placeholder = f"__COMMENT_{len(comments)}__" + comments.append((placeholder, match.group(0))) + return placeholder + + processed_html = self.comment_pattern.sub(replace_comment, html) + return processed_html, comments + + def restore_comments(self, html: str, comments: List[Tuple[str, str]]) -> str: + for placeholder, comment in comments: + html = html.replace(placeholder, comment) + return html + + def get_replacements(self, html: str) -> List[Tuple[str, str]]: + replacements = [] + for match in self.tag_pattern.finditer(html): + is_closing, tag_name, attrs, self_closing = match.groups() + is_self_closing = bool(self_closing) + + tag = Tag( + html=html[match.start() : match.end()], + tag_name=f"c-{tag_name}", + attrs=attrs or "", + is_closing=bool(is_closing), + is_self_closing=is_self_closing, + ) + + try: + template_tag = tag.get_template_tag() + if template_tag != tag.html: + replacements.append((tag.html, template_tag)) + except ValueError as e: + # Add context about the position of the error in the template + position = match.start() + line_number = html[:position].count("\n") + 1 + raise ValueError(f"Error in template at line {line_number}: {str(e)}") from e + + return replacements + + def process(self, html: str) -> str: + # Exclude comments + processed_html, comments = self.exclude_comments(html) + + # Perform replacements + replacements = self.get_replacements(processed_html) + for original, replacement in replacements: + processed_html = processed_html.replace(original, replacement) + + # Restore comments + final_html = self.restore_comments(processed_html, comments) + + return final_html + + class UnsortedAttributes(HTMLFormatter): """This keeps BS4 from re-ordering attributes""" @@ -99,7 +201,7 @@ def attributes(self, tag): yield k, v -class CottonCompiler: +class CottonBs4Compiler: DJANGO_SYNTAX_PLACEHOLDER_PREFIX = "__django_syntax__" COTTON_VERBATIM_PATTERN = re.compile( r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}", re.DOTALL diff --git a/django_cotton/tests/test_attributes.py b/django_cotton/tests/test_attributes.py index 3f93a04..bfb51aa 100644 --- a/django_cotton/tests/test_attributes.py +++ b/django_cotton/tests/test_attributes.py @@ -218,7 +218,6 @@ def test_attributes_can_contain_django_native_tags(self): """ test @@ -228,6 +227,20 @@ def test_attributes_can_contain_django_native_tags(self): context={"name": "Will", "test": "world"}, ) + print( + get_compiled( + """ + + test + + """ + ) + ) + self.create_template( "cotton/native_tags_in_attributes.html", """ diff --git a/django_cotton/tests/test_compiler.py b/django_cotton/tests/test_compiler.py new file mode 100644 index 0000000..82cac97 --- /dev/null +++ b/django_cotton/tests/test_compiler.py @@ -0,0 +1,41 @@ +from django_cotton.tests.utils import CottonTestCase, get_compiled + + +class CompileTests(CottonTestCase): + def test_compile(self): + compiled = get_compiled( + """ + + Default! + Named! + + """ + ) + + print(compiled) + + self.assertTrue(True) + + def test_basic(self): + self.create_template( + "cotton/render_basic.html", + """
+ default: {{ slot }} + named: {{ named }} +
""", + ) + + self.create_template( + "view.html", + """ + Default! + Named! + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, "default: Default!") + self.assertContains(response, "named: Named!") From 3f9cd858728d565c0240d629ed11fefa8ef94874 Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sun, 29 Sep 2024 16:30:50 +0100 Subject: [PATCH 2/6] moved completely to the regex compiler --- dev/example_project/render_load_test.py | 18 +- django_cotton/apps.py | 14 +- django_cotton/compiler_bs4.py | 326 +++++++++++++++ django_cotton/compiler_regex.py | 142 +++++++ django_cotton/cotton_loader.py | 373 +----------------- django_cotton/tests/test_attributes.py | 17 +- django_cotton/tests/test_compiler.py | 85 ++-- django_cotton/tests/test_cvars.py | 2 +- django_cotton/tests/test_slots.py | 10 +- django_cotton/utils.py | 65 --- .../docs_project/templates/compiler.html | 1 + .../templates/cotton/compiler/comp.html | 3 + docs/docs_project/docs_project/urls.py | 1 + 13 files changed, 565 insertions(+), 492 deletions(-) create mode 100644 django_cotton/compiler_bs4.py create mode 100644 django_cotton/compiler_regex.py create mode 100644 docs/docs_project/docs_project/templates/compiler.html create mode 100644 docs/docs_project/docs_project/templates/cotton/compiler/comp.html diff --git a/dev/example_project/render_load_test.py b/dev/example_project/render_load_test.py index 11db6f2..4264100 100644 --- a/dev/example_project/render_load_test.py +++ b/dev/example_project/render_load_test.py @@ -22,14 +22,14 @@ def configure_django(): "DIRS": ["example_project/templates"], "OPTIONS": { "loaders": [ - ( - "django.template.loaders.cached.Loader", - [ - "django_cotton.cotton_loader.Loader", - "django.template.loaders.filesystem.Loader", - "django.template.loaders.app_directories.Loader", - ], - ), + # ( + # "django.template.loaders.cached.Loader", + # [ + "django_cotton.cotton_loader.Loader", + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + # ], + # ), ], "builtins": [ "django_cotton.templatetags.cotton", @@ -85,7 +85,7 @@ def main(): configure_django() runs = 5 - iterations = 500 + iterations = 5000 print(f"Running benchmarks with {runs} runs, {iterations} iterations each") diff --git a/django_cotton/apps.py b/django_cotton/apps.py index 89a9b26..7eac5fd 100644 --- a/django_cotton/apps.py +++ b/django_cotton/apps.py @@ -3,7 +3,7 @@ App configuration to set up the cotton loader and builtins automatically. """ - +import re from contextlib import suppress import django.contrib.admin @@ -65,6 +65,11 @@ class LoaderAppConfig(AppConfig): default = True def ready(self): + from django.template import base + + # Support for multiline tags + base.tag_re = re.compile(base.tag_re.pattern, re.DOTALL) + wrap_loaders("django") @@ -75,3 +80,10 @@ class SimpleAppConfig(AppConfig): """ name = "django_cotton" + + def ready(self): + pass + from django.template import base + + # Support for multiline tags + base.tag_re = re.compile(base.tag_re.pattern, re.DOTALL) diff --git a/django_cotton/compiler_bs4.py b/django_cotton/compiler_bs4.py new file mode 100644 index 0000000..f2d891c --- /dev/null +++ b/django_cotton/compiler_bs4.py @@ -0,0 +1,326 @@ +import random +import re +import warnings +from html.parser import HTMLParser + +from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning +from bs4.builder._htmlparser import BeautifulSoupHTMLParser, HTMLParserTreeBuilder +from bs4.formatter import HTMLFormatter + +warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) + + +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 CottonHTMLParser(BeautifulSoupHTMLParser): + """Extending the default HTML parser to override handle_starttag so we can preserve the intended value of the + attribute from the developer so that we can differentiate boolean attributes and simply empty ones. + """ + + 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. ''""" + attr_dict = {} + for key, value in attrs: + # Cotton edit: We want to permit valueless / "boolean" attributes + # if value is None: + # value = '' + + 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) + + # Cotton edit: We do not need to validate the root element + # if self._root_tag is None: + # self._root_tag_encountered(name) + + +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() + + +class CottonBs4Compiler: + DJANGO_SYNTAX_PLACEHOLDER_PREFIX = "__django_syntax__" + COTTON_VERBATIM_PATTERN = re.compile( + r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}", re.DOTALL + ) + DJANGO_TAG_PATTERN = re.compile(r"(\s?)(\{%.*?%\})(\s?)") + DJANGO_VAR_PATTERN = re.compile(r"(\s?)(\{\{.*?\}\})(\s?)") + HTML_ENTITY_PATTERN = re.compile(r"&[a-zA-Z]+;|&#[0-9]+;|&#x[a-fA-F0-9]+;") + + def __init__(self): + self.django_syntax_placeholders = [] + self.html_entity_placeholders = [] + + def process(self, content: str): + processors = [ + self._replace_syntax_with_placeholders, + self._replace_html_entities_with_placeholders, + self._compile_cotton_to_django, + self._replace_placeholders_with_syntax, + self._replace_placeholders_with_html_entities, + self._remove_duplicate_attribute_markers, + ] + + for processor in processors: + # noinspection PyArgumentList + content = processor(content) + + return content + + def _replace_html_entities_with_placeholders(self, content): + """Replace HTML entities with placeholders so they dont get touched by BS4""" + + def replace_entity(match): + entity = match.group(0) + self.html_entity_placeholders.append(entity) + return f"__HTML_ENTITY_{len(self.html_entity_placeholders) - 1}__" + + return self.HTML_ENTITY_PATTERN.sub(replace_entity, content) + + def _replace_placeholders_with_html_entities(self, content: str): + for i, entity in enumerate(self.html_entity_placeholders): + content = content.replace(f"__HTML_ENTITY_{i}__", entity) + return content + + def _replace_syntax_with_placeholders(self, content: str): + """Replace {% ... %} and {{ ... }} with placeholders so they dont get touched + or encoded by bs4. We will replace them back after bs4 has done its job.""" + self.django_syntax_placeholders = [] + + def replace_pattern(pattern, replacement_func): + return pattern.sub(replacement_func, content) + + def replace_cotton_verbatim(match): + """{% cotton_verbatim %} protects the content through the bs4 parsing process when we want to actually print + cotton syntax in
 blocks."""
+            inner_content = match.group(1)
+            self.django_syntax_placeholders.append({"type": "verbatim", "content": inner_content})
+            return (
+                f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
+            )
+
+        def replace_django_syntax(match):
+            """Store if the match had at least one space on the left or right side of the syntax so we can restore it later"""
+            left_space, syntax, right_space = match.groups()
+            self.django_syntax_placeholders.append(
+                {
+                    "type": "django",
+                    "content": syntax,
+                    "left_space": bool(left_space),
+                    "right_space": bool(right_space),
+                }
+            )
+            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, content: str):
+        """Convert cotton  -->  --> 
+                """
+                left_group = r"( ?)" if not placeholder["left_space"] else ""
+                right_group = r"( ?)" if not placeholder["right_space"] else ""
+                placeholder_pattern = (
+                    f"{left_group}{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{i}__{right_group}"
+                )
+
+                content = re.sub(placeholder_pattern, placeholder["content"], content)
+
+        return content
+
+    def _remove_duplicate_attribute_markers(self, content: str):
+        return re.sub(r"__COTTON_DUPE_ATTR__[0-9A-F]{5}", "", content, flags=re.IGNORECASE)
+
+    def _wrap_with_cotton_vars_frame(self, soup, cvars_el):
+        """If the user has defined a  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
+        same component's context, we wrap the entire contents in another component: cotton_vars_frame. Only when 
+        is present."""
+
+        cvars_attrs = []
+        for k, v in cvars_el.attrs.items():
+            if v is None:
+                cvars_attrs.append(k)
+            else:
+                if k == "class":
+                    v = " ".join(v)
+                cvars_attrs.append(f'{k}="{v}"')
+
+        cvars_el.decompose()
+        opening = f"{{% vars {' '.join(cvars_attrs)} %}}"
+        opening = opening.replace("\n", "")
+        closing = "{% endvars %}"
+
+        # 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
+        )
+        new_soup = self._make_soup(wrapped_content)
+        return new_soup
+
+    def _transform_components(self, soup):
+        """Replace  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)
+
+                continue
+
+            component_key = tag.name[2:]
+            opening_tag = f"{{% c {component_key} "
+
+            # Store attributes that contain template expressions, they are when we use '{{' or '{%' in the value of an attribute
+            complex_attrs = []
+
+            # 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)
+
+                # 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 or "=" in value:
+                    complex_attrs.append((key, value))
+                    continue
+
+                opening_tag += ' {}="{}"'.format(key, value)
+            opening_tag += " %}"
+
+            component_tag = opening_tag
+
+            if complex_attrs:
+                for key, value in complex_attrs:
+                    component_tag += f"{{% attr {key} %}}{value}{{% endattr %}}"
+
+            if tag.contents:
+                tag_soup = self._make_soup(tag.decode_contents(formatter=UnsortedAttributes()))
+                self._transform_components(tag_soup)
+                component_tag += str(
+                    tag_soup.encode(formatter=UnsortedAttributes()).decode("utf-8")
+                    # tag_soup.decode_contents(formatter=UnsortedAttributes())
+                )
+
+            component_tag += "{% endc %}"
+
+            # Replace the original tag with the compiled django syntax
+            new_soup = self._make_soup(component_tag)
+            tag.replace_with(new_soup)
+
+        return soup
+
+    def _transform_named_slot(self, slot_tag):
+        """Compile  to {% slot %}"""
+        slot_name = slot_tag.get("name", "").strip()
+        inner_html = slot_tag.decode_contents(formatter=UnsortedAttributes())
+
+        # Check and process any components in the slot content
+        slot_soup = self._make_soup(inner_html)
+        self._transform_components(slot_soup)
+
+        cotton_slot_tag = f"{{% slot {slot_name} %}}{str(slot_soup.encode(formatter=UnsortedAttributes()).decode('utf-8'))}{{% endslot %}}"
+
+        slot_tag.replace_with(self._make_soup(cotton_slot_tag))
+
+    def _make_soup(self, content):
+        return BeautifulSoup(
+            content,
+            "html.parser",
+            builder=CottonHTMLTreeBuilder(on_duplicate_attribute=handle_duplicate_attributes),
+        )
+
+
+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
+    any XML-like tag. It's perfectly fine to expect duplicate attributes per tag in DTL:
+
+    Hello
+
+    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
diff --git a/django_cotton/compiler_regex.py b/django_cotton/compiler_regex.py
new file mode 100644
index 0000000..09546db
--- /dev/null
+++ b/django_cotton/compiler_regex.py
@@ -0,0 +1,142 @@
+import re
+from typing import List, Tuple
+
+
+class Tag:
+    tag_pattern = re.compile(
+        r"<(/?)c-([^\s/>]+)((?:\s+[^\s/>\"'=<>`]+(?:\s*=\s*(?:\"[^\"]*\"|'[^']*'|\S+))?)*)\s*(/?)\s*>",
+        re.DOTALL,
+    )
+    attr_pattern = re.compile(r'([^\s/>\"\'=<>`]+)(?:\s*=\s*(?:(["\'])(.*?)\2|(\S+)))?', re.DOTALL)
+
+    def __init__(self, match: re.Match):
+        self.html = match.group(0)
+        self.tag_name = f"c-{match.group(2)}"
+        self.attrs = match.group(3) or ""
+        self.is_closing = bool(match.group(1))
+        self.is_self_closing = bool(match.group(4))
+
+    def get_template_tag(self) -> str:
+        """Convert a cotton tag to a Django template tag"""
+        if self.tag_name == "c-vars":
+            return ""  # c-vars tags will be handled separately
+        elif self.tag_name == "c-slot":
+            return self._process_slot()
+        elif self.tag_name.startswith("c-"):
+            return self._process_component()
+        else:
+            return self.html
+
+    def _process_slot(self) -> str:
+        """Convert a c-slot tag to a Django template slot tag"""
+        if self.is_closing:
+            return "{% endslot %}"
+        name_match = re.search(r'name=(["\'])(.*?)\1', self.attrs, re.DOTALL)
+        if not name_match:
+            raise ValueError(f"c-slot tag must have a name attribute: {self.html}")
+        slot_name = name_match.group(2)
+        return f"{{% slot {slot_name} %}}"
+
+    def _process_component(self) -> str:
+        """Convert a c- component tag to a Django template component tag"""
+        component_name = self.tag_name[2:]
+        if self.is_closing:
+            return "{% endc %}"
+        processed_attrs, extracted_attrs = self._process_attributes()
+        opening_tag = f"{{% c {component_name}{processed_attrs} %}}"
+        if self.is_self_closing:
+            return f"{opening_tag}{extracted_attrs}{{% endc %}}"
+        return f"{opening_tag}{extracted_attrs}"
+
+    def _process_attributes(self) -> Tuple[str, str]:
+        """Move any complex attributes to the {% attr %} tag"""
+        processed_attrs = []
+        extracted_attrs = []
+
+        for match in self.attr_pattern.finditer(self.attrs):
+            key, quote, value, unquoted_value = match.groups()
+            if value is None and unquoted_value is None:
+                processed_attrs.append(key)
+            else:
+                actual_value = value if value is not None else unquoted_value
+                if any(s in actual_value for s in ("{{", "{%", "=", "__COTTON_IGNORE_")):
+                    extracted_attrs.append(f"{{% attr {key} %}}{actual_value}{{% endattr %}}")
+                else:
+                    processed_attrs.append(f'{key}="{actual_value}"')
+
+        return " " + " ".join(processed_attrs), "".join(extracted_attrs)
+
+
+class CottonCompiler:
+    def __init__(self):
+        self.c_vars_pattern = re.compile(r"]*)(?:/>|>(.*?))", re.DOTALL)
+        self.ignore_pattern = re.compile(
+            # cotton_verbatim isnt a real template tag, it's just a way to ignore  Tuple[str, List[Tuple[str, str]]]:
+        ignorables = []
+
+        def replace_ignorable(match):
+            placeholder = f"__COTTON_IGNORE_{len(ignorables)}__"
+            ignorables.append((placeholder, match.group(0)))
+            return placeholder
+
+        processed_html = self.ignore_pattern.sub(replace_ignorable, html)
+        return processed_html, ignorables
+
+    def restore_ignorables(self, html: str, ignorables: List[Tuple[str, str]]) -> str:
+        for placeholder, content in ignorables:
+            if content.strip().startswith("{% cotton_verbatim %}"):
+                # Extract content between cotton_verbatim tags, we don't want to leave these in
+                match = self.cotton_verbatim_pattern.search(content)
+                if match:
+                    content = match.group(1)
+            html = html.replace(placeholder, content)
+        return html
+
+    def get_replacements(self, html: str) -> List[Tuple[str, str]]:
+        replacements = []
+        for match in Tag.tag_pattern.finditer(html):
+            tag = Tag(match)
+            try:
+                template_tag = tag.get_template_tag()
+                if template_tag != tag.html:
+                    replacements.append((tag.html, template_tag))
+            except ValueError as e:
+                # Find the line number of the error
+                position = match.start()
+                line_number = html[:position].count("\n") + 1
+                raise ValueError(f"Error in template at line {line_number}: {str(e)}") from e
+
+        return replacements
+
+    def process_c_vars(self, html: str) -> Tuple[str, str]:
+        """Extract c-vars content and remove c-vars tags from the html"""
+        match = self.c_vars_pattern.search(html)
+        if match:
+            attrs = match.group(1)
+            vars_content = f"{{% vars {attrs.strip()} %}}"
+            html = self.c_vars_pattern.sub("", html)  # Remove all c-vars tags
+            return vars_content, html
+        return "", html
+
+    def process(self, html: str) -> str:
+        """Putting it all together"""
+        processed_html, ignorables = self.exclude_ignorables(html)
+        vars_content, processed_html = self.process_c_vars(processed_html)
+        replacements = self.get_replacements(processed_html)
+        for original, replacement in replacements:
+            processed_html = processed_html.replace(original, replacement)
+        if vars_content:
+            processed_html = f"{vars_content}{processed_html}{{% endvars %}}"
+        return self.restore_ignorables(processed_html, ignorables)
diff --git a/django_cotton/cotton_loader.py b/django_cotton/cotton_loader.py
index 21d9632..ff02d7b 100755
--- a/django_cotton/cotton_loader.py
+++ b/django_cotton/cotton_loader.py
@@ -1,8 +1,5 @@
-import warnings
 import hashlib
-import random
 import os
-import re
 from functools import lru_cache
 
 from django.template.loaders.base import Loader as BaseLoader
@@ -12,12 +9,7 @@
 from django.template import Template
 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)
+from django_cotton.compiler_regex import CottonCompiler
 
 
 class Loader(BaseLoader):
@@ -93,351 +85,11 @@ def get_template_sources(self, template_name):
             )
 
 
-from typing import List, Tuple
-
-
-class Tag:
-    def __init__(
-        self, html: str, tag_name: str, attrs: str, is_closing: bool, is_self_closing: bool
-    ):
-        self.html = html
-        self.tag_name = tag_name
-        self.attrs = attrs
-        self.is_closing = is_closing
-        self.is_self_closing = is_self_closing
-
-    def get_template_tag(self) -> str:
-        if self.tag_name == "c-vars":
-            return f"{{% cvars{self.attrs} %}}{{% endcvars %}}"
-        elif self.tag_name == "c-slot":
-            if self.is_closing:
-                return "{% endslot %}"
-            else:
-                # Extract the name attribute
-                name_match = re.search(r'name\s*=\s*["\'](.*?)["\']', self.attrs)
-                if not name_match:
-                    raise ValueError(f"c-slot tag must have a name attribute: {self.html}")
-                slot_name = name_match.group(1)
-                return f"{{% slot {slot_name} %}}"
-        elif self.tag_name.startswith("c-"):
-            component_name = self.tag_name[2:]
-            if self.is_closing:
-                return "{% endc %}"
-            elif self.is_self_closing:
-                return f"{{% c {component_name}{self.attrs} %}}{{% endc %}}"
-            else:
-                return f"{{% c {component_name}{self.attrs} %}}"
-        else:
-            return self.html  # This case should never be reached with the current regex
-
-
-class CottonCompiler:
-    def __init__(self):
-        self.tag_pattern = re.compile(r"<(/?)c-([^\s>]+)(\s[^>]*?)?(/?)>")
-        self.comment_pattern = re.compile(
-            r"({%\s*comment\s*%}.*?{%\s*endcomment\s*%}|{#.*?#})", re.DOTALL
-        )
-
-    def exclude_comments(self, html: str) -> Tuple[str, List[Tuple[str, str]]]:
-        comments = []
-
-        def replace_comment(match):
-            placeholder = f"__COMMENT_{len(comments)}__"
-            comments.append((placeholder, match.group(0)))
-            return placeholder
-
-        processed_html = self.comment_pattern.sub(replace_comment, html)
-        return processed_html, comments
-
-    def restore_comments(self, html: str, comments: List[Tuple[str, str]]) -> str:
-        for placeholder, comment in comments:
-            html = html.replace(placeholder, comment)
-        return html
-
-    def get_replacements(self, html: str) -> List[Tuple[str, str]]:
-        replacements = []
-        for match in self.tag_pattern.finditer(html):
-            is_closing, tag_name, attrs, self_closing = match.groups()
-            is_self_closing = bool(self_closing)
-
-            tag = Tag(
-                html=html[match.start() : match.end()],
-                tag_name=f"c-{tag_name}",
-                attrs=attrs or "",
-                is_closing=bool(is_closing),
-                is_self_closing=is_self_closing,
-            )
-
-            try:
-                template_tag = tag.get_template_tag()
-                if template_tag != tag.html:
-                    replacements.append((tag.html, template_tag))
-            except ValueError as e:
-                # Add context about the position of the error in the template
-                position = match.start()
-                line_number = html[:position].count("\n") + 1
-                raise ValueError(f"Error in template at line {line_number}: {str(e)}") from e
-
-        return replacements
-
-    def process(self, html: str) -> str:
-        # Exclude comments
-        processed_html, comments = self.exclude_comments(html)
-
-        # Perform replacements
-        replacements = self.get_replacements(processed_html)
-        for original, replacement in replacements:
-            processed_html = processed_html.replace(original, replacement)
-
-        # Restore comments
-        final_html = self.restore_comments(processed_html, comments)
-
-        return final_html
-
-
-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 CottonBs4Compiler:
-    DJANGO_SYNTAX_PLACEHOLDER_PREFIX = "__django_syntax__"
-    COTTON_VERBATIM_PATTERN = re.compile(
-        r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}", re.DOTALL
-    )
-    DJANGO_TAG_PATTERN = re.compile(r"(\s?)(\{%.*?%\})(\s?)")
-    DJANGO_VAR_PATTERN = re.compile(r"(\s?)(\{\{.*?\}\})(\s?)")
-    HTML_ENTITY_PATTERN = re.compile(r"&[a-zA-Z]+;|&#[0-9]+;|&#x[a-fA-F0-9]+;")
-
-    def __init__(self):
-        self.django_syntax_placeholders = []
-        self.html_entity_placeholders = []
-
-    def process(self, content: str):
-        processors = [
-            self._replace_syntax_with_placeholders,
-            self._replace_html_entities_with_placeholders,
-            self._compile_cotton_to_django,
-            self._replace_placeholders_with_syntax,
-            self._replace_placeholders_with_html_entities,
-            self._remove_duplicate_attribute_markers,
-        ]
-
-        for processor in processors:
-            # noinspection PyArgumentList
-            content = processor(content)
-
-        return content
-
-    def _replace_html_entities_with_placeholders(self, content):
-        """Replace HTML entities with placeholders so they dont get touched by BS4"""
-
-        def replace_entity(match):
-            entity = match.group(0)
-            self.html_entity_placeholders.append(entity)
-            return f"__HTML_ENTITY_{len(self.html_entity_placeholders) - 1}__"
-
-        return self.HTML_ENTITY_PATTERN.sub(replace_entity, content)
-
-    def _replace_placeholders_with_html_entities(self, content: str):
-        for i, entity in enumerate(self.html_entity_placeholders):
-            content = content.replace(f"__HTML_ENTITY_{i}__", entity)
-        return content
-
-    def _replace_syntax_with_placeholders(self, content: str):
-        """Replace {% ... %} and {{ ... }} with placeholders so they dont get touched
-        or encoded by bs4. We will replace them back after bs4 has done its job."""
-        self.django_syntax_placeholders = []
-
-        def replace_pattern(pattern, replacement_func):
-            return pattern.sub(replacement_func, content)
-
-        def replace_cotton_verbatim(match):
-            """{% cotton_verbatim %} protects the content through the bs4 parsing process when we want to actually print
-            cotton syntax in 
 blocks."""
-            inner_content = match.group(1)
-            self.django_syntax_placeholders.append({"type": "verbatim", "content": inner_content})
-            return (
-                f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
-            )
-
-        def replace_django_syntax(match):
-            """Store if the match had at least one space on the left or right side of the syntax so we can restore it later"""
-            left_space, syntax, right_space = match.groups()
-            self.django_syntax_placeholders.append(
-                {
-                    "type": "django",
-                    "content": syntax,
-                    "left_space": bool(left_space),
-                    "right_space": bool(right_space),
-                }
-            )
-            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, content: str):
-        """Convert cotton  -->  --> 
-                """
-                left_group = r"( ?)" if not placeholder["left_space"] else ""
-                right_group = r"( ?)" if not placeholder["right_space"] else ""
-                placeholder_pattern = (
-                    f"{left_group}{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{i}__{right_group}"
-                )
-
-                content = re.sub(placeholder_pattern, placeholder["content"], content)
-
-        return content
-
-    def _remove_duplicate_attribute_markers(self, content: str):
-        return re.sub(r"__COTTON_DUPE_ATTR__[0-9A-F]{5}", "", content, flags=re.IGNORECASE)
-
-    def _wrap_with_cotton_vars_frame(self, soup, cvars_el):
-        """If the user has defined a  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
-        same component's context, we wrap the entire contents in another component: cotton_vars_frame. Only when 
-        is present."""
-
-        cvars_attrs = []
-        for k, v in cvars_el.attrs.items():
-            if v is None:
-                cvars_attrs.append(k)
-            else:
-                if k == "class":
-                    v = " ".join(v)
-                cvars_attrs.append(f'{k}="{v}"')
-
-        cvars_el.decompose()
-        opening = f"{{% vars {' '.join(cvars_attrs)} %}}"
-        opening = opening.replace("\n", "")
-        closing = "{% endvars %}"
-
-        # 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
-        )
-        new_soup = self._make_soup(wrapped_content)
-        return new_soup
-
-    def _transform_components(self, soup):
-        """Replace  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)
-
-                continue
-
-            component_key = tag.name[2:]
-            opening_tag = f"{{% c {component_key} "
-
-            # Store attributes that contain template expressions, they are when we use '{{' or '{%' in the value of an attribute
-            complex_attrs = []
-
-            # 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)
-
-                # 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 or "=" in value:
-                    complex_attrs.append((key, value))
-                    continue
-
-                opening_tag += ' {}="{}"'.format(key, value)
-            opening_tag += " %}"
-
-            component_tag = opening_tag
-
-            if complex_attrs:
-                for key, value in complex_attrs:
-                    component_tag += f"{{% attr {key} %}}{value}{{% endattr %}}"
-
-            if tag.contents:
-                tag_soup = self._make_soup(tag.decode_contents(formatter=UnsortedAttributes()))
-                self._transform_components(tag_soup)
-                component_tag += str(
-                    tag_soup.encode(formatter=UnsortedAttributes()).decode("utf-8")
-                    # tag_soup.decode_contents(formatter=UnsortedAttributes())
-                )
-
-            component_tag += "{% endc %}"
-
-            # Replace the original tag with the compiled django syntax
-            new_soup = self._make_soup(component_tag)
-            tag.replace_with(new_soup)
-
-        return soup
-
-    def _transform_named_slot(self, slot_tag):
-        """Compile  to {% slot %}"""
-        slot_name = slot_tag.get("name", "").strip()
-        inner_html = slot_tag.decode_contents(formatter=UnsortedAttributes())
-
-        # Check and process any components in the slot content
-        slot_soup = self._make_soup(inner_html)
-        self._transform_components(slot_soup)
-
-        cotton_slot_tag = f"{{% slot {slot_name} %}}{str(slot_soup.encode(formatter=UnsortedAttributes()).decode('utf-8'))}{{% endslot %}}"
-
-        slot_tag.replace_with(self._make_soup(cotton_slot_tag))
-
-    def _make_soup(self, content):
-        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
-    more performant than this one but it acts a decent fallback when the loader is not defined in the Django cache loader.
+    """This mimics the simple template caching mechanism in Django's cached.Loader which acts a decent fallback when
+    the user has not configured the cache loader manually.
 
-    TODO: implement integration to the cache backend instead of just memory. one which can also be controlled / warmed
-    by the user and lasts beyond runtime restart.
+    TODO: implement cache warming functionality e.g. to be used at deployment
     """
 
     def __init__(self):
@@ -462,20 +114,3 @@ def generate_hash(self, values):
 
     def reset(self):
         self.template_cache.clear()
-
-
-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
-    any XML-like tag. It's perfectly fine to expect duplicate attributes per tag in DTL:
-
-    Hello
-
-    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
diff --git a/django_cotton/tests/test_attributes.py b/django_cotton/tests/test_attributes.py
index bfb51aa..1f05c07 100644
--- a/django_cotton/tests/test_attributes.py
+++ b/django_cotton/tests/test_attributes.py
@@ -218,6 +218,7 @@ def test_attributes_can_contain_django_native_tags(self):
             """                                
                 
                     test
@@ -227,20 +228,6 @@ def test_attributes_can_contain_django_native_tags(self):
             context={"name": "Will", "test": "world"},
         )
 
-        print(
-            get_compiled(
-                """
-                
-                    test
-                        
-        """
-            )
-        )
-
         self.create_template(
             "cotton/native_tags_in_attributes.html",
             """
@@ -303,7 +290,7 @@ def test_attribute_passing(self):
         with self.settings(ROOT_URLCONF=self.url_conf()):
             response = self.client.get("/view/")
             self.assertContains(
-                response, '
' + response, '
' ) def test_loader_preserves_duplicate_attributes(self): diff --git a/django_cotton/tests/test_compiler.py b/django_cotton/tests/test_compiler.py index 82cac97..b2a3a8c 100644 --- a/django_cotton/tests/test_compiler.py +++ b/django_cotton/tests/test_compiler.py @@ -2,40 +2,71 @@ class CompileTests(CottonTestCase): - def test_compile(self): - compiled = get_compiled( - """ - - Default! - Named! - - """ - ) - - print(compiled) - - self.assertTrue(True) - - def test_basic(self): + def test_regex_compile(self): self.create_template( - "cotton/render_basic.html", - """
- default: {{ slot }} - named: {{ named }} -
""", + "cotton/new_compiler.html", + """ + +
+ default: {{ slot }} + named: {{ named }} + lexed: {{ lexed }} +
+ """, ) self.create_template( - "view.html", - """ - Default! - Named! - """, + "new_compiler_view.html", + """ + + Default! + Named! + + """, "view/", + context={"some_var": "value"}, ) - # Override URLconf with self.settings(ROOT_URLCONF=self.url_conf()): response = self.client.get("/view/") - self.assertContains(response, "default: Default!") + self.assertContains(response, "default: \n Default!") self.assertContains(response, "named: Named!") + self.assertContains(response, "lexed: value") + + def test_compile_stage_ignores_django_vars_and_tags(self): + compiled = get_compiled( + """ + {# I'm a comment with a cotton tag #} + {% comment %}I'm a django comment with a cotton tag {% endcomment %} + {{ ''|safe }} + {% cotton_verbatim %}{% endcotton_verbatim %} + """ + ) + + self.assertTrue( + "{# I'm a comment with a cotton tag #}" in compiled, + "Compilation should ignore comments", + ) + + self.assertTrue( + "{% comment %}I'm a django comment with a cotton tag {% endcomment %}" + in compiled, + "Compilation should ignore comments", + ) + + self.assertTrue( + "{{ ''|safe }}" in compiled, + "Compilation should not touch the internals of variables or tags", + ) + + self.assertTrue( + "" in compiled, + "{% cotton_verbatim %} contents should be left untouched", + ) + + self.assertTrue( + "{% cotton_verbatim %}" not in compiled, + "Compilation should not leave {% cotton_verbatim %} tags in the output", + ) diff --git a/django_cotton/tests/test_cvars.py b/django_cotton/tests/test_cvars.py index 016a294..86cbfe7 100644 --- a/django_cotton/tests/test_cvars.py +++ b/django_cotton/tests/test_cvars.py @@ -332,7 +332,7 @@ def test_overwriting_cvars_dynamic_defaults(self): "cotton/dynamic_default_overwrite_cvars.html", """ - + {% if dynamic_default is True %}expected{% endif %} {% if dynamic_default is False %}not{% endif %} """, diff --git a/django_cotton/tests/test_slots.py b/django_cotton/tests/test_slots.py index b6bd2c5..2383fdf 100644 --- a/django_cotton/tests/test_slots.py +++ b/django_cotton/tests/test_slots.py @@ -64,16 +64,16 @@ def test_named_slots_dont_bleed_into_sibling_components(self): def test_vars_are_converted_to_vars_frame_tags(self): compiled = get_compiled( - """ - - - content + """ + content """ ) self.assertEquals( compiled, - """{% vars var1="string with space" %}content{% endvars %}""", + """{% vars var1="string with space" %} + content + {% endvars %}""", ) def test_named_slot_missing(self): diff --git a/django_cotton/utils.py b/django_cotton/utils.py index 087c264..4dcc4bc 100644 --- a/django_cotton/utils.py +++ b/django_cotton/utils.py @@ -1,8 +1,5 @@ import ast -from bs4.builder._htmlparser import BeautifulSoupHTMLParser, HTMLParserTreeBuilder -from html.parser import HTMLParser - def eval_string(value): """ @@ -27,65 +24,3 @@ def get_cotton_data(context): if "cotton_data" not in context: context["cotton_data"] = {"stack": [], "vars": {}} return context["cotton_data"] - - -class CottonHTMLParser(BeautifulSoupHTMLParser): - """Extending the default HTML parser to override handle_starttag so we can preserve the intended value of the - attribute from the developer so that we can differentiate boolean attributes and simply empty ones. - """ - - 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. ''""" - attr_dict = {} - for key, value in attrs: - # Cotton edit: We want to permit valueless / "boolean" attributes - # if value is None: - # value = '' - - 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) - - # Cotton edit: We do not need to validate the root element - # if self._root_tag is None: - # self._root_tag_encountered(name) - - -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() diff --git a/docs/docs_project/docs_project/templates/compiler.html b/docs/docs_project/docs_project/templates/compiler.html new file mode 100644 index 0000000..0bd8c90 --- /dev/null +++ b/docs/docs_project/docs_project/templates/compiler.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/docs_project/docs_project/templates/cotton/compiler/comp.html b/docs/docs_project/docs_project/templates/cotton/compiler/comp.html new file mode 100644 index 0000000..0013731 --- /dev/null +++ b/docs/docs_project/docs_project/templates/cotton/compiler/comp.html @@ -0,0 +1,3 @@ +{{ simple }} + +{{ complex }} \ No newline at end of file diff --git a/docs/docs_project/docs_project/urls.py b/docs/docs_project/docs_project/urls.py index 8997663..7e32c52 100644 --- a/docs/docs_project/docs_project/urls.py +++ b/docs/docs_project/docs_project/urls.py @@ -2,6 +2,7 @@ from django.urls import path urlpatterns = [ + path("compiler", views.build_view("compiler"), name="compiler"), path( "", views.build_view( From 4fd457f5444ed5c7cb2989e935518905fa8ec8c2 Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sun, 29 Sep 2024 16:41:32 +0100 Subject: [PATCH 3/6] minor cleanup --- django_cotton/cotton_loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/django_cotton/cotton_loader.py b/django_cotton/cotton_loader.py index ff02d7b..f9886a1 100755 --- a/django_cotton/cotton_loader.py +++ b/django_cotton/cotton_loader.py @@ -13,8 +13,6 @@ class Loader(BaseLoader): - is_usable = True - def __init__(self, engine, dirs=None): super().__init__(engine) self.cotton_compiler = CottonCompiler() From e457e9a1e711b7f424ce328fb25422707230292f Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sun, 29 Sep 2024 16:51:38 +0100 Subject: [PATCH 4/6] docs nav update --- .../templates/cotton/icons/github.html | 12 ++++++------ .../docs_project/templates/cotton/icons/sun.html | 14 ++++++++++++-- .../docs_project/templates/cotton/navbar.html | 6 +++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/docs_project/docs_project/templates/cotton/icons/github.html b/docs/docs_project/docs_project/templates/cotton/icons/github.html index 77141ee..98f4724 100644 --- a/docs/docs_project/docs_project/templates/cotton/icons/github.html +++ b/docs/docs_project/docs_project/templates/cotton/icons/github.html @@ -1,9 +1,9 @@ -{% comment %} - - - -{% endcomment %} - + +{# + + +#} diff --git a/docs/docs_project/docs_project/templates/cotton/icons/sun.html b/docs/docs_project/docs_project/templates/cotton/icons/sun.html index 5531d7e..e603284 100644 --- a/docs/docs_project/docs_project/templates/cotton/icons/sun.html +++ b/docs/docs_project/docs_project/templates/cotton/icons/sun.html @@ -1,3 +1,13 @@ - - +{% comment %} + + + {% endcomment %} + + + + + diff --git a/docs/docs_project/docs_project/templates/cotton/navbar.html b/docs/docs_project/docs_project/templates/cotton/navbar.html index 683b100..3f4aacd 100644 --- a/docs/docs_project/docs_project/templates/cotton/navbar.html +++ b/docs/docs_project/docs_project/templates/cotton/navbar.html @@ -64,10 +64,10 @@ localStorage.theme = this.dark ? 'dark' : 'light' } }"> - + - - + +
From 272a16400c9aeee9e10a6bf912439110612f76f3 Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sun, 29 Sep 2024 17:08:14 +0100 Subject: [PATCH 5/6] cleanup --- dev/example_project/render_load_test.py | 16 ++++++++-------- .../docs_project/templates/compiler.html | 1 - .../templates/cotton/compiler/comp.html | 3 --- docs/docs_project/docs_project/urls.py | 1 - 4 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 docs/docs_project/docs_project/templates/compiler.html delete mode 100644 docs/docs_project/docs_project/templates/cotton/compiler/comp.html diff --git a/dev/example_project/render_load_test.py b/dev/example_project/render_load_test.py index 4264100..d39863a 100644 --- a/dev/example_project/render_load_test.py +++ b/dev/example_project/render_load_test.py @@ -22,14 +22,14 @@ def configure_django(): "DIRS": ["example_project/templates"], "OPTIONS": { "loaders": [ - # ( - # "django.template.loaders.cached.Loader", - # [ - "django_cotton.cotton_loader.Loader", - "django.template.loaders.filesystem.Loader", - "django.template.loaders.app_directories.Loader", - # ], - # ), + ( + "django.template.loaders.cached.Loader", + [ + "django_cotton.cotton_loader.Loader", + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ], + ), ], "builtins": [ "django_cotton.templatetags.cotton", diff --git a/docs/docs_project/docs_project/templates/compiler.html b/docs/docs_project/docs_project/templates/compiler.html deleted file mode 100644 index 0bd8c90..0000000 --- a/docs/docs_project/docs_project/templates/compiler.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/docs_project/docs_project/templates/cotton/compiler/comp.html b/docs/docs_project/docs_project/templates/cotton/compiler/comp.html deleted file mode 100644 index 0013731..0000000 --- a/docs/docs_project/docs_project/templates/cotton/compiler/comp.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ simple }} - -{{ complex }} \ No newline at end of file diff --git a/docs/docs_project/docs_project/urls.py b/docs/docs_project/docs_project/urls.py index 7e32c52..8997663 100644 --- a/docs/docs_project/docs_project/urls.py +++ b/docs/docs_project/docs_project/urls.py @@ -2,7 +2,6 @@ from django.urls import path urlpatterns = [ - path("compiler", views.build_view("compiler"), name="compiler"), path( "", views.build_view( From 9be11c326d6c1bb4157f09d8d711b4fd3f16ff21 Mon Sep 17 00:00:00 2001 From: wrabit Date: Sun, 29 Sep 2024 17:09:20 +0100 Subject: [PATCH 6/6] Manual bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fc34a2c..bad20f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "django-cotton" -version = "1.0.12" +version = "1.1.0" description = "Bringing component based design to Django templates." authors = [ "Will Abbott ",] license = "MIT"