webwidgets 0.2.0__py3-none-any.whl → 1.0.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.
- webwidgets/__init__.py +4 -1
- webwidgets/compilation/css/__init__.py +2 -1
- webwidgets/compilation/css/css.py +90 -31
- webwidgets/compilation/html/__init__.py +2 -1
- webwidgets/compilation/html/html_node.py +45 -5
- webwidgets/compilation/html/html_tags.py +38 -1
- webwidgets/utility/__init__.py +2 -0
- webwidgets/utility/indentation.py +25 -0
- webwidgets/utility/representation.py +34 -0
- webwidgets/website/__init__.py +14 -0
- webwidgets/website/compiled_website.py +34 -0
- webwidgets/website/website.py +88 -0
- webwidgets/widgets/__init__.py +14 -0
- webwidgets/widgets/containers/__init__.py +14 -0
- webwidgets/widgets/containers/container.py +38 -0
- webwidgets/widgets/containers/page.py +59 -0
- webwidgets/widgets/widget.py +34 -0
- webwidgets-1.0.0.dist-info/METADATA +66 -0
- webwidgets-1.0.0.dist-info/RECORD +24 -0
- webwidgets-0.2.0.dist-info/METADATA +0 -18
- webwidgets-0.2.0.dist-info/RECORD +0 -14
- {webwidgets-0.2.0.dist-info → webwidgets-1.0.0.dist-info}/WHEEL +0 -0
- {webwidgets-0.2.0.dist-info → webwidgets-1.0.0.dist-info}/licenses/LICENSE +0 -0
webwidgets/__init__.py
CHANGED
@@ -10,6 +10,9 @@
|
|
10
10
|
#
|
11
11
|
# =======================================================================
|
12
12
|
|
13
|
-
__version__ = "0.
|
13
|
+
__version__ = "1.0.0" # Dynamically set by build backend
|
14
14
|
|
15
15
|
from . import compilation
|
16
|
+
from . import utility
|
17
|
+
from .website import *
|
18
|
+
from .widgets import *
|
@@ -11,31 +11,50 @@
|
|
11
11
|
# =======================================================================
|
12
12
|
|
13
13
|
import itertools
|
14
|
-
from typing import Dict, List, Union
|
14
|
+
from typing import Callable, Dict, List, Union
|
15
15
|
from webwidgets.compilation.html.html_node import HTMLNode
|
16
|
+
from webwidgets.utility.indentation import get_indentation
|
17
|
+
from webwidgets.utility.representation import ReprMixin
|
16
18
|
from webwidgets.utility.validation import validate_css_identifier
|
17
19
|
|
18
20
|
|
19
|
-
class
|
21
|
+
class CSSRule(ReprMixin):
|
22
|
+
"""A rule in a style sheet.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, name: str, declarations: Dict[str, str]):
|
26
|
+
"""Stores the name and declarations of the rule.
|
27
|
+
|
28
|
+
:param name: The name of the rule.
|
29
|
+
:type name: str
|
30
|
+
:param declarations: The CSS declarations for the rule, specified as a
|
31
|
+
dictionary where keys are property names and values are their
|
32
|
+
corresponding values. For example: `{'color': 'red'}`
|
33
|
+
:type declarations: Dict[str, str]
|
34
|
+
"""
|
35
|
+
super().__init__()
|
36
|
+
self.name = name
|
37
|
+
self.declarations = declarations
|
38
|
+
|
39
|
+
|
40
|
+
class CompiledCSS(ReprMixin):
|
20
41
|
"""A utility class to hold compiled CSS rules.
|
21
42
|
"""
|
22
43
|
|
23
|
-
def __init__(self, trees: List[HTMLNode], rules:
|
24
|
-
mapping: Dict[int, List[
|
44
|
+
def __init__(self, trees: List[HTMLNode], rules: List[CSSRule],
|
45
|
+
mapping: Dict[int, List[CSSRule]]):
|
25
46
|
"""Stores compiled CSS rules.
|
26
47
|
|
27
48
|
:param trees: The HTML trees at the origin of the compilation. These
|
28
49
|
are the elements that have been styled with CSS properties.
|
29
50
|
:type trees: List[HTMLNode]
|
30
|
-
:param rules: The compiled CSS rules
|
31
|
-
|
32
|
-
`{'r0': {'color': 'red'}}`.
|
33
|
-
:type rules: Dict[str, Dict[str, str]]
|
51
|
+
:param rules: The compiled CSS rules.
|
52
|
+
:type rules: List[CSSRule]
|
34
53
|
:param mapping: A dictionary mapping each node ID to a list of rules
|
35
|
-
that achieve the same style.
|
36
|
-
|
37
|
-
:type mapping: Dict[int, List[str]]
|
54
|
+
that achieve the same style.
|
55
|
+
:type mapping: Dict[int, List[CSSRule]]
|
38
56
|
"""
|
57
|
+
super().__init__()
|
39
58
|
self.trees = trees
|
40
59
|
self.rules = rules
|
41
60
|
self.mapping = mapping
|
@@ -44,9 +63,9 @@ class CompiledCSS:
|
|
44
63
|
"""Converts the `rules` dictionary of the :py:class:`CompiledCSS`
|
45
64
|
object into CSS code.
|
46
65
|
|
47
|
-
|
48
|
-
is validated with :py:func:`validate_css_identifier`
|
49
|
-
converted.
|
66
|
+
Rule names are converted to class selectors. Note that each rule and
|
67
|
+
property name is validated with :py:func:`validate_css_identifier`
|
68
|
+
before being converted.
|
50
69
|
|
51
70
|
:param indent_size: The number of spaces to use for indentation in the
|
52
71
|
CSS code. Defaults to 4.
|
@@ -56,12 +75,13 @@ class CompiledCSS:
|
|
56
75
|
"""
|
57
76
|
# Initializing code and defining indentation
|
58
77
|
css_code = ""
|
59
|
-
indentation =
|
78
|
+
indentation = get_indentation(level=1, size=indent_size)
|
60
79
|
|
61
|
-
# Writing down each rule
|
62
|
-
for i,
|
63
|
-
|
64
|
-
|
80
|
+
# Writing down each rule
|
81
|
+
for i, rule in enumerate(self.rules):
|
82
|
+
validate_css_identifier(rule.name)
|
83
|
+
css_code += f".{rule.name}" + " {\n"
|
84
|
+
for property_name, value in rule.declarations.items():
|
65
85
|
validate_css_identifier(property_name)
|
66
86
|
css_code += f"{indentation}{property_name}: {value};\n"
|
67
87
|
css_code += "}" + ('\n\n' if i < len(self.rules) - 1 else '')
|
@@ -69,7 +89,9 @@ class CompiledCSS:
|
|
69
89
|
return css_code
|
70
90
|
|
71
91
|
|
72
|
-
def compile_css(trees: Union[HTMLNode, List[HTMLNode]]
|
92
|
+
def compile_css(trees: Union[HTMLNode, List[HTMLNode]],
|
93
|
+
rule_namer: Callable[[List[CSSRule], int],
|
94
|
+
str] = None) -> CompiledCSS:
|
73
95
|
"""Computes optimized CSS rules from the given HTML trees.
|
74
96
|
|
75
97
|
The main purpose of this function is to reduce the number of CSS rules
|
@@ -97,15 +119,31 @@ def compile_css(trees: Union[HTMLNode, List[HTMLNode]]) -> CompiledCSS:
|
|
97
119
|
|
98
120
|
>>> compiled_css = compile_css(tree)
|
99
121
|
>>> print(compiled_css.rules)
|
100
|
-
|
101
|
-
'r0'
|
102
|
-
'r1'
|
103
|
-
'r2'
|
104
|
-
|
122
|
+
[
|
123
|
+
CSSRule(name='r0', declarations={'color': 'blue'}),
|
124
|
+
CSSRule(name='r1', declarations={'margin': '0'}),
|
125
|
+
CSSRule(name='r2', declarations={'padding': '0'})
|
126
|
+
]
|
105
127
|
|
106
128
|
:param trees: A single tree or a list of trees to optimize over. All
|
107
129
|
children are recursively included in the compilation.
|
108
130
|
:type trees: Union[HTMLNode, List[HTMLNode]]
|
131
|
+
:param rule_namer: A callable that takes two arguments, which are the list
|
132
|
+
of all compiled rules and an index within that list, and returns a
|
133
|
+
unique name for the rule at the given index.
|
134
|
+
|
135
|
+
This argument allows to customize the rule naming process and use names
|
136
|
+
other than the default `"r0"`, `"r1"`, etc. For example, it can be used
|
137
|
+
to achieve something similar to Tailwind CSS and name rules according
|
138
|
+
to what they achieve, e.g. by prefixing their name with `"m"` for
|
139
|
+
margin rules or `"p"` for padding rules. Note that all rule names will
|
140
|
+
be validated with the :py:func:`validate_css_identifier` function
|
141
|
+
before being written into CSS code.
|
142
|
+
|
143
|
+
Defaults to the :py:func:`default_rule_namer` function which implements
|
144
|
+
a default naming strategy where each rule is named `"r{i}"` where `i`
|
145
|
+
is the index of the rule in the list.
|
146
|
+
:type rule_namer: Callable[[List[CSSRule], int], str]
|
109
147
|
:return: The :py:class:`CompiledCSS` object containing the optimized rules.
|
110
148
|
Every HTML node present in one or more of the input trees is included
|
111
149
|
in the :py:attr:`CompiledCSS.mapping` attribute, even if the node does
|
@@ -117,14 +155,21 @@ def compile_css(trees: Union[HTMLNode, List[HTMLNode]]) -> CompiledCSS:
|
|
117
155
|
if isinstance(trees, HTMLNode):
|
118
156
|
trees = [trees]
|
119
157
|
|
120
|
-
#
|
121
|
-
|
158
|
+
# Handling default rule_namer
|
159
|
+
rule_namer = default_rule_namer if rule_namer is None else rule_namer
|
160
|
+
|
161
|
+
# We compute a simple mapping where each CSS property defines its own
|
162
|
+
# ruleset
|
122
163
|
styles = {k: v for tree in trees for k, v in tree.get_styles().items()}
|
123
164
|
properties = set(itertools.chain.from_iterable(s.items()
|
124
165
|
for s in styles.values()))
|
125
|
-
rules =
|
126
|
-
|
127
|
-
|
166
|
+
rules = [CSSRule(None, dict([p])) # Initializing with no name
|
167
|
+
for p in sorted(properties)]
|
168
|
+
for i, rule in enumerate(rules): # Assigning name from callback
|
169
|
+
rule.name = rule_namer(rules, i)
|
170
|
+
rules = sorted(rules, key=lambda r: r.name) # Sorting by name
|
171
|
+
mapping = {node_id: [r for r in rules if
|
172
|
+
set(r.declarations.items()).issubset(style.items())]
|
128
173
|
for node_id, style in styles.items()}
|
129
174
|
return CompiledCSS(trees, rules, mapping)
|
130
175
|
|
@@ -156,7 +201,7 @@ def apply_css(css: CompiledCSS, tree: HTMLNode) -> None:
|
|
156
201
|
|
157
202
|
# Listing rules to add as classes. We do not add rules that are already
|
158
203
|
# there.
|
159
|
-
rules_to_add = [r for r in css.mapping[id(tree)] if r
|
204
|
+
rules_to_add = [r.name for r in css.mapping[id(tree)] if r.name not in
|
160
205
|
tree.attributes.get('class', '').split(' ')]
|
161
206
|
|
162
207
|
# Updating the class attribute. If it already exists and is not empty,
|
@@ -169,3 +214,17 @@ def apply_css(css: CompiledCSS, tree: HTMLNode) -> None:
|
|
169
214
|
# Recursively applying the CSS rules to all child nodes of the tree
|
170
215
|
for child in tree.children:
|
171
216
|
apply_css(css, child)
|
217
|
+
|
218
|
+
|
219
|
+
def default_rule_namer(rules: List[CSSRule], index: int) -> str:
|
220
|
+
"""Default rule naming function. Returns a string like "r{i}" where {i} is
|
221
|
+
the index of the rule.
|
222
|
+
|
223
|
+
:param rules: List of all compiled CSSRule objects. This argument is not
|
224
|
+
used in this function, but it can be used in other naming strategies.
|
225
|
+
:type rules: List[CSSRule]
|
226
|
+
:param index: Index of the rule being named.
|
227
|
+
:type index: int
|
228
|
+
:return: A string like `"r{i}"` where `i` is the index of the rule.
|
229
|
+
"""
|
230
|
+
return f'r{index}'
|
@@ -10,5 +10,6 @@
|
|
10
10
|
#
|
11
11
|
# =======================================================================
|
12
12
|
|
13
|
-
from .html_node import HTMLNode, no_start_tag, no_end_tag, one_line, RawText
|
13
|
+
from .html_node import HTMLNode, no_start_tag, no_end_tag, one_line, RawText, \
|
14
|
+
RootNode
|
14
15
|
from .html_tags import TextNode
|
@@ -13,11 +13,13 @@
|
|
13
13
|
import copy
|
14
14
|
import itertools
|
15
15
|
from typing import Any, Dict, List, Union
|
16
|
+
from webwidgets.utility.indentation import get_indentation
|
17
|
+
from webwidgets.utility.representation import ReprMixin
|
16
18
|
from webwidgets.utility.sanitizing import sanitize_html_text
|
17
19
|
from webwidgets.utility.validation import validate_html_class
|
18
20
|
|
19
21
|
|
20
|
-
class HTMLNode:
|
22
|
+
class HTMLNode(ReprMixin):
|
21
23
|
"""Represents an HTML node (for example, a div or a span).
|
22
24
|
"""
|
23
25
|
|
@@ -34,6 +36,7 @@ class HTMLNode:
|
|
34
36
|
:param style: Dictionary of CSS properties for the node. Defaults to an empty dictionary.
|
35
37
|
:type style: Dict[str, str]
|
36
38
|
"""
|
39
|
+
super().__init__()
|
37
40
|
self.children = [] if children is None else children
|
38
41
|
self.attributes = {} if attributes is None else attributes
|
39
42
|
self.style = {} if style is None else style
|
@@ -51,11 +54,13 @@ class HTMLNode:
|
|
51
54
|
def _render_attributes(self) -> str:
|
52
55
|
"""Renders the attributes of the HTML node into a string that can be added to the start tag.
|
53
56
|
|
57
|
+
Attributes are sorted alphabetically by name.
|
58
|
+
|
54
59
|
:return: A string containing all attribute key-value pairs separated by spaces.
|
55
60
|
:rtype: str
|
56
61
|
"""
|
57
62
|
return ' '.join(
|
58
|
-
f'{
|
63
|
+
f'{k}="{v}"' for k, v in sorted(self.attributes.items())
|
59
64
|
)
|
60
65
|
|
61
66
|
def add(self, child: 'HTMLNode') -> None:
|
@@ -134,7 +139,14 @@ class HTMLNode:
|
|
134
139
|
:type collapse_empty: bool
|
135
140
|
:param indent_size: The number of spaces to use for each indentation level.
|
136
141
|
:type indent_size: int
|
137
|
-
:param indent_level: The current level of indentation in the HTML
|
142
|
+
:param indent_level: The current level of indentation in the HTML
|
143
|
+
output.
|
144
|
+
|
145
|
+
This argument supports negative values as a way to flatten the HTML
|
146
|
+
output down to a certain depth with indentation resuming as normal
|
147
|
+
afterwards. If negative, `indent_level` is construed as an offset
|
148
|
+
on the depth in the HTML tree represented by the node, in which
|
149
|
+
case the node will wait for that depth before starting indentation.
|
138
150
|
:type indent_level: int
|
139
151
|
:param force_one_line: If True, forces all child elements to be rendered on a single line without additional
|
140
152
|
indentation. Defaults to False.
|
@@ -149,7 +161,8 @@ class HTMLNode:
|
|
149
161
|
:rtype: str or List[str]
|
150
162
|
"""
|
151
163
|
# Opening the element
|
152
|
-
indentation = "" if force_one_line else
|
164
|
+
indentation = "" if force_one_line else get_indentation(
|
165
|
+
indent_level, indent_size)
|
153
166
|
html_lines = [indentation + self.start_tag]
|
154
167
|
|
155
168
|
# If content must be in one line
|
@@ -262,7 +275,34 @@ class RawText(HTMLNode):
|
|
262
275
|
"""
|
263
276
|
sanitized = sanitize_html_text(
|
264
277
|
self.text, replace_all_entities=replace_all_entities)
|
265
|
-
line =
|
278
|
+
line = get_indentation(indent_level, indent_size) + sanitized
|
266
279
|
if return_lines:
|
267
280
|
return [line]
|
268
281
|
return line
|
282
|
+
|
283
|
+
|
284
|
+
@no_start_tag
|
285
|
+
@no_end_tag
|
286
|
+
class RootNode(HTMLNode):
|
287
|
+
"""The root node of an HTML document.
|
288
|
+
|
289
|
+
This is the top-level node that contains all other nodes.
|
290
|
+
"""
|
291
|
+
|
292
|
+
def to_html(self, indent_level: int = 0, **kwargs: Any) -> Union[str, List[str]]:
|
293
|
+
"""Converts the root node to HTML code.
|
294
|
+
|
295
|
+
This method overrides :py:meth:`HTMLNode.to_html`. The only difference
|
296
|
+
between this method and that of the base class is that the indentation
|
297
|
+
level is adjusted by one level, so the root node acts as an array of
|
298
|
+
elements.
|
299
|
+
|
300
|
+
:param indent_level: See :py:meth:`HTMLNode.to_html`.
|
301
|
+
:type indent_level: int
|
302
|
+
:param kwargs: Other keyword arguments. These are passed to
|
303
|
+
:py:meth:`HTMLNode.to_html`.
|
304
|
+
:type kwargs: Any
|
305
|
+
:return: See :py:meth:`HTMLNode.to_html`.
|
306
|
+
:type return: str or List[str]
|
307
|
+
"""
|
308
|
+
return super().to_html(indent_level=indent_level - 1, **kwargs)
|
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# =======================================================================
|
12
12
|
|
13
|
-
from .html_node import HTMLNode, one_line, RawText
|
13
|
+
from .html_node import HTMLNode, no_end_tag, one_line, RawText
|
14
14
|
from typing import Dict
|
15
15
|
|
16
16
|
|
@@ -38,3 +38,40 @@ class TextNode(HTMLNode):
|
|
38
38
|
super().__init__(children=[
|
39
39
|
RawText(text)
|
40
40
|
], attributes=attributes, style=style)
|
41
|
+
|
42
|
+
|
43
|
+
class Body(HTMLNode):
|
44
|
+
"""The `<body>` element containing the visible content of a document."""
|
45
|
+
pass
|
46
|
+
|
47
|
+
|
48
|
+
@one_line
|
49
|
+
@no_end_tag
|
50
|
+
class Doctype(HTMLNode):
|
51
|
+
"""The `<!DOCTYPE html>` doctype declaration of a document."""
|
52
|
+
|
53
|
+
def __init__(self):
|
54
|
+
"""Creates a `<!DOCTYPE html>` doctype declaration element."""
|
55
|
+
super().__init__()
|
56
|
+
|
57
|
+
@property
|
58
|
+
def start_tag(self) -> str:
|
59
|
+
"""Overrides the start tag for this node."""
|
60
|
+
return "<!DOCTYPE html>"
|
61
|
+
|
62
|
+
|
63
|
+
class Head(HTMLNode):
|
64
|
+
"""The `<head>` element containing metadata about a document."""
|
65
|
+
pass
|
66
|
+
|
67
|
+
|
68
|
+
class Html(HTMLNode):
|
69
|
+
"""The `<html>` element of an HTML document."""
|
70
|
+
pass
|
71
|
+
|
72
|
+
|
73
|
+
@one_line
|
74
|
+
@no_end_tag
|
75
|
+
class Link(HTMLNode):
|
76
|
+
"""A `<link>` element for linking to external resources."""
|
77
|
+
pass
|
webwidgets/utility/__init__.py
CHANGED
@@ -0,0 +1,25 @@
|
|
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
|
+
def get_indentation(level: int, size: int = 4) -> str:
|
14
|
+
"""Returns an indentation string for the given level.
|
15
|
+
|
16
|
+
:param level: The level of indentation. If negative, this
|
17
|
+
function will return an empty string representing no indentation.
|
18
|
+
:type level: int
|
19
|
+
:param size: The number of spaces to use for each indentation level.
|
20
|
+
Defaults to 4 spaces.
|
21
|
+
:type size: int
|
22
|
+
:return: A string representing the indentation.
|
23
|
+
:rtype: str
|
24
|
+
"""
|
25
|
+
return ' ' * (max(level, 0) * size)
|
@@ -0,0 +1,34 @@
|
|
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
|
+
class ReprMixin:
|
14
|
+
"""A mixin class that is represented with its variables when printed.
|
15
|
+
|
16
|
+
For example:
|
17
|
+
|
18
|
+
>>> class MyClass(RepresentedWithVars):
|
19
|
+
... def __init__(self, a, b):
|
20
|
+
... self.a = a
|
21
|
+
... self.b = b
|
22
|
+
>>> obj = MyClass(1, 2)
|
23
|
+
>>> print(obj)
|
24
|
+
MyClass(a=1, b=2)
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __repr__(self) -> str:
|
28
|
+
"""Returns a string exposing all member variables of the class.
|
29
|
+
|
30
|
+
:return: A string representing the class with its variables.
|
31
|
+
:rtype: str
|
32
|
+
"""
|
33
|
+
variables = ', '.join(f'{k}={repr(v)}' for k, v in vars(self).items())
|
34
|
+
return f"{self.__class__.__name__}({variables})"
|
@@ -0,0 +1,14 @@
|
|
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
|
+
from .compiled_website import CompiledWebsite
|
14
|
+
from .website import Website
|
@@ -0,0 +1,34 @@
|
|
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
|
+
from typing import List
|
14
|
+
from webwidgets.utility.representation import ReprMixin
|
15
|
+
|
16
|
+
|
17
|
+
class CompiledWebsite(ReprMixin):
|
18
|
+
"""A utility class to store compiled HTML and CSS code obtained from a
|
19
|
+
:py:class:`Website` object.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, html_content: List[str], css_content: str):
|
23
|
+
"""Stores compiled HTML and CSS content.
|
24
|
+
|
25
|
+
:param html_content: The compiled HTML code of each page in the
|
26
|
+
website.
|
27
|
+
:type html_content: List[str]
|
28
|
+
:param css_content: The compiled CSS code of the website, shared across
|
29
|
+
all pages.
|
30
|
+
:type css_content: str
|
31
|
+
"""
|
32
|
+
super().__init__()
|
33
|
+
self.html_content = html_content
|
34
|
+
self.css_content = css_content
|
@@ -0,0 +1,88 @@
|
|
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
|
+
from .compiled_website import CompiledWebsite
|
14
|
+
from typing import Any, Callable, List
|
15
|
+
from webwidgets.compilation.css import compile_css, CSSRule, apply_css
|
16
|
+
from webwidgets.utility.representation import ReprMixin
|
17
|
+
from webwidgets.widgets.containers.page import Page
|
18
|
+
|
19
|
+
|
20
|
+
class Website(ReprMixin):
|
21
|
+
"""A collection of :py:class:`Page` objects that make up the structure of a
|
22
|
+
web site."""
|
23
|
+
|
24
|
+
def __init__(self, pages: List[Page] = None):
|
25
|
+
"""Creates a new website with an optional list of pages.
|
26
|
+
|
27
|
+
:param pages: The pages of the website. Defaults to an empty list.
|
28
|
+
:type pages: List[Page]
|
29
|
+
"""
|
30
|
+
super().__init__()
|
31
|
+
self.pages = [] if pages is None else pages
|
32
|
+
|
33
|
+
def add(self, page: Page):
|
34
|
+
"""Adds a new page to the website.
|
35
|
+
|
36
|
+
:param page: The page to be added.
|
37
|
+
:type page: Page
|
38
|
+
"""
|
39
|
+
self.pages.append(page)
|
40
|
+
|
41
|
+
def compile(self,
|
42
|
+
collapse_empty: bool = True,
|
43
|
+
css_file_name: str = "styles.css",
|
44
|
+
force_one_line: bool = False,
|
45
|
+
indent_level: int = 0,
|
46
|
+
indent_size: int = 4,
|
47
|
+
rule_namer: Callable[[List[CSSRule], int], str] = None,
|
48
|
+
**kwargs: Any) -> CompiledWebsite:
|
49
|
+
"""Compiles the website into HTML and CSS code.
|
50
|
+
|
51
|
+
:param collapse_empty: See :py:meth:`HTMLNode.to_html`.
|
52
|
+
:type collapse_empty: bool
|
53
|
+
:param css_file_name: See :py:meth:`Page.build`.
|
54
|
+
:type css_file_name: str
|
55
|
+
:param force_one_line: See :py:meth:`HTMLNode.to_html`.
|
56
|
+
:type force_one_line: bool
|
57
|
+
:param indent_level: See :py:meth:`HTMLNode.to_html`.
|
58
|
+
:type indent_level: int
|
59
|
+
:param indent_size: See :py:meth:`HTMLNode.to_html` and
|
60
|
+
:py:meth:`CompiledCSS.to_css`.
|
61
|
+
:type indent_size: int
|
62
|
+
:param rule_namer: See :py:func:`compile_css`.
|
63
|
+
:type rule_namer: Callable[[List[CSSRule], int], str]
|
64
|
+
:param kwargs: See :py:meth:`HTMLNode.to_html`.
|
65
|
+
:type kwargs: Any
|
66
|
+
:return: A new :py:class:`CompiledWebsite` object containing the
|
67
|
+
compiled HTML and CSS code.
|
68
|
+
:rtype: CompiledWebsite
|
69
|
+
"""
|
70
|
+
# Building the HTML representation of each page
|
71
|
+
trees = [page.build(css_file_name=css_file_name)
|
72
|
+
for page in self.pages]
|
73
|
+
|
74
|
+
# Compiling HTML and CSS code
|
75
|
+
compiled_css = compile_css(trees, rule_namer)
|
76
|
+
for tree in trees:
|
77
|
+
apply_css(compiled_css, tree)
|
78
|
+
html_content = [tree.to_html(
|
79
|
+
collapse_empty=collapse_empty,
|
80
|
+
force_one_line=force_one_line,
|
81
|
+
indent_level=indent_level,
|
82
|
+
indent_size=indent_size,
|
83
|
+
**kwargs
|
84
|
+
) for tree in trees]
|
85
|
+
css_content = compiled_css.to_css(indent_size=indent_size)
|
86
|
+
|
87
|
+
# Storing the result in a new CompiledWebsite object
|
88
|
+
return CompiledWebsite(html_content, css_content)
|
@@ -0,0 +1,14 @@
|
|
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
|
+
from .containers import *
|
14
|
+
from .widget import Widget
|
@@ -0,0 +1,14 @@
|
|
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
|
+
from .container import Container
|
14
|
+
from .page import Page
|
@@ -0,0 +1,38 @@
|
|
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
|
+
from typing import List
|
14
|
+
from webwidgets.widgets.widget import Widget
|
15
|
+
|
16
|
+
|
17
|
+
class Container(Widget):
|
18
|
+
"""
|
19
|
+
A widget that can contain other widgets.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, widgets: List[Widget] = None):
|
23
|
+
"""Creates a new Container with optional widgets inside.
|
24
|
+
|
25
|
+
:param widgets: A list of widgets to be contained within the container.
|
26
|
+
Defaults to an empty list.
|
27
|
+
:type widgets: List[Widget]
|
28
|
+
"""
|
29
|
+
super().__init__()
|
30
|
+
self.widgets = [] if widgets is None else widgets
|
31
|
+
|
32
|
+
def add(self, widget: Widget) -> None:
|
33
|
+
"""Adds a widget to the container.
|
34
|
+
|
35
|
+
:param widget: The widget to add to the container.
|
36
|
+
:type widget: Widget
|
37
|
+
"""
|
38
|
+
self.widgets.append(widget)
|
@@ -0,0 +1,59 @@
|
|
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
|
+
from .container import Container
|
14
|
+
from webwidgets.compilation.html.html_node import RootNode
|
15
|
+
from webwidgets.compilation.html.html_tags import Body, Doctype, Head, Html, \
|
16
|
+
Link
|
17
|
+
|
18
|
+
|
19
|
+
class Page(Container):
|
20
|
+
"""A widget representing a web page. It contains other widgets and is
|
21
|
+
responsible for laying them out within the page.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def build(self, css_file_name: str = "styles.css") -> RootNode:
|
25
|
+
"""Builds the HTML representation of the page.
|
26
|
+
|
27
|
+
This method constructs an HTML structure that includes a doctype
|
28
|
+
declaration, a head section with meta tags, and a body section
|
29
|
+
containing the widgets. The widgets are rendered recurisvely by calling
|
30
|
+
their :py:meth:`build` method.
|
31
|
+
|
32
|
+
:param css_file_name: The name of the CSS file to link to the page if
|
33
|
+
the page elements contain any styles. Defaults to "styles.css".
|
34
|
+
:type css_file_name: str
|
35
|
+
:return: An :py:class:`RootNode` object representing the page.
|
36
|
+
:rtype: RootNode
|
37
|
+
"""
|
38
|
+
# Building nodes from the page's widgets
|
39
|
+
nodes = [w.build() for w in self.widgets]
|
40
|
+
|
41
|
+
# Initializing the head section of the page
|
42
|
+
head = Head()
|
43
|
+
|
44
|
+
# Checking if there is any style sheet to link to the page.
|
45
|
+
# To do so, we just check if any child node has a non-empty style.
|
46
|
+
if any(style for n in nodes for style in n.get_styles().values()):
|
47
|
+
head.add(Link(
|
48
|
+
attributes={"href": css_file_name, "rel": "stylesheet"}
|
49
|
+
))
|
50
|
+
|
51
|
+
# Building the HTML representation of the page
|
52
|
+
return RootNode(
|
53
|
+
children=[
|
54
|
+
Doctype(),
|
55
|
+
Html(
|
56
|
+
children=[head, Body(children=nodes)]
|
57
|
+
)
|
58
|
+
]
|
59
|
+
)
|
@@ -0,0 +1,34 @@
|
|
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
|
+
from abc import ABC, abstractmethod
|
14
|
+
from webwidgets.compilation.html.html_node import HTMLNode
|
15
|
+
from webwidgets.utility.representation import ReprMixin
|
16
|
+
|
17
|
+
|
18
|
+
class Widget(ABC, ReprMixin):
|
19
|
+
"""
|
20
|
+
Abstract base class for all widgets.
|
21
|
+
|
22
|
+
All subclasses of :py:class:`Widget` must implement a :py:meth:`build`
|
23
|
+
method that returns an :py:class:`HTMLNode` object.
|
24
|
+
"""
|
25
|
+
|
26
|
+
@abstractmethod
|
27
|
+
def build(self) -> HTMLNode:
|
28
|
+
"""Builds the widget and returns the corresponding :py:class:`HTMLNode`
|
29
|
+
object.
|
30
|
+
|
31
|
+
This method must be overridden by subclasses to create specific HTML
|
32
|
+
elements.
|
33
|
+
"""
|
34
|
+
pass
|
@@ -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
|
+

|
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,24 @@
|
|
1
|
+
webwidgets/__init__.py,sha256=IZAAOuEwU_cC_oy0dJ2WSw9wXnwrLyZ4mqszBPma7MA,549
|
2
|
+
webwidgets/compilation/__init__.py,sha256=hb61nhmPTghIzuA_hun98xT5Ngv7QFAgMHD44g-9uOo,433
|
3
|
+
webwidgets/compilation/css/__init__.py,sha256=ZisiFaw9RqNuVqewOjnEOLLLZwgjfW95yruMXGRGp_I,484
|
4
|
+
webwidgets/compilation/css/css.py,sha256=93WuxY-ADUDK_cBuGS-hUuV4HctZjtaG3cqeoE4S3oQ,9725
|
5
|
+
webwidgets/compilation/html/__init__.py,sha256=iupXt6punHDLAFdygshmQeFVOLCeZ8HbwCWclL1FH54,521
|
6
|
+
webwidgets/compilation/html/html_node.py,sha256=lOf1LEoVx23teWHfdlt5IpSv14cE6EI-aDyFGC_BXSU,11697
|
7
|
+
webwidgets/compilation/html/html_tags.py,sha256=tFc5P6_rqyetFXJKlxt8n_IhBD758LHPql9peU9__6o,2188
|
8
|
+
webwidgets/utility/__init__.py,sha256=Sl-dzpPPTHykkmLSfobhqHmlzUSPtvhaR4xtJy_tiOg,505
|
9
|
+
webwidgets/utility/indentation.py,sha256=BaOQRqWdG7T5k_g1-ia9jewPFZjD3afjZH_Fc4NSVwo,906
|
10
|
+
webwidgets/utility/representation.py,sha256=lQ15v_DZOHBQKLM8pzRE1tuJkU_modhPTpWpSJ2lBCE,1061
|
11
|
+
webwidgets/utility/sanitizing.py,sha256=OKJRDqk-OXYCWeK6ie3GdfQvb49wTs93kd971mg5oK0,5770
|
12
|
+
webwidgets/utility/validation.py,sha256=bUjpiGP59GW3DPvQ1hwR5ezBMmcSd6v4xlDLwTHZv_A,4261
|
13
|
+
webwidgets/website/__init__.py,sha256=zp4N3CtY0SLNfDV9p2Y0tqbta-vFOX1PSJr7eQ9rQdk,471
|
14
|
+
webwidgets/website/compiled_website.py,sha256=lR_sabYtdWiRWicyxEFs4yxRUB_TbMowpsNz3CtqQBQ,1129
|
15
|
+
webwidgets/website/website.py,sha256=a5Qmm4DOIYMXHoBDyKzB6Ex2kaPFCSPgodF7LAVyrPE,3339
|
16
|
+
webwidgets/widgets/__init__.py,sha256=J2br7F-16URKvWshkJcc4nth27YQsaLrdVZu0xXx5CU,449
|
17
|
+
webwidgets/widgets/widget.py,sha256=8ZRcVmmtjQzeA_uGZi10H4XvqgGEtGmr9275FId8zt0,1039
|
18
|
+
webwidgets/widgets/containers/__init__.py,sha256=6LPlYaxXiMgC5YPHhi0HLhn7iCeh_5IFY70mv7a-wSA,452
|
19
|
+
webwidgets/widgets/containers/container.py,sha256=blpO2y9IiZ_4opwe9pqsSJPAZEN1P_ZRK4a27NpnMrg,1158
|
20
|
+
webwidgets/widgets/containers/page.py,sha256=sJ8QDmZ_6jzRFt4lyiAEPwjPvDTKm8EWOjz_Tq_cdjU,2180
|
21
|
+
webwidgets-1.0.0.dist-info/METADATA,sha256=DHfGCHdolxBicrkQjAzWGm_B7d_SgY2OnwZ261tSxUk,1434
|
22
|
+
webwidgets-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
23
|
+
webwidgets-1.0.0.dist-info/licenses/LICENSE,sha256=LISw1mw5eK6i8adFSlx6zltZxrJFwurngVdZAEU8g_I,1064
|
24
|
+
webwidgets-1.0.0.dist-info/RECORD,,
|
@@ -1,18 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: webwidgets
|
3
|
-
Version: 0.2.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
|
-

|
17
|
-
|
18
|
-
A Python package for creating web UIs
|
@@ -1,14 +0,0 @@
|
|
1
|
-
webwidgets/__init__.py,sha256=jkCnUIqDE1LjArrZKtl1Bpm9bf-ZmLUP5JBRTh3gayA,481
|
2
|
-
webwidgets/compilation/__init__.py,sha256=hb61nhmPTghIzuA_hun98xT5Ngv7QFAgMHD44g-9uOo,433
|
3
|
-
webwidgets/compilation/css/__init__.py,sha256=Yzk_Bq1ey8Bf4dcKNd_priwj6_DA9CwG9wFs-BbZRGE,449
|
4
|
-
webwidgets/compilation/css/css.py,sha256=8VjlXQtjY2fuhg49VtUi1RZF__Z7-qtj9-FAw3ZAtZA,7160
|
5
|
-
webwidgets/compilation/html/__init__.py,sha256=NRccbCUKdhmK51MnYFO8q1sS8fWhAggRnMiGDG7lQMQ,505
|
6
|
-
webwidgets/compilation/html/html_node.py,sha256=IEkb_zfIexU59cEk2Gf7nBo9Tm13DN_siLF7TBIGF1k,10095
|
7
|
-
webwidgets/compilation/html/html_tags.py,sha256=U2HmhLkV6BOqTmgvIMlmMCQysQ7i2nEAVWJzZe74ucA,1388
|
8
|
-
webwidgets/utility/__init__.py,sha256=_L0RxTAzAhjgZqg0eKEqpCJJeN_W2P9p5Clu7PETqCQ,448
|
9
|
-
webwidgets/utility/sanitizing.py,sha256=OKJRDqk-OXYCWeK6ie3GdfQvb49wTs93kd971mg5oK0,5770
|
10
|
-
webwidgets/utility/validation.py,sha256=bUjpiGP59GW3DPvQ1hwR5ezBMmcSd6v4xlDLwTHZv_A,4261
|
11
|
-
webwidgets-0.2.0.dist-info/METADATA,sha256=WDQK0YqoVt7imhRbXDw3QjrG9UgCflSvbHYDiNRM1mU,550
|
12
|
-
webwidgets-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
13
|
-
webwidgets-0.2.0.dist-info/licenses/LICENSE,sha256=LISw1mw5eK6i8adFSlx6zltZxrJFwurngVdZAEU8g_I,1064
|
14
|
-
webwidgets-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|