webwidgets 1.0.0__py3-none-any.whl → 1.1.1__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 +3 -1
- webwidgets/compilation/css/__init__.py +3 -2
- webwidgets/compilation/css/css.py +106 -123
- webwidgets/compilation/css/css_rule.py +106 -0
- webwidgets/compilation/css/sections/__init__.py +14 -0
- webwidgets/compilation/css/sections/css_section.py +106 -0
- webwidgets/compilation/css/sections/preamble.py +43 -0
- webwidgets/compilation/css/sections/rule_section.py +43 -0
- webwidgets/compilation/html/__init__.py +1 -1
- webwidgets/compilation/html/html_node.py +27 -27
- webwidgets/compilation/html/html_tags.py +5 -0
- webwidgets/utility/__init__.py +2 -0
- webwidgets/utility/enums.py +18 -0
- webwidgets/utility/sizes/__init__.py +14 -0
- webwidgets/utility/sizes/size.py +92 -0
- webwidgets/utility/sizes/sizes.py +31 -0
- webwidgets/utility/validation.py +100 -2
- webwidgets/website/website.py +5 -5
- webwidgets/widgets/containers/__init__.py +1 -0
- webwidgets/widgets/containers/box.py +138 -0
- webwidgets/widgets/containers/container.py +7 -2
- webwidgets/widgets/containers/page.py +1 -1
- webwidgets/widgets/widget.py +1 -2
- {webwidgets-1.0.0.dist-info → webwidgets-1.1.1.dist-info}/METADATA +6 -1
- webwidgets-1.1.1.dist-info/RECORD +34 -0
- webwidgets-1.0.0.dist-info/RECORD +0 -24
- {webwidgets-1.0.0.dist-info → webwidgets-1.1.1.dist-info}/WHEEL +0 -0
- {webwidgets-1.0.0.dist-info → webwidgets-1.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -63,6 +63,33 @@ class HTMLNode(ReprMixin):
|
|
|
63
63
|
f'{k}="{v}"' for k, v in sorted(self.attributes.items())
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
+
@property
|
|
67
|
+
def end_tag(self) -> str:
|
|
68
|
+
"""Returns the closing tag of the HTML node.
|
|
69
|
+
|
|
70
|
+
:return: A string containing the closing tag of the element.
|
|
71
|
+
:rtype: str
|
|
72
|
+
"""
|
|
73
|
+
return f"</{self._get_tag_name()}>"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def start_tag(self) -> str:
|
|
77
|
+
"""Returns the opening tag of the HTML node, including any attributes.
|
|
78
|
+
|
|
79
|
+
Attributes are validated with :py:meth:`HTMLNode.validate_attributes`
|
|
80
|
+
before rendering.
|
|
81
|
+
|
|
82
|
+
:return: A string containing the opening tag of the element with its attributes.
|
|
83
|
+
:rtype: str
|
|
84
|
+
"""
|
|
85
|
+
# Rendering attributes
|
|
86
|
+
self.validate_attributes()
|
|
87
|
+
attributes = self._render_attributes()
|
|
88
|
+
maybe_space = ' ' if attributes else ''
|
|
89
|
+
|
|
90
|
+
# Building start tag
|
|
91
|
+
return f"<{self._get_tag_name()}{maybe_space}{attributes}>"
|
|
92
|
+
|
|
66
93
|
def add(self, child: 'HTMLNode') -> None:
|
|
67
94
|
"""Adds a child to the HTML node.
|
|
68
95
|
|
|
@@ -101,33 +128,6 @@ class HTMLNode(ReprMixin):
|
|
|
101
128
|
styles.update(child.get_styles())
|
|
102
129
|
return styles
|
|
103
130
|
|
|
104
|
-
@property
|
|
105
|
-
def start_tag(self) -> str:
|
|
106
|
-
"""Returns the opening tag of the HTML node, including any attributes.
|
|
107
|
-
|
|
108
|
-
Attributes are validated with :py:meth:`HTMLNode.validate_attributes`
|
|
109
|
-
before rendering.
|
|
110
|
-
|
|
111
|
-
:return: A string containing the opening tag of the element with its attributes.
|
|
112
|
-
:rtype: str
|
|
113
|
-
"""
|
|
114
|
-
# Rendering attributes
|
|
115
|
-
self.validate_attributes()
|
|
116
|
-
attributes = self._render_attributes()
|
|
117
|
-
maybe_space = ' ' if attributes else ''
|
|
118
|
-
|
|
119
|
-
# Building start tag
|
|
120
|
-
return f"<{self._get_tag_name()}{maybe_space}{attributes}>"
|
|
121
|
-
|
|
122
|
-
@property
|
|
123
|
-
def end_tag(self) -> str:
|
|
124
|
-
"""Returns the closing tag of the HTML node.
|
|
125
|
-
|
|
126
|
-
:return: A string containing the closing tag of the element.
|
|
127
|
-
:rtype: str
|
|
128
|
-
"""
|
|
129
|
-
return f"</{self._get_tag_name()}>"
|
|
130
|
-
|
|
131
131
|
def to_html(self, collapse_empty: bool = True,
|
|
132
132
|
indent_size: int = 4, indent_level: int = 0,
|
|
133
133
|
force_one_line: bool = False, return_lines: bool = False,
|
webwidgets/utility/__init__.py
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
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 enum import auto, Enum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Direction(Enum):
|
|
17
|
+
HORIZONTAL = auto()
|
|
18
|
+
VERTICAL = auto()
|
|
@@ -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 .size import *
|
|
14
|
+
from .sizes import *
|
|
@@ -0,0 +1,92 @@
|
|
|
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 webwidgets.utility.representation import ReprMixin
|
|
14
|
+
from typing import Callable, Type, Union
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Size(ReprMixin):
|
|
18
|
+
"""Base class representing a length.
|
|
19
|
+
|
|
20
|
+
Sizes are specified by a numerical value and a CSS unit (e.g. `px`, `%`,
|
|
21
|
+
etc.). The value is provided upon creation and the unit is derived from the
|
|
22
|
+
class name of the :py:class:`Size` object.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, value: Union[float, int]):
|
|
26
|
+
"""Creates a new :py:class:`Size` object with the given numerical
|
|
27
|
+
value.
|
|
28
|
+
|
|
29
|
+
:param value: The numerical value of the size.
|
|
30
|
+
:type value: Union[float, int]
|
|
31
|
+
"""
|
|
32
|
+
self.value = value
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def unit(self) -> str:
|
|
36
|
+
"""Returns the unit of the size's numerical value.
|
|
37
|
+
|
|
38
|
+
The unit of a size object is the name of its class in lowercase.
|
|
39
|
+
|
|
40
|
+
:return: The unit of the size.
|
|
41
|
+
:rtype: str
|
|
42
|
+
"""
|
|
43
|
+
return self.__class__.__name__.lower()
|
|
44
|
+
|
|
45
|
+
def to_css(self) -> str:
|
|
46
|
+
"""Compiles and returns the CSS representation of the :py:class:`Size`
|
|
47
|
+
object.
|
|
48
|
+
|
|
49
|
+
The CSS representation is obtained by concatenating the value of the
|
|
50
|
+
size with its unit (e.g. `"10px"`).
|
|
51
|
+
|
|
52
|
+
:return: The CSS representation of the size.
|
|
53
|
+
:rtype: str
|
|
54
|
+
"""
|
|
55
|
+
return str(self.value) + self.unit
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AbsoluteSize(Size):
|
|
59
|
+
"""A size whose unit is an absolute unit that does not depend on any
|
|
60
|
+
context. Examples include pixels (`"px"`) and centimeters (`"cm"`).
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RelativeSize(Size):
|
|
66
|
+
"""A size whose unit is relative to the size of other elements, such as the
|
|
67
|
+
size of a display or a font. Examples include percentages (`"%"`) and ems
|
|
68
|
+
(`"em"`).
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def with_unit(unit: str) -> Callable[[Type[Size]], Type[Size]]:
|
|
74
|
+
"""Returns a decorator to override the unit of a Size subclass with the
|
|
75
|
+
given unit.
|
|
76
|
+
|
|
77
|
+
:param unit: The unit to be used for the Size subclass.
|
|
78
|
+
:type unit: str
|
|
79
|
+
:return: A decorator that overrides the unit of a Size subclass with the
|
|
80
|
+
given unit.
|
|
81
|
+
:rtype: Callable[[Type[Size]], Type[Size]]
|
|
82
|
+
"""
|
|
83
|
+
def _decorator(cls):
|
|
84
|
+
"""Decorator to override the unit of a Size subclass.
|
|
85
|
+
|
|
86
|
+
:param cls: A subclass of Size whose unit should be overridden.
|
|
87
|
+
:return: The given class with a new unit.
|
|
88
|
+
"""
|
|
89
|
+
cls.unit = property(
|
|
90
|
+
lambda _: unit, doc=f"Always returns '{unit}'.")
|
|
91
|
+
return cls
|
|
92
|
+
return _decorator
|
|
@@ -0,0 +1,31 @@
|
|
|
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 .size import AbsoluteSize, RelativeSize, with_unit
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Only exposing concrete size classes when using a wildcard import
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Percent",
|
|
19
|
+
"Px"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@with_unit("%")
|
|
24
|
+
class Percent(RelativeSize):
|
|
25
|
+
"""A size expressed in percentage (`"%"`)."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Px(AbsoluteSize):
|
|
30
|
+
"""A size expressed in pixels (`"px"`)."""
|
|
31
|
+
pass
|
webwidgets/utility/validation.py
CHANGED
|
@@ -10,10 +10,37 @@
|
|
|
10
10
|
#
|
|
11
11
|
# =======================================================================
|
|
12
12
|
|
|
13
|
-
from typing import *
|
|
14
13
|
import re
|
|
15
14
|
|
|
16
15
|
|
|
16
|
+
# CSS selectors that are considered valid as selectors but not as identifiers
|
|
17
|
+
# according to the `validate_css_identifier()` function.
|
|
18
|
+
SPECIAL_SELECTORS = [
|
|
19
|
+
"*", "*::before", "*::after"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_css_comment(comment: str) -> None:
|
|
24
|
+
"""Checks if the given comment is a valid CSS comment according to the CSS
|
|
25
|
+
syntax rules and raises an exception if not.
|
|
26
|
+
|
|
27
|
+
This function just checks that the comment does not contain any closing
|
|
28
|
+
sequence `*/` as defined in the CSS Syntax Module Level 3, paragraph 4.3.2
|
|
29
|
+
(see source: https://www.w3.org/TR/css-syntax-3/#consume-comment).
|
|
30
|
+
|
|
31
|
+
:param comment: The CSS comment to validate, without its opening and
|
|
32
|
+
closing sequences. It can include any number of opening sequences
|
|
33
|
+
(`/*`) as part of its content, in which case it is still a valid
|
|
34
|
+
comment per the CSS specification, but it cannot contain any closing
|
|
35
|
+
sequences (`*/`).
|
|
36
|
+
:type comment: str
|
|
37
|
+
:raises ValueError: If the comment is not a valid CSS comment.
|
|
38
|
+
"""
|
|
39
|
+
if "*/" in comment:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Invalid CSS comment: '{comment}' contains closing sequence '*/'")
|
|
42
|
+
|
|
43
|
+
|
|
17
44
|
def validate_css_identifier(identifier: str) -> None:
|
|
18
45
|
"""Checks if the given identifier is a valid identifier token according to
|
|
19
46
|
the CSS syntax rules and raises an exception if not.
|
|
@@ -57,6 +84,77 @@ def validate_css_identifier(identifier: str) -> None:
|
|
|
57
84
|
"allowed.")
|
|
58
85
|
|
|
59
86
|
|
|
87
|
+
def validate_css_selector(selector: str) -> None:
|
|
88
|
+
"""Checks if the given CSS selector is valid and raises an exception if
|
|
89
|
+
not.
|
|
90
|
+
|
|
91
|
+
To be valid, the selector must either be:
|
|
92
|
+
- a special selector, which is defined as either `*`, `*::before`, or
|
|
93
|
+
`*::after`
|
|
94
|
+
- any combination of special selectors separated by a comma and a single
|
|
95
|
+
space (e.g. `*::before, *::after`)
|
|
96
|
+
- or a class selector, which is defined as a dot `.` followed by a valid
|
|
97
|
+
CSS identifier, as defined and enforced by the
|
|
98
|
+
:py:func:`validate_css_identifier` function
|
|
99
|
+
|
|
100
|
+
Note that this function imposes stricter rules than the official CSS
|
|
101
|
+
Selector Level 4 specification (see source:
|
|
102
|
+
https://www.w3.org/TR/selectors-4/). For example, this function does not
|
|
103
|
+
allow selectors with the relational pseudo-class `:has()` whereas the
|
|
104
|
+
specification does.
|
|
105
|
+
|
|
106
|
+
:param selector: The CSS selector to validate.
|
|
107
|
+
:type selector: str
|
|
108
|
+
:raises ValueError: If the selector is not a special selector nor a valid
|
|
109
|
+
CSS identifier.
|
|
110
|
+
"""
|
|
111
|
+
# Checking if the selector is a special selector
|
|
112
|
+
if selector in SPECIAL_SELECTORS:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Checking if the selector is a combination of special selectors
|
|
116
|
+
if all(part in SPECIAL_SELECTORS for part in selector.split(", ")):
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Otherwise, checking if the selector is a class selector
|
|
120
|
+
if not selector.startswith("."):
|
|
121
|
+
raise ValueError("Class selector must start with '.' but got: "
|
|
122
|
+
f"{selector}")
|
|
123
|
+
validate_css_identifier(selector[1:])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_css_value(value: str) -> None:
|
|
127
|
+
"""Checks if the given value is a valid CSS property value and raises an
|
|
128
|
+
exception if not.
|
|
129
|
+
|
|
130
|
+
To be valid, the value must only contain:
|
|
131
|
+
- letters (`a-z`, `A-Z`)
|
|
132
|
+
- digits (`0-9`)
|
|
133
|
+
- dots (`.`)
|
|
134
|
+
- spaces (` `)
|
|
135
|
+
- hyphens (`-`)
|
|
136
|
+
- percent characters (`%`)
|
|
137
|
+
- hashtags (`#`)
|
|
138
|
+
|
|
139
|
+
Note that this function imposes stricter rules than the official CSS
|
|
140
|
+
specification - more precisely, than chapter 2 of the CSS Values and Units
|
|
141
|
+
Module Level 3 (see source:
|
|
142
|
+
https://www.w3.org/TR/css-values-3/#value-defs). For example, this function
|
|
143
|
+
does not allow functional notations like `calc()` whereas the specification
|
|
144
|
+
does.
|
|
145
|
+
|
|
146
|
+
:param value: The value to validate as a CSS property value.
|
|
147
|
+
:type value: str
|
|
148
|
+
:raises ValueError: If the value is not a valid CSS property value.
|
|
149
|
+
"""
|
|
150
|
+
if not re.match(r'^[a-zA-Z0-9. \-%#]+$', value):
|
|
151
|
+
invalid_chars = re.findall(r'[^a-zA-Z0-9. \-%#]', value)
|
|
152
|
+
raise ValueError("Invalid character(s) in CSS property value "
|
|
153
|
+
f"'{value}': {', '.join(invalid_chars)}\n"
|
|
154
|
+
"Only letters, digits, dots, spaces, hyphens, "
|
|
155
|
+
"percent characters, and hashtags are allowed.")
|
|
156
|
+
|
|
157
|
+
|
|
60
158
|
def validate_html_class(class_attribute: str) -> None:
|
|
61
159
|
"""Checks if the given HTML class attribute is valid and raises an
|
|
62
160
|
exception if not.
|
|
@@ -65,7 +163,7 @@ def validate_html_class(class_attribute: str) -> None:
|
|
|
65
163
|
- the class attribute cannot start nor end with a space
|
|
66
164
|
- the class attribute cannot contain double spaces
|
|
67
165
|
- each class in the attribute must be a valid CSS identifier, as validated
|
|
68
|
-
by the :py:func:`validate_css_identifier` function
|
|
166
|
+
by the :py:func:`validate_css_identifier` function
|
|
69
167
|
|
|
70
168
|
Note that this function imposes stricter rules than rule 2.3.7 of the HTML5
|
|
71
169
|
specification (see source:
|
webwidgets/website/website.py
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
from .compiled_website import CompiledWebsite
|
|
14
14
|
from typing import Any, Callable, List
|
|
15
|
-
from webwidgets.compilation.css import
|
|
15
|
+
from webwidgets.compilation.css import apply_css, compile_css, ClassRule
|
|
16
16
|
from webwidgets.utility.representation import ReprMixin
|
|
17
17
|
from webwidgets.widgets.containers.page import Page
|
|
18
18
|
|
|
@@ -44,7 +44,7 @@ class Website(ReprMixin):
|
|
|
44
44
|
force_one_line: bool = False,
|
|
45
45
|
indent_level: int = 0,
|
|
46
46
|
indent_size: int = 4,
|
|
47
|
-
|
|
47
|
+
class_namer: Callable[[List[ClassRule], int], str] = None,
|
|
48
48
|
**kwargs: Any) -> CompiledWebsite:
|
|
49
49
|
"""Compiles the website into HTML and CSS code.
|
|
50
50
|
|
|
@@ -59,8 +59,8 @@ class Website(ReprMixin):
|
|
|
59
59
|
:param indent_size: See :py:meth:`HTMLNode.to_html` and
|
|
60
60
|
:py:meth:`CompiledCSS.to_css`.
|
|
61
61
|
:type indent_size: int
|
|
62
|
-
:param
|
|
63
|
-
:type
|
|
62
|
+
:param class_namer: See :py:func:`compile_css`.
|
|
63
|
+
:type class_namer: Callable[[List[ClassRule], int], str]
|
|
64
64
|
:param kwargs: See :py:meth:`HTMLNode.to_html`.
|
|
65
65
|
:type kwargs: Any
|
|
66
66
|
:return: A new :py:class:`CompiledWebsite` object containing the
|
|
@@ -72,7 +72,7 @@ class Website(ReprMixin):
|
|
|
72
72
|
for page in self.pages]
|
|
73
73
|
|
|
74
74
|
# Compiling HTML and CSS code
|
|
75
|
-
compiled_css = compile_css(trees,
|
|
75
|
+
compiled_css = compile_css(trees, class_namer)
|
|
76
76
|
for tree in trees:
|
|
77
77
|
apply_css(compiled_css, tree)
|
|
78
78
|
html_content = [tree.to_html(
|
|
@@ -0,0 +1,138 @@
|
|
|
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 dataclasses import dataclass
|
|
15
|
+
from typing import Dict, Union
|
|
16
|
+
from webwidgets.compilation.html.html_tags import Div
|
|
17
|
+
from webwidgets.utility.enums import Direction
|
|
18
|
+
from webwidgets.utility.sizes.sizes import AbsoluteSize
|
|
19
|
+
from webwidgets.widgets.widget import Widget
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Box(Container):
|
|
23
|
+
"""A widget that lays out its child widgets inside a row or a column.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, direction: Direction):
|
|
27
|
+
"""Creates a new Box with the given direction.
|
|
28
|
+
|
|
29
|
+
:param direction: The direction in which the child widgets should be
|
|
30
|
+
laid out. Can be either `Direction.HORIZONTAL` or
|
|
31
|
+
`Direction.VERTICAL`.
|
|
32
|
+
:type direction: Direction
|
|
33
|
+
"""
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.direction = direction
|
|
36
|
+
self._properties: Dict[int, BoxItemProperties] = {}
|
|
37
|
+
|
|
38
|
+
def add(self, widget: Widget,
|
|
39
|
+
space: Union[int, float, AbsoluteSize] = 1) -> None:
|
|
40
|
+
"""Adds a widget to the box with an optional space coefficient.
|
|
41
|
+
|
|
42
|
+
This function overrides :py:meth:`Container.add` from the base class to
|
|
43
|
+
extend its functionality and save additional properties on each widget
|
|
44
|
+
added to the box.
|
|
45
|
+
|
|
46
|
+
:param widget: The widget to add to the box.
|
|
47
|
+
:type widget: Widget
|
|
48
|
+
:param space: The amount of space to allocate for the widget to live
|
|
49
|
+
in.
|
|
50
|
+
|
|
51
|
+
If a numeric value (int or float), it must be at least 1, and it is
|
|
52
|
+
construed as the weight to give to the widget during space
|
|
53
|
+
allocation within the entire box. For example, if widget A has a
|
|
54
|
+
space of 1 and widget B has a space of 2, B will be allocated twice
|
|
55
|
+
as much space as A, i.e. a total of 2/3 of the entire box if the
|
|
56
|
+
only widgets the box contains are A and B.
|
|
57
|
+
|
|
58
|
+
If an instance of :py:class:`AbsoluteSize`, it is construed as the
|
|
59
|
+
exact size to allocate for the widget. For example, if widget A has
|
|
60
|
+
a space of `Px(100)` (i.e. 100px) and widget B has a space of 1, A
|
|
61
|
+
will be allocated exactly 100px while B will be allocated all the
|
|
62
|
+
remaining space if the only widgets the box contains are A and B.
|
|
63
|
+
|
|
64
|
+
Note that this value controls the amount of free space available
|
|
65
|
+
for the widget to grow in, not the size of the widget itself.
|
|
66
|
+
:type space: Union[int, float, AbsoluteSize]
|
|
67
|
+
"""
|
|
68
|
+
super().add(widget=widget)
|
|
69
|
+
self._properties[id(widget)] = BoxItemProperties(space=space)
|
|
70
|
+
|
|
71
|
+
def build(self) -> Div:
|
|
72
|
+
"""Builds the HTML representation of the Box.
|
|
73
|
+
|
|
74
|
+
The box is constructed as a `<div>` element with a flexbox layout. Its
|
|
75
|
+
`flex-direction` property is set to either "row" or "column" based on
|
|
76
|
+
the direction parameter, and it has a `data-role` attribute of "box".
|
|
77
|
+
|
|
78
|
+
Each child widget is wrapped inside its own `<div>` element with a
|
|
79
|
+
`data-role` attribute of "box-item". The items are centered within
|
|
80
|
+
their own `<div>`.
|
|
81
|
+
|
|
82
|
+
:return: A :py:class:`Div` element representing the Box.
|
|
83
|
+
:rtype: Div
|
|
84
|
+
"""
|
|
85
|
+
# Building child nodes and retrieving their properties
|
|
86
|
+
nodes = [w.build() for w in self.widgets]
|
|
87
|
+
properties = [self._properties[id(w)] for w in self.widgets]
|
|
88
|
+
|
|
89
|
+
# Building box items that wrap around child nodes
|
|
90
|
+
items = [Div(
|
|
91
|
+
children=[node],
|
|
92
|
+
attributes={"data-role": "box-item"},
|
|
93
|
+
style={
|
|
94
|
+
"display": "flex",
|
|
95
|
+
"flex-direction": "row",
|
|
96
|
+
"align-items": "center",
|
|
97
|
+
"justify-content": "center"
|
|
98
|
+
} | props.to_style()) for node, props in zip(nodes, properties)]
|
|
99
|
+
|
|
100
|
+
# Assembling the box
|
|
101
|
+
flex_dir = "row" if self.direction == Direction.HORIZONTAL else "column"
|
|
102
|
+
box = Div(children=items, attributes={"data-role": "box"}, style={
|
|
103
|
+
"display": "flex",
|
|
104
|
+
"flex-direction": flex_dir
|
|
105
|
+
})
|
|
106
|
+
return box
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class BoxItemProperties:
|
|
111
|
+
"""A utility dataclass to store extra properties to apply to a widget
|
|
112
|
+
contained in a :py:class:`Box` during compilation.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
space: Union[int, float, AbsoluteSize]
|
|
116
|
+
|
|
117
|
+
def to_style(self) -> Dict[str, str]:
|
|
118
|
+
"""Converts the properties of the :py:class:`BoxItemProperties`
|
|
119
|
+
instance into a dictionary of CSS properties that can be added to the
|
|
120
|
+
style of an HTML node.
|
|
121
|
+
|
|
122
|
+
:return: A dictionary of CSS properties.
|
|
123
|
+
:rtype: Dict[str, str]
|
|
124
|
+
"""
|
|
125
|
+
# If a numeric value, the space serves as a relative weight
|
|
126
|
+
if isinstance(self.space, (int, float)):
|
|
127
|
+
return {
|
|
128
|
+
"flex-basis": "0",
|
|
129
|
+
"flex-grow": str(self.space),
|
|
130
|
+
"flex-shrink": str(self.space)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# If an AbsoluteSize object, the space is a fixed size
|
|
134
|
+
return {
|
|
135
|
+
"flex-basis": self.space.to_css(),
|
|
136
|
+
"flex-grow": "0",
|
|
137
|
+
"flex-shrink": "0"
|
|
138
|
+
}
|
|
@@ -27,7 +27,12 @@ class Container(Widget):
|
|
|
27
27
|
:type widgets: List[Widget]
|
|
28
28
|
"""
|
|
29
29
|
super().__init__()
|
|
30
|
-
self.
|
|
30
|
+
self._widgets = [] if widgets is None else widgets
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def widgets(self) -> List[Widget]:
|
|
34
|
+
"""Returns the list of widgets contained within the container."""
|
|
35
|
+
return self._widgets
|
|
31
36
|
|
|
32
37
|
def add(self, widget: Widget) -> None:
|
|
33
38
|
"""Adds a widget to the container.
|
|
@@ -35,4 +40,4 @@ class Container(Widget):
|
|
|
35
40
|
:param widget: The widget to add to the container.
|
|
36
41
|
:type widget: Widget
|
|
37
42
|
"""
|
|
38
|
-
self.
|
|
43
|
+
self._widgets.append(widget)
|
|
@@ -32,7 +32,7 @@ class Page(Container):
|
|
|
32
32
|
:param css_file_name: The name of the CSS file to link to the page if
|
|
33
33
|
the page elements contain any styles. Defaults to "styles.css".
|
|
34
34
|
:type css_file_name: str
|
|
35
|
-
:return:
|
|
35
|
+
:return: A :py:class:`RootNode` object representing the page.
|
|
36
36
|
:rtype: RootNode
|
|
37
37
|
"""
|
|
38
38
|
# Building nodes from the page's widgets
|
webwidgets/widgets/widget.py
CHANGED
|
@@ -16,8 +16,7 @@ from webwidgets.utility.representation import ReprMixin
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class Widget(ABC, ReprMixin):
|
|
19
|
-
"""
|
|
20
|
-
Abstract base class for all widgets.
|
|
19
|
+
"""Abstract base class for all widgets.
|
|
21
20
|
|
|
22
21
|
All subclasses of :py:class:`Widget` must implement a :py:meth:`build`
|
|
23
22
|
method that returns an :py:class:`HTMLNode` object.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webwidgets
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: A Python package for designing web UIs.
|
|
5
5
|
Project-URL: Source code, https://github.com/mlaasri/WebWidgets
|
|
6
6
|
Author: mlaasri
|
|
@@ -9,6 +9,11 @@ Keywords: design,webui
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Requires-Python: >=3.9
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: numpy; extra == 'dev'
|
|
14
|
+
Requires-Dist: pillow; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
16
|
+
Requires-Dist: selenium; extra == 'dev'
|
|
12
17
|
Description-Content-Type: text/markdown
|
|
13
18
|
|
|
14
19
|
# WebWidgets
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
webwidgets/__init__.py,sha256=fkkQsGyTLNHc3pL-njm0KmAZRqDfvmwDhun0ZT7N92w,613
|
|
2
|
+
webwidgets/compilation/__init__.py,sha256=hb61nhmPTghIzuA_hun98xT5Ngv7QFAgMHD44g-9uOo,433
|
|
3
|
+
webwidgets/compilation/css/__init__.py,sha256=A4VUGyy92Vn32Km2VuJL5bBmLdovN6PDfv_DctRdzJk,534
|
|
4
|
+
webwidgets/compilation/css/css.py,sha256=v2GSyegop8JmE_9ITZ-2oc9P_8bE33-YWsjiO8FGaWI,9351
|
|
5
|
+
webwidgets/compilation/css/css_rule.py,sha256=8NKQwNfETc52KDrMoy_rJtie6si1SrdfnxKRBRjLJqM,3594
|
|
6
|
+
webwidgets/compilation/css/sections/__init__.py,sha256=qPLq_w0kIPGvIIXxsblF7iUdm9AOA-CUl06krTMnHHI,465
|
|
7
|
+
webwidgets/compilation/css/sections/css_section.py,sha256=LfQojXSYbsh9UcbqD4vn6heEO7qjGNrwy7Py5bsvy9U,4020
|
|
8
|
+
webwidgets/compilation/css/sections/preamble.py,sha256=zDU3JZpIYoc9Xynquc50awqhvYxNi0EbprbwCVScvoY,1355
|
|
9
|
+
webwidgets/compilation/css/sections/rule_section.py,sha256=BBeIJyVRz2ZeBxEC6VrEymXF6LjzE_GPu6X04A-lGF8,1456
|
|
10
|
+
webwidgets/compilation/html/__init__.py,sha256=h8eWh8BbjLZA1wIGAeOxyhZIUM1e36ZgMTwL5SQajHc,514
|
|
11
|
+
webwidgets/compilation/html/html_node.py,sha256=QJgUWpAsvJSd4C9FUAvtcCZWlCLA0fZK5A0c8_pbsWc,11697
|
|
12
|
+
webwidgets/compilation/html/html_tags.py,sha256=R13axcPAOnqRVPM8FP0k04QIW7gZMSJt3jlsv7Q-fXg,2276
|
|
13
|
+
webwidgets/utility/__init__.py,sha256=GDS2bOS36He7EAde1YAcSQ_P49zkKIPGIRP8NRCFJbs,547
|
|
14
|
+
webwidgets/utility/enums.py,sha256=RDgYT2S85DrhKL-3T7oHN20iW4vYoD8TKLsEmsO2OaE,495
|
|
15
|
+
webwidgets/utility/indentation.py,sha256=BaOQRqWdG7T5k_g1-ia9jewPFZjD3afjZH_Fc4NSVwo,906
|
|
16
|
+
webwidgets/utility/representation.py,sha256=lQ15v_DZOHBQKLM8pzRE1tuJkU_modhPTpWpSJ2lBCE,1061
|
|
17
|
+
webwidgets/utility/sanitizing.py,sha256=OKJRDqk-OXYCWeK6ie3GdfQvb49wTs93kd971mg5oK0,5770
|
|
18
|
+
webwidgets/utility/validation.py,sha256=ZKKEkhkdhuC7JxVG1_BKFBLT80DOGaSCOCo6ZJgzwmU,8239
|
|
19
|
+
webwidgets/utility/sizes/__init__.py,sha256=2tmSdXRBlIWuOfZUmToc0_gFiVKJIFUCh6H9B2Bf9rw,437
|
|
20
|
+
webwidgets/utility/sizes/size.py,sha256=UjffvXNNV4BhgyJ_OV_ZArYas7FP4Bhul_ujktsQbQM,2867
|
|
21
|
+
webwidgets/utility/sizes/sizes.py,sha256=DoKIruTw2RjuvH9MmM-CG5C2_WnmLt0mlAWphPstM6E,747
|
|
22
|
+
webwidgets/website/__init__.py,sha256=zp4N3CtY0SLNfDV9p2Y0tqbta-vFOX1PSJr7eQ9rQdk,471
|
|
23
|
+
webwidgets/website/compiled_website.py,sha256=lR_sabYtdWiRWicyxEFs4yxRUB_TbMowpsNz3CtqQBQ,1129
|
|
24
|
+
webwidgets/website/website.py,sha256=2vdIxYLXOu_otiF-KTOBigoic0aUWwgglfXAAZRzURg,3349
|
|
25
|
+
webwidgets/widgets/__init__.py,sha256=J2br7F-16URKvWshkJcc4nth27YQsaLrdVZu0xXx5CU,449
|
|
26
|
+
webwidgets/widgets/widget.py,sha256=NRyiy_vBStX2r-LBeJpqFy-AMw1aaqxrx4sz-GIdJnE,1034
|
|
27
|
+
webwidgets/widgets/containers/__init__.py,sha256=0y63aqzzJzC77oKOK1rcqQGDk710lkalc6vyZr7iJWo,473
|
|
28
|
+
webwidgets/widgets/containers/box.py,sha256=iwr-n09ePMICQLF4MZeAzRjBuPCdqHF2cFF8OLSH_oA,5435
|
|
29
|
+
webwidgets/widgets/containers/container.py,sha256=-UAnDEC0o-rP2PBF1ji8xUGRQYRnDvKmVSDaQH3_DYE,1317
|
|
30
|
+
webwidgets/widgets/containers/page.py,sha256=JQBV9U803nfiC20_4kJ4RkibqVeMKf30FZOGmii6YV4,2179
|
|
31
|
+
webwidgets-1.1.1.dist-info/METADATA,sha256=UBIZPHX_nOWMcxrnmWEgOcDJdzTuVK90DuX43JMhMi4,1607
|
|
32
|
+
webwidgets-1.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
33
|
+
webwidgets-1.1.1.dist-info/licenses/LICENSE,sha256=LISw1mw5eK6i8adFSlx6zltZxrJFwurngVdZAEU8g_I,1064
|
|
34
|
+
webwidgets-1.1.1.dist-info/RECORD,,
|