webwidgets 0.1.0__py3-none-any.whl → 0.2.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 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.0" # 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,13 @@
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, CompiledCSS, apply_css
@@ -0,0 +1,171 @@
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 Dict, List, Union
15
+ from webwidgets.compilation.html.html_node import HTMLNode
16
+ from webwidgets.utility.validation import validate_css_identifier
17
+
18
+
19
+ class CompiledCSS:
20
+ """A utility class to hold compiled CSS rules.
21
+ """
22
+
23
+ def __init__(self, trees: List[HTMLNode], rules: Dict[str, Dict[str, str]],
24
+ mapping: Dict[int, List[str]]):
25
+ """Stores compiled CSS rules.
26
+
27
+ :param trees: The HTML trees at the origin of the compilation. These
28
+ are the elements that have been styled with CSS properties.
29
+ :type trees: List[HTMLNode]
30
+ :param rules: The compiled CSS rules, specified as a dictionary mapping
31
+ the rule's name to its corresponding CSS declarations. For example:
32
+ `{'r0': {'color': 'red'}}`.
33
+ :type rules: Dict[str, Dict[str, str]]
34
+ :param mapping: A dictionary mapping each node ID to a list of rules
35
+ that achieve the same style. Rules must be specified by their name.
36
+ For example: `{123: ['r0', 'r2'], 456: ['r1']}`.
37
+ :type mapping: Dict[int, List[str]]
38
+ """
39
+ self.trees = trees
40
+ self.rules = rules
41
+ self.mapping = mapping
42
+
43
+ def to_css(self, indent_size: int = 4) -> str:
44
+ """Converts the `rules` dictionary of the :py:class:`CompiledCSS`
45
+ object into CSS code.
46
+
47
+ Each rule name is converted to a class selector and each property name
48
+ is validated with :py:func:`validate_css_identifier` before being
49
+ converted.
50
+
51
+ :param indent_size: The number of spaces to use for indentation in the
52
+ CSS code. Defaults to 4.
53
+ :type indent_size: int
54
+ :return: The CSS code as a string.
55
+ :rtype: str
56
+ """
57
+ # Initializing code and defining indentation
58
+ css_code = ""
59
+ indentation = ' ' * indent_size
60
+
61
+ # Writing down each rule from the rules dictionary
62
+ for i, (name, declarations) in enumerate(self.rules.items()):
63
+ css_code += f".{name}" + " {\n"
64
+ for property_name, value in declarations.items():
65
+ validate_css_identifier(property_name)
66
+ css_code += f"{indentation}{property_name}: {value};\n"
67
+ css_code += "}" + ('\n\n' if i < len(self.rules) - 1 else '')
68
+
69
+ return css_code
70
+
71
+
72
+ def compile_css(trees: Union[HTMLNode, List[HTMLNode]]) -> CompiledCSS:
73
+ """Computes optimized CSS rules from the given HTML trees.
74
+
75
+ The main purpose of this function is to reduce the number of CSS rules
76
+ required to achieve a particular style across one or more HTML trees. The
77
+ function takes a list of HTML nodes as input (not necessarily from the same
78
+ tree) and computes an optimized set of CSS rules that achieves the same
79
+ style across all nodes. The resulting :py:class:`CompiledCSS` object
80
+ contains the optimized rules and their mapping to each node.
81
+
82
+ For example, the following tree:
83
+
84
+ .. code-block:: python
85
+
86
+ tree = HTMLNode(
87
+ style={"margin": "0", "padding": "0"},
88
+ children=[
89
+ HTMLNode(style={"margin": "0", "padding": "0"}),
90
+ HTMLNode(style={"margin": "0", "color": "blue"}),
91
+ ]
92
+ )
93
+
94
+ can be stylistically described with only 3 CSS rules:
95
+
96
+ .. code-block:: python
97
+
98
+ >>> compiled_css = compile_css(tree)
99
+ >>> print(compiled_css.rules)
100
+ {
101
+ 'r0': {'color': 'blue'},
102
+ 'r1': {'margin': '0'},
103
+ 'r2': {'padding': '0'}
104
+ }
105
+
106
+ :param trees: A single tree or a list of trees to optimize over. All
107
+ children are recursively included in the compilation.
108
+ :type trees: Union[HTMLNode, List[HTMLNode]]
109
+ :return: The :py:class:`CompiledCSS` object containing the optimized rules.
110
+ Every HTML node present in one or more of the input trees is included
111
+ in the :py:attr:`CompiledCSS.mapping` attribute, even if the node does
112
+ not have a style. Rules are alphabetically ordered by name in the
113
+ mapping.
114
+ :rtype: CompiledCSS
115
+ """
116
+ # Handling case of a single tree
117
+ if isinstance(trees, HTMLNode):
118
+ trees = [trees]
119
+
120
+ # For now, we just return a simple mapping where each CSS property defines
121
+ # its own ruleset
122
+ styles = {k: v for tree in trees for k, v in tree.get_styles().items()}
123
+ properties = set(itertools.chain.from_iterable(s.items()
124
+ for s in styles.values()))
125
+ rules = {f"r{i}": dict([p]) for i, p in enumerate(sorted(properties))}
126
+ mapping = {node_id: sorted([n for n, r in rules.items() if
127
+ set(r.items()).issubset(style.items())])
128
+ for node_id, style in styles.items()}
129
+ return CompiledCSS(trees, rules, mapping)
130
+
131
+
132
+ def apply_css(css: CompiledCSS, tree: HTMLNode) -> None:
133
+ """Applies the CSS rules to the given tree.
134
+
135
+ Rules are added as HTML classes to each node with a style in the tree. If a
136
+ node does not have a `class` attribute yet, it will be created for that
137
+ node. Nodes that do not have any style are left untouched.
138
+
139
+ Note that this function is recursive and calls itself on each child node of
140
+ the tree.
141
+
142
+ :param css: The compiled CSS object containing the rules to apply and the
143
+ mapping to each node. It should have been created by invoking
144
+ :py:func:`compile_css` on the given tree, but it can be modified before
145
+ passing it to this function, as long as its content remains consistent.
146
+ :type css: CompiledCSS
147
+ :param tree: The tree to which the CSS rules should be applied. It will be
148
+ modified in place by this function. If you want to keep the original
149
+ tree unchanged, make a deep copy of it using its
150
+ :py:meth:`HTMLNode.copy` method and pass this copy instead.
151
+ :type tree: HTMLNode
152
+ """
153
+ # Only modifying nodes if they have a style (and therefore if the list of
154
+ # rules mapped to them in `css.mapping` is not empty)
155
+ if tree.style:
156
+
157
+ # Listing rules to add as classes. We do not add rules that are already
158
+ # there.
159
+ rules_to_add = [r for r in css.mapping[id(tree)] if r and r not in
160
+ tree.attributes.get('class', '').split(' ')]
161
+
162
+ # Updating the class attribute. If it already exists and is not empty,
163
+ # we need to insert a space before adding the CSS classes.
164
+ maybe_space = ' ' if tree.attributes.get(
165
+ 'class', None) and rules_to_add else ''
166
+ tree.attributes['class'] = tree.attributes.get(
167
+ 'class', '') + maybe_space + ' '.join(rules_to_add)
168
+
169
+ # Recursively applying the CSS rules to all child nodes of the tree
170
+ for child in tree.children:
171
+ apply_css(css, child)
@@ -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,9 +10,11 @@
10
10
  #
11
11
  # =======================================================================
12
12
 
13
+ import copy
13
14
  import itertools
14
15
  from typing import Any, Dict, List, Union
15
16
  from webwidgets.utility.sanitizing import sanitize_html_text
17
+ from webwidgets.utility.validation import validate_html_class
16
18
 
17
19
 
18
20
  class HTMLNode:
@@ -21,14 +23,20 @@ class HTMLNode:
21
23
 
22
24
  one_line: bool = False
23
25
 
24
- def __init__(self, children: List['HTMLNode'] = [], attributes: Dict[str, str] = {}):
25
- """Creates an HTMLNode with optional children and attributes.
26
+ def __init__(self, children: List['HTMLNode'] = None,
27
+ attributes: Dict[str, str] = None, style: Dict[str, str] = None):
28
+ """Creates an HTMLNode with optional children, attributes, and style.
26
29
 
27
30
  :param children: List of child HTML nodes. Defaults to an empty list.
31
+ :type children: List[HTMLNode]
28
32
  :param attributes: Dictionary of attributes for the node. Defaults to an empty dictionary.
33
+ :type attributes: Dict[str, str]
34
+ :param style: Dictionary of CSS properties for the node. Defaults to an empty dictionary.
35
+ :type style: Dict[str, str]
29
36
  """
30
- self.children = children
31
- self.attributes = attributes
37
+ self.children = [] if children is None else children
38
+ self.attributes = {} if attributes is None else attributes
39
+ self.style = {} if style is None else style
32
40
 
33
41
  def _get_tag_name(self) -> str:
34
42
  """Returns the tag name of the HTML node.
@@ -51,21 +59,55 @@ class HTMLNode:
51
59
  )
52
60
 
53
61
  def add(self, child: 'HTMLNode') -> None:
54
- """
55
- Adds a child to the HTML node.
62
+ """Adds a child to the HTML node.
56
63
 
57
64
  :param child: The child to be added.
58
65
  """
59
66
  self.children.append(child)
60
67
 
68
+ def copy(self, deep: bool = False) -> 'HTMLNode':
69
+ """Returns a copy of the HTML node.
70
+
71
+ This method is just a convenient wrapper around Python's
72
+ `copy.copy()` and `copy.deepcopy()` methods.
73
+
74
+ :param deep: If True, creates a deep copy of the node and its children,
75
+ recursively. Otherwise, creates a shallow copy. Defaults to False.
76
+ :type deep: bool
77
+ :return: A new HTMLNode object that is a copy of the original.
78
+ :rtype: HTMLNode
79
+ """
80
+ if deep:
81
+ return copy.deepcopy(self)
82
+ return copy.copy(self)
83
+
84
+ def get_styles(self) -> Dict[int, Dict[str, str]]:
85
+ """Returns a dictionary mapping the node and all its children,
86
+ recursively, to their style.
87
+
88
+ Nodes are identified by their ID as obtained from Python's built-in
89
+ `id()` function.
90
+
91
+ :return: A dictionary mapping node IDs to styles.
92
+ :rtype: Dict[int, Dict[str, str]]
93
+ """
94
+ styles = {id(self): self.style}
95
+ for child in self.children:
96
+ styles.update(child.get_styles())
97
+ return styles
98
+
61
99
  @property
62
100
  def start_tag(self) -> str:
63
101
  """Returns the opening tag of the HTML node, including any attributes.
64
102
 
103
+ Attributes are validated with :py:meth:`HTMLNode.validate_attributes`
104
+ before rendering.
105
+
65
106
  :return: A string containing the opening tag of the element with its attributes.
66
107
  :rtype: str
67
108
  """
68
109
  # Rendering attributes
110
+ self.validate_attributes()
69
111
  attributes = self._render_attributes()
70
112
  maybe_space = ' ' if attributes else ''
71
113
 
@@ -87,8 +129,8 @@ class HTMLNode:
87
129
  **kwargs: Any) -> Union[str, List[str]]:
88
130
  """Converts the HTML node into HTML code.
89
131
 
90
- :param collapse_empty: If True, collapses empty elements into a single line.
91
- Defaults to True.
132
+ :param collapse_empty: If True, collapses elements without any children
133
+ into a single line. Defaults to True.
92
134
  :type collapse_empty: bool
93
135
  :param indent_size: The number of spaces to use for each indentation level.
94
136
  :type indent_size: int
@@ -141,6 +183,13 @@ class HTMLNode:
141
183
  # Otherwise, return a single string
142
184
  return '\n'.join(html_lines)
143
185
 
186
+ def validate_attributes(self) -> None:
187
+ """Validate the node's attributes and raises an exception with a
188
+ descriptive error message if any attribute is invalid.
189
+ """
190
+ if "class" in self.attributes:
191
+ validate_html_class(self.attributes["class"])
192
+
144
193
 
145
194
  def no_start_tag(cls):
146
195
  """Decorator to remove the start tag from an HTMLNode subclass.
@@ -164,13 +213,22 @@ def no_end_tag(cls):
164
213
  return cls
165
214
 
166
215
 
216
+ def one_line(cls):
217
+ """Decorator to make an HTMLNode subclass a one-line element.
218
+
219
+ :param cls: A subclass of HTMLNode.
220
+ :return: The given class with the `one_line` attribute set to True.
221
+ """
222
+ cls.one_line = True
223
+ return cls
224
+
225
+
167
226
  @no_start_tag
168
227
  @no_end_tag
228
+ @one_line
169
229
  class RawText(HTMLNode):
170
230
  """A raw text node that contains text without any HTML tags."""
171
231
 
172
- one_line = True
173
-
174
232
  def __init__(self, text: str):
175
233
  """Creates a raw text node.
176
234
 
@@ -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)
@@ -11,3 +11,4 @@
11
11
  # =======================================================================
12
12
 
13
13
  from .sanitizing import *
14
+ from .validation import *
@@ -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.0
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,14 @@
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,,
@@ -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,,