webwidgets 0.2.1__py3-none-any.whl → 1.1.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 +3 -2
- webwidgets/compilation/css/css.py +107 -123
- webwidgets/compilation/css/css_rule.py +104 -0
- webwidgets/compilation/css/sections/__init__.py +14 -0
- webwidgets/compilation/css/sections/css_section.py +106 -0
- webwidgets/compilation/css/sections/preamble.py +40 -0
- webwidgets/compilation/css/sections/rule_section.py +43 -0
- webwidgets/compilation/html/__init__.py +3 -2
- webwidgets/compilation/html/html_node.py +42 -4
- webwidgets/compilation/html/html_tags.py +43 -1
- webwidgets/utility/__init__.py +1 -0
- webwidgets/utility/indentation.py +25 -0
- webwidgets/utility/validation.py +68 -1
- 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 +15 -0
- webwidgets/widgets/containers/box.py +70 -0
- webwidgets/widgets/containers/container.py +38 -0
- webwidgets/widgets/containers/page.py +59 -0
- webwidgets/widgets/widget.py +33 -0
- webwidgets-1.1.0.dist-info/METADATA +71 -0
- webwidgets-1.1.0.dist-info/RECORD +30 -0
- webwidgets-0.2.1.dist-info/METADATA +0 -18
- webwidgets-0.2.1.dist-info/RECORD +0 -15
- {webwidgets-0.2.1.dist-info → webwidgets-1.1.0.dist-info}/WHEEL +0 -0
- {webwidgets-0.2.1.dist-info → webwidgets-1.1.0.dist-info}/licenses/LICENSE +0 -0
webwidgets/__init__.py
CHANGED
@@ -10,6 +10,9 @@
|
|
10
10
|
#
|
11
11
|
# =======================================================================
|
12
12
|
|
13
|
-
__version__ = "
|
13
|
+
__version__ = "1.1.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 *
|
@@ -10,5 +10,6 @@
|
|
10
10
|
#
|
11
11
|
# =======================================================================
|
12
12
|
|
13
|
-
from .css import
|
14
|
-
|
13
|
+
from .css import apply_css, compile_css, CompiledCSS, default_class_namer
|
14
|
+
from .css_rule import ClassRule, CSSRule
|
15
|
+
from . import sections
|
@@ -10,87 +10,102 @@
|
|
10
10
|
#
|
11
11
|
# =======================================================================
|
12
12
|
|
13
|
+
from .css_rule import ClassRule
|
13
14
|
import itertools
|
15
|
+
from .sections.preamble import Preamble
|
16
|
+
from .sections.rule_section import RuleSection
|
14
17
|
from typing import Callable, Dict, List, Union
|
15
18
|
from webwidgets.compilation.html.html_node import HTMLNode
|
16
19
|
from webwidgets.utility.representation import ReprMixin
|
17
|
-
from webwidgets.utility.validation import validate_css_identifier
|
18
|
-
|
19
|
-
|
20
|
-
class CSSRule(ReprMixin):
|
21
|
-
"""A rule in a style sheet.
|
22
|
-
"""
|
23
|
-
|
24
|
-
def __init__(self, name: str, declarations: Dict[str, str]):
|
25
|
-
"""Stores the name and declarations of the rule.
|
26
|
-
|
27
|
-
:param name: The name of the rule.
|
28
|
-
:type name: str
|
29
|
-
:param declarations: The CSS declarations for the rule, specified as a
|
30
|
-
dictionary where keys are property names and values are their
|
31
|
-
corresponding values. For example: `{'color': 'red'}`
|
32
|
-
:type declarations: Dict[str, str]
|
33
|
-
"""
|
34
|
-
super().__init__()
|
35
|
-
self.name = name
|
36
|
-
self.declarations = declarations
|
37
20
|
|
38
21
|
|
39
22
|
class CompiledCSS(ReprMixin):
|
40
23
|
"""A utility class to hold compiled CSS rules.
|
41
24
|
"""
|
42
25
|
|
43
|
-
def __init__(self, trees: List[HTMLNode],
|
44
|
-
mapping: Dict[int, List[
|
45
|
-
"""Stores compiled CSS rules
|
26
|
+
def __init__(self, trees: List[HTMLNode], core: RuleSection,
|
27
|
+
mapping: Dict[int, List[ClassRule]]):
|
28
|
+
"""Stores compiled CSS rules and their mapping to the nodes in the
|
29
|
+
given trees.
|
46
30
|
|
47
31
|
:param trees: The HTML trees at the origin of the compilation. These
|
48
32
|
are the elements that have been styled with CSS properties.
|
49
33
|
:type trees: List[HTMLNode]
|
50
|
-
:param rules: The compiled CSS rules.
|
51
|
-
:type rules:
|
34
|
+
:param rules: The CSS section containing the compiled CSS rules.
|
35
|
+
:type rules: RuleSection
|
52
36
|
:param mapping: A dictionary mapping each node ID to a list of rules
|
53
37
|
that achieve the same style.
|
54
|
-
:type mapping: Dict[int, List[
|
38
|
+
:type mapping: Dict[int, List[ClassRule]]
|
55
39
|
"""
|
56
40
|
super().__init__()
|
57
41
|
self.trees = trees
|
58
|
-
self.
|
42
|
+
self.preamble = Preamble()
|
43
|
+
self.core = core
|
59
44
|
self.mapping = mapping
|
60
45
|
|
61
46
|
def to_css(self, indent_size: int = 4) -> str:
|
62
|
-
"""Converts the `
|
63
|
-
object into CSS code.
|
47
|
+
"""Converts the `preamble` and `core` sections of the
|
48
|
+
:py:class:`CompiledCSS` object into CSS code.
|
64
49
|
|
65
|
-
|
66
|
-
|
67
|
-
before being converted.
|
50
|
+
Sections are converted with their :py:meth:`RuleSection.to_css`
|
51
|
+
methods.
|
68
52
|
|
69
|
-
:param indent_size:
|
70
|
-
CSS code. Defaults to 4.
|
53
|
+
:param indent_size: See :py:meth:`RuleSection.to_css`.
|
71
54
|
:type indent_size: int
|
72
55
|
:return: The CSS code as a string.
|
73
56
|
:rtype: str
|
74
57
|
"""
|
75
|
-
|
76
|
-
|
77
|
-
|
58
|
+
return '\n\n'.join(
|
59
|
+
section.to_css(indent_size=indent_size) for section in (
|
60
|
+
self.preamble, self.core
|
61
|
+
))
|
78
62
|
|
79
|
-
# Writing down each rule
|
80
|
-
for i, rule in enumerate(self.rules):
|
81
|
-
validate_css_identifier(rule.name)
|
82
|
-
css_code += f".{rule.name}" + " {\n"
|
83
|
-
for property_name, value in rule.declarations.items():
|
84
|
-
validate_css_identifier(property_name)
|
85
|
-
css_code += f"{indentation}{property_name}: {value};\n"
|
86
|
-
css_code += "}" + ('\n\n' if i < len(self.rules) - 1 else '')
|
87
63
|
|
88
|
-
|
64
|
+
def apply_css(css: CompiledCSS, tree: HTMLNode) -> None:
|
65
|
+
"""Applies the CSS rules to the given tree.
|
66
|
+
|
67
|
+
Rules are added as HTML classes to each node with a style in the tree. If a
|
68
|
+
node does not have a `class` attribute yet, it will be created for that
|
69
|
+
node. Nodes that do not have any style are left untouched.
|
70
|
+
|
71
|
+
Note that this function is recursive and calls itself on each child node of
|
72
|
+
the tree.
|
73
|
+
|
74
|
+
:param css: The compiled CSS object containing the rules to apply and the
|
75
|
+
mapping to each node. It should have been created by invoking
|
76
|
+
:py:func:`compile_css` on the given tree, but it can be modified before
|
77
|
+
passing it to this function, as long as its content remains consistent.
|
78
|
+
:type css: CompiledCSS
|
79
|
+
:param tree: The tree to which the CSS rules should be applied. It will be
|
80
|
+
modified in place by this function. If you want to keep the original
|
81
|
+
tree unchanged, make a deep copy of it using its
|
82
|
+
:py:meth:`HTMLNode.copy` method and pass this copy instead.
|
83
|
+
:type tree: HTMLNode
|
84
|
+
"""
|
85
|
+
# Only modifying nodes if they have a style (and therefore if the list of
|
86
|
+
# rules mapped to them in `css.mapping` is not empty)
|
87
|
+
if tree.style:
|
88
|
+
|
89
|
+
# Listing rules to add as classes. We do not add rules that are already
|
90
|
+
# there.
|
91
|
+
rules_to_add = [r.name for r in css.mapping[id(tree)] if r.name not in
|
92
|
+
tree.attributes.get('class', '').split(' ')]
|
93
|
+
|
94
|
+
# Updating the class attribute. If it already exists and is not empty,
|
95
|
+
# we need to insert a space before adding the CSS classes.
|
96
|
+
maybe_space = ' ' if tree.attributes.get(
|
97
|
+
'class', None) and rules_to_add else ''
|
98
|
+
tree.attributes['class'] = tree.attributes.get(
|
99
|
+
'class', '') + maybe_space + ' '.join(rules_to_add)
|
100
|
+
|
101
|
+
# Recursively applying the CSS rules to all child nodes of the tree
|
102
|
+
for child in tree.children:
|
103
|
+
apply_css(css, child)
|
89
104
|
|
90
105
|
|
91
106
|
def compile_css(trees: Union[HTMLNode, List[HTMLNode]],
|
92
|
-
|
93
|
-
|
107
|
+
class_namer: Callable[[List[ClassRule], int],
|
108
|
+
str] = None) -> CompiledCSS:
|
94
109
|
"""Computes optimized CSS rules from the given HTML trees.
|
95
110
|
|
96
111
|
The main purpose of this function is to reduce the number of CSS rules
|
@@ -117,36 +132,44 @@ def compile_css(trees: Union[HTMLNode, List[HTMLNode]],
|
|
117
132
|
.. code-block:: python
|
118
133
|
|
119
134
|
>>> compiled_css = compile_css(tree)
|
120
|
-
>>> print(compiled_css.rules)
|
135
|
+
>>> print(compiled_css.core.rules)
|
121
136
|
[
|
122
|
-
|
123
|
-
|
124
|
-
|
137
|
+
ClassRule(selector='.c0', declarations={'color': 'blue'}, ...),
|
138
|
+
ClassRule(selector='.c1', declarations={'margin': '0'}, ...),
|
139
|
+
ClassRule(selector='.c2', declarations={'padding': '0'}, ...)
|
125
140
|
]
|
126
141
|
|
142
|
+
Internally, each optimized rule gets compiled into a :py:class:`ClassRule`
|
143
|
+
object, which represents a CSS rule whose selector targets the HTML `class`
|
144
|
+
attribute. Each rule gets assigned a unique HTML class and all classes can
|
145
|
+
then be added to the trees with :py:func:`apply_css`. Classes are named
|
146
|
+
`"c0"`, `"c1"`, and so on by default, but this naming process can be
|
147
|
+
customized using the `class_namer` argument.
|
148
|
+
|
127
149
|
:param trees: A single tree or a list of trees to optimize over. All
|
128
150
|
children are recursively included in the compilation.
|
129
151
|
:type trees: Union[HTMLNode, List[HTMLNode]]
|
130
|
-
:param
|
152
|
+
:param class_namer: A callable that takes two arguments, which are the list
|
131
153
|
of all compiled rules and an index within that list, and returns a
|
132
|
-
unique name for the rule at the given
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
to
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
154
|
+
unique name for the HTML class to associate with the rule at the given
|
155
|
+
index.
|
156
|
+
|
157
|
+
This argument allows to customize the class naming process and use names
|
158
|
+
other than the default `"c0"`, `"c1"`, etc. For example, it can be used
|
159
|
+
to achieve something similar to Tailwind CSS and name HTML classes
|
160
|
+
according to what they achieve, e.g. by prefixing their name with `"m"`
|
161
|
+
for margin rules or `"p"` for padding rules. Note that all class
|
162
|
+
selectors will be validated with the :py:func:`validate_css_selector`
|
163
|
+
function before being written into CSS code.
|
164
|
+
|
165
|
+
Defaults to the :py:func:`default_class_namer` function which
|
166
|
+
implements a default naming strategy where each class is named `"c{i}"`
|
167
|
+
where `i` is the index of the rule in the list.
|
168
|
+
:type class_namer: Callable[[List[ClassRule], int], str]
|
146
169
|
:return: The :py:class:`CompiledCSS` object containing the optimized rules.
|
147
170
|
Every HTML node present in one or more of the input trees is included
|
148
171
|
in the :py:attr:`CompiledCSS.mapping` attribute, even if the node does
|
149
|
-
not have a style. Rules are alphabetically ordered by name in the
|
172
|
+
not have a style. Rules are alphabetically ordered by class name in the
|
150
173
|
mapping.
|
151
174
|
:rtype: CompiledCSS
|
152
175
|
"""
|
@@ -154,76 +177,37 @@ def compile_css(trees: Union[HTMLNode, List[HTMLNode]],
|
|
154
177
|
if isinstance(trees, HTMLNode):
|
155
178
|
trees = [trees]
|
156
179
|
|
157
|
-
# Handling default
|
158
|
-
|
180
|
+
# Handling default class_namer
|
181
|
+
class_namer = default_class_namer if class_namer is None else class_namer
|
159
182
|
|
160
|
-
#
|
161
|
-
#
|
183
|
+
# We compute a simple mapping where each CSS property defines its own
|
184
|
+
# ruleset
|
162
185
|
styles = {k: v for tree in trees for k, v in tree.get_styles().items()}
|
163
186
|
properties = set(itertools.chain.from_iterable(s.items()
|
164
187
|
for s in styles.values()))
|
165
|
-
rules = [
|
188
|
+
rules = [ClassRule("", dict([p])) # Initializing with empty name
|
166
189
|
for p in sorted(properties)]
|
167
190
|
for i, rule in enumerate(rules): # Assigning name from callback
|
168
|
-
rule.name =
|
191
|
+
rule.name = class_namer(rules, i)
|
169
192
|
rules = sorted(rules, key=lambda r: r.name) # Sorting by name
|
170
193
|
mapping = {node_id: [r for r in rules if
|
171
194
|
set(r.declarations.items()).issubset(style.items())]
|
172
195
|
for node_id, style in styles.items()}
|
173
|
-
return CompiledCSS(trees, rules, mapping)
|
174
|
-
|
175
|
-
|
176
|
-
def apply_css(css: CompiledCSS, tree: HTMLNode) -> None:
|
177
|
-
"""Applies the CSS rules to the given tree.
|
178
|
-
|
179
|
-
Rules are added as HTML classes to each node with a style in the tree. If a
|
180
|
-
node does not have a `class` attribute yet, it will be created for that
|
181
|
-
node. Nodes that do not have any style are left untouched.
|
182
|
-
|
183
|
-
Note that this function is recursive and calls itself on each child node of
|
184
|
-
the tree.
|
185
|
-
|
186
|
-
:param css: The compiled CSS object containing the rules to apply and the
|
187
|
-
mapping to each node. It should have been created by invoking
|
188
|
-
:py:func:`compile_css` on the given tree, but it can be modified before
|
189
|
-
passing it to this function, as long as its content remains consistent.
|
190
|
-
:type css: CompiledCSS
|
191
|
-
:param tree: The tree to which the CSS rules should be applied. It will be
|
192
|
-
modified in place by this function. If you want to keep the original
|
193
|
-
tree unchanged, make a deep copy of it using its
|
194
|
-
:py:meth:`HTMLNode.copy` method and pass this copy instead.
|
195
|
-
:type tree: HTMLNode
|
196
|
-
"""
|
197
|
-
# Only modifying nodes if they have a style (and therefore if the list of
|
198
|
-
# rules mapped to them in `css.mapping` is not empty)
|
199
|
-
if tree.style:
|
200
|
-
|
201
|
-
# Listing rules to add as classes. We do not add rules that are already
|
202
|
-
# there.
|
203
|
-
rules_to_add = [r.name for r in css.mapping[id(tree)] if r.name not in
|
204
|
-
tree.attributes.get('class', '').split(' ')]
|
205
196
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
'class', None) and rules_to_add else ''
|
210
|
-
tree.attributes['class'] = tree.attributes.get(
|
211
|
-
'class', '') + maybe_space + ' '.join(rules_to_add)
|
212
|
-
|
213
|
-
# Recursively applying the CSS rules to all child nodes of the tree
|
214
|
-
for child in tree.children:
|
215
|
-
apply_css(css, child)
|
197
|
+
# Packaging the results into a CompiledCSS object
|
198
|
+
core = RuleSection(rules=rules, title="Core")
|
199
|
+
return CompiledCSS(trees, core, mapping)
|
216
200
|
|
217
201
|
|
218
|
-
def
|
219
|
-
"""Default
|
202
|
+
def default_class_namer(rules: List[ClassRule], index: int) -> str:
|
203
|
+
"""Default class naming function. Returns a string like "c{i}" where {i} is
|
220
204
|
the index of the rule.
|
221
205
|
|
222
|
-
:param rules: List of all compiled
|
206
|
+
:param rules: List of all compiled ClassRule objects. This argument is not
|
223
207
|
used in this function, but it can be used in other naming strategies.
|
224
|
-
:type rules: List[
|
225
|
-
:param index: Index of the rule being named.
|
208
|
+
:type rules: List[ClassRule]
|
209
|
+
:param index: Index of the rule whose class is being named.
|
226
210
|
:type index: int
|
227
|
-
:return: A string like `"
|
211
|
+
:return: A string like `"c{i}"` where `i` is the index of the rule.
|
228
212
|
"""
|
229
|
-
return f'
|
213
|
+
return f'c{index}'
|
@@ -0,0 +1,104 @@
|
|
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 Dict
|
14
|
+
from webwidgets.utility.indentation import get_indentation
|
15
|
+
from webwidgets.utility.representation import ReprMixin
|
16
|
+
from webwidgets.utility.validation import validate_css_identifier, validate_css_selector
|
17
|
+
|
18
|
+
|
19
|
+
class CSSRule(ReprMixin):
|
20
|
+
"""A rule in a style sheet.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, selector: str, declarations: Dict[str, str]):
|
24
|
+
"""Stores the selector and declarations of the rule.
|
25
|
+
|
26
|
+
:param selector: The selector of the rule.
|
27
|
+
:type selector: str
|
28
|
+
:param declarations: The CSS declarations for the rule, specified as a
|
29
|
+
dictionary where keys are property names and values are their
|
30
|
+
corresponding values. For example: `{'color': 'red'}`
|
31
|
+
:type declarations: Dict[str, str]
|
32
|
+
"""
|
33
|
+
super().__init__()
|
34
|
+
self.selector = selector
|
35
|
+
self.declarations = declarations
|
36
|
+
|
37
|
+
def to_css(self, indent_size: int = 4) -> str:
|
38
|
+
"""Converts the rule into CSS code.
|
39
|
+
|
40
|
+
The rule's name is converted to a class selector.
|
41
|
+
|
42
|
+
Note that the rule's name and all property names are validated before
|
43
|
+
being converted. The rule's name is validated with
|
44
|
+
:py:func:`validate_css_selector` while the property names are validated
|
45
|
+
with :py:func:`validate_css_identifier`.
|
46
|
+
|
47
|
+
:param indent_size: The number of spaces to use for indentation in the
|
48
|
+
CSS code. Defaults to 4.
|
49
|
+
:type indent_size: int
|
50
|
+
:return: The CSS code as a string.
|
51
|
+
:rtype: str
|
52
|
+
"""
|
53
|
+
# Defining indentation
|
54
|
+
indentation = get_indentation(level=1, size=indent_size)
|
55
|
+
|
56
|
+
# Validating the selector
|
57
|
+
validate_css_selector(self.selector)
|
58
|
+
|
59
|
+
# Writing down each property
|
60
|
+
css_code = self.selector + " {\n"
|
61
|
+
for property_name, value in self.declarations.items():
|
62
|
+
validate_css_identifier(property_name)
|
63
|
+
css_code += f"{indentation}{property_name}: {value};\n"
|
64
|
+
css_code += "}"
|
65
|
+
|
66
|
+
return css_code
|
67
|
+
|
68
|
+
|
69
|
+
class ClassRule(CSSRule):
|
70
|
+
"""A CSS rule that targets a CSS class.
|
71
|
+
|
72
|
+
The class dynamically sets its selector based on its class name.
|
73
|
+
"""
|
74
|
+
|
75
|
+
def __init__(self, name: str, declarations: Dict[str, str]):
|
76
|
+
"""Creates a new CSS class rule.
|
77
|
+
|
78
|
+
:param name: The name of the CSS class.
|
79
|
+
:type name: str
|
80
|
+
:param declarations: See :py:meth:`CSSRule.__init__`.
|
81
|
+
:type declarations: Dict[str, str]
|
82
|
+
"""
|
83
|
+
super().__init__(None, declarations) # Starting without a selector
|
84
|
+
self._name = None # Starting without a name
|
85
|
+
self.name = name # Setting both the selector and the name here
|
86
|
+
|
87
|
+
@property
|
88
|
+
def name(self) -> str:
|
89
|
+
"""Returns the name of the CSS class.
|
90
|
+
|
91
|
+
:return: The name of the CSS class.
|
92
|
+
:rtype: str
|
93
|
+
"""
|
94
|
+
return self._name
|
95
|
+
|
96
|
+
@name.setter
|
97
|
+
def name(self, value: str) -> None:
|
98
|
+
"""Sets the name of the CSS class.
|
99
|
+
|
100
|
+
:param value: The new name of the CSS class.
|
101
|
+
:type value: str
|
102
|
+
"""
|
103
|
+
self._name = value
|
104
|
+
self.selector = f".{value}" # Updating the selector
|
@@ -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 .preamble import Preamble
|
14
|
+
from .rule_section import RuleSection
|
@@ -0,0 +1,106 @@
|
|
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 typing import Any
|
15
|
+
from webwidgets.utility.representation import ReprMixin
|
16
|
+
from webwidgets.utility.validation import validate_css_comment
|
17
|
+
|
18
|
+
|
19
|
+
class CSSSection(ABC, ReprMixin):
|
20
|
+
"""Abstract base class representing a section of a CSS file.
|
21
|
+
|
22
|
+
All subclasses of :py:class:`CSSSection` must implement a
|
23
|
+
:py:meth:`compile_content` method that returns a string.
|
24
|
+
"""
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def prettify_title(title: str, min_length: int) -> str:
|
28
|
+
"""Returns a prettified version of the given title with decorative
|
29
|
+
characters `=` around it.
|
30
|
+
|
31
|
+
This function will add the minimum number of decorative characters to
|
32
|
+
the title while keeping symmetry and remaining over the given minimum
|
33
|
+
length. In particular, if the given title is already above the minimum
|
34
|
+
length, this function will return it as is.
|
35
|
+
|
36
|
+
:param title: The title to prettify.
|
37
|
+
:type title: str
|
38
|
+
:param max_length: The minimum length of the prettified title.
|
39
|
+
:type max_length: int
|
40
|
+
:return: The prettified title.
|
41
|
+
:rtype: str
|
42
|
+
"""
|
43
|
+
# If the title is already above min_length, we don't add decorative
|
44
|
+
# characters
|
45
|
+
if len(title) >= min_length:
|
46
|
+
return title
|
47
|
+
|
48
|
+
# Otherwise, we add decorative characters around the title
|
49
|
+
remaining = min_length - len(title)
|
50
|
+
characters = "=" * max(((remaining - 1) // 2), 1)
|
51
|
+
return characters + ' ' + title + ' ' + characters
|
52
|
+
|
53
|
+
def __init__(self, title: str = None):
|
54
|
+
"""Creates a new section with an optional title.
|
55
|
+
|
56
|
+
:param title: The title of the section. If provided, the section will
|
57
|
+
be preceded by a comment containing the title in the output CSS
|
58
|
+
code. If None, no title will be used to separate the section from
|
59
|
+
the rest of the code.
|
60
|
+
:type title: str
|
61
|
+
"""
|
62
|
+
super().__init__()
|
63
|
+
self.title = title
|
64
|
+
|
65
|
+
@abstractmethod
|
66
|
+
def compile_content(self) -> str:
|
67
|
+
"""Converts the content of the CSSSection object (excluding the title)
|
68
|
+
into CSS code.
|
69
|
+
|
70
|
+
This method must be overridden by subclasses to compile specific CSS
|
71
|
+
code.
|
72
|
+
"""
|
73
|
+
pass
|
74
|
+
|
75
|
+
def to_css(self, *args: Any, **kwargs: Any) -> str:
|
76
|
+
"""Converts the CSSSection object into CSS code.
|
77
|
+
|
78
|
+
If the section has a title, it will be prettified with
|
79
|
+
:py:meth:`CSSSection.prettify_title` and turned into a comment. That
|
80
|
+
comment will be validated with :py:func:`validate_css_comment` and
|
81
|
+
inserted before the result of :py:meth:`CSSSection.compile_content` in
|
82
|
+
the CSS code.
|
83
|
+
|
84
|
+
If the section has no title, this function will produce the same result
|
85
|
+
as :py:meth:`CSSSection.compile_content`.
|
86
|
+
|
87
|
+
:param args: Arguments to pass to
|
88
|
+
:py:meth:`CSSSection.compile_content`.
|
89
|
+
:type args: Any
|
90
|
+
:param kwargs: Keyword arguments to pass to
|
91
|
+
:py:meth:`CSSSection.compile_content`.
|
92
|
+
:type kwargs: Any
|
93
|
+
:return: The CSS code for the section.
|
94
|
+
:rtype: str
|
95
|
+
"""
|
96
|
+
# If no title, we just return the compiled content
|
97
|
+
if self.title is None:
|
98
|
+
return self.compile_content(*args, **kwargs)
|
99
|
+
|
100
|
+
# Otherwise, we turn the title into a comment and validate it
|
101
|
+
comment = ' ' + CSSSection.prettify_title(self.title, 40) + ' '
|
102
|
+
validate_css_comment(comment)
|
103
|
+
|
104
|
+
# Adding the comment before the compiled content
|
105
|
+
return "/*" + comment + "*/\n\n" + \
|
106
|
+
self.compile_content(*args, **kwargs)
|
@@ -0,0 +1,40 @@
|
|
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 .rule_section import RuleSection
|
14
|
+
from webwidgets.compilation.css.css_rule import CSSRule
|
15
|
+
|
16
|
+
|
17
|
+
class Preamble(RuleSection):
|
18
|
+
"""A set of CSS rules that apply globally to all HTML elements.
|
19
|
+
|
20
|
+
The CSS preamble serves as a global default for multiple properties. It is
|
21
|
+
used to define the document's box model and set all margin and padding
|
22
|
+
values to 0.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self):
|
26
|
+
"""Creates a new CSS preamble."""
|
27
|
+
super().__init__(
|
28
|
+
rules=[
|
29
|
+
CSSRule("*, *::before, *::after", {
|
30
|
+
|
31
|
+
# Defining the box model to border-box
|
32
|
+
"box-sizing": "border-box",
|
33
|
+
|
34
|
+
# Setting all margin and padding values to 0
|
35
|
+
"margin": "0",
|
36
|
+
"padding": "0"
|
37
|
+
})
|
38
|
+
],
|
39
|
+
title="Preamble"
|
40
|
+
)
|
@@ -0,0 +1,43 @@
|
|
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 .css_section import CSSSection
|
14
|
+
from typing import List
|
15
|
+
from webwidgets.compilation.css.css_rule import CSSRule
|
16
|
+
|
17
|
+
|
18
|
+
class RuleSection(CSSSection):
|
19
|
+
"""A section containing a set of CSS rules.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, rules: List[CSSRule] = None, title: str = None):
|
23
|
+
"""Creates a new section with the given rules and title.
|
24
|
+
|
25
|
+
:param rules: A list of CSSRule objects to include in the section.
|
26
|
+
:type rules: List[CSSRule]
|
27
|
+
:param title: The title of the section.
|
28
|
+
:type title: str
|
29
|
+
"""
|
30
|
+
super().__init__(title=title)
|
31
|
+
self.rules = [] if rules is None else rules
|
32
|
+
|
33
|
+
def compile_content(self, indent_size: int = 4) -> str:
|
34
|
+
"""Compiles the CSS representation of the rules contained in the
|
35
|
+
section.
|
36
|
+
|
37
|
+
:param indent_size: See :py:meth:`CSSRule.to_css`.
|
38
|
+
:type indent_size: int
|
39
|
+
:return: The CSS representation of the rules.
|
40
|
+
:rtype: str
|
41
|
+
"""
|
42
|
+
return "\n\n".join([
|
43
|
+
rule.to_css(indent_size=indent_size) for rule in self.rules])
|
@@ -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
|
14
|
-
|
13
|
+
from .html_node import HTMLNode, no_start_tag, no_end_tag, one_line, RawText, \
|
14
|
+
RootNode
|
15
|
+
from .html_tags import *
|