mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-12-23 10:11:57 +00:00
docs work
This commit is contained in:
parent
fb0d95aae6
commit
8f1fa5ee3b
23 changed files with 234 additions and 193 deletions
|
|
@ -1,9 +1,9 @@
|
|||
{% c benchmarks.partials.main %}
|
||||
{% cotton benchmarks.partials.main %}
|
||||
I'm default
|
||||
{% slot top %}
|
||||
{% cotton:slot top %}
|
||||
I'm top
|
||||
{% endslot %}
|
||||
{% slot bottom %}
|
||||
{% endcotton:slot %}
|
||||
{% cotton:slot bottom %}
|
||||
I'm bottom
|
||||
{% endslot %}
|
||||
{% endc %}
|
||||
{% endcotton:slot %}
|
||||
{% endcotton %}
|
||||
|
|
@ -1,45 +1,45 @@
|
|||
<h1>Welcome to our simplified Cotton demo</h1>
|
||||
|
||||
|
||||
{% c button class="btn-primary" %}
|
||||
{% cotton button class="btn-primary" %}
|
||||
Click me!
|
||||
{% endc %}
|
||||
{% endcotton %}
|
||||
|
||||
{% c card title="hello" :id="2" class="green" some_prop :testy="view_context" %}
|
||||
{% slot name=":some_prop" %}None{% endslot %}
|
||||
{% endc %}
|
||||
{% cotton card title="hello" :id="2" class="green" some_prop :testy="view_context" %}
|
||||
{% cotton:slot name=":some_prop" %}None{% endcotton:slot %}
|
||||
{% endcotton %}
|
||||
|
||||
{% c card title="My Card" %}
|
||||
{% cotton card title="My Card" %}
|
||||
|
||||
Default content here
|
||||
|
||||
{% c card var_override="sssasasasasa" title="My Override" some_attr="1+1" %}
|
||||
{% cotton card var_override="sssasasasasa" title="My Override" some_attr="1+1" %}
|
||||
This is the content of my card.
|
||||
{% c button class="btn-secondary" %}
|
||||
{% cotton button class="btn-secondary" %}
|
||||
Card button
|
||||
{% endc %}
|
||||
{% slot name="footer" %}
|
||||
{% endcotton %}
|
||||
{% cotton:slot name="footer" %}
|
||||
This is the footer of my card. {{ view_context }}
|
||||
{% endslot %}
|
||||
{% endc %}
|
||||
{% endcotton:slot %}
|
||||
{% endcotton %}
|
||||
|
||||
{% c card title="My Override" %}
|
||||
{% cotton card title="My Override" %}
|
||||
This is the content of my card.
|
||||
{% c button class="btn-secondary" %}
|
||||
{% cotton button class="btn-secondary" %}
|
||||
Card button
|
||||
{% endc %}
|
||||
{% slot name="footer" %}
|
||||
{% endcotton %}
|
||||
{% cotton:slot name="footer" %}
|
||||
This is the footer of my card. {{ view_context }}
|
||||
{% endslot %}
|
||||
{% endc %}
|
||||
{% endcotton:slot %}
|
||||
{% endcotton %}
|
||||
|
||||
{% c button class="btn-secondary" %}
|
||||
{% cotton button class="btn-secondary" %}
|
||||
Card button
|
||||
{% endc %}
|
||||
{% slot name="footer" %}
|
||||
{% endcotton %}
|
||||
{% cotton:slot name="footer" %}
|
||||
This is the footer of my card.
|
||||
{% endslot %}
|
||||
{% endc %}
|
||||
{% endcotton:slot %}
|
||||
{% endcotton %}
|
||||
|
||||
<p>Current time: {{ current_time }}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{% for d in data %}{% c benchmarks.cotton-include %}{% endc %}{% endfor %}
|
||||
{% for d in data %}{% cotton benchmarks.cotton-include %}{% endcotton %}{% endfor %}
|
||||
|
|
@ -66,7 +66,7 @@ class LoaderAppConfig(AppConfig):
|
|||
def ready(self):
|
||||
from django_cotton.nested_tag_support import enable_nested_tag_support
|
||||
|
||||
# Enable nested template tags in {% c %} and {% vars %} attributes
|
||||
# Enable nested template tags in {% cotton %} and {% cotton:vars %} attributes
|
||||
enable_nested_tag_support()
|
||||
|
||||
wrap_loaders("django")
|
||||
|
|
@ -83,5 +83,5 @@ class SimpleAppConfig(AppConfig):
|
|||
def ready(self):
|
||||
from django_cotton.nested_tag_support import enable_nested_tag_support
|
||||
|
||||
# Enable nested template tags in {% c %} and {% vars %} attributes
|
||||
# Enable nested template tags in {% cotton %} and {% cotton:vars %} attributes
|
||||
enable_nested_tag_support()
|
||||
|
|
|
|||
|
|
@ -30,22 +30,22 @@ class Tag:
|
|||
def _process_slot(self) -> str:
|
||||
"""Convert a c-slot tag to a Django template slot tag"""
|
||||
if self.is_closing:
|
||||
return "{% endc:slot %}"
|
||||
return "{% endcotton:slot %}"
|
||||
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"{{% c:slot {slot_name} %}}"
|
||||
return f"{{% cotton: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 %}"
|
||||
return "{% endcotton %}"
|
||||
processed_attrs, extracted_attrs = self._process_attributes()
|
||||
opening_tag = f"{{% c {component_name}{processed_attrs} %}}"
|
||||
opening_tag = f"{{% cotton {component_name}{processed_attrs} %}}"
|
||||
if self.is_self_closing:
|
||||
return f"{opening_tag}{extracted_attrs}{{% endc %}}"
|
||||
return f"{opening_tag}{extracted_attrs}{{% endcotton %}}"
|
||||
return f"{opening_tag}{extracted_attrs}"
|
||||
|
||||
def _process_attributes(self) -> Tuple[str, str]:
|
||||
|
|
@ -64,7 +64,8 @@ class Tag:
|
|||
# With nested tag support, all attributes can be passed directly
|
||||
processed_attrs.append(f'{key}={quote_char}{actual_value}{quote_char}')
|
||||
|
||||
return " " + " ".join(processed_attrs), "".join(extracted_attrs)
|
||||
attrs_str = " ".join(processed_attrs)
|
||||
return " " + attrs_str if attrs_str else "", "".join(extracted_attrs)
|
||||
|
||||
|
||||
class CottonCompiler:
|
||||
|
|
@ -137,8 +138,8 @@ class CottonCompiler:
|
|||
match = matches[0] if matches else None
|
||||
if match:
|
||||
attrs = match.group(1)
|
||||
# Create standalone c:vars tag (no wrapping)
|
||||
vars_content = f"{{% c:vars {attrs.strip()} %}}"
|
||||
# Create standalone cotton:vars tag (no wrapping)
|
||||
vars_content = f"{{% cotton:vars {attrs.strip()} %}}"
|
||||
html = self.c_vars_pattern.sub("", html) # Remove c-vars tags from html
|
||||
return vars_content, html
|
||||
|
||||
|
|
@ -152,6 +153,6 @@ class CottonCompiler:
|
|||
for original, replacement in replacements:
|
||||
processed_html = processed_html.replace(original, replacement)
|
||||
if vars_content:
|
||||
# Insert standalone c:vars tag at the top (no wrapping)
|
||||
# Insert standalone cotton:vars tag at the top (no wrapping)
|
||||
processed_html = f"{vars_content}{processed_html}"
|
||||
return self.restore_ignorables(processed_html, ignorables)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Enable nested tag support for Django's template lexer to handle Cotton tags specially.
|
||||
|
||||
This allows template tags inside quoted attributes of {% c %}, {% c:vars %}, and {% c:slot %} tags to work properly.
|
||||
This allows template tags inside quoted attributes of {% cotton %}, {% cotton:vars %}, and {% cotton:slot %} tags to work properly.
|
||||
|
||||
For example:
|
||||
<c-my-component label="{% trans 'Loading' %}" />
|
||||
|
|
@ -11,7 +11,7 @@ from django.template import base as template_base
|
|||
|
||||
|
||||
def _create_smart_tokenize(original_lexer_tokenize, original_debug_lexer_tokenize):
|
||||
"""Create a smart tokenizer that handles {% c %} and {% vars %} tags specially."""
|
||||
"""Create a smart tokenizer that handles {% cotton %} and {% cotton:vars %} tags specially."""
|
||||
|
||||
# Store originals at creation time
|
||||
# We check isinstance(self, DebugLexer) to respect each engine's debug setting
|
||||
|
|
@ -20,7 +20,7 @@ def _create_smart_tokenize(original_lexer_tokenize, original_debug_lexer_tokeniz
|
|||
|
||||
def smart_tokenize(self):
|
||||
"""
|
||||
Enhanced tokenizer that treats {% c %} and {% vars %} tags specially.
|
||||
Enhanced tokenizer that treats {% cotton %} and {% cotton:vars %} tags specially.
|
||||
|
||||
For Cotton tags, we parse the entire tag as one unit,
|
||||
preserving template tags inside quoted attribute values.
|
||||
|
|
@ -29,11 +29,11 @@ def _create_smart_tokenize(original_lexer_tokenize, original_debug_lexer_tokeniz
|
|||
template_string = self.template_string
|
||||
|
||||
# Check if there are any Cotton tags at all
|
||||
has_c_tags = "{% c " in template_string or "{%c " in template_string
|
||||
has_c_vars_tags = "{% c:vars " in template_string or "{%c:vars " in template_string
|
||||
has_c_slot_tags = "{% c:slot " in template_string or "{%c:slot " in template_string
|
||||
has_cotton_tags = "{% cotton " in template_string or "{%cotton " in template_string
|
||||
has_cotton_vars_tags = "{% cotton:vars " in template_string or "{%cotton:vars " in template_string
|
||||
has_cotton_slot_tags = "{% cotton:slot " in template_string or "{%cotton:slot " in template_string
|
||||
|
||||
if not has_c_tags and not has_c_vars_tags and not has_c_slot_tags:
|
||||
if not has_cotton_tags and not has_cotton_vars_tags and not has_cotton_slot_tags:
|
||||
# No Cotton tags - use Django's original tokenizer
|
||||
# Use DebugLexer tokenize if this is a DebugLexer instance (respects engine.debug)
|
||||
if isinstance(self, template_base.DebugLexer):
|
||||
|
|
@ -46,16 +46,16 @@ def _create_smart_tokenize(original_lexer_tokenize, original_debug_lexer_tokeniz
|
|||
position = 0
|
||||
|
||||
while position < len(template_string):
|
||||
# Look for the next Cotton tag ({% c %}, {% c:vars %}, or {% c:slot %})
|
||||
next_c = template_string.find("{% c ", position)
|
||||
next_c_no_space = template_string.find("{%c ", position)
|
||||
next_c_vars = template_string.find("{% c:vars ", position)
|
||||
next_c_vars_no_space = template_string.find("{%c:vars ", position)
|
||||
next_c_slot = template_string.find("{% c:slot ", position)
|
||||
next_c_slot_no_space = template_string.find("{%c:slot ", position)
|
||||
# Look for the next Cotton tag ({% cotton %}, {% cotton:vars %}, or {% cotton:slot %})
|
||||
next_cotton_tag = template_string.find("{% cotton ", position)
|
||||
next_cotton_no_space = template_string.find("{%cotton ", position)
|
||||
next_cotton_vars = template_string.find("{% cotton:vars ", position)
|
||||
next_cotton_vars_no_space = template_string.find("{%cotton:vars ", position)
|
||||
next_cotton_slot = template_string.find("{% cotton:slot ", position)
|
||||
next_cotton_slot_no_space = template_string.find("{%cotton:slot ", position)
|
||||
|
||||
# Find the earliest Cotton tag
|
||||
candidates = [next_c, next_c_no_space, next_c_vars, next_c_vars_no_space, next_c_slot, next_c_slot_no_space]
|
||||
candidates = [next_cotton_tag, next_cotton_no_space, next_cotton_vars, next_cotton_vars_no_space, next_cotton_slot, next_cotton_slot_no_space]
|
||||
valid_candidates = [c for c in candidates if c != -1]
|
||||
|
||||
if not valid_candidates:
|
||||
|
|
@ -99,17 +99,17 @@ def _create_smart_tokenize(original_lexer_tokenize, original_debug_lexer_tokeniz
|
|||
tag_start = next_cotton
|
||||
position = next_cotton + 2 # Skip {%
|
||||
|
||||
# Skip whitespace and tag name ('c', 'c:vars', or 'c:slot')
|
||||
# Skip whitespace and tag name ('cotton', 'cotton:vars', or 'cotton:slot')
|
||||
while position < len(template_string) and template_string[position] in " \t\n":
|
||||
position += 1
|
||||
|
||||
# Skip tag name
|
||||
if template_string[position : position + 6] == "c:vars":
|
||||
if template_string[position : position + 11] == "cotton:vars":
|
||||
position += 11
|
||||
elif template_string[position : position + 11] == "cotton:slot":
|
||||
position += 11
|
||||
elif template_string[position : position + 6] == "cotton":
|
||||
position += 6
|
||||
elif template_string[position : position + 6] == "c:slot":
|
||||
position += 6
|
||||
elif template_string[position] == "c":
|
||||
position += 1
|
||||
|
||||
# Now find the end %}, respecting quotes
|
||||
in_quotes = False
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Parser for Cotton's native Django template tag syntax.
|
||||
|
||||
This module provides proper parsing of {% c %} and {% vars %} tags that handle quoted strings
|
||||
This module provides proper parsing of {% cotton %} and {% cotton:vars %} tags that handle quoted strings
|
||||
as atomic units, allowing template tags and variables within attribute values.
|
||||
|
||||
Based on django-components' approach to handle complex attribute values.
|
||||
|
|
@ -13,39 +13,45 @@ from django.template.exceptions import TemplateSyntaxError
|
|||
def parse_component_tag(tag_content: str) -> Tuple[str, Dict[str, Any], bool]:
|
||||
"""
|
||||
Parse a Cotton component tag like:
|
||||
{% c component-name attr="value" :dynamic="expr" %}
|
||||
|
||||
{% cotton component-name attr="value" :dynamic="expr" %}
|
||||
|
||||
Properly handles:
|
||||
- Quoted strings with spaces
|
||||
- Template tags/variables inside quotes (preserves them as-is)
|
||||
- Dynamic attributes (prefixed with :)
|
||||
- Boolean attributes
|
||||
- The 'only' flag
|
||||
|
||||
- Self-closing syntax: {% cotton name /%} or {% cotton name / %}
|
||||
|
||||
This parser works character-by-character and treats quoted strings as atomic units,
|
||||
preserving any template tags inside them for later evaluation.
|
||||
|
||||
|
||||
Args:
|
||||
tag_content: The full content of the tag including 'c' (e.g., "c component-name attr='value'")
|
||||
|
||||
tag_content: The full content of the tag including 'cotton' (e.g., "cotton component-name attr='value'")
|
||||
|
||||
Returns:
|
||||
Tuple of (component_name, attrs_dict, only_flag)
|
||||
"""
|
||||
# Skip the tag name ('c') and whitespace
|
||||
# Skip the tag name ('cotton') and whitespace
|
||||
index = 0
|
||||
|
||||
# Skip 'c' and any following whitespace
|
||||
if tag_content.startswith('c '):
|
||||
index = 2
|
||||
elif tag_content.startswith('c\t') or tag_content.startswith('c\n'):
|
||||
index = 2
|
||||
|
||||
# Remove trailing self-closing syntax if present
|
||||
tag_content = tag_content.rstrip()
|
||||
if tag_content.endswith('/'):
|
||||
tag_content = tag_content[:-1].rstrip()
|
||||
|
||||
# Skip 'cotton' and any following whitespace
|
||||
if tag_content.startswith('cotton '):
|
||||
index = 7
|
||||
elif tag_content.startswith('cotton\t') or tag_content.startswith('cotton\n'):
|
||||
index = 7
|
||||
else:
|
||||
# Maybe just 'c' without space?
|
||||
if tag_content == 'c':
|
||||
# Maybe just 'cotton' without space?
|
||||
if tag_content == 'cotton':
|
||||
raise TemplateSyntaxError("Component tag must have a name")
|
||||
index = 1
|
||||
|
||||
# Skip whitespace after 'c'
|
||||
index = 6
|
||||
|
||||
# Skip whitespace after 'cotton'
|
||||
while index < len(tag_content) and tag_content[index] in ' \t\n':
|
||||
index += 1
|
||||
|
||||
|
|
@ -150,7 +156,7 @@ def parse_component_tag(tag_content: str) -> Tuple[str, Dict[str, Any], bool]:
|
|||
def parse_vars_tag(tag_content: str) -> Tuple[Dict[str, Any], List[str]]:
|
||||
"""
|
||||
Parse a Cotton vars tag like:
|
||||
{% vars attr="value" :dynamic="expr" empty_attr %}
|
||||
{% cotton:vars attr="value" :dynamic="expr" empty_attr %}
|
||||
|
||||
Properly handles:
|
||||
- Quoted strings with spaces
|
||||
|
|
@ -162,26 +168,26 @@ def parse_vars_tag(tag_content: str) -> Tuple[Dict[str, Any], List[str]]:
|
|||
preserving any template tags inside them for later evaluation.
|
||||
|
||||
Args:
|
||||
tag_content: The full content of the tag including 'vars' (e.g., "vars attr='value'")
|
||||
tag_content: The full content of the tag including 'cotton:vars' (e.g., "cotton:vars attr='value'")
|
||||
|
||||
Returns:
|
||||
Tuple of (attrs_dict, empty_attrs_list)
|
||||
"""
|
||||
# Skip the tag name ('vars') and whitespace
|
||||
# Skip the tag name ('cotton:vars') and whitespace
|
||||
index = 0
|
||||
|
||||
# Skip 'vars' and any following whitespace
|
||||
if tag_content.startswith('vars '):
|
||||
index = 5
|
||||
elif tag_content.startswith('vars\t') or tag_content.startswith('vars\n'):
|
||||
index = 5
|
||||
# Skip 'cotton:vars' and any following whitespace
|
||||
if tag_content.startswith('cotton:vars '):
|
||||
index = 12
|
||||
elif tag_content.startswith('cotton:vars\t') or tag_content.startswith('cotton:vars\n'):
|
||||
index = 12
|
||||
else:
|
||||
# Maybe just 'vars' without space?
|
||||
if tag_content == 'vars':
|
||||
# Maybe just 'cotton:vars' without space?
|
||||
if tag_content == 'cotton:vars':
|
||||
return {}, []
|
||||
index = 4
|
||||
index = 11
|
||||
|
||||
# Skip whitespace after 'vars'
|
||||
# Skip whitespace after 'cotton:vars'
|
||||
while index < len(tag_content) and tag_content[index] in ' \t\n':
|
||||
index += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -202,8 +202,13 @@ def cotton_component(parser, token):
|
|||
Parse a cotton component tag and return a CottonComponentNode.
|
||||
|
||||
Uses custom parser to preserve quotes and handle template tags in attributes.
|
||||
Supports self-closing syntax: {% cotton name /%} or {% cotton name / %}
|
||||
"""
|
||||
from django_cotton.tag_parser import parse_component_tag
|
||||
from django.template import NodeList
|
||||
|
||||
# Check if this is a self-closing tag
|
||||
is_self_closing = token.contents.rstrip().endswith('/') or token.contents.rstrip().endswith(' /')
|
||||
|
||||
# Use the custom parser that preserves quotes and handles nested template tags
|
||||
component_name, attrs, only = parse_component_tag(token.contents)
|
||||
|
|
@ -211,7 +216,11 @@ def cotton_component(parser, token):
|
|||
# Capture which template libraries were loaded at parse time
|
||||
loaded_libraries = list(parser.libraries.keys()) if hasattr(parser, 'libraries') else []
|
||||
|
||||
nodelist = parser.parse(("endc",))
|
||||
parser.delete_first_token()
|
||||
if is_self_closing:
|
||||
# Self-closing tag has no content
|
||||
nodelist = NodeList()
|
||||
else:
|
||||
nodelist = parser.parse(("endcotton",))
|
||||
parser.delete_first_token()
|
||||
|
||||
return CottonComponentNode(component_name, nodelist, attrs, only, loaded_libraries)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def cotton_slot(parser, token):
|
|||
if len(bits) < 1:
|
||||
raise TemplateSyntaxError("cotton slot tag must include a 'name'")
|
||||
|
||||
nodelist = parser.parse(("endc:slot",))
|
||||
nodelist = parser.parse(("endcotton:slot",))
|
||||
parser.delete_first_token()
|
||||
return CottonSlotNode(bits[0], nodelist)
|
||||
|
||||
|
|
|
|||
|
|
@ -91,10 +91,10 @@ class CottonVarsNode(Node):
|
|||
|
||||
def cotton_cvars(parser, token):
|
||||
"""
|
||||
Parse standalone c-vars template tag using a custom character-by-character parser.
|
||||
Parse standalone cotton:vars template tag using a custom character-by-character parser.
|
||||
|
||||
This allows template tags like {% trans %} to work in c-vars defaults:
|
||||
{% c:vars label="{% trans 'Loading' %}" %}
|
||||
This allows template tags like {% trans %} to work in cotton:vars defaults:
|
||||
{% cotton:vars label="{% trans 'Loading' %}" %}
|
||||
|
||||
The custom parser treats quoted strings as atomic units, preserving
|
||||
template tags inside them for Django to evaluate naturally at render time.
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ from django_cotton.templatetags._vars import cotton_cvars
|
|||
from django_cotton.templatetags._slot import cotton_slot
|
||||
|
||||
register = template.Library()
|
||||
register.tag("c", cotton_component)
|
||||
register.tag("c:slot", cotton_slot)
|
||||
register.tag("c:vars", cotton_cvars)
|
||||
register.tag("cotton", cotton_component)
|
||||
register.tag("cotton:slot", cotton_slot)
|
||||
register.tag("cotton:vars", cotton_cvars)
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class SlotTests(CottonTestCase):
|
|||
|
||||
self.assertEqual(
|
||||
compiled,
|
||||
"""{% c:vars var1="string with space" %}
|
||||
"""{% cotton:vars var1="string with space" %}
|
||||
content
|
||||
""",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -362,20 +362,6 @@ context = {
|
|||
|
||||
<h2>9. Context Isolation</h2>
|
||||
|
||||
{#
|
||||
<p>Cotton follows the component isolation approach used in React, Vue, and Svelte, where components don't share state by default. This prevents data leaks and hard-to-trace side effects between components.</p>
|
||||
<p>By default, each component's context only contains:</p>
|
||||
|
||||
<c-ul>
|
||||
<li>Attributes directly declared on the component</li>
|
||||
<li>Any data set using <code>{{ "<c-vars />"|force_escape }}</code></li>
|
||||
<li>All data from enabled context processors (from custom or built-in processors which typically provide: <code>user</code>, <code>request</code>, <code>messages</code>, <code>perms</code> etc.)</li>
|
||||
</c-ul>
|
||||
|
||||
|
||||
<h3>Further isolation using 'only'</h3>
|
||||
#}
|
||||
|
||||
<p>You can pass the <c-highlight>only</c-highlight> attribute to the component, which will prevent it from adopting any context other than its direct attributes.</p>
|
||||
|
||||
<c-hr id="alpine-js-support" />
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_DIR</code>
|
||||
<div>str (default: 'cotton')</div>
|
||||
<code class="!text-teal-600">COTTON_DIR</code>
|
||||
<div class="text-sm">str (default: 'cotton')</div>
|
||||
</div>
|
||||
<div>
|
||||
Change the default path in your templates directory where cotton components can be placed, for example "components".
|
||||
|
|
@ -17,12 +17,12 @@
|
|||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_BASE_DIR</code>
|
||||
<div>str (default: None)</div>
|
||||
<code class="!text-teal-600">COTTON_BASE_DIR</code>
|
||||
<div class="text-sm">str (default: None)</div>
|
||||
</div>
|
||||
<div>
|
||||
The base directory where - in addition to the app folders - cotton will search for the "templates" directory (see above).
|
||||
If not set, the `BASE_DIR` generated by `django-admin startproject` is used as a fallback, if it exists.
|
||||
If not set, the <code class="!text-teal-600">BASE_DIR</code> generated by `django-admin startproject` is used as a fallback, if it exists.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -30,46 +30,28 @@
|
|||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_SNAKE_CASED_NAMES</code>
|
||||
<div>bool (default: True)</div>
|
||||
<code class="!text-teal-600">COTTON_SNAKE_CASED_NAMES</code>
|
||||
<div class="text-sm">bool (default: True)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">By default cotton will look for snake case versions of your component names. To turn this behaviour off (useful if you want to permit hyphenated filenames) then set this key to <code>False</code>.</div>
|
||||
<div class="mb-4">By default cotton will look for snake case versions of your component names. To turn this behaviour off (useful if you want to permit hyphenated filenames) then set this key to <code class="!text-teal-600">False</code>.</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="mb-1">Example:</h6>
|
||||
<code>{{ '<c-my-button />'|force_escape }}</code>
|
||||
<code class="!text-teal-600">{{ '<c-my-button />'|force_escape }}</code>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6>As <code>True</code> (default)</h6>
|
||||
<h6><code class="!text-teal-600">True</code> (default)</h6>
|
||||
Filepath: `cotton/my_button.html`
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>As <code>False</code></h6>
|
||||
<h6><code class="!text-teal-600">False</code></h6>
|
||||
Filepath: `cotton/my-button.html`
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#
|
||||
<c-hr />
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<code>COTTON_ENABLE_CONTEXT_ISOLATION</code>
|
||||
<div>bool (default: True)</div>
|
||||
</div>
|
||||
<div>
|
||||
Limits component context to only direct attributes and context processor data.
|
||||
When set to <code>False</code>, all components can access the entire view's context.
|
||||
See <a href="{% url 'components' %}#context-isolation">context isolation</a> for more details.
|
||||
</div>
|
||||
</div>
|
||||
#}
|
||||
|
||||
|
||||
|
||||
</c-layouts.with-sidebar>
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||
|
Before Width: | Height: | Size: 665 B After Width: | Height: | Size: 368 B |
|
|
@ -1,3 +1 @@
|
|||
{#<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" viewBox="0 0 512 512" ><path d="M210.822 0h90.353c20.792 0 37.647 16.855 37.647 37.647v15.059H173.175V37.647C173.175 16.855 190.03 0 210.822 0zm128 459.294v15.059c0 20.792-16.855 37.647-37.647 37.647h-90.353c-20.792 0-37.647-16.855-37.647-37.647v-15.059zm-76.213-384h66.683L85.046 230.843l4.086-44.943zm-56.021 0-114.009 72.69 1.628-17.908c2.82-31.026 28.834-54.782 59.988-54.782zm172.617 3.919c20.955 7.971 36.444 27.324 38.584 50.863l2.75 30.256-89.836-50.23zm44.64 117.473 3.856 42.418-158.04-90.126 32.192-20.501zm7.18 78.985 4.08 44.875-224.296-134.089 30.13-19.188zm7.453 81.981 1.215 13.365c.817 8.985-.384 17.689-3.229 25.663l-166.972-99.529 32.512-21.086zm-18.935 64.004c-9.267 8.184-21.128 13.597-34.385 14.803a60.177 60.177 0 0 1-5.454.247h-3.078l-169.164-99.324 33.858-21.96zm-102.399 15.05H170.496l69.453-45.325zM118.001 435c-12.405-3.018-23.307-9.878-31.375-19.247l92.574-60.041 31.74 18.636zm-44.739-46.477a60.502 60.502 0 0 1-.959-17.507l2.046-22.502 167.945-108.146 30.909 18.478zm4.546-78.057 3.793-41.721 100.789-64.188 31.057 18.567z"/></svg>#}
|
||||
|
||||
<svg {{ attrs }} xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" viewBox="0 0 512 512"><path d="M173.174 451.765h-32.419c-41.584 0-75.294-33.71-75.294-75.294 0-2.087.087-4.173.26-6.253L85.8 129.277c3.252-39.024 35.874-69.041 75.034-69.041h12.34V37.647C173.174 16.855 190.03 0 210.821 0h90.353c20.792 0 37.647 16.855 37.647 37.647v22.588h12.34c39.16 0 71.782 30.017 75.034 69.041l20.079 240.941c3.454 41.44-27.341 77.834-68.781 81.287a75.42 75.42 0 0 1-6.253.26h-32.419v22.588c0 20.792-16.855 37.647-37.647 37.647h-90.353c-20.792 0-37.647-16.855-37.647-37.647zm135.53-414.118a7.529 7.529 0 0 0-7.529-7.529h-90.353a7.529 7.529 0 0 0-7.529 7.529v22.588h105.412V37.647zm28.012 384h34.524c1.252 0 2.504-.052 3.752-.156a44.985 44.985 0 0 0 19.302-6.157L241.087 322.85l-34.025 21.932 129.142 76.547c.174.103.344.209.512.318zm-59.043 0-38.055-22.556-34.993 22.556zm-67.014-39.721-31.893-18.904-70.137 45.21c8.189 8.283 19.559 13.415 32.127 13.415h8.28zm203.06 9.955a44.995 44.995 0 0 0 2.541-19.162l-1.704-20.447-113.15-68.303-32.137 20.715zm-2.249-76.653-3.784-45.404c-.107-.057-.213-.117-.319-.177L239.808 174.15l-30.188 19.23zM272.701 266.64l-30.748-18.561-142.556 90.809c-.291.186-.587.36-.886.523l-2.775 33.308a45.892 45.892 0 0 0 .038 7.967zm-59.531-35.936-31.906-19.261-76.131 48.496-3.483 41.804zm191.484 2.732-3.546-42.543-100.392-55.54-32.186 20.503zm-6.553-78.627-1.919-23.032c-1.492-17.905-13.289-32.649-29.264-38.597l-37.284 23.75zm-82.802-64.456h-54.905l-149.056 95.131-3.062 36.745zm-110.887 0h-43.578c-23.496 0-43.069 18.01-45.021 41.425l-1.331 15.97zm-1.12 384a7.529 7.529 0 0 0 7.529 7.529h90.353a7.529 7.529 0 0 0 7.529-7.529v-22.588H203.292z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
|
@ -20,7 +20,8 @@
|
|||
|
||||
<script>
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
// Default to dark mode if no preference is set
|
||||
if (localStorage.theme === 'dark' || !('theme' in localStorage)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
|
|
|
|||
|
|
@ -41,29 +41,15 @@
|
|||
|
||||
<a href="/" class="flex item-center items-center space-x-1.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<c-icons.logo class="w-8 h-8 text-teal-600" />
|
||||
<c-icons.logo class="size-8 text-teal-600" />
|
||||
<span class="font-bold text-2xl ml-1.5 text-[#1a384a] dark:text-gray-100">cotton</span>
|
||||
<div class=" text-gray-700 dark:text-white text-sm opacity-50 mt-0.5 px-1">for</div>
|
||||
<c-icons.django class=" h-6 text-teal-600 mt-1.5" />
|
||||
<c-icons.django class=" h-[1.4rem] text-teal-600 mt-1.5" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Syntax Preference Toggle -->
|
||||
<button
|
||||
@click="$store.syntaxPreference = $store.syntaxPreference === 'html' ? 'template' : 'html'"
|
||||
class="hidden sm:flex items-center space-x-2 px-3 py-1.5 rounded-lg border-2 border-teal-600/30 dark:border-teal-600/50 hover:border-teal-600/60 dark:hover:border-teal-600/70 transition-colors"
|
||||
:title="$store.syntaxPreference === 'html' ? 'Switch to Template Tag syntax' : 'Switch to HTML syntax'"
|
||||
x-data="{
|
||||
get buttonText() {
|
||||
return this.$store.syntaxPreference === 'html' ? '<c->' : '{'+String.fromCharCode(37)+' c '+String.fromCharCode(37)+'}';
|
||||
}
|
||||
}"
|
||||
>
|
||||
<span class="text-xs font-semibold text-gray-700 dark:text-gray-200" x-text="buttonText"></span>
|
||||
</button>
|
||||
|
||||
<a href="#" @click.prevent="toggleDark" x-data="{
|
||||
'dark': false,
|
||||
init() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,41 @@
|
|||
<div class="sticky top-[68px] pb-6 pt-2">
|
||||
<!-- Syntax Preference Switcher -->
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-4">syntax</div>
|
||||
<div class="mb-3 pb-3 border-bborder-gray-200 dark:border-gray-700" x-data>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
|
||||
<div class="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5 gap-0.5">
|
||||
<!-- Buttons with individual backgrounds -->
|
||||
<button
|
||||
@click="$store.syntaxPreference = 'html'"
|
||||
class="px-2.5 py-1 text-sm transition-all duration-200 rounded-md min-w-[2rem]"
|
||||
:class="$store.syntaxPreference === 'html'
|
||||
? 'bg-teal-600/20 text-teal-600 font-semibold'
|
||||
: 'text-gray-700 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'"
|
||||
>
|
||||
<c-
|
||||
</button>
|
||||
<button
|
||||
@click="$store.syntaxPreference = 'template'"
|
||||
class="px-2.5 py-1 text-sm transition-all duration-200 rounded-md min-w-[2rem]"
|
||||
:class="$store.syntaxPreference === 'template'
|
||||
? 'bg-teal-600/20 text-teal-600 font-semibold'
|
||||
: 'text-gray-700 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'"
|
||||
x-text="'{' + String.fromCharCode(37) + ' cotton'"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-500">
|
||||
<div x-show="$store.syntaxPreference === 'html'">
|
||||
Snippets will be shown in cotton's HTML-like syntax.
|
||||
</div>
|
||||
<div x-show="$store.syntaxPreference === 'template'">
|
||||
Snippets will be shown in Django template tag syntax.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<c-sidebar-block>
|
||||
<c-slot name="title">Getting Started</c-slot>
|
||||
<ul>
|
||||
|
|
@ -11,6 +48,9 @@
|
|||
<c-sidebar-link url="{% url 'usage-patterns' %}">
|
||||
Usage Patterns
|
||||
</c-sidebar-link>
|
||||
<c-sidebar-link url="{% url 'configuration' %}">
|
||||
Configuration
|
||||
</c-sidebar-link>
|
||||
</ul>
|
||||
</c-sidebar-block>
|
||||
<c-sidebar-block>
|
||||
|
|
@ -33,9 +73,6 @@
|
|||
<c-sidebar-block>
|
||||
<c-slot name="title">More</c-slot>
|
||||
<ul>
|
||||
<c-sidebar-link url="{% url 'configuration' %}">
|
||||
Configuration
|
||||
</c-sidebar-link>
|
||||
<c-sidebar-link url="{% url 'django-template-partials' %}">
|
||||
Django Template Partials
|
||||
</c-sidebar-link>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<div class="dark:text-white dark:opacity-35 text-yellow-900/40 font-semibold uppercase text-[14px] mb-2 tracking-wider">{{ slot }}</div>
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-500 mb-3">{{ slot }}</div>
|
||||
|
|
@ -3,10 +3,16 @@
|
|||
<c-vars language="xml" rounded="rounded-xl" />
|
||||
|
||||
<div class="group shadow relative flex-1 flex flex-col justify-start w-full {{ rounded }} overflow-hidden mb-3"
|
||||
x-data="{
|
||||
'code': '',
|
||||
x-data="{% verbatim %}{
|
||||
get currentCode() {
|
||||
return this.$root.querySelector('[x-show=\"$store.syntaxPreference === \\\'html\\\'\"] code, [x-show=\"$store.syntaxPreference === \\\'template\\\'\"] code:not([style*=\\\'display: none\\\'])').innerText;
|
||||
const htmlCode = $refs.htmlCode;
|
||||
const templateCode = $refs.templateCode;
|
||||
if ($store.syntaxPreference === 'html' && htmlCode) {
|
||||
return htmlCode.innerText;
|
||||
} else if (templateCode) {
|
||||
return templateCode.innerText;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
copy() {
|
||||
const text = this.currentCode;
|
||||
|
|
@ -28,29 +34,60 @@
|
|||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}"
|
||||
x-init="
|
||||
}{% endverbatim %}"
|
||||
x-init="{% verbatim %}
|
||||
$watch('$store.syntaxPreference', () => {
|
||||
setTimeout(() => hljs.highlightAll(), 50);
|
||||
});
|
||||
"
|
||||
{% endverbatim %}"
|
||||
>
|
||||
{% if label %}<div class="absolute left-0 top-0 text-left w-full px-5 pt-3 text-sm text-gray-400 text-opacity-80 font-semibold">{{ label }}</div>{% endif %}
|
||||
{% if label %}<div class="absolute left-0 top-0 text-left w-full px-5 pt-3 text-sm text-gray-500 font-semibold">{{ label }}</div>{% endif %}
|
||||
|
||||
<!-- Syntax indicator badge -->
|
||||
<div class="absolute left-0 bottom-0 px-3 py-1 text-xs text-gray-500 font-medium uppercase tracking-wide" x-text="$store.syntaxPreference === 'html' ? 'HTML Syntax' : 'Template Tags'"></div>
|
||||
|
||||
<button @click.prevent="copy" class="opacity-0 transition-opacity transition-duration-500ms group-hover:opacity-100 absolute right-0 top-0 pt-3 pr-5"><c-icons.copy class="w-6 h-6 text-gray-500" /></button>
|
||||
|
||||
<!-- HTML Syntax Version -->
|
||||
<div x-show="$store.syntaxPreference === 'html'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
|
||||
<pre {{ attrs.dict|merge:'class: flex-1 flex flex-col !leading-7 !p-0 !m-0 rounded-none !text-[14.5px]' }}><code x-ref="htmlCode" class="language-{{ language }} flex-1 !pb-4 {% if label %} !pt-11 {% else %} !pt-4 {% endif %} !px-6 !m-0 !bg-gray-800 dark:!bg-gray-800">{{ slot|strip }}</code></pre>
|
||||
<!-- Top-right controls: syntax switcher + copy button -->
|
||||
<div class="absolute right-0 top-0 pt-3 pr-5 flex items-center gap-3">
|
||||
{% if "<c-" in slot %}
|
||||
<div class="inline-flex rounded-lg p-0.5 gap-0.5">
|
||||
<!-- Buttons with individual backgrounds -->
|
||||
<button
|
||||
@click="$store.syntaxPreference = 'html'"
|
||||
class="px-2.5 py-1 text-[12px] transition-all duration-200 rounded-md min-w-[2rem]"
|
||||
:class="$store.syntaxPreference === 'html'
|
||||
? 'bg-teal-600/20 text-teal-600 font-semibold'
|
||||
: 'text-gray-400 hover:text-gray-300'"
|
||||
>
|
||||
<c-
|
||||
</button>
|
||||
<button
|
||||
@click="$store.syntaxPreference = 'template'"
|
||||
class="px-2.5 py-1 text-[12px] transition-all duration-200 rounded-md min-w-[2rem]"
|
||||
:class="$store.syntaxPreference === 'template'
|
||||
? 'bg-teal-600/20 text-teal-600 font-semibold'
|
||||
: 'text-gray-400 hover:text-gray-300'"
|
||||
x-text="'{' + String.fromCharCode(37) + ' cotton'"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Copy button - always visible -->
|
||||
<button @click.prevent="copy" class="transition-opacity text-gray-500 duration-200 hover:text-gray-400">
|
||||
<c-icons.copy class="size-[1.1rem]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Template Tag Syntax Version -->
|
||||
<div x-show="$store.syntaxPreference === 'template'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
|
||||
<pre {{ attrs.dict|merge:'class: flex-1 flex flex-col !leading-7 !p-0 !m-0 rounded-none !text-[14.5px]' }}><code x-ref="templateCode" class="language-django flex-1 !pb-4 {% if label %} !pt-11 {% else %} !pt-4 {% endif %} !px-6 !m-0 !bg-gray-800 dark:!bg-gray-800">{{ slot|strip|to_template_tags }}</code></pre>
|
||||
</div>
|
||||
{% if "<c-" in slot %}
|
||||
<!-- HTML Syntax Version -->
|
||||
<div x-show="$store.syntaxPreference === 'html'">
|
||||
<pre {{ attrs.dict|merge:'class: flex-1 flex flex-col !leading-7 !p-0 !m-0 rounded-none !text-[14.5px]' }}><code x-ref="htmlCode" class="language-{{ language }} flex-1 !pb-4 {% if label %} !pt-14 {% else %} !pt-4 {% endif %} !px-6 !m-0 !bg-gray-800 dark:!bg-gray-800">{{ slot|strip }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Template Tag Syntax Version -->
|
||||
<div x-show="$store.syntaxPreference === 'template'">
|
||||
<pre {{ attrs.dict|merge:'class: flex-1 flex flex-col !leading-7 !p-0 !m-0 rounded-none !text-[14.5px]' }}><code x-ref="templateCode" class="language-{{ language }} flex-1 !pb-4 {% if label %} !pt-14 {% else %} !pt-4 {% endif %} !px-6 !m-0 !bg-gray-800 dark:!bg-gray-800">{{ slot|strip|to_template_tags }}</code></pre>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- No Cotton tags - show single version -->
|
||||
<pre {{ attrs.dict|merge:'class: flex-1 flex flex-col !leading-7 !p-0 !m-0 rounded-none !text-[14.5px]' }}><code x-ref="htmlCode" class="language-{{ language }} flex-1 !pb-4 {% if label %} !pt-14 {% else %} !pt-4 {% endif %} !px-6 !m-0 !bg-gray-800 dark:!bg-gray-800">{{ slot|strip }}</code></pre>
|
||||
{% endif %}
|
||||
|
||||
{% if preview %}
|
||||
<div class="px-6 py-6 prose-headings:text-gray-800 shadow bg-white dark:bg-gray-50 rounded-b-xl dark:!text-gray-800">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<div class="w-full shrink-0"></div>
|
||||
<span class="pt-0 md:pt-5 clear-both flex-wrap inline-flex justify-center items-center space-x-2">
|
||||
<span class="">Hello</span>
|
||||
<span class="shrink-0 px-3 py-1 bg-teal-500/10 border-[3px] border-teal-600 font-mono text-xl sm:text-2xl md:text-4xl rounded-3xl text-teal-600 dark:text-white inline-block font-normal leading-normal">{{ '<c-comp />'|force_escape }}</span>
|
||||
<span class="shrink-0 px-3 py-1 bg-teal-500/10 border-[3px] border-teal-600 font-mono text-xl sm:text-2xl md:text-4xl rounded-3xl text-teal-600 dark:text-white inline-block font-normal leading-normal">{{ '<c-cotton>'|force_escape }}</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def compile_to_template_tags(html_code):
|
|||
html_code: HTML-style Cotton code (e.g., '<c-button>Click</c-button>')
|
||||
|
||||
Returns:
|
||||
Formatted template tag syntax (e.g., '{% c button %}Click{% endc %}')
|
||||
Formatted template tag syntax (e.g., '{% cotton button %}Click{% endcotton %}')
|
||||
"""
|
||||
try:
|
||||
# Check if there are any Cotton tags to convert
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue