webwidgets 0.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 ADDED
@@ -0,0 +1,15 @@
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
+ __version__ = "0.1.0" # Dynamically set by build backend
14
+
15
+ from . import compilation
@@ -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 . 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 .html_node import HTMLNode, no_start_tag, no_end_tag, RawText
@@ -0,0 +1,210 @@
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 Any, Dict, List, Union
15
+ from webwidgets.utility.sanitizing import sanitize_html_text
16
+
17
+
18
+ class HTMLNode:
19
+ """Represents an HTML node (for example, a div or a span).
20
+ """
21
+
22
+ one_line: bool = False
23
+
24
+ def __init__(self, children: List['HTMLNode'] = [], attributes: Dict[str, str] = {}):
25
+ """Creates an HTMLNode with optional children and attributes.
26
+
27
+ :param children: List of child HTML nodes. Defaults to an empty list.
28
+ :param attributes: Dictionary of attributes for the node. Defaults to an empty dictionary.
29
+ """
30
+ self.children = children
31
+ self.attributes = attributes
32
+
33
+ def _get_tag_name(self) -> str:
34
+ """Returns the tag name of the HTML node.
35
+
36
+ The tag name of a node object is the name of its class in lowercase.
37
+
38
+ :return: The tag name of the HTML node.
39
+ :rtype: str
40
+ """
41
+ return self.__class__.__name__.lower()
42
+
43
+ def _render_attributes(self) -> str:
44
+ """Renders the attributes of the HTML node into a string that can be added to the start tag.
45
+
46
+ :return: A string containing all attribute key-value pairs separated by spaces.
47
+ :rtype: str
48
+ """
49
+ return ' '.join(
50
+ f'{key}="{value}"' for key, value in self.attributes.items()
51
+ )
52
+
53
+ def add(self, child: 'HTMLNode') -> None:
54
+ """
55
+ Adds a child to the HTML node.
56
+
57
+ :param child: The child to be added.
58
+ """
59
+ self.children.append(child)
60
+
61
+ @property
62
+ def start_tag(self) -> str:
63
+ """Returns the opening tag of the HTML node, including any attributes.
64
+
65
+ :return: A string containing the opening tag of the element with its attributes.
66
+ :rtype: str
67
+ """
68
+ # Rendering attributes
69
+ attributes = self._render_attributes()
70
+ maybe_space = ' ' if attributes else ''
71
+
72
+ # Building start tag
73
+ return f"<{self._get_tag_name()}{maybe_space}{attributes}>"
74
+
75
+ @property
76
+ def end_tag(self) -> str:
77
+ """Returns the closing tag of the HTML node.
78
+
79
+ :return: A string containing the closing tag of the element.
80
+ :rtype: str
81
+ """
82
+ return f"</{self._get_tag_name()}>"
83
+
84
+ def to_html(self, collapse_empty: bool = True,
85
+ indent_size: int = 4, indent_level: int = 0,
86
+ force_one_line: bool = False, return_lines: bool = False,
87
+ **kwargs: Any) -> Union[str, List[str]]:
88
+ """Converts the HTML node into HTML code.
89
+
90
+ :param collapse_empty: If True, collapses empty elements into a single line.
91
+ Defaults to True.
92
+ :type collapse_empty: bool
93
+ :param indent_size: The number of spaces to use for each indentation level.
94
+ :type indent_size: int
95
+ :param indent_level: The current level of indentation in the HTML output.
96
+ :type indent_level: int
97
+ :param force_one_line: If True, forces all child elements to be rendered on a single line without additional
98
+ indentation. Defaults to False.
99
+ :type force_one_line: bool
100
+ :param return_lines: Whether to return the lines of HTML code individually. Defaults to False.
101
+ :type return_lines: bool
102
+ :param **kwargs: Additional keyword arguments to pass down to child elements.
103
+ :type **kwargs: Any
104
+ :return: A string containing the HTML representation of the element if
105
+ `return_lines` is `False` (default), or the list of individual lines
106
+ from that HTML code if `return_lines` is `True`.
107
+ :rtype: str or List[str]
108
+ """
109
+ # Opening the element
110
+ indentation = "" if force_one_line else ' ' * indent_size * indent_level
111
+ html_lines = [indentation + self.start_tag]
112
+
113
+ # If content must be in one line
114
+ if self.one_line or force_one_line or (collapse_empty
115
+ and not self.children):
116
+ html_lines += list(itertools.chain.from_iterable(
117
+ [c.to_html(collapse_empty=collapse_empty,
118
+ indent_level=0, force_one_line=True, return_lines=True,
119
+ **kwargs)
120
+ for c in self.children]))
121
+ html_lines += [self.end_tag]
122
+ html_lines = [''.join(html_lines)] # Flattening the line
123
+
124
+ # If content spans multi-line
125
+ else:
126
+ html_lines += list(itertools.chain.from_iterable(
127
+ [c.to_html(collapse_empty=collapse_empty,
128
+ indent_size=indent_size,
129
+ indent_level=indent_level + 1,
130
+ return_lines=True,
131
+ **kwargs)
132
+ for c in self.children]))
133
+ html_lines += [indentation + self.end_tag]
134
+ html_lines = [l for l in html_lines if any(
135
+ c != ' ' for c in l)] # Trimming empty lines
136
+
137
+ # If return_lines is True, return a list of lines
138
+ if return_lines:
139
+ return html_lines
140
+
141
+ # Otherwise, return a single string
142
+ return '\n'.join(html_lines)
143
+
144
+
145
+ def no_start_tag(cls):
146
+ """Decorator to remove the start tag from an HTMLNode subclass.
147
+
148
+ :param cls: A subclass of HTMLNode whose start tag should be removed.
149
+ :return: The given class with an empty start tag.
150
+ """
151
+ cls.start_tag = property(
152
+ lambda _: '', doc="This element does not have a start tag")
153
+ return cls
154
+
155
+
156
+ def no_end_tag(cls):
157
+ """Decorator to remove the end tag from an HTMLNode subclass.
158
+
159
+ :param cls: A subclass of HTMLNode whose end tag should be removed.
160
+ :return: The given class with an empty end tag.
161
+ """
162
+ cls.end_tag = property(
163
+ lambda _: '', doc="This element does not have an end tag")
164
+ return cls
165
+
166
+
167
+ @no_start_tag
168
+ @no_end_tag
169
+ class RawText(HTMLNode):
170
+ """A raw text node that contains text without any HTML tags."""
171
+
172
+ one_line = True
173
+
174
+ def __init__(self, text: str):
175
+ """Creates a raw text node.
176
+
177
+ :param text: The text content of the node. It will be sanitized in
178
+ :py:meth:`RawText.to_html` before being written into HTML code.
179
+ :type text: str
180
+ """
181
+ super().__init__()
182
+ self.text = text
183
+
184
+ def to_html(self, indent_size: int = 4, indent_level: int = 0,
185
+ return_lines: bool = False, replace_all_entities: bool = False,
186
+ **kwargs: Any) -> Union[str, List[str]]:
187
+ """Converts the raw text node to HTML.
188
+
189
+ The text is sanitized by the :py:func:`sanitize_html_text` function before
190
+ being written into HTML code.
191
+
192
+ :param indent_size: See :py:meth:`HTMLNode.to_html`.
193
+ :type indent_size: int
194
+ :param indent_level: See :py:meth:`HTMLNode.to_html`.
195
+ :type indent_level: int
196
+ :param return_lines: See :py:meth:`HTMLNode.to_html`.
197
+ :type return_lines: bool
198
+ :param replace_all_entities: See :py:func:`sanitize_html_text`.
199
+ :type replace_all_entities: bool
200
+ :param kwargs: Other keyword arguments. These are ignored.
201
+ :type kwargs: Any
202
+ :return: See :py:meth:`HTMLNode.to_html`.
203
+ :rtype: str or List[str]
204
+ """
205
+ sanitized = sanitize_html_text(
206
+ self.text, replace_all_entities=replace_all_entities)
207
+ line = ' ' * indent_size * indent_level + sanitized
208
+ if return_lines:
209
+ return [line]
210
+ return line
@@ -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 .sanitizing import *
@@ -0,0 +1,132 @@
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.entities import html5 as HTML_ENTITIES
14
+ import re
15
+ from typing import Tuple
16
+
17
+
18
+ # Maps characters to their corresponding character references. If a character can be
19
+ # represented by multiple entities, the preferred one is placed first in the tuple.
20
+ # Preference is given to the shortest one with a semicolon, in lowercase if possible
21
+ # (e.g. "&amp;").
22
+ CHAR_TO_HTML_ENTITIES = {v: sorted([
23
+ k for k in HTML_ENTITIES if HTML_ENTITIES[k] == v
24
+ ], key=len) for v in HTML_ENTITIES.values()}
25
+ for _, entities in CHAR_TO_HTML_ENTITIES.items():
26
+ e = next((e for e in entities if ';' in e), entities[0])
27
+ i = entities.index(e.lower() if e.lower() in entities else e)
28
+ entities[i], entities[0] = entities[0], entities[i]
29
+ CHAR_TO_HTML_ENTITIES = {k: tuple(v)
30
+ for k, v in CHAR_TO_HTML_ENTITIES.items()}
31
+
32
+
33
+ # Regular expression mathing all isolated '&' characters that are not part of an
34
+ # HTML entity.
35
+ _REGEX_AMP = re.compile(f"&(?!({'|'.join(HTML_ENTITIES.keys())}))")
36
+
37
+
38
+ # Regular expression matching all isolated ';' characters that are not part of an
39
+ # HTML entity. The expression essentially concatenates one lookbehind per entity.
40
+ _REGEXP_SEMI = re.compile(
41
+ ''.join(f"(?<!&{e.replace(';', '')})"
42
+ for e in HTML_ENTITIES if ';' in e) + ';')
43
+
44
+
45
+ # Entities that are always replaced during sanitization. These are: <, >, /,
46
+ # according to rule 13.1.2.6 of the HTML5 specification, as well as single quotes
47
+ # ', double quotes ", and new line characters '\n'.
48
+ # Source: https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
49
+ _ALWAYS_SANITIZED = ("\u003C", "\u003E", "\u002F", "'", "\"", "\n")
50
+
51
+
52
+ # Entities other than new line characters '\n' (which require special treatment)
53
+ # that are always replaced during sanitization.
54
+ _ALWAYS_SANITIZED_BUT_NEW_LINES = tuple(
55
+ e for e in _ALWAYS_SANITIZED if e != '\n')
56
+
57
+
58
+ # Entities other than the ampersand and semicolon (which require special treatment
59
+ # because they are part of other entities) that are replaced by default during
60
+ # sanitization but can also be skipped for speed. This set of entities consists of
61
+ # all remaining entities but the ampersand and semicolon.
62
+ _OPTIONALLY_SANITIZED_BUT_AMP_SEMI = tuple(
63
+ set(CHAR_TO_HTML_ENTITIES.keys()) - set(_ALWAYS_SANITIZED) - set({'&', ';'}))
64
+
65
+
66
+ def replace_html_entities(text: str, characters: Tuple[str]) -> str:
67
+ """Replaces characters with their corresponding HTML entities in the given text.
68
+
69
+ If a character can be represented by multiple entities, preference is given to
70
+ the shortest one that contains a semicolon, in lowercase if possible.
71
+
72
+ :param text: The input text containing HTML entities.
73
+ :type text: str
74
+ :param characters: The characters to be replaced by their HTML entity. Usually
75
+ each item in the tuple is a single character, but some entities span
76
+ multiple characters.
77
+ :type characters: Tuple[str]
78
+ :return: The text with HTML entities replaced.
79
+ :rtype: str
80
+ """
81
+ for c in characters:
82
+ entity = CHAR_TO_HTML_ENTITIES[c][0] # Preferred is first
83
+ text = text.replace(c, '&' + entity)
84
+ return text
85
+
86
+
87
+ def sanitize_html_text(text: str, replace_all_entities: bool = False) -> str:
88
+ """Sanitizes raw HTML text by replacing certain characters with HTML-friendly equivalents.
89
+
90
+ Sanitization affects the following characters:
91
+ - `<`, `/`, and `>`, replaced with their corresponding HTML entities `lt;`,
92
+ `gt;`, and `sol;` according to rule 13.1.2.6 of the HTML5 specification
93
+ (see source:
94
+ https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions)
95
+ - single quotes `'` and double quotes `"`, replaced with their corresponding
96
+ HTML entities `apos;` and `quot;`
97
+ - new line characters '\\n', replaced with `br` tags
98
+ - if `replace_all_entities` is True, every character that can be represented by
99
+ an HTML entity is replaced with that entity. If a character can be
100
+ represented by multiple entities, preference is given to the shortest one
101
+ that contains a semicolon, in lowercase if possible.
102
+
103
+ See https://html.spec.whatwg.org/multipage/named-characters.html for a list of
104
+ all supported entities.
105
+
106
+ :param text: The raw HTML text that needs sanitization.
107
+ :type text: str
108
+ :param replace_all_entities: Whether to replace every character that can be
109
+ represented by an HTML entity. Use False to skip non-mandatory characters
110
+ and increase speed. Default is False.
111
+ :type replace_all_entities: bool
112
+ :return: The sanitized HTML text.
113
+ :rtype: str
114
+ """
115
+ # We start with all optional HTML entities, which enables us to replace all '&'
116
+ # and ';' before subsequently introducing more of them.
117
+ if replace_all_entities:
118
+
119
+ # Replacing '&' ONLY when not part of an HTML entity itself
120
+ text = _REGEX_AMP.sub('&amp;', text)
121
+
122
+ # Replacing ';' ONLY when not part of an HTML entity itself
123
+ text = _REGEXP_SEMI.sub('&semi;', text)
124
+
125
+ # Replacing the remaining HTML entities
126
+ text = replace_html_entities(text, _OPTIONALLY_SANITIZED_BUT_AMP_SEMI)
127
+
128
+ # Then we replace all mandatory HTML entities
129
+ text = replace_html_entities(text, _ALWAYS_SANITIZED_BUT_NEW_LINES)
130
+ text = text.replace('\n', '<br>') # Has to be last because of < and >
131
+
132
+ return text
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: webwidgets
3
+ Version: 0.1.0
4
+ Summary: A Python package for designing web UIs.
5
+ Project-URL: Source code, https://github.com/mlaasri/WebWidgets
6
+ Author: mlaasri
7
+ License-File: LICENSE
8
+ Keywords: design,webui
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ # WebWidgets
15
+
16
+ ![CI Status](https://img.shields.io/github/actions/workflow/status/mlaasri/WebWidgets/ci-full.yml?branch=main)
17
+
18
+ A Python package for creating web UIs
@@ -0,0 +1,10 @@
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mlaasri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.