django-cotton-bs5 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cotton_bs5/fixtures.py ADDED
@@ -0,0 +1,217 @@
1
+ """Pytest fixtures for testing Django Cotton BS5 components."""
2
+
3
+ import pytest
4
+ from bs4 import BeautifulSoup
5
+ from django.template import Context, Template
6
+ from django.test import RequestFactory
7
+ from django_cotton import render_component
8
+ from django_cotton.compiler_regex import CottonCompiler
9
+
10
+ compiler = CottonCompiler()
11
+
12
+ @pytest.fixture
13
+ def cotton_render():
14
+ """
15
+ Fixture that renders Django-Cotton components and returns raw HTML.
16
+
17
+ Automatically provides a request object to be DRY. Component variables
18
+ are passed as kwargs.
19
+
20
+ Usage:
21
+ def test_something(cotton_render):
22
+ html = cotton_render(
23
+ 'cotton_bs5.alert',
24
+ message="Hello",
25
+ type="success"
26
+ )
27
+ assert 'alert-success' in html
28
+ """
29
+ factory = RequestFactory()
30
+
31
+ def _render(component_name, context=None, **kwargs):
32
+ """
33
+ Render a Cotton component with automatic request injection.
34
+
35
+ Args:
36
+ component_name: Component name in dotted notation (e.g., "cotton_bs5.alert")
37
+ context: Optional context dict to pass as component attributes
38
+ **kwargs: Component attributes (alternative to context dict)
39
+
40
+ Returns:
41
+ Rendered HTML string
42
+ """
43
+ request = factory.get("/")
44
+ return render_component(request, component_name, context, **kwargs)
45
+
46
+ return _render
47
+
48
+
49
+ @pytest.fixture
50
+ def cotton_render_soup():
51
+ """
52
+ Fixture that renders Django-Cotton components and returns BeautifulSoup parsed HTML.
53
+
54
+ Automatically provides a request object and parses the result for easy testing.
55
+ Component variables are passed as kwargs.
56
+
57
+ Usage:
58
+ def test_something(cotton_render_soup):
59
+ soup = cotton_render_soup(
60
+ 'cotton_bs5.alert',
61
+ message="Hello",
62
+ type="success"
63
+ )
64
+ assert soup.find('div')['class'] == ['alert', 'alert-success']
65
+ """
66
+ factory = RequestFactory()
67
+
68
+ def _render(component_name, context=None, **kwargs):
69
+ """
70
+ Render a Cotton component with automatic request injection and parse with BeautifulSoup.
71
+
72
+ Args:
73
+ component_name: Component name in dotted notation (e.g., "cotton_bs5.alert")
74
+ context: Optional context dict to pass as component attributes
75
+ **kwargs: Component attributes (alternative to context dict)
76
+
77
+ Returns:
78
+ BeautifulSoup parsed HTML object
79
+ """
80
+ request = factory.get("/")
81
+ html = render_component(request, component_name, context, **kwargs)
82
+ return BeautifulSoup(html, "html.parser")
83
+
84
+ return _render
85
+
86
+
87
+ @pytest.fixture
88
+ def cotton_render_string():
89
+ """
90
+ Fixture that compiles and renders Django template strings containing Cotton component syntax.
91
+
92
+ This fixture takes a raw template string with Cotton components (e.g., <c-button>),
93
+ compiles it through django-cotton's compiler, then renders it through Django's
94
+ Template system. Useful for testing inline component markup without creating
95
+ separate template files.
96
+
97
+ Usage:
98
+ def test_button_in_template(cotton_render_string):
99
+ html = cotton_render_string("<c-button variant='primary'>Click me</c-button>")
100
+ assert 'btn-primary' in html
101
+
102
+ def test_with_context(cotton_render_string):
103
+ html = cotton_render_string(
104
+ "<c-alert>{{ message }}</c-alert>",
105
+ context={'message': 'Hello World'}
106
+ )
107
+ assert 'Hello World' in html
108
+
109
+ Returns:
110
+ A callable function that accepts:
111
+ - template_string (str): Django template string with Cotton component syntax
112
+ - context (dict, optional): Template context variables
113
+
114
+ The function returns the rendered HTML as a string.
115
+ """
116
+ factory = RequestFactory()
117
+
118
+ def _render(template_string, context=None):
119
+ """
120
+ Compile and render a template string with Cotton components.
121
+
122
+ Args:
123
+ template_string: The Django template string containing Cotton component syntax
124
+ context: Optional context dict for template variables
125
+
126
+ Returns:
127
+ Rendered HTML string
128
+ """
129
+ if context is None:
130
+ context = {}
131
+ request = factory.get("/")
132
+ context['request'] = request
133
+
134
+ # Compile Cotton component syntax into Django template syntax
135
+ compiled_template = compiler.process(template_string)
136
+
137
+ # Render through Django's Template system
138
+ django_template = Template(compiled_template)
139
+ django_context = Context(context)
140
+ return django_template.render(django_context)
141
+
142
+ return _render
143
+
144
+
145
+ @pytest.fixture
146
+ def cotton_render_string_soup():
147
+ """
148
+ Fixture that compiles and renders Django template strings with Cotton components,
149
+ returning BeautifulSoup parsed HTML.
150
+
151
+ This fixture combines the capabilities of cotton_render_string with BeautifulSoup
152
+ parsing for easier DOM traversal and assertions. Particularly useful for testing
153
+ multi-component features where you need to verify nested structure and relationships.
154
+
155
+ Usage:
156
+ def test_nested_list(cotton_render_string_soup):
157
+ soup = cotton_render_string_soup(
158
+ "<c-ul><c-li text='first' /><c-li text='second' /></c-ul>"
159
+ )
160
+ items = soup.find_all('li')
161
+ assert len(items) == 2
162
+ assert items[0].get_text() == 'first'
163
+ assert items[1].get_text() == 'second'
164
+
165
+ def test_complex_layout_with_context(cotton_render_string_soup):
166
+ template = '''
167
+ <c-card>
168
+ <c-card.title>{{ title }}</c-card.title>
169
+ <c-card.body>
170
+ <c-button variant='primary'>{{ action }}</c-button>
171
+ </c-card.body>
172
+ </c-card>
173
+ '''
174
+ soup = cotton_render_string_soup(template, context={
175
+ 'title': 'My Card',
176
+ 'action': 'Click Here'
177
+ })
178
+ assert soup.find('h5').get_text() == 'My Card'
179
+ assert 'btn-primary' in soup.find('button')['class']
180
+
181
+ Returns:
182
+ A callable function that accepts:
183
+ - template_string (str): Django template string with Cotton component syntax
184
+ - context (dict, optional): Template context variables
185
+
186
+ The function returns a BeautifulSoup parsed HTML object.
187
+ """
188
+ factory = RequestFactory()
189
+
190
+ def _render(template_string, context=None):
191
+ """
192
+ Compile, render, and parse a template string with Cotton components.
193
+
194
+ Args:
195
+ template_string: The Django template string containing Cotton component syntax
196
+ context: Optional context dict for template variables
197
+
198
+ Returns:
199
+ BeautifulSoup parsed HTML object
200
+ """
201
+ if context is None:
202
+ context = {}
203
+ request = factory.get("/")
204
+ context['request'] = request
205
+
206
+ # Compile Cotton component syntax into Django template syntax
207
+ compiled_template = compiler.process(template_string)
208
+
209
+ # Render through Django's Template system
210
+ django_template = Template(compiled_template)
211
+ django_context = Context(context)
212
+ html = django_template.render(django_context)
213
+
214
+ # Parse with BeautifulSoup for easy DOM traversal
215
+ return BeautifulSoup(html, "html.parser")
216
+
217
+ return _render
@@ -1,4 +1,4 @@
1
- <c-vars flush parent class />
1
+ <c-vars flush parent class id="something" />
2
2
  <div id="{{ id }}"
3
3
  class="accordion{% if flush %} accordion-flush{% endif %} {{ class }}"
4
4
  {{ attrs }}>
@@ -1,8 +1,14 @@
1
- <c-vars id text target parent header class />
1
+ {% load cotton_bs5 %}
2
+ <c-vars id text target header class show />
3
+ {% cotton_parent as parent %}
4
+ {% genid "accordion-item" as uid %}
5
+ {% firstof id uid as target %}
2
6
  <div class="accordion-item {{ class }}" {{ attrs }}>
3
- {% if text %}<c-accordion.header :attrs="attrs" />{% endif %}
7
+ {% if text %}<c-accordion.header target="{{ target }}" :attrs="attrs" />{% endif %}
4
8
  {{ header }}
5
- <c-accordion.body id="{{ target }}" :attrs="attrs">
9
+ <c-accordion.body id="{{ target }}"
10
+ parent="{{ parent.id }}"
11
+ :attrs="attrs">
6
12
  {{ slot }}
7
13
  </c-accordion.body>
8
14
  </div>
@@ -1,5 +1,5 @@
1
1
  {% load i18n %}
2
- <c-vars ol_class divider="/" items class />
2
+ <c-vars ol_class divider="/" items />
3
3
  <nav aria-label="{% trans "breadcrumb" %}"
4
4
  style="--bs-breadcrumb-divider: '{{ divider }}'"
5
5
  {{ attrs }}>
@@ -1,13 +1,10 @@
1
- <c-vars href text class />
2
- <li class="breadcrumb-item{% if not href %} active{% endif %} {{ class }}"
3
- {% if not href %}aria-current="page"{% endif %}>
4
- {% if href %}
5
- <a href="{{ href }}"
6
- class="link-underline link-underline-opacity-0 link-underline-opacity-75-hover"
7
- {{ attrs }}>
8
- {{ text }}{{ slot }}
9
- </a>
10
- {% else %}
11
- {{ text }}{{ slot }}
12
- {% endif %}
1
+ <c-vars text class />
2
+ {% with element=href|yesno:"a,span" %}
3
+ {# djlint:off #}
4
+ <li class="breadcrumb-item{% if element == 'span' %} active{% endif %} {{ class }}"{% if element == 'span' %} aria-current="page"{% endif %}>
5
+ <{{ element }}{% if href %} class="link-underline link-underline-opacity-0 link-underline-opacity-75-hover"{% endif %} {{ attrs }}>
6
+ {{ text }}{{ slot }}
7
+ </{{ element }}>
13
8
  </li>
9
+ {# djlint:on #}
10
+ {% endwith %}
@@ -1,15 +1,8 @@
1
1
  <c-vars variant="primary" size="" outline="" text class />
2
2
  {# djlint:off #}
3
- <{% if href %}a{% else %}button{% endif %}
4
- class="btn {% if outline %} btn-outline-{{ variant }}{% else %} btn-{{ variant }}{% endif %}
5
- {% if size %}btn-{{ size }}{% endif %}
6
- {{ class }}" {{ attrs }}>
7
- {# djlint:on #}
8
- {% if slot %}
9
- {{ slot }}
10
- {% else %}
11
- {{ text }}
12
- {% endif %}
13
- {# djlint:off #}
14
- </{% if href %}a{% else %}button{% endif %}>
3
+ {% with element=href|yesno:"a,button" %}
4
+ <{{ element }} class="btn{% if outline %} btn-outline-{{ variant }}{% else %} btn-{{ variant }}{% endif %}{% if size %} btn-{{ size }}{% endif %} {{ class }}" {{ attrs }}>
5
+ {{ slot }} {{ text }}
6
+ </{{ element }}>
7
+ {% endwith %}
15
8
  {# djlint:on #}
@@ -0,0 +1,8 @@
1
+ {% load cotton_bs5 %}
2
+ <section id="{{ title|slugify }}">
3
+ <h2>{{ title }}</h2>
4
+ <p>{{ help }}</p>
5
+ {% show_code %}
6
+ {{ slot }}
7
+ {% endshow_code %}
8
+ </section>
@@ -1,15 +1,6 @@
1
- <c-vars href text active disabled class />
2
- {% if href %}
3
- <a class="dropdown-item{% if active %} active{% endif %}{% if disabled %} disabled{% endif %} {{ class }}"
4
- {% if disabled %}tabindex="-1" aria-disabled="true"{% endif %}
5
- {{ attrs }}>
6
- {{ text }} {{ slot }}
7
- </a>
8
- {% else %}
9
- <button class="dropdown-item{% if active %} active{% endif %}{% if disabled %} disabled{% endif %} {{ class }}"
10
- type="button"
11
- {% if disabled %}disabled{% endif %}
12
- {{ attrs }}>
13
- {{ text }} {{ slot }}
14
- </button>
15
- {% endif %}
1
+ <c-vars text active disabled class />
2
+ {% with element=href|yesno:"a,button" %}
3
+ {# djlint:off #}
4
+ <{{ element }} class="dropdown-item{% if active %} active{% endif %}{% if disabled %} disabled{% endif %}{% if class %} {{ class }}{% endif %}"{% if element == "button" %} type="button"{% endif %}{% if disabled %}{% if element == "a" %} tabindex="-1" aria-disabled="true"{% else %} disabled{% endif %}{% endif %} {{ attrs }}>{{ text }}{{ slot }}</{{ element }}>
5
+ {# djlint:on #}
6
+ {% endwith %}
@@ -1,5 +1,5 @@
1
- <c-vars width="" :responsive="{}" offset="" class />
2
- <div class="{% if width %}col-{{ width }} {% else %} col {% endif %} {% for bp, width in responsive.items %}col-{{ bp }}-{{ width }}{% endfor %} {{ class }}"
1
+ <c-vars width="" :responsive="{}" offset="" gap=0 class />
2
+ <div class="{% if width %}col-{{ width }}{% else %}col{% endif %}{% for bp, width in responsive.items %} col-{{ bp }}-{{ width }}{% endfor %} gap-{{ gap }} {{ class }}"
3
3
  {{ attrs }}>
4
4
  {{ slot }}
5
5
  </div>
@@ -1,5 +1,6 @@
1
- <c-vars cols="" gap="1" :responsive="{}" class />
2
- <div class="row{% if cols %} row-cols-{{ cols }}{% endif %}{% if gap %} g-{{ gap }}{% endif %}{% for bp,cols in responsive.items %} row-cols-{{ bp }}-{{ cols }}{% endfor %} {{ class }}"
1
+ <c-vars cols=1 gap="1" sm md lg xl xxl class />
2
+ <div class="row{% if cols %} row-cols-{{ cols }}{% endif %}{% if gap %} g-{{ gap }}{% endif %}{% if sm %} row-cols-sm-{{ sm }}{% endif %}{% if md %} row-cols-md-{{ sm }}{% endif %}{% if lg %} row-cols-lg-{{ lg }}{% endif %}{% if xl %} row-cols-xl-{{ xl }}{% endif %}{% if xl %} row-cols-xl-{{ xl }}{% endif %}{% if xxl %} row-cols-xxl-{{ xxl }}{% endif %}{{ class }}"
3
3
  {{ attrs }}>
4
4
  {{ slot }}
5
5
  </div>
6
+
@@ -0,0 +1,4 @@
1
+ <c-vars gap=1 class />
2
+ <div class="hstack gap-{{ gap }} {{ class }}"
3
+ {{ attrs }}>{{ slot }}
4
+ </div>
@@ -3,9 +3,11 @@
3
3
  :horizontal="False"
4
4
  class />
5
5
  {# djlint:off #}
6
- <{% if numbered %}ol{% else %}ul{% endif %}
7
- class="list-group{% if flush %} list-group-flush{% endif %}{% if horizontal %} list-group-horizontal{% if horizontal != True %}-{{ horizontal }}{% endif %}{% endif %}{{ class }}"
6
+ {% with numbered|yesno:"ol,ul" as element %}
7
+ <{{ element }}
8
+ class="list-group{% if flush %} list-group-flush{% endif %}{% if horizontal %} list-group-horizontal{% if horizontal != True %}-{{ horizontal }}{% endif %}{% endif %} {{ class }}"
8
9
  {{ attrs }}>
9
10
  {{ slot }}
10
- </{% if numbered %}ol{% else %}ul{% endif %}>
11
+ </{{ element }}>
12
+ {% endwith %}
11
13
  {# djlint:on #}
@@ -1,9 +1,8 @@
1
- <c-vars text active disabled variant href tag="li" class />
1
+ <c-vars text active disabled variant class />
2
2
  {# djlint:off #}
3
- <{% if href %}a href="{{ href }}"{% else %}{{ tag }}{% endif %}
4
- class="list-group-item{% if href %} list-group-item-action{% endif %}{% if active %} active{% elif disabled %} disabled{% endif %}{% if variant %} list-group-item-{{ variant }}{% endif %} {{ class }}"
5
- {% if active %}aria-current="true"{% elif disabled %}aria-disabled="true"{% endif %}
6
- {{ attrs }}>
7
- {{ text }} {{ slot }}
8
- </{% if href %}a{% else %}{{ tag }}{% endif %}>
3
+ {% with element=href|yesno:"a,li" %}
4
+ <{{ element }} class="list-group-item{% if href %} list-group-item-action{% endif %}{% if active %} active{% elif disabled %} disabled{% endif %}{% if variant %} list-group-item-{{ variant }}{% endif %} {{ class }}"{% if active %} aria-current="true"{% elif disabled %} aria-disabled="true"{% endif %} {{ attrs }}>
5
+ {{ text }}{{ slot }}
6
+ </{{ element }}>
7
+ {% endwith %}
9
8
  {# djlint:on #}
@@ -0,0 +1,4 @@
1
+ <c-vars class link_class />
2
+ <li class="nav-item {{ class }}">
3
+ <c-nav.link :attrs="attrs" class="{{ link_class }}" />
4
+ </li>
@@ -0,0 +1,4 @@
1
+ <c-vars gap=1 class />
2
+ <div class="vstack gap-{{ gap }} {{ class }}"
3
+ {{ attrs }}>{{ slot }}
4
+ </div>
@@ -0,0 +1,101 @@
1
+ {% load cotton_bs5 %}
2
+ <div class="card component-demo-card">
3
+ <!-- Rendered Component Preview -->
4
+ <div class="demo-preview border-bottom p-4">{{ rendered }}</div>
5
+ <!-- Code Tabs Navigation -->
6
+ <div class="border-bottom">
7
+ <div role="tablist"
8
+ class="nav nav-tabs border-0 px-3 pt-2">
9
+ {% genid "syntax-tab" as syntax_tab_id %}
10
+ <button type="button"
11
+ class="nav-link active"
12
+ id="{{ syntax_tab_id }}"
13
+ data-bs-toggle="tab"
14
+ data-bs-target="#{{ syntax_tab_id }}-pane"
15
+ role="tab"
16
+ aria-controls="{{ syntax_tab_id }}-pane"
17
+ aria-selected="true">
18
+ <svg xmlns="http://www.w3.org/2000/svg"
19
+ width="14"
20
+ height="14"
21
+ fill="currentColor"
22
+ class="bi bi-code-slash me-1"
23
+ viewBox="0 0 16 16">
24
+ <path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294l4-13zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0zm6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0z" />
25
+ </svg>
26
+ Cotton Syntax
27
+ </button>
28
+ {% genid "html-tab" as html_tab_id %}
29
+ <button type="button"
30
+ class="nav-link"
31
+ id="{{ html_tab_id }}"
32
+ data-bs-toggle="tab"
33
+ data-bs-target="#{{ html_tab_id }}-pane"
34
+ role="tab"
35
+ aria-controls="{{ html_tab_id }}-pane"
36
+ aria-selected="false">
37
+ <svg xmlns="http://www.w3.org/2000/svg"
38
+ width="14"
39
+ height="14"
40
+ fill="currentColor"
41
+ class="bi bi-file-code me-1"
42
+ viewBox="0 0 16 16">
43
+ <path d="M6.646 5.646a.5.5 0 1 1 .708.708L5.707 8l1.647 1.646a.5.5 0 0 1-.708.708l-2-2a.5.5 0 0 1 0-.708l2-2zm2.708 0a.5.5 0 1 0-.708.708L10.293 8 8.646 9.646a.5.5 0 0 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2z" />
44
+ <path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z" />
45
+ </svg>
46
+ HTML Output
47
+ </button>
48
+ </div>
49
+ </div>
50
+ <!-- Code Tabs Content -->
51
+ <div class="tab-content">
52
+ <div class="tab-pane fade show active"
53
+ id="{{ syntax_tab_id }}-pane"
54
+ role="tabpanel"
55
+ aria-labelledby="{{ syntax_tab_id }}"
56
+ tabindex="0">
57
+ <div class="position-relative">
58
+ <button class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0 m-3 copy-btn"
59
+ data-copy-target="{{ syntax_tab_id }}-code"
60
+ aria-label="Copy Cotton syntax to clipboard"
61
+ title="Copy code">
62
+ <svg xmlns="http://www.w3.org/2000/svg"
63
+ width="14"
64
+ height="14"
65
+ fill="currentColor"
66
+ class="bi bi-clipboard"
67
+ viewBox="0 0 16 16">
68
+ <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
69
+ <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
70
+ </svg>
71
+ Copy
72
+ </button>
73
+ <pre class="mb-0 p-3"><code id="{{ syntax_tab_id }}-code" class="language-html">{{ code }}</code></pre>
74
+ </div>
75
+ </div>
76
+ <div class="tab-pane fade"
77
+ id="{{ html_tab_id }}-pane"
78
+ role="tabpanel"
79
+ aria-labelledby="{{ html_tab_id }}"
80
+ tabindex="0">
81
+ <div class="position-relative">
82
+ <button class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0 m-3 copy-btn"
83
+ data-copy-target="{{ html_tab_id }}-code"
84
+ aria-label="Copy HTML output to clipboard"
85
+ title="Copy code">
86
+ <svg xmlns="http://www.w3.org/2000/svg"
87
+ width="14"
88
+ height="14"
89
+ fill="currentColor"
90
+ class="bi bi-clipboard"
91
+ viewBox="0 0 16 16">
92
+ <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
93
+ <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
94
+ </svg>
95
+ Copy
96
+ </button>
97
+ <pre class="mb-0 p-3"><code id="{{ html_tab_id }}-code" class="language-html">{{ html }}</code></pre>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
File without changes
@@ -0,0 +1,267 @@
1
+ """Template tags and filters for MVP navbar widgets."""
2
+
3
+ import secrets
4
+ import string
5
+ import textwrap
6
+
7
+ from django import template
8
+ from django.template.loader import render_to_string
9
+ from django.utils.html import escape
10
+ from django.utils.safestring import mark_safe
11
+ from django_cotton.compiler_regex import CottonCompiler
12
+
13
+ # Conditional import for BeautifulSoup
14
+ try:
15
+ from bs4 import BeautifulSoup
16
+
17
+ HAS_BEAUTIFULSOUP = True
18
+ except ImportError:
19
+ HAS_BEAUTIFULSOUP = False
20
+
21
+ register = template.Library()
22
+
23
+ compiler = CottonCompiler()
24
+
25
+
26
+ @register.filter
27
+ def slot_is_empty(slot):
28
+ """Check if a template slot is empty after stripping whitespace.
29
+
30
+ Django Cotton slots may contain strings with unwanted whitespace or newlines,
31
+ making direct template checks unreliable. This filter properly handles these
32
+ cases by stripping whitespace before comparison.
33
+
34
+ Args:
35
+ slot: The slot content to check (typically a string, but may be other types).
36
+
37
+ Returns:
38
+ bool: True if slot is a string with only whitespace or is empty, False otherwise.
39
+ Returns None implicitly if slot is not a string type.
40
+
41
+ Example:
42
+ {% if slot_content|slot_is_empty %}
43
+ <p>No content provided</p>
44
+ {% else %}
45
+ {{ slot_content }}
46
+ {% endif %}
47
+ """
48
+ if isinstance(slot, str):
49
+ return slot.strip() == ""
50
+
51
+
52
+ @register.filter
53
+ def beautify_html(html):
54
+ """Clean up and format unformatted HTML with proper indentation.
55
+
56
+ Removes common leading whitespace and strips leading/trailing blank lines
57
+ to produce clean, readable HTML.
58
+
59
+ Args:
60
+ html (str): Unformatted HTML string
61
+
62
+ Returns:
63
+ str: Formatted HTML with normalized indentation
64
+
65
+ Example:
66
+ Input: " <div>\n <p>Hello</p>\n </div>"
67
+ Output: "<div>\n <p>Hello</p>\n</div>"
68
+ """
69
+ return textwrap.dedent(html).strip("\n")
70
+
71
+
72
+ @register.simple_tag
73
+ def genid(prefix="", length=6):
74
+ """Generate a unique random ID for use in HTML attributes.
75
+
76
+ Produces a short random string suitable for use as unique HTML element IDs.
77
+ Can optionally include a prefix for semantic naming.
78
+
79
+ Args:
80
+ prefix (str): Optional prefix to prepend to the random string.
81
+ If provided, the format is "{prefix}-{random_string}".
82
+ Defaults to empty string (no prefix).
83
+ length (int): Length of the random string to generate.
84
+ Defaults to 6 characters.
85
+
86
+ Returns:
87
+ str: Generated ID. If prefix is provided, returns "{prefix}-{random_string}",
88
+ otherwise returns just the random string.
89
+
90
+ Example:
91
+ {% genid %} # Returns something like "a3b2c1"
92
+ {% genid "tab" %} # Returns something like "tab-a3b2c1"
93
+ {% genid "modal" 8 %} # Returns something like "modal-a3b2c1d9"
94
+ {% genid prefix="button" length=10 %} # Returns something like "button-a3b2c1d9e7"
95
+ """
96
+ random_str = "".join(
97
+ secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)
98
+ )
99
+ if prefix:
100
+ return f"{prefix}-{random_str}"
101
+ return random_str
102
+
103
+
104
+ @register.simple_tag(takes_context=True)
105
+ def cotton_parent(context):
106
+ cotton_data = context.get("cotton_data", {})
107
+ if not cotton_data:
108
+ return None
109
+ stack = cotton_data.get("stack", [])
110
+ if not stack:
111
+ return None
112
+
113
+ stack_length = len(stack)
114
+
115
+ parent_idx = stack_length - 2 # Get the index of the parent element
116
+
117
+ # this will ONLY return attrs declared on the component itself, not c-vars
118
+ return stack[parent_idx]["attrs"]
119
+
120
+
121
+ @register.simple_tag(takes_context=True)
122
+ def responsive(context, root: str):
123
+ """Generate responsive Bootstrap grid classes from context variables.
124
+
125
+ This tag generates responsive variants of a root class name (e.g., 'col')
126
+ by combining it with responsive breakpoint suffixes (xs, sm, md, lg, xl, xxl).
127
+ Context variables for each breakpoint determine whether the suffix is included
128
+ and its value.
129
+
130
+ Args:
131
+ context (dict): Django template context, expected to contain optional keys:
132
+ - xs (str): Extra small breakpoint value
133
+ - sm (str): Small breakpoint value
134
+ - md (str): Medium breakpoint value
135
+ - lg (str): Large breakpoint value
136
+ - xl (str): Extra large breakpoint value
137
+ - xxl (str): Extra extra large breakpoint value
138
+ root (str): Base class name to which responsive variants are appended
139
+ (e.g., 'col', 'offset', 'gutter')
140
+
141
+ Returns:
142
+ str: Space-separated responsive class names. For each breakpoint variable
143
+ present in context, returns "{root}-{breakpoint}-{value}".
144
+ Returns empty string if no breakpoint variables are defined.
145
+
146
+ Example:
147
+ Context: {'md': '6', 'lg': '4', 'xl': '3'}
148
+ Tag: {% responsive 'col' %}
149
+ Output: 'col-md-6 col-lg-4 col-xl-3'
150
+ """
151
+ # The idea is to take a root class name (e.g., "col") and
152
+ # and generate responsive variants based on context variables xs, sm, md, lg, xl, xxl).
153
+ # If a context variable is present, the value should be added to root along with the responsive
154
+ # name (e.g., "col-md-6").
155
+
156
+ responsive_values = {
157
+ responsive: context.get(responsive)
158
+ for responsive in ["xs", "sm", "md", "lg", "xl", "xxl"]
159
+ }
160
+
161
+ return " ".join(
162
+ f"{root}-{key}-{value}"
163
+ for key, value in responsive_values.items()
164
+ if value is not None
165
+ )
166
+
167
+
168
+ @register.tag(name="show_code")
169
+ def show_code(parser, token):
170
+ """Parse the show_code block tag.
171
+
172
+ Collects the template content between {% show_code %} and {% endshow_code %} tags
173
+ for processing and rendering.
174
+
175
+ Args:
176
+ parser: Django template parser
177
+ token: Template token with tag name
178
+
179
+ Returns:
180
+ ShowCodeNode: Node instance that will handle rendering
181
+ """
182
+ nodelist = parser.parse(("endshow_code",))
183
+ parser.delete_first_token()
184
+ return ShowCodeNode(nodelist)
185
+
186
+
187
+ class ShowCodeNode(template.Node):
188
+ """Template node for the show_code tag.
189
+
190
+ Processes template content to display both executable code and its rendered output.
191
+ Handles indentation normalization, HTML escaping, Cotton template compilation,
192
+ and HTML rendering.
193
+
194
+ Attributes:
195
+ nodelist (NodeList): Template nodes between show_code block start and end tags
196
+ """
197
+
198
+ def __init__(self, nodelist):
199
+ """Initialize the node with template content.
200
+
201
+ Args:
202
+ nodelist: Template node list from parser between block tags
203
+ """
204
+ self.nodelist = nodelist
205
+
206
+ def render(self, context):
207
+ """Render the code block with normalized formatting and display.
208
+
209
+ Processes the captured template content through the following steps:
210
+ 1. Render the template content to raw text
211
+ 2. Normalize indentation (remove common leading whitespace)
212
+ 3. Remove leading/trailing blank lines
213
+ 4. Escape HTML special characters for safe display
214
+ 5. Compile Cotton template syntax
215
+ 6. Execute the compiled template
216
+ 7. Display both formatted code and rendered output side-by-side
217
+
218
+ Args:
219
+ context: Django template context for rendering
220
+
221
+ Returns:
222
+ str: HTML markup displaying the code and its rendered result
223
+ """
224
+ raw = self.nodelist.render(context)
225
+
226
+ # Beautifulsoup does annoying things to the Cotton syntax, so we'll skip that step and clean ourselves
227
+ # soup = BeautifulSoup(raw, "html.parser")
228
+ # cleaned = soup.prettify(formatter="html5")
229
+
230
+ cleaned = textwrap.dedent(raw).strip("\n")
231
+
232
+ # 3. Escape Cotton syntax for HTML display in code tag
233
+ code = escape(cleaned)
234
+
235
+ # 4. Compile Cotton syntax
236
+ compiled = compiler.process(cleaned)
237
+
238
+ # 5. Render the compiled template
239
+ t = template.Template(compiled)
240
+ rendered_raw = t.render(context)
241
+
242
+ # 6. Clean the rendered HTML using BeautifulSoup for proper formatting
243
+ if not HAS_BEAUTIFULSOUP:
244
+ raise ImportError(
245
+ "BeautifulSoup4 is required for the show_code template tag. "
246
+ "Install it with: pip install beautifulsoup4"
247
+ )
248
+
249
+ soup = BeautifulSoup(rendered_raw, "html.parser")
250
+ rendered_cleaned = soup.prettify()
251
+
252
+ # rendered_cleaned = textwrap.dedent(rendered_raw).strip("\n")
253
+ # rendered_cleaned = re.sub(r'\n\s*\n(\s*\n)+', '\n\n', rendered_cleaned)
254
+
255
+ # Remove excessive blank lines (more than one consecutive blank line)
256
+ # rendered_cleaned = rendered_cleaned.strip()
257
+
258
+ # 7. Mark safe for actual rendering on page
259
+ rendered = mark_safe(rendered_cleaned)
260
+
261
+ # 8. Escape cleaned HTML for display in code tag
262
+ html = escape(rendered_cleaned)
263
+
264
+ return render_to_string(
265
+ "cotton_bs5/document_component.html",
266
+ {"code": code, "rendered": rendered, "html": html},
267
+ )
@@ -1,20 +1,21 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-cotton-bs5
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Bootstrap 5 components for use with Django Cotton.
5
5
  License: MIT
6
6
  Author: Sam
7
7
  Author-email: samuel.scott.jennings@gmail.com
8
- Requires-Python: >=3.10,<4.0
8
+ Requires-Python: >=3.12,<4.00
9
+ Classifier: Framework :: Django
10
+ Classifier: Framework :: Pytest
9
11
  Classifier: License :: OSI Approved :: MIT License
10
12
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Provides-Extra: django-compressor
15
+ Requires-Dist: beautifulsoup4 (>=4.14.3,<5.0.0)
15
16
  Requires-Dist: django-compressor (>=4.5.1,<5.0.0) ; extra == "django-compressor"
16
- Requires-Dist: django-cotton (>=2.3.1)
17
- Requires-Dist: django-libsass (>=0.9,<0.10)
17
+ Requires-Dist: django-cotton (>=2.6.1,<3.0.0)
18
+ Requires-Dist: django-libsass (>=0.9,<0.10) ; extra == "django-compressor"
18
19
  Description-Content-Type: text/markdown
19
20
 
20
21
  # Django Cotton BS5
@@ -62,6 +63,90 @@ The following Bootstrap 5 components are currently available as Django Cotton co
62
63
 
63
64
  More components are planned. Please request additional Bootstrap 5 components or features via the [issue tracker](https://github.com/SamuelJennings/cotton-bs5/issues).
64
65
 
66
+ ## Testing Components
67
+
68
+ This package provides four pytest fixtures for testing Django Cotton components:
69
+
70
+ ### `cotton_render` Fixture
71
+
72
+ Renders a component and returns the raw HTML as a string.
73
+
74
+ ```python
75
+ def test_alert_component(cotton_render):
76
+ html = cotton_render(
77
+ 'cotton_bs5.alert',
78
+ message="Hello World",
79
+ variant="success"
80
+ )
81
+ assert 'alert-success' in html
82
+ assert 'Hello World' in html
83
+ ```
84
+
85
+ ### `cotton_render_soup` Fixture
86
+
87
+ Renders a component and returns a BeautifulSoup parsed HTML object for easier DOM traversal and assertions.
88
+
89
+ ```python
90
+ def test_alert_component(cotton_render_soup):
91
+ soup = cotton_render_soup(
92
+ 'cotton_bs5.alert',
93
+ message="Hello World",
94
+ variant="success"
95
+ )
96
+ alert_div = soup.find('div', class_='alert')
97
+ assert 'alert-success' in alert_div['class']
98
+ assert alert_div.get_text() == 'Hello World'
99
+ ```
100
+
101
+ ### `cotton_render_string` Fixture
102
+
103
+ Compiles and renders template strings containing Cotton component syntax. Useful for testing multi-component markup and complex layouts inline without creating separate template files.
104
+
105
+ ```python
106
+ def test_button_inline(cotton_render_string):
107
+ html = cotton_render_string("<c-button variant='primary'>Click me</c-button>")
108
+ assert 'btn-primary' in html
109
+
110
+ def test_nested_components(cotton_render_string):
111
+ html = cotton_render_string(
112
+ "<c-ul><c-li text='first' /><c-li text='second' /></c-ul>"
113
+ )
114
+ assert 'first' in html
115
+ assert 'second' in html
116
+ ```
117
+
118
+ ### `cotton_render_string_soup` Fixture
119
+
120
+ Combines `cotton_render_string` with BeautifulSoup parsing for easier DOM traversal and assertions on multi-component structures.
121
+
122
+ ```python
123
+ def test_nested_list(cotton_render_string_soup):
124
+ soup = cotton_render_string_soup(
125
+ "<c-ul><c-li text='first' /><c-li text='second' /></c-ul>"
126
+ )
127
+ items = soup.find_all('li')
128
+ assert len(items) == 2
129
+ assert items[0].get_text() == 'first'
130
+
131
+ def test_complex_layout(cotton_render_string_soup):
132
+ template = '''
133
+ <c-card>
134
+ <c-card.title>{{ title }}</c-card.title>
135
+ <c-card.body>
136
+ <c-button variant='primary'>{{ action }}</c-button>
137
+ </c-card.body>
138
+ </c-card>
139
+ '''
140
+ soup = cotton_render_string_soup(template, context={
141
+ 'title': 'My Card',
142
+ 'action': 'Click Here'
143
+ })
144
+ assert soup.find('h5').get_text() == 'My Card'
145
+ assert 'btn-primary' in soup.find('button')['class']
146
+ ```
147
+
148
+ All fixtures automatically inject a request object, so you don't need to create one manually.
149
+
65
150
  ## Contributing
66
151
 
67
152
  This library follows django-cotton conventions and Bootstrap 5 standards. When adding new components:
@@ -1,5 +1,6 @@
1
1
  cotton_bs5/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cotton_bs5/apps.py,sha256=IwPyZAgGWygK8LpsA2_Yz4lUjyYhzEYaBL_-KVR9KK8,94
3
+ cotton_bs5/fixtures.py,sha256=XrUKC2p-_HoTHW-YNAoO24aYNd9pAh-hzPFcmeI5-GE,7502
3
4
  cotton_bs5/static/_variables.scss,sha256=B5jORYuNv-a6xxsnnqj5u_JgS_qjEWySneZCInancy4,1906
4
5
  cotton_bs5/static/bs5/_accordion.scss,sha256=-puTNym6ujT9oyWBBCKJx4xrE4Vm7Dknay6zlbgqyOw,5077
5
6
  cotton_bs5/static/bs5/_alert.scss,sha256=3Cl_x0tBd-YWCTzPJnjhzzzxWBUTdj8yq5WV80VLD94,2073
@@ -96,14 +97,14 @@ cotton_bs5/static/bs5/vendor/_rfs.scss,sha256=JsQPrGMGFQNZ-LCv25Rv_rWHI_fX1koYba
96
97
  cotton_bs5/static/stylesheet.scss,sha256=8czc7O81g7KUDSG4hVfcz_5YdYDOhEurn1uqgeQ-sLI,2529
97
98
  cotton_bs5/templates/cotton/accordion/body.html,sha256=Y-lw1saKRb31k8s1kLyCarKNWMcVp7eCjHsV8qtRGW4,219
98
99
  cotton_bs5/templates/cotton/accordion/header.html,sha256=B38j90UQ_Q5PXKRduFfCToOV08gFPVTK8EbF4DUNyKw,442
99
- cotton_bs5/templates/cotton/accordion/index.html,sha256=JDmnwJLGYcbDOqUSVP9EvONiQVbjwnJJ69btdxUkf9c,163
100
- cotton_bs5/templates/cotton/accordion/item.html,sha256=lsaDkKDGA5bfgb0ER7Z9nsS5M43dKyg-ip-01z8SE7E,276
100
+ cotton_bs5/templates/cotton/accordion/index.html,sha256=a-QTsbHCiHeEXA_HzN5z7oYRMejlNVCGc7zwhuHqTTI,178
101
+ cotton_bs5/templates/cotton/accordion/item.html,sha256=IbRIfpcLTTimI5tPAhXjkM0jlLh_B5ICYqLjvSNG6OE,480
101
102
  cotton_bs5/templates/cotton/alert.html,sha256=PfRSBUW4cFvx6lmTXzpP8R0JDGvPX7k1i5jMjD1YiuY,386
102
103
  cotton_bs5/templates/cotton/badge.html,sha256=7B84yZIZWhtjdNsoeM6-FMiZEsTyTtauzIzMSws3fXU,303
103
- cotton_bs5/templates/cotton/breadcrumbs/index.html,sha256=Z4UHmU84mu121otLHUy7GBasmgvryG_ioBrEB9E-lMw,376
104
- cotton_bs5/templates/cotton/breadcrumbs/item.html,sha256=h0_RmjgC35XQ-3cJ3MgPcVCRwUN6ors2UzJX--JwjxE,397
104
+ cotton_bs5/templates/cotton/breadcrumbs/index.html,sha256=P4Q1TaGXoXOCoK-vtWmZqPAp31UTSmN6D50gN1IJFLs,370
105
+ cotton_bs5/templates/cotton/breadcrumbs/item.html,sha256=aeIl7FUOyoEEcs9oNNtI3eDf7aFmr8HgAk7TLCj1EOQ,436
105
106
  cotton_bs5/templates/cotton/button/dismiss.html,sha256=Q1pBNnh8mFACc73XmQzLD9TgmPToZ374sW26SNALnCI,253
106
- cotton_bs5/templates/cotton/button/index.html,sha256=bW8yrGFxJk-lkbla9RgM3vaUyxyDnwWVoMiZncBPQv4,440
107
+ cotton_bs5/templates/cotton/button/index.html,sha256=ASta4ny_ZQWCctdmW3Qh4B0aJ4Sgwx-PG9AJFGqRfQ4,360
107
108
  cotton_bs5/templates/cotton/button_group.html,sha256=HuS7PDniLkVCXovonzRqFVQrGqYfezyQfq0ihTVbwM0,429
108
109
  cotton_bs5/templates/cotton/card/body.html,sha256=V7_erMPFbYfCWeMeI9BCtiqrKpr4Qr2LGd4uxrQhkA4,162
109
110
  cotton_bs5/templates/cotton/card/footer.html,sha256=5wop-wWc-Pj5HU8cZLNY4L7t-cEXskyz6WiECAKvOs8,87
@@ -116,21 +117,24 @@ cotton_bs5/templates/cotton/carousel/caption.html,sha256=BD4Vgdrho5T0mvvvKmtbQYI
116
117
  cotton_bs5/templates/cotton/carousel/index.html,sha256=MhulHDibT3e9B4KFW-tCpLYz0ORCfjG8kfe9zBmSXYA,452
117
118
  cotton_bs5/templates/cotton/carousel/item.html,sha256=Q_kpQa7_uyi0_0BUxTH5_tuqzF-4PHs8WnH9WpHdfkQ,142
118
119
  cotton_bs5/templates/cotton/collapse.html,sha256=N9lcZgBa9OxcgCHgNolmU17CM5uTbxkx6kNcY-BGvlc,344
120
+ cotton_bs5/templates/cotton/cotton_bs5/document.html,sha256=nc71O-Ex8F3qlIhhFjdqObkaLmK7CArL9IIi-8UiDOI,165
119
121
  cotton_bs5/templates/cotton/dropdown/divider.html,sha256=EuKZtrn23FJSIXy1VNb96Ft1aZTNzAzEoSrehZsiGtI,32
120
122
  cotton_bs5/templates/cotton/dropdown/header.html,sha256=vi27RD2jKOvJCdLqceQtB1PaJLDkrRoikpkue0DGA-A,83
121
123
  cotton_bs5/templates/cotton/dropdown/index.html,sha256=tana4NotkE2h3Q0tRNS94TMc8NcgDlLMO5EdgyyH9FI,290
122
- cotton_bs5/templates/cotton/dropdown/item.html,sha256=8hVy3gl_ZPkO4r07NjQKQ9T0NdKJjIDpw7wLAgjCysM,554
124
+ cotton_bs5/templates/cotton/dropdown/item.html,sha256=a3_iPn7MixG8N47743RLofMKzvacBfCwsYxUaAuMhuo,492
123
125
  cotton_bs5/templates/cotton/dropdown/toggle.html,sha256=cZDrs80yuwyMk3bCV2hGn5XycyC1WT6k7NeMYxxi0Mk,447
124
- cotton_bs5/templates/cotton/grid/col.html,sha256=nIcLePAN8BJs7rlz5UJdCknIxEq2uOFpZbKSlPlI47s,255
125
- cotton_bs5/templates/cotton/grid/index.html,sha256=8D9xxCJ9Vu4IeX2AzYdeYYpIZSUU2WxImOJxc9Anxu0,274
126
- cotton_bs5/templates/cotton/list_group/index.html,sha256=OBqm8NHIjMMWxx_PNFMH3c2HLJgrTnQritCmpfVFacw,420
127
- cotton_bs5/templates/cotton/list_group/item.html,sha256=jBPbBUlzvDKuz_ltD4xLNVO_DbFB4J2S5fNDmi1t63U,526
126
+ cotton_bs5/templates/cotton/grid/col.html,sha256=DvrOJ71hts9IktSiy8R_4HHb4vFlbAtZnfnaX7RJEJ0,272
127
+ cotton_bs5/templates/cotton/grid/index.html,sha256=KbcZR4i63BSdpXzSfv4B7jbKF8lbcfnAcGJx24Cj8io,455
128
+ cotton_bs5/templates/cotton/hstack.html,sha256=1NxBx5ccVoPEHWc1PDsraQYL0yd9z6zNF0MX_xtugBU,104
129
+ cotton_bs5/templates/cotton/list_group/index.html,sha256=vQ8XCi9gKtPfBBlrDTgcEXEOwyt0jPZydaQ62c4gveg,422
130
+ cotton_bs5/templates/cotton/list_group/item.html,sha256=uZXDWpKBynWeC5bD4HDygETj0rXlU_jB7YUgXn44r4M,485
128
131
  cotton_bs5/templates/cotton/modal/body.html,sha256=ANNl7-edJij4ECMcjnX5Ms464yXIhgC8jEKIb1TbTYE,41
129
132
  cotton_bs5/templates/cotton/modal/footer.html,sha256=Oxg9w02dcmPAmgGnqk1AfVsSk7LRqZigsClNAktP3_U,85
130
133
  cotton_bs5/templates/cotton/modal/header.html,sha256=lMEqRfpR8soBRhRAG-6i4gVBCuftTnoe9GmJObwPStU,246
131
134
  cotton_bs5/templates/cotton/modal/index.html,sha256=c_GZQV83gjJjv969GpKyxqXDoNKRVeQARdCQcA2qstw,652
132
135
  cotton_bs5/templates/cotton/modal/title.html,sha256=lVPJDKK0Zk2U5VxL_jisnzPrCwgcVtcuTbIPDNQv690,138
133
136
  cotton_bs5/templates/cotton/nav/index.html,sha256=9OczfhoKQGdQr6d52dODR6WxWymFhdbEXNIB4xXI6fw,262
137
+ cotton_bs5/templates/cotton/nav/item.html,sha256=iu34dEiR3ID-27PjVPCpEhmBiAumNQYAHzSInrtPb9Q,125
134
138
  cotton_bs5/templates/cotton/nav/link.html,sha256=TyTdMfv6I1hhVhTNv84hAqMdebj2Glx7sjp28cPyn_o,262
135
139
  cotton_bs5/templates/cotton/navbar/index.html,sha256=Xtk6dg_lg05kAyajRIxZIBSgWQhPDWCC84LDkytbG78,872
136
140
  cotton_bs5/templates/cotton/navbar/link.html,sha256=fPtVWCZCFbMWvJW04I7N2A9L8Mk_-Yqfxw8Tv4_nu3s,364
@@ -156,7 +160,12 @@ cotton_bs5/templates/cotton/toast/activate.html,sha256=D-qh8lfEWoJWMKdUG0PT4jdNA
156
160
  cotton_bs5/templates/cotton/toast/body.html,sha256=cneCr26QtF48gYR7kW5wa_QO0Qv5nGJDlXR6PjVueMU,71
157
161
  cotton_bs5/templates/cotton/toast/header.html,sha256=izlXp-D5cKEIeIj297pma9XYgGNo6lkrgvHw1QscOR8,115
158
162
  cotton_bs5/templates/cotton/toast/index.html,sha256=av7EWcLGcB90KUBCru6qwtl78PkM228n5OxuJewc4OI,304
159
- django_cotton_bs5-0.5.0.dist-info/LICENSE,sha256=jAcYHTznehUzk6dbbjLdSq9Xo2S_OmscgUKA94hUoMo,1072
160
- django_cotton_bs5-0.5.0.dist-info/METADATA,sha256=mG0iOvYWEuJHeFQWKpmHhT9dNO1RWtBJ838DXu0qAcU,3223
161
- django_cotton_bs5-0.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
162
- django_cotton_bs5-0.5.0.dist-info/RECORD,,
163
+ cotton_bs5/templates/cotton/vstack.html,sha256=okGz8WD93e0PUR-FFI9p20r_PP5FQ9Ql8obVLMLtm64,104
164
+ cotton_bs5/templates/cotton_bs5/document_component.html,sha256=SLyce5I7CpBfMhuiEnkLU-O9OYbTQbi5fEjQ-_tvYzU,4826
165
+ cotton_bs5/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
+ cotton_bs5/templatetags/cotton_bs5.py,sha256=4UreJ8ZW2_L2Xfd3EKLUKlP7tHh53TH6VZ6TwDmCYNA,9128
167
+ django_cotton_bs5-0.6.0.dist-info/LICENSE,sha256=jAcYHTznehUzk6dbbjLdSq9Xo2S_OmscgUKA94hUoMo,1072
168
+ django_cotton_bs5-0.6.0.dist-info/METADATA,sha256=FK2Ob4-rLyvSM6Ss-wdc7DYgLYRxzbPpJitnFZez2QU,5853
169
+ django_cotton_bs5-0.6.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
170
+ django_cotton_bs5-0.6.0.dist-info/entry_points.txt,sha256=5FA9XkZ8EBll5jE94vwF1gp7xD1F9HTdhdKutznbTSU,43
171
+ django_cotton_bs5-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [pytest11]
2
+ cotton_bs5=cotton_bs5.fixtures
3
+