webwidgets 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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.