webwidgets 0.1.0__py3-none-any.whl → 0.2.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 CHANGED
@@ -10,6 +10,6 @@
10
10
  #
11
11
  # =======================================================================
12
12
 
13
- __version__ = "0.1.0" # Dynamically set by build backend
13
+ __version__ = "0.2.1" # Dynamically set by build backend
14
14
 
15
15
  from . import compilation
@@ -10,4 +10,5 @@
10
10
  #
11
11
  # =======================================================================
12
12
 
13
+ from . import css
13
14
  from . import html
@@ -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 .css import compile_css, CSSRule, CompiledCSS, apply_css, \
14
+ default_rule_namer
@@ -0,0 +1,229 @@
1
+ # =======================================================================
2
+ #
3
+ # This file is part of WebWidgets, a Python package for designing web
4
+ # UIs.
5
+ #
6
+ # You should have received a copy of the MIT License along with
7
+ # WebWidgets. If not, see <https://opensource.org/license/mit>.
8
+ #
9
+ # Copyright(C) 2025, mlaasri
10
+ #
11
+ # =======================================================================
12
+
13
+ import itertools
14
+ from typing import Callable, Dict, List, Union
15
+ from webwidgets.compilation.html.html_node import HTMLNode
16
+ 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
+
38
+
39
+ class CompiledCSS(ReprMixin):
40
+ """A utility class to hold compiled CSS rules.
41
+ """
42
+
43
+ def __init__(self, trees: List[HTMLNode], rules: List[CSSRule],
44
+ mapping: Dict[int, List[CSSRule]]):
45
+ """Stores compiled CSS rules.
46
+
47
+ :param trees: The HTML trees at the origin of the compilation. These
48
+ are the elements that have been styled with CSS properties.
49
+ :type trees: List[HTMLNode]
50
+ :param rules: The compiled CSS rules.
51
+ :type rules: List[CSSRule]
52
+ :param mapping: A dictionary mapping each node ID to a list of rules
53
+ that achieve the same style.
54
+ :type mapping: Dict[int, List[CSSRule]]
55
+ """
56
+ super().__init__()
57
+ self.trees = trees
58
+ self.rules = rules
59
+ self.mapping = mapping
60
+
61
+ def to_css(self, indent_size: int = 4) -> str:
62
+ """Converts the `rules` dictionary of the :py:class:`CompiledCSS`
63
+ object into CSS code.
64
+
65
+ Rule names are converted to class selectors. Note that each rule and
66
+ property name is validated with :py:func:`validate_css_identifier`
67
+ before being converted.
68
+
69
+ :param indent_size: The number of spaces to use for indentation in the
70
+ CSS code. Defaults to 4.
71
+ :type indent_size: int
72
+ :return: The CSS code as a string.
73
+ :rtype: str
74
+ """
75
+ # Initializing code and defining indentation
76
+ css_code = ""
77
+ indentation = ' ' * indent_size
78
+
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
+
88
+ return css_code
89
+
90
+
91
+ def compile_css(trees: Union[HTMLNode, List[HTMLNode]],
92
+ rule_namer: Callable[[List[CSSRule], int],
93
+ str] = None) -> CompiledCSS:
94
+ """Computes optimized CSS rules from the given HTML trees.
95
+
96
+ The main purpose of this function is to reduce the number of CSS rules
97
+ required to achieve a particular style across one or more HTML trees. The
98
+ function takes a list of HTML nodes as input (not necessarily from the same
99
+ tree) and computes an optimized set of CSS rules that achieves the same
100
+ style across all nodes. The resulting :py:class:`CompiledCSS` object
101
+ contains the optimized rules and their mapping to each node.
102
+
103
+ For example, the following tree:
104
+
105
+ .. code-block:: python
106
+
107
+ tree = HTMLNode(
108
+ style={"margin": "0", "padding": "0"},
109
+ children=[
110
+ HTMLNode(style={"margin": "0", "padding": "0"}),
111
+ HTMLNode(style={"margin": "0", "color": "blue"}),
112
+ ]
113
+ )
114
+
115
+ can be stylistically described with only 3 CSS rules:
116
+
117
+ .. code-block:: python
118
+
119
+ >>> compiled_css = compile_css(tree)
120
+ >>> print(compiled_css.rules)
121
+ [
122
+ CSSRule(name='r0', declarations={'color': 'blue'}),
123
+ CSSRule(name='r1', declarations={'margin': '0'}),
124
+ CSSRule(name='r2', declarations={'padding': '0'})
125
+ ]
126
+
127
+ :param trees: A single tree or a list of trees to optimize over. All
128
+ children are recursively included in the compilation.
129
+ :type trees: Union[HTMLNode, List[HTMLNode]]
130
+ :param rule_namer: A callable that takes two arguments, which are the list
131
+ of all compiled rules and an index within that list, and returns a
132
+ unique name for the rule at the given index.
133
+
134
+ This argument allows to customize the rule naming process and use names
135
+ other than the default `"r0"`, `"r1"`, etc. For example, it can be used
136
+ to achieve something similar to Tailwind CSS and name rules according
137
+ to what they achieve, e.g. by prefixing their name with `"m"` for
138
+ margin rules or `"p"` for padding rules. Note that all rule names will
139
+ be validated with the :py:func:`validate_css_identifier` function
140
+ before being written into CSS code.
141
+
142
+ Defaults to the :py:func:`default_rule_namer` function which implements
143
+ a default naming strategy where each rule is named `"r{i}"` where `i`
144
+ is the index of the rule in the list.
145
+ :type rule_namer: Callable[[List[CSSRule], int], str]
146
+ :return: The :py:class:`CompiledCSS` object containing the optimized rules.
147
+ Every HTML node present in one or more of the input trees is included
148
+ 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
150
+ mapping.
151
+ :rtype: CompiledCSS
152
+ """
153
+ # Handling case of a single tree
154
+ if isinstance(trees, HTMLNode):
155
+ trees = [trees]
156
+
157
+ # Handling default rule_namer
158
+ rule_namer = default_rule_namer if rule_namer is None else rule_namer
159
+
160
+ # For now, we just return a simple mapping where each CSS property defines
161
+ # its own ruleset
162
+ styles = {k: v for tree in trees for k, v in tree.get_styles().items()}
163
+ properties = set(itertools.chain.from_iterable(s.items()
164
+ for s in styles.values()))
165
+ rules = [CSSRule(None, dict([p])) # Initializing with no name
166
+ for p in sorted(properties)]
167
+ for i, rule in enumerate(rules): # Assigning name from callback
168
+ rule.name = rule_namer(rules, i)
169
+ rules = sorted(rules, key=lambda r: r.name) # Sorting by name
170
+ mapping = {node_id: [r for r in rules if
171
+ set(r.declarations.items()).issubset(style.items())]
172
+ 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
+
206
+ # Updating the class attribute. If it already exists and is not empty,
207
+ # we need to insert a space before adding the CSS classes.
208
+ maybe_space = ' ' if tree.attributes.get(
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)
216
+
217
+
218
+ def default_rule_namer(rules: List[CSSRule], index: int) -> str:
219
+ """Default rule naming function. Returns a string like "r{i}" where {i} is
220
+ the index of the rule.
221
+
222
+ :param rules: List of all compiled CSSRule objects. This argument is not
223
+ used in this function, but it can be used in other naming strategies.
224
+ :type rules: List[CSSRule]
225
+ :param index: Index of the rule being named.
226
+ :type index: int
227
+ :return: A string like `"r{i}"` where `i` is the index of the rule.
228
+ """
229
+ return f'r{index}'
@@ -10,4 +10,5 @@
10
10
  #
11
11
  # =======================================================================
12
12
 
13
- from .html_node import HTMLNode, no_start_tag, no_end_tag, RawText
13
+ from .html_node import HTMLNode, no_start_tag, no_end_tag, one_line, RawText
14
+ from .html_tags import TextNode
@@ -10,25 +10,35 @@
10
10
  #
11
11
  # =======================================================================
12
12
 
13
+ import copy
13
14
  import itertools
14
15
  from typing import Any, Dict, List, Union
16
+ from webwidgets.utility.representation import ReprMixin
15
17
  from webwidgets.utility.sanitizing import sanitize_html_text
18
+ from webwidgets.utility.validation import validate_html_class
16
19
 
17
20
 
18
- class HTMLNode:
21
+ class HTMLNode(ReprMixin):
19
22
  """Represents an HTML node (for example, a div or a span).
20
23
  """
21
24
 
22
25
  one_line: bool = False
23
26
 
24
- def __init__(self, children: List['HTMLNode'] = [], attributes: Dict[str, str] = {}):
25
- """Creates an HTMLNode with optional children and attributes.
27
+ def __init__(self, children: List['HTMLNode'] = None,
28
+ attributes: Dict[str, str] = None, style: Dict[str, str] = None):
29
+ """Creates an HTMLNode with optional children, attributes, and style.
26
30
 
27
31
  :param children: List of child HTML nodes. Defaults to an empty list.
32
+ :type children: List[HTMLNode]
28
33
  :param attributes: Dictionary of attributes for the node. Defaults to an empty dictionary.
34
+ :type attributes: Dict[str, str]
35
+ :param style: Dictionary of CSS properties for the node. Defaults to an empty dictionary.
36
+ :type style: Dict[str, str]
29
37
  """
30
- self.children = children
31
- self.attributes = attributes
38
+ super().__init__()
39
+ self.children = [] if children is None else children
40
+ self.attributes = {} if attributes is None else attributes
41
+ self.style = {} if style is None else style
32
42
 
33
43
  def _get_tag_name(self) -> str:
34
44
  """Returns the tag name of the HTML node.
@@ -51,21 +61,55 @@ class HTMLNode:
51
61
  )
52
62
 
53
63
  def add(self, child: 'HTMLNode') -> None:
54
- """
55
- Adds a child to the HTML node.
64
+ """Adds a child to the HTML node.
56
65
 
57
66
  :param child: The child to be added.
58
67
  """
59
68
  self.children.append(child)
60
69
 
70
+ def copy(self, deep: bool = False) -> 'HTMLNode':
71
+ """Returns a copy of the HTML node.
72
+
73
+ This method is just a convenient wrapper around Python's
74
+ `copy.copy()` and `copy.deepcopy()` methods.
75
+
76
+ :param deep: If True, creates a deep copy of the node and its children,
77
+ recursively. Otherwise, creates a shallow copy. Defaults to False.
78
+ :type deep: bool
79
+ :return: A new HTMLNode object that is a copy of the original.
80
+ :rtype: HTMLNode
81
+ """
82
+ if deep:
83
+ return copy.deepcopy(self)
84
+ return copy.copy(self)
85
+
86
+ def get_styles(self) -> Dict[int, Dict[str, str]]:
87
+ """Returns a dictionary mapping the node and all its children,
88
+ recursively, to their style.
89
+
90
+ Nodes are identified by their ID as obtained from Python's built-in
91
+ `id()` function.
92
+
93
+ :return: A dictionary mapping node IDs to styles.
94
+ :rtype: Dict[int, Dict[str, str]]
95
+ """
96
+ styles = {id(self): self.style}
97
+ for child in self.children:
98
+ styles.update(child.get_styles())
99
+ return styles
100
+
61
101
  @property
62
102
  def start_tag(self) -> str:
63
103
  """Returns the opening tag of the HTML node, including any attributes.
64
104
 
105
+ Attributes are validated with :py:meth:`HTMLNode.validate_attributes`
106
+ before rendering.
107
+
65
108
  :return: A string containing the opening tag of the element with its attributes.
66
109
  :rtype: str
67
110
  """
68
111
  # Rendering attributes
112
+ self.validate_attributes()
69
113
  attributes = self._render_attributes()
70
114
  maybe_space = ' ' if attributes else ''
71
115
 
@@ -87,8 +131,8 @@ class HTMLNode:
87
131
  **kwargs: Any) -> Union[str, List[str]]:
88
132
  """Converts the HTML node into HTML code.
89
133
 
90
- :param collapse_empty: If True, collapses empty elements into a single line.
91
- Defaults to True.
134
+ :param collapse_empty: If True, collapses elements without any children
135
+ into a single line. Defaults to True.
92
136
  :type collapse_empty: bool
93
137
  :param indent_size: The number of spaces to use for each indentation level.
94
138
  :type indent_size: int
@@ -141,6 +185,13 @@ class HTMLNode:
141
185
  # Otherwise, return a single string
142
186
  return '\n'.join(html_lines)
143
187
 
188
+ def validate_attributes(self) -> None:
189
+ """Validate the node's attributes and raises an exception with a
190
+ descriptive error message if any attribute is invalid.
191
+ """
192
+ if "class" in self.attributes:
193
+ validate_html_class(self.attributes["class"])
194
+
144
195
 
145
196
  def no_start_tag(cls):
146
197
  """Decorator to remove the start tag from an HTMLNode subclass.
@@ -164,13 +215,22 @@ def no_end_tag(cls):
164
215
  return cls
165
216
 
166
217
 
218
+ def one_line(cls):
219
+ """Decorator to make an HTMLNode subclass a one-line element.
220
+
221
+ :param cls: A subclass of HTMLNode.
222
+ :return: The given class with the `one_line` attribute set to True.
223
+ """
224
+ cls.one_line = True
225
+ return cls
226
+
227
+
167
228
  @no_start_tag
168
229
  @no_end_tag
230
+ @one_line
169
231
  class RawText(HTMLNode):
170
232
  """A raw text node that contains text without any HTML tags."""
171
233
 
172
- one_line = True
173
-
174
234
  def __init__(self, text: str):
175
235
  """Creates a raw text node.
176
236
 
@@ -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 .html_node import HTMLNode, one_line, RawText
14
+ from typing import Dict
15
+
16
+
17
+ @one_line
18
+ class TextNode(HTMLNode):
19
+ """A one-line HTML element that only contains raw text (like `<h1>`).
20
+
21
+ A text node renders on one line and only contains one child: a
22
+ :py:class:`RawText` node with the text to be rendered.
23
+ """
24
+
25
+ def __init__(self, text: str, attributes: Dict[str, str] = None,
26
+ style: Dict[str, str] = None):
27
+ """Creates a new text node with the given text and attributes.
28
+
29
+ :param text: The text content of the node.
30
+ :type text: str
31
+ :param attributes: See :py:meth:`HTMLNode.__init__`. Defaults to an
32
+ empty dictionary.
33
+ :type attributes: Dict[str, str]
34
+ :param style: See :py:meth:`HTMLNode.__init__`. Defaults to an empty
35
+ dictionary.
36
+ :type style: Dict[str, str]
37
+ """
38
+ super().__init__(children=[
39
+ RawText(text)
40
+ ], attributes=attributes, style=style)
@@ -10,4 +10,6 @@
10
10
  #
11
11
  # =======================================================================
12
12
 
13
+ from .representation import *
13
14
  from .sanitizing import *
15
+ from .validation import *
@@ -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})"
@@ -30,7 +30,7 @@ CHAR_TO_HTML_ENTITIES = {k: tuple(v)
30
30
  for k, v in CHAR_TO_HTML_ENTITIES.items()}
31
31
 
32
32
 
33
- # Regular expression mathing all isolated '&' characters that are not part of an
33
+ # Regular expression matching all isolated '&' characters that are not part of an
34
34
  # HTML entity.
35
35
  _REGEX_AMP = re.compile(f"&(?!({'|'.join(HTML_ENTITIES.keys())}))")
36
36
 
@@ -107,7 +107,7 @@ def sanitize_html_text(text: str, replace_all_entities: bool = False) -> str:
107
107
  :type text: str
108
108
  :param replace_all_entities: Whether to replace every character that can be
109
109
  represented by an HTML entity. Use False to skip non-mandatory characters
110
- and increase speed. Default is False.
110
+ and increase speed. Defaults to False.
111
111
  :type replace_all_entities: bool
112
112
  :return: The sanitized HTML text.
113
113
  :rtype: str
@@ -0,0 +1,97 @@
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 *
14
+ import re
15
+
16
+
17
+ def validate_css_identifier(identifier: str) -> None:
18
+ """Checks if the given identifier is a valid identifier token according to
19
+ the CSS syntax rules and raises an exception if not.
20
+
21
+ An identifier token is a sequence of characters that can be used as part of
22
+ a CSS rule, like a class name or an ID. The concept essentially corresponds
23
+ to that of an `ident-token` in the official CSS specification.
24
+
25
+ This function enforces the following rules:
26
+ - the identifier must only contain letters (`a-z`, `A-Z`), digits (`0-9`),
27
+ underscores (`_`), and hyphens (`-`)
28
+ - the identifier must start with either a letter, an underscore, or a
29
+ double hyphen (`--`)
30
+
31
+ Note that this function imposes stricter rules on identifier tokens than
32
+ the official CSS specification - more precisely, than chapter 4 of the CSS
33
+ Syntax Module Level 3 (see source:
34
+ https://www.w3.org/TR/css-syntax-3/#tokenization - note that this chapter
35
+ remains the same in the current draft for Level 4). For example, this
36
+ function does not allow escaped special characters nor identifier tokens
37
+ starting with a single hyphen whereas the specification does.
38
+
39
+ :param identifier: The string to be validated as an identifier token.
40
+ :type identifier: str
41
+ :raises ValueError: If the identifier is not a valid identifier token and
42
+ does not respect the specified rules.
43
+ """
44
+ # Check if identifier starts with anything else than a letter, an
45
+ # underscore, or a double hyphen
46
+ if not re.match(r'^[a-zA-Z_]+|--', identifier):
47
+ raise ValueError("CSS identifier must start with either a letter, an "
48
+ "underscore, or a double hyphen (`--`), but got: "
49
+ f"'{identifier}'")
50
+
51
+ # Check if identifier contains invalid characters
52
+ if not re.match(r'^[a-zA-Z0-9_-]+$', identifier):
53
+ invalid_chars = re.findall('[^a-zA-Z0-9_-]', identifier)
54
+ raise ValueError("Invalid character(s) in CSS idenfitier "
55
+ f"'{identifier}': {', '.join(invalid_chars)}\n"
56
+ "Only letters, digits, hyphens, and underscores are "
57
+ "allowed.")
58
+
59
+
60
+ def validate_html_class(class_attribute: str) -> None:
61
+ """Checks if the given HTML class attribute is valid and raises an
62
+ exception if not.
63
+
64
+ This function enforces the following rules:
65
+ - the class attribute cannot start nor end with a space
66
+ - the class attribute cannot contain double spaces
67
+ - each class in the attribute must be a valid CSS identifier, as validated
68
+ by the :py:func:`validate_css_identifier` function.
69
+
70
+ Note that this function imposes stricter rules than rule 2.3.7 of the HTML5
71
+ specification (see source:
72
+ https://html.spec.whatwg.org/#set-of-space-separated-tokens). For example,
73
+ it does not allow for leading nor trailing spaces whereas the specification
74
+ does.
75
+
76
+ :param class_attribute: The HTML class attribute to be validated.
77
+ :type class_attribute: str
78
+ :raises ValueError: If the class attribute is invalid and does not respect
79
+ the specified rules.
80
+ """
81
+ # Allow for empty attribute
82
+ if not class_attribute:
83
+ return
84
+
85
+ # Check if the class attribute starts or ends with a space
86
+ if class_attribute.startswith(' ') or class_attribute.endswith(' '):
87
+ raise ValueError("Class attribute cannot start nor end with a space, "
88
+ f"but got: '{class_attribute}'")
89
+
90
+ # Check for double spaces in the class attribute
91
+ if ' ' in class_attribute:
92
+ raise ValueError("Class attribute cannot contain double spaces, "
93
+ f"but got: '{class_attribute}'")
94
+
95
+ # Check each class individually
96
+ for c in class_attribute.split(' '):
97
+ validate_css_identifier(c)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webwidgets
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -0,0 +1,15 @@
1
+ webwidgets/__init__.py,sha256=lWweaTCcPhSUa2zi5l7UnoDSgup_q81dh0pn_kGYYT0,481
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=msFTyuIjEvLTKk4H-tvvr9HK1K6N-_hcF1pmOJWT1S8,9654
5
+ webwidgets/compilation/html/__init__.py,sha256=NRccbCUKdhmK51MnYFO8q1sS8fWhAggRnMiGDG7lQMQ,505
6
+ webwidgets/compilation/html/html_node.py,sha256=QptDwSD2ljzR5S-r5rXuRBeOz8EF55OlZlWTXgQzI4Y,10189
7
+ webwidgets/compilation/html/html_tags.py,sha256=U2HmhLkV6BOqTmgvIMlmMCQysQ7i2nEAVWJzZe74ucA,1388
8
+ webwidgets/utility/__init__.py,sha256=nY-j5tu45qGl09lRyMyxkT4zsUf2jCfeXWcx954yIvM,478
9
+ webwidgets/utility/representation.py,sha256=lQ15v_DZOHBQKLM8pzRE1tuJkU_modhPTpWpSJ2lBCE,1061
10
+ webwidgets/utility/sanitizing.py,sha256=OKJRDqk-OXYCWeK6ie3GdfQvb49wTs93kd971mg5oK0,5770
11
+ webwidgets/utility/validation.py,sha256=bUjpiGP59GW3DPvQ1hwR5ezBMmcSd6v4xlDLwTHZv_A,4261
12
+ webwidgets-0.2.1.dist-info/METADATA,sha256=Adl61Eg47Lw7AuZ5sJN_9-MMumjgsEVFZ33glOceGUk,550
13
+ webwidgets-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ webwidgets-0.2.1.dist-info/licenses/LICENSE,sha256=LISw1mw5eK6i8adFSlx6zltZxrJFwurngVdZAEU8g_I,1064
15
+ webwidgets-0.2.1.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- webwidgets/__init__.py,sha256=V5LcquUtoHyCfL072Oo3HNH73msF9lKd4Vx5odghyZk,481
2
- webwidgets/compilation/__init__.py,sha256=rebNuvAfuSVcRiuMOezktEod6C7qKl9tp__ddV4PNMg,415
3
- webwidgets/compilation/html/__init__.py,sha256=t7xIAup9slzxfyp-spbXFM8YxPBJF0JJyaAmMe5NmXI,463
4
- webwidgets/compilation/html/html_node.py,sha256=R4SsjvmbiM4WvcVuwhyXLwxYqdk9MoqVnr4idqFpZ0s,7846
5
- webwidgets/utility/__init__.py,sha256=6lYOxZIb_wcNGpu55FjPN9fTben37x8I2XU6HOPmADo,422
6
- webwidgets/utility/sanitizing.py,sha256=L72-E6er9p5e3nLupJJqcMMB62O_4lYzaDyMEyhBjE4,5768
7
- webwidgets-0.1.0.dist-info/METADATA,sha256=NgmpZVNES4wgiMZSIWqE1CKDtPy3YRrnlIe7DV0dcH0,550
8
- webwidgets-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- webwidgets-0.1.0.dist-info/licenses/LICENSE,sha256=LISw1mw5eK6i8adFSlx6zltZxrJFwurngVdZAEU8g_I,1064
10
- webwidgets-0.1.0.dist-info/RECORD,,