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 +15 -0
- webwidgets/compilation/__init__.py +13 -0
- webwidgets/compilation/html/__init__.py +13 -0
- webwidgets/compilation/html/html_node.py +210 -0
- webwidgets/utility/__init__.py +13 -0
- webwidgets/utility/sanitizing.py +132 -0
- webwidgets-0.1.0.dist-info/METADATA +18 -0
- webwidgets-0.1.0.dist-info/RECORD +10 -0
- webwidgets-0.1.0.dist-info/WHEEL +4 -0
- webwidgets-0.1.0.dist-info/licenses/LICENSE +21 -0
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. "&").
|
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('&', text)
|
121
|
+
|
122
|
+
# Replacing ';' ONLY when not part of an HTML entity itself
|
123
|
+
text = _REGEXP_SEMI.sub(';', 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
|
+

|
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,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.
|