webwidgets 0.2.1__tar.gz → 1.0.0__tar.gz

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.
Files changed (45) hide show
  1. webwidgets-1.0.0/.gitignore +2 -0
  2. webwidgets-1.0.0/PKG-INFO +66 -0
  3. webwidgets-1.0.0/README.md +53 -0
  4. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/compilation/test_html_node.py +44 -4
  5. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/compilation/test_html_tags.py +18 -4
  6. webwidgets-1.0.0/tests/utility/test_indentation.py +29 -0
  7. webwidgets-1.0.0/tests/website/__init__.py +11 -0
  8. webwidgets-1.0.0/tests/website/test_website.py +402 -0
  9. webwidgets-1.0.0/tests/widgets/__init__.py +11 -0
  10. webwidgets-1.0.0/tests/widgets/test_page.py +143 -0
  11. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/__init__.py +4 -1
  12. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/compilation/css/css.py +4 -3
  13. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/compilation/html/__init__.py +2 -1
  14. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/compilation/html/html_node.py +42 -4
  15. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/compilation/html/html_tags.py +38 -1
  16. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/utility/__init__.py +1 -0
  17. webwidgets-1.0.0/webwidgets/utility/indentation.py +25 -0
  18. webwidgets-1.0.0/webwidgets/website/__init__.py +14 -0
  19. webwidgets-1.0.0/webwidgets/website/compiled_website.py +34 -0
  20. webwidgets-1.0.0/webwidgets/website/website.py +88 -0
  21. webwidgets-1.0.0/webwidgets/widgets/__init__.py +14 -0
  22. webwidgets-1.0.0/webwidgets/widgets/containers/__init__.py +14 -0
  23. webwidgets-1.0.0/webwidgets/widgets/containers/container.py +38 -0
  24. webwidgets-1.0.0/webwidgets/widgets/containers/page.py +59 -0
  25. webwidgets-1.0.0/webwidgets/widgets/widget.py +34 -0
  26. webwidgets-0.2.1/.gitignore +0 -1
  27. webwidgets-0.2.1/PKG-INFO +0 -18
  28. webwidgets-0.2.1/README.md +0 -5
  29. {webwidgets-0.2.1 → webwidgets-1.0.0}/.github/workflows/cd.yml +0 -0
  30. {webwidgets-0.2.1 → webwidgets-1.0.0}/.github/workflows/ci-full.yml +0 -0
  31. {webwidgets-0.2.1 → webwidgets-1.0.0}/.github/workflows/ci-quick.yml +0 -0
  32. {webwidgets-0.2.1 → webwidgets-1.0.0}/LICENSE +0 -0
  33. {webwidgets-0.2.1 → webwidgets-1.0.0}/pyproject.toml +0 -0
  34. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/__init__.py +0 -0
  35. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/compilation/__init__.py +0 -0
  36. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/compilation/test_css.py +0 -0
  37. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/utility/__init__.py +0 -0
  38. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/utility/test_representation.py +0 -0
  39. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/utility/test_sanitizing.py +0 -0
  40. {webwidgets-0.2.1 → webwidgets-1.0.0}/tests/utility/test_validation.py +0 -0
  41. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/compilation/__init__.py +0 -0
  42. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/compilation/css/__init__.py +0 -0
  43. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/utility/representation.py +0 -0
  44. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/utility/sanitizing.py +0 -0
  45. {webwidgets-0.2.1 → webwidgets-1.0.0}/webwidgets/utility/validation.py +0 -0
@@ -0,0 +1,2 @@
1
+ __pycache__
2
+ ignore
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: webwidgets
3
+ Version: 1.0.0
4
+ Summary: A Python package for designing web UIs.
5
+ Project-URL: Source code, https://github.com/mlaasri/WebWidgets
6
+ Author: mlaasri
7
+ License-File: LICENSE
8
+ Keywords: design,webui
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ # WebWidgets
15
+
16
+ ![CI Status](https://img.shields.io/github/actions/workflow/status/mlaasri/WebWidgets/ci-full.yml?branch=main)
17
+
18
+ A Python package for creating web UIs
19
+
20
+ ## Installation
21
+
22
+ You can install **WebWidgets** with `pip`. To install the latest stable version, run:
23
+
24
+ ```bash
25
+ pip install webwidgets
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ **WebWidgets** allows you to create custom widgets and build websites with them. For example:
31
+
32
+ ```python
33
+ import webwidgets as ww
34
+ from webwidgets.compilation.html import HTMLNode, RawText
35
+
36
+ # A <div> element
37
+ class Div(HTMLNode):
38
+ pass
39
+
40
+ # A simple text widget
41
+ class Text(ww.Widget):
42
+ def build(self):
43
+ return Div([RawText("Hello, World!")])
44
+
45
+ # A website with one page containing a Text widget
46
+ page = ww.Page([Text()])
47
+ website = ww.Website([page])
48
+
49
+ # Compile the website into HTML code
50
+ compiled = website.compile()
51
+ print(compiled.html_content[0])
52
+ ```
53
+
54
+ Prints the following result:
55
+
56
+ ```console
57
+ <!DOCTYPE html>
58
+ <html>
59
+ <head></head>
60
+ <body>
61
+ <div>
62
+ Hello, World!
63
+ </div>
64
+ </body>
65
+ </html>
66
+ ```
@@ -0,0 +1,53 @@
1
+ # WebWidgets
2
+
3
+ ![CI Status](https://img.shields.io/github/actions/workflow/status/mlaasri/WebWidgets/ci-full.yml?branch=main)
4
+
5
+ A Python package for creating web UIs
6
+
7
+ ## Installation
8
+
9
+ You can install **WebWidgets** with `pip`. To install the latest stable version, run:
10
+
11
+ ```bash
12
+ pip install webwidgets
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ **WebWidgets** allows you to create custom widgets and build websites with them. For example:
18
+
19
+ ```python
20
+ import webwidgets as ww
21
+ from webwidgets.compilation.html import HTMLNode, RawText
22
+
23
+ # A <div> element
24
+ class Div(HTMLNode):
25
+ pass
26
+
27
+ # A simple text widget
28
+ class Text(ww.Widget):
29
+ def build(self):
30
+ return Div([RawText("Hello, World!")])
31
+
32
+ # A website with one page containing a Text widget
33
+ page = ww.Page([Text()])
34
+ website = ww.Website([page])
35
+
36
+ # Compile the website into HTML code
37
+ compiled = website.compile()
38
+ print(compiled.html_content[0])
39
+ ```
40
+
41
+ Prints the following result:
42
+
43
+ ```console
44
+ <!DOCTYPE html>
45
+ <html>
46
+ <head></head>
47
+ <body>
48
+ <div>
49
+ Hello, World!
50
+ </div>
51
+ </body>
52
+ </html>
53
+ ```
@@ -12,7 +12,7 @@
12
12
 
13
13
  import pytest
14
14
  from webwidgets.compilation.html.html_node import HTMLNode, no_start_tag, \
15
- no_end_tag, one_line, RawText
15
+ no_end_tag, one_line, RawText, RootNode
16
16
 
17
17
 
18
18
  class TestHTMLNode:
@@ -63,9 +63,14 @@ class TestHTMLNode:
63
63
 
64
64
  def test_attributes(self):
65
65
  node = HTMLNode(attributes={'id': 'test-id', 'class': 'test-class'})
66
- assert node.start_tag == '<htmlnode id="test-id" class="test-class">'
66
+ assert node.start_tag == '<htmlnode class="test-class" id="test-id">'
67
67
  assert node.end_tag == '</htmlnode>'
68
- assert node.to_html() == '<htmlnode id="test-id" class="test-class"></htmlnode>'
68
+ assert node.to_html() == '<htmlnode class="test-class" id="test-id"></htmlnode>'
69
+
70
+ def test_attributes_order(self):
71
+ node = HTMLNode(attributes={'d': '0', 'a': '1', 'c': '2', 'b': '3'})
72
+ assert node._render_attributes() == 'a="1" b="3" c="2" d="0"'
73
+ assert node.to_html() == '<htmlnode a="1" b="3" c="2" d="0"></htmlnode>'
69
74
 
70
75
  def test_no_start_tag(self):
71
76
  node = TestHTMLNode.NoStartNode()
@@ -230,7 +235,7 @@ class TestHTMLNode:
230
235
  assert node.to_html() == expected_html
231
236
 
232
237
  @pytest.mark.parametrize("indent_level", [0, 1, 2])
233
- @pytest.mark.parametrize("indent_size", [3, 4, 8])
238
+ @pytest.mark.parametrize("indent_size", [2, 3, 4, 8])
234
239
  def test_indentation(self, indent_level: int, indent_size: int):
235
240
  """Test the to_html method with different indentation parameters."""
236
241
 
@@ -270,6 +275,28 @@ class TestHTMLNode:
270
275
  indent_size=indent_size, indent_level=indent_level)
271
276
  assert actual_html == expected_html
272
277
 
278
+ @pytest.mark.parametrize("indent_level", [-3, -2, -1])
279
+ def test_negative_indent_level(self, indent_level: int):
280
+ node = HTMLNode(children=[
281
+ RawText('child1'),
282
+ RawText('child2'),
283
+ HTMLNode(children=[
284
+ RawText('grandchild1'),
285
+ RawText('grandchild2')
286
+ ])
287
+ ])
288
+ expected_html = "\n".join([
289
+ "<htmlnode>",
290
+ "child1",
291
+ "child2",
292
+ "<htmlnode>",
293
+ f"{' ' if indent_level == -1 else ''}grandchild1",
294
+ f"{' ' if indent_level == -1 else ''}grandchild2",
295
+ "</htmlnode>",
296
+ "</htmlnode>"
297
+ ])
298
+ assert node.to_html(indent_level=indent_level) == expected_html
299
+
273
300
  def test_collapse_empty(self):
274
301
  node = HTMLNode(children=[
275
302
  TestHTMLNode.CustomNode(),
@@ -441,3 +468,16 @@ class TestHTMLNode:
441
468
  copied_node.children.append(child)
442
469
  assert len(node.children) == 1
443
470
  assert id(node.children[0]) == id(child)
471
+
472
+ def test_empty_root_node(self):
473
+ node = RootNode()
474
+ assert node.to_html() == ""
475
+
476
+ @pytest.mark.parametrize("n", [1, 2, 3])
477
+ def test_root_node_with_children(self, n):
478
+ node = RootNode(
479
+ children=[HTMLNode()] * n
480
+ )
481
+ expected_html = "\n".join(["<htmlnode></htmlnode>"] * n)
482
+ print(expected_html)
483
+ assert node.to_html() == expected_html
@@ -21,7 +21,21 @@ class TestHTMLTags:
21
21
  def test_text_node_with_attributes(self):
22
22
  text_node = TextNode("Hello, World!",
23
23
  attributes={"class": "my-class", "id": "my-id"})
24
- assert text_node.to_html() in [
25
- '<textnode class="my-class" id="my-id">Hello, World!</textnode>',
26
- '<textnode id="my-id" class="my-class">Hello, World!</textnode>',
27
- ]
24
+ assert text_node.to_html() == \
25
+ '<textnode class="my-class" id="my-id">Hello, World!</textnode>'
26
+
27
+ def test_body(self):
28
+ body = Body()
29
+ assert body.to_html() == "<body></body>"
30
+
31
+ def test_doctype(self):
32
+ doctype = Doctype()
33
+ assert doctype.to_html() == "<!DOCTYPE html>"
34
+
35
+ def test_head(self):
36
+ head = Head()
37
+ assert head.to_html() == "<head></head>"
38
+
39
+ def test_html(self):
40
+ html = Html()
41
+ assert html.to_html() == "<html></html>"
@@ -0,0 +1,29 @@
1
+ # =======================================================================
2
+ #
3
+ # This file is part of WebWidgets, a Python package for designing web
4
+ # UIs.
5
+ #
6
+ # You should have received a copy of the MIT License along with
7
+ # WebWidgets. If not, see <https://opensource.org/license/mit>.
8
+ #
9
+ # Copyright(C) 2025, mlaasri
10
+ #
11
+ # =======================================================================
12
+
13
+ import pytest
14
+ from webwidgets.utility.indentation import get_indentation
15
+
16
+
17
+ class TestIndentation:
18
+ @pytest.mark.parametrize("indent_level", [0, 1, 2])
19
+ @pytest.mark.parametrize("indent_size", [3, 4, 8])
20
+ def test_get_indentation(self, indent_level: int, indent_size: int):
21
+ """Tests get_indentation with different indentation levels and sizes."""
22
+ expected_indentation = ' ' * indent_size * indent_level
23
+ assert get_indentation(
24
+ indent_level, indent_size) == expected_indentation
25
+
26
+ @pytest.mark.parametrize("indent_level", [-2, -1, 0])
27
+ @pytest.mark.parametrize("indent_size", [3, 4, 8])
28
+ def test_get_indentation_for_negative_levels(self, indent_level, indent_size):
29
+ assert get_indentation(indent_level, indent_size) == ''
@@ -0,0 +1,11 @@
1
+ # =======================================================================
2
+ #
3
+ # This file is part of WebWidgets, a Python package for designing web
4
+ # UIs.
5
+ #
6
+ # You should have received a copy of the MIT License along with
7
+ # WebWidgets. If not, see <https://opensource.org/license/mit>.
8
+ #
9
+ # Copyright(C) 2025, mlaasri
10
+ #
11
+ # =======================================================================
@@ -0,0 +1,402 @@
1
+ # =======================================================================
2
+ #
3
+ # This file is part of WebWidgets, a Python package for designing web
4
+ # UIs.
5
+ #
6
+ # You should have received a copy of the MIT License along with
7
+ # WebWidgets. If not, see <https://opensource.org/license/mit>.
8
+ #
9
+ # Copyright(C) 2025, mlaasri
10
+ #
11
+ # =======================================================================
12
+
13
+ import pytest
14
+ from typing import Dict
15
+ import webwidgets as ww
16
+ from webwidgets.compilation.html.html_node import HTMLNode, RawText
17
+
18
+
19
+ class TestWebsite:
20
+ class Empty(ww.Widget):
21
+ def build(self):
22
+ return HTMLNode()
23
+
24
+ class Text(ww.Widget):
25
+ def __init__(self, text: str, style: Dict[str, str] = None):
26
+ super().__init__()
27
+ self.text = text
28
+ self.style = style
29
+
30
+ def build(self):
31
+ return HTMLNode(children=[RawText(self.text)], style=self.style)
32
+
33
+ class SimpleWebsite(ww.Website):
34
+ def __init__(self):
35
+ page = ww.Page([
36
+ TestWebsite.Text("Text!", {"padding": "0"}),
37
+ TestWebsite.Text("Another Text!", {"margin": "0"}),
38
+ ])
39
+ super().__init__([page])
40
+
41
+ @pytest.mark.parametrize("num_pages", [1, 2, 3])
42
+ @pytest.mark.parametrize("num_widgets", [1, 2, 3])
43
+ @pytest.mark.parametrize("text", ["a", "b", "c"])
44
+ def test_compile_website_without_css(self, num_pages, num_widgets, text):
45
+ # Create a new website object
46
+ website = ww.Website()
47
+ for _ in range(num_pages):
48
+ website.add(ww.Page([TestWebsite.Text(text)] * num_widgets))
49
+
50
+ # Compile the website to HTML
51
+ compiled = website.compile()
52
+
53
+ # Check if the compiled HTML contains the expected code
54
+ expected_html = "\n".join([
55
+ "<!DOCTYPE html>",
56
+ "<html>",
57
+ " <head></head>",
58
+ " <body>"
59
+ ]) + "\n" + "\n".join([
60
+ " <htmlnode>",
61
+ f" {text}",
62
+ " </htmlnode>",
63
+ ] * num_widgets) + "\n" + "\n".join([
64
+ " </body>",
65
+ "</html>"
66
+ ])
67
+ assert len(compiled.html_content) == num_pages
68
+ assert all(c == expected_html for c in compiled.html_content)
69
+
70
+ # Check if the compiled CSS contains the expected code
71
+ assert compiled.css_content == "" # No CSS in this case
72
+
73
+ @pytest.mark.parametrize("num_pages", [1, 2, 3, 4, 5, 6])
74
+ @pytest.mark.parametrize("num_widgets", [1, 2, 3])
75
+ @pytest.mark.parametrize("text", ["a", "b", "c"])
76
+ def test_compile_website_with_css(self, num_pages, num_widgets, text):
77
+ # Defining a set of styles to pick from
78
+ styles = [{"margin": "0"}, {"color": "blue"}, {"font-size": "16px"}]
79
+
80
+ # Compile expected rule names based on number of pages involved
81
+ rule_names = {
82
+ 1: ["r0"],
83
+ 2: ["r1", "r0"],
84
+ 3: ["r2", "r0", "r1"]
85
+ }
86
+ for k in range(4, num_pages + 1):
87
+ rule_names[k] = [rule_names[3][i % len(styles)] for i in range(k)]
88
+
89
+ # Create a new website object
90
+ website = ww.Website()
91
+ for i in range(num_pages):
92
+ website.add(
93
+ ww.Page([
94
+ TestWebsite.Text(text, styles[i % len(styles)])
95
+ ] * num_widgets))
96
+
97
+ # Compile the website to HTML
98
+ compiled = website.compile()
99
+
100
+ # Check if the compiled HTML contains the expected code
101
+ expected_html = [(
102
+ "\n".join([
103
+ "<!DOCTYPE html>",
104
+ "<html>",
105
+ " <head>",
106
+ ' <link href="styles.css" rel="stylesheet">',
107
+ " </head>",
108
+ " <body>"
109
+ ]) + "\n" + "\n".join([
110
+ f' <htmlnode class="{rule_names[num_pages][i % len(rule_names)]}">',
111
+ f" {text}",
112
+ " </htmlnode>",
113
+ ] * num_widgets) + "\n" + "\n".join([
114
+ " </body>",
115
+ "</html>"
116
+ ])) for i in range(num_pages)]
117
+ assert compiled.html_content == expected_html
118
+
119
+ # Check if the compiled CSS contains the expected code
120
+ sorted_rules = sorted(list(set(
121
+ zip(rule_names[num_pages],
122
+ [list(styles[i % len(styles)].items())[0]
123
+ for i in range(num_pages)]))), key=lambda x: x[0])
124
+ expected_css = "\n\n".join([
125
+ '\n'.join([
126
+ f".{name} " + "{",
127
+ f" {p}: {v};",
128
+ "}"
129
+ ]) for name, (p, v) in sorted_rules
130
+ ])
131
+ assert compiled.css_content == expected_css
132
+
133
+ def test_compile_collapse_empty(self):
134
+ website = ww.Website([ww.Page([TestWebsite.Empty()])])
135
+
136
+ # Collapse empty elements
137
+ compiled_true = website.compile(collapse_empty=True)
138
+ expected_html_true = "\n".join([
139
+ "<!DOCTYPE html>",
140
+ "<html>",
141
+ " <head></head>",
142
+ " <body>",
143
+ " <htmlnode></htmlnode>",
144
+ " </body>",
145
+ "</html>"
146
+ ])
147
+ assert len(compiled_true.html_content) == 1
148
+ assert compiled_true.html_content[0] == expected_html_true
149
+ assert compiled_true.css_content == ""
150
+
151
+ # Don't collapse empty elements
152
+ compiled_false = website.compile(collapse_empty=False)
153
+ expected_html_false = "\n".join([
154
+ "<!DOCTYPE html>",
155
+ "<html>",
156
+ " <head>",
157
+ " </head>",
158
+ " <body>",
159
+ " <htmlnode>",
160
+ " </htmlnode>",
161
+ " </body>",
162
+ "</html>"
163
+ ])
164
+ assert len(compiled_false.html_content) == 1
165
+ assert compiled_false.html_content[0] == expected_html_false
166
+ assert compiled_false.css_content == ""
167
+
168
+ @pytest.mark.parametrize("css_file_name",
169
+ ["style.css", "s.css", "css.css"])
170
+ def test_compile_css_file_name(self, css_file_name):
171
+ website = TestWebsite.SimpleWebsite()
172
+ compiled = website.compile(css_file_name=css_file_name)
173
+ expected_html = "\n".join([
174
+ "<!DOCTYPE html>",
175
+ "<html>",
176
+ " <head>",
177
+ f' <link href="{css_file_name}" rel="stylesheet">',
178
+ " </head>",
179
+ " <body>",
180
+ ' <htmlnode class="r1">',
181
+ " Text!",
182
+ " </htmlnode>",
183
+ ' <htmlnode class="r0">',
184
+ " Another Text!",
185
+ " </htmlnode>",
186
+ " </body>",
187
+ "</html>"
188
+ ])
189
+ expected_css = "\n".join([
190
+ ".r0 {",
191
+ " margin: 0;",
192
+ "}",
193
+ "",
194
+ ".r1 {",
195
+ " padding: 0;",
196
+ "}"
197
+ ])
198
+ assert len(compiled.html_content) == 1
199
+ assert compiled.html_content[0] == expected_html
200
+ assert compiled.css_content == expected_css
201
+
202
+ def test_compile_force_one_line(self):
203
+ website = TestWebsite.SimpleWebsite()
204
+ expected_css = "\n".join([
205
+ ".r0 {",
206
+ " margin: 0;",
207
+ "}",
208
+ "",
209
+ ".r1 {",
210
+ " padding: 0;",
211
+ "}"
212
+ ])
213
+
214
+ # Force one line HTML
215
+ compiled_true = website.compile(force_one_line=True)
216
+ expected_html_true = ''.join([
217
+ "<!DOCTYPE html>",
218
+ "<html>",
219
+ "<head>",
220
+ '<link href="styles.css" rel="stylesheet">',
221
+ "</head>",
222
+ "<body>",
223
+ '<htmlnode class="r1">',
224
+ "Text!",
225
+ "</htmlnode>",
226
+ '<htmlnode class="r0">',
227
+ "Another Text!",
228
+ "</htmlnode>",
229
+ "</body>",
230
+ "</html>"
231
+ ])
232
+ assert len(compiled_true.html_content) == 1
233
+ assert compiled_true.html_content[0] == expected_html_true
234
+ assert compiled_true.css_content == expected_css
235
+
236
+ # Don't force one line HTML
237
+ compiled_false = website.compile(force_one_line=False)
238
+ expected_html_false = "\n".join([
239
+ "<!DOCTYPE html>",
240
+ "<html>",
241
+ " <head>",
242
+ ' <link href="styles.css" rel="stylesheet">',
243
+ " </head>",
244
+ " <body>",
245
+ ' <htmlnode class="r1">',
246
+ " Text!",
247
+ " </htmlnode>",
248
+ ' <htmlnode class="r0">',
249
+ " Another Text!",
250
+ " </htmlnode>",
251
+ " </body>",
252
+ "</html>"
253
+ ])
254
+ assert len(compiled_false.html_content) == 1
255
+ assert compiled_false.html_content[0] == expected_html_false
256
+ assert compiled_false.css_content == expected_css
257
+
258
+ @pytest.mark.parametrize("indent_level", [0, 1, 2])
259
+ @pytest.mark.parametrize("indent_size", [2, 3, 4, 8])
260
+ def test_compile_indentation(self, indent_level: int, indent_size: int):
261
+ """Test the `compile` method with custom indentation levels and sizes."""
262
+ website = TestWebsite.SimpleWebsite()
263
+ compiled = website.compile(
264
+ indent_level=indent_level,
265
+ indent_size=indent_size
266
+ )
267
+
268
+ # Check the results
269
+ expected_html = "\n".join([
270
+ f"{' ' * indent_size * indent_level}<!DOCTYPE html>",
271
+ f"{' ' * indent_size * indent_level}<html>",
272
+ f"{' ' * indent_size * (indent_level + 1)}<head>",
273
+ f'{" " * indent_size * (indent_level + 2)}<link href="styles.css" rel="stylesheet">',
274
+ f"{' ' * indent_size * (indent_level + 1)}</head>",
275
+ f"{' ' * indent_size * (indent_level + 1)}<body>",
276
+ f'{" " * indent_size * (indent_level + 2)}<htmlnode class="r1">',
277
+ f"{' ' * indent_size * (indent_level + 3)}Text!",
278
+ f"{' ' * indent_size * (indent_level + 2)}</htmlnode>",
279
+ f'{" " * indent_size * (indent_level + 2)}<htmlnode class="r0">',
280
+ f"{' ' * indent_size * (indent_level + 3)}Another Text!",
281
+ f"{' ' * indent_size * (indent_level + 2)}</htmlnode>",
282
+ f"{' ' * indent_size * (indent_level + 1)}</body>",
283
+ f"{' ' * indent_size * indent_level}</html>"
284
+ ])
285
+ assert len(compiled.html_content) == 1
286
+ assert compiled.html_content[0] == expected_html
287
+ expected_css = "\n".join([
288
+ ".r0 {",
289
+ f"{' ' * indent_size}margin: 0;",
290
+ "}",
291
+ "",
292
+ ".r1 {",
293
+ f"{' ' * indent_size}padding: 0;",
294
+ "}"
295
+ ])
296
+ assert compiled.css_content == expected_css
297
+
298
+ @pytest.mark.parametrize("indent_level", [-2, -1])
299
+ def test_compile_negative_indent_levels(self, indent_level: int):
300
+ website = TestWebsite.SimpleWebsite()
301
+ compiled = website.compile(indent_level=indent_level)
302
+ indent = " " if indent_level == -1 else ""
303
+ expected_html = "\n".join([
304
+ "<!DOCTYPE html>",
305
+ "<html>",
306
+ "<head>",
307
+ f'{indent}<link href="styles.css" rel="stylesheet">',
308
+ "</head>",
309
+ "<body>",
310
+ f'{indent}<htmlnode class="r1">',
311
+ f"{indent} Text!",
312
+ f"{indent}</htmlnode>",
313
+ f'{indent}<htmlnode class="r0">',
314
+ f"{indent} Another Text!",
315
+ f"{indent}</htmlnode>",
316
+ "</body>",
317
+ "</html>"
318
+ ])
319
+ expected_css = "\n".join([
320
+ ".r0 {",
321
+ " margin: 0;",
322
+ "}",
323
+ "",
324
+ ".r1 {",
325
+ " padding: 0;",
326
+ "}"
327
+ ])
328
+ assert len(compiled.html_content) == 1
329
+ assert compiled.html_content[0] == expected_html
330
+ assert compiled.css_content == expected_css
331
+
332
+ def test_compile_rule_namer(self):
333
+ """Test the `compile` method with a custom rule namer function."""
334
+ # Define a custom rule namer function
335
+ def custom_rule_namer(rules, index):
336
+ return f"custom_{index}_{list(rules[index].declarations.keys())[0]}"
337
+
338
+ # Compile a simple website with the custom rule namer
339
+ website = TestWebsite.SimpleWebsite()
340
+ compiled = website.compile(rule_namer=custom_rule_namer)
341
+
342
+ # Check the results
343
+ expected_html = "\n".join([
344
+ "<!DOCTYPE html>",
345
+ "<html>",
346
+ " <head>",
347
+ ' <link href="styles.css" rel="stylesheet">',
348
+ " </head>",
349
+ " <body>",
350
+ ' <htmlnode class="custom_1_padding">',
351
+ " Text!",
352
+ " </htmlnode>",
353
+ ' <htmlnode class="custom_0_margin">',
354
+ " Another Text!",
355
+ " </htmlnode>",
356
+ " </body>",
357
+ "</html>"
358
+ ])
359
+ assert len(compiled.html_content) == 1
360
+ assert compiled.html_content[0] == expected_html
361
+ expected_css = "\n".join([
362
+ ".custom_0_margin {",
363
+ " margin: 0;",
364
+ "}",
365
+ "",
366
+ ".custom_1_padding {",
367
+ " padding: 0;",
368
+ "}"
369
+ ])
370
+ assert compiled.css_content == expected_css
371
+
372
+ def test_compile_kwargs(self):
373
+ website = TestWebsite.SimpleWebsite()
374
+ compiled = website.compile(replace_all_entities=True)
375
+ expected_html = "\n".join([
376
+ "<!DOCTYPE html>",
377
+ "<html>",
378
+ " <head>",
379
+ ' <link href="styles.css" rel="stylesheet">',
380
+ " </head>",
381
+ " <body>",
382
+ ' <htmlnode class="r1">',
383
+ " Text&excl;",
384
+ " </htmlnode>",
385
+ ' <htmlnode class="r0">',
386
+ " Another Text&excl;",
387
+ " </htmlnode>",
388
+ " </body>",
389
+ "</html>"
390
+ ])
391
+ expected_css = "\n".join([
392
+ ".r0 {",
393
+ " margin: 0;",
394
+ "}",
395
+ "",
396
+ ".r1 {",
397
+ " padding: 0;",
398
+ "}"
399
+ ])
400
+ assert len(compiled.html_content) == 1
401
+ assert compiled.html_content[0] == expected_html
402
+ assert compiled.css_content == expected_css
@@ -0,0 +1,11 @@
1
+ # =======================================================================
2
+ #
3
+ # This file is part of WebWidgets, a Python package for designing web
4
+ # UIs.
5
+ #
6
+ # You should have received a copy of the MIT License along with
7
+ # WebWidgets. If not, see <https://opensource.org/license/mit>.
8
+ #
9
+ # Copyright(C) 2025, mlaasri
10
+ #
11
+ # =======================================================================