html5cc 0.3.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.
html5/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """HTML5 document and CSS3 helpers."""
2
+
3
+ from .css import (
4
+ CSSAtRule,
5
+ CSSComment,
6
+ CSSDeclaration,
7
+ CSSLink,
8
+ CSSImportRule,
9
+ CSSInlineStyle,
10
+ CSSKeyframe,
11
+ CSSKeyframesRule,
12
+ CSSLayerRule,
13
+ CSSMediaRule,
14
+ CSSNode,
15
+ CSSRule,
16
+ CSSStyleElement,
17
+ CSSStyleSheet,
18
+ CSSSupportsRule,
19
+ BOOTSTRAP5_CSS_URL,
20
+ GOOGLE_FONTS_PRECONNECT_URL,
21
+ GOOGLE_FONTS_STATIC_URL,
22
+ TAILWIND_PLAY_CDN_URL,
23
+ bootstrap5_stylesheet,
24
+ google_fonts_assets,
25
+ google_fonts_url,
26
+ inline_style,
27
+ tailwind_script,
28
+ style_tag,
29
+ )
30
+ from .document import Comment, Doctype, Element, HtmlDocument, Node, Raw, Text, comment, doctype, doctype_node, element, raw, text
31
+ from .elements import *
32
+ from .version import __version__
33
+ from .writer import MarkupWriter
34
+ from .js import (
35
+ JSNode,
36
+ JSScript,
37
+ bootstrap5_bundle_script,
38
+ google_charts_loader,
39
+ google_charts_package_loader,
40
+ javascript_link,
41
+ javascript_script,
42
+ )
html5/css.py ADDED
@@ -0,0 +1,384 @@
1
+ """CSS3 stylesheet helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Mapping, Sequence
7
+ from urllib.parse import quote_plus
8
+
9
+ from .document import Raw, element, raw
10
+ from .js import JSScript
11
+
12
+
13
+ def _normalize_property_name(name: str) -> str:
14
+ return name.replace("_", "-")
15
+
16
+
17
+ def _normalize_at_rule_name(name: str) -> str:
18
+ return name.removeprefix("@").replace("_", "-")
19
+
20
+
21
+ class CSSNode:
22
+ """Base class for CSS document nodes."""
23
+
24
+ def render(self) -> str:
25
+ raise NotImplementedError
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class CSSComment(CSSNode):
30
+ """Render a CSS comment."""
31
+
32
+ value: str
33
+
34
+ def render(self) -> str:
35
+ return f"/* {self.value} */"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class CSSDeclaration(CSSNode):
40
+ """Render a CSS declaration pair."""
41
+
42
+ property_name: str
43
+ value: Any
44
+ important: bool = False
45
+
46
+ def render(self) -> str:
47
+ if self.value is None:
48
+ return ""
49
+ suffix = " !important" if self.important else ""
50
+ return f"{_normalize_property_name(self.property_name)}: {self.value}{suffix};"
51
+
52
+
53
+ def _coerce_declaration(value: CSSDeclaration | tuple[str, Any] | Mapping[str, Any]) -> CSSDeclaration:
54
+ if isinstance(value, CSSDeclaration):
55
+ return value
56
+ if isinstance(value, Mapping):
57
+ if len(value) != 1:
58
+ raise ValueError("CSS declaration mapping must contain exactly one property")
59
+ property_name, property_value = next(iter(value.items()))
60
+ return CSSDeclaration(property_name=str(property_name), value=property_value)
61
+ property_name, property_value = value
62
+ return CSSDeclaration(property_name=str(property_name), value=property_value)
63
+
64
+
65
+ def _render_declarations(declarations: tuple[CSSDeclaration | tuple[str, Any] | Mapping[str, Any], ...]) -> str:
66
+ parts: list[str] = []
67
+ for declaration in declarations:
68
+ rendered = _coerce_declaration(declaration).render()
69
+ if rendered:
70
+ parts.append(rendered)
71
+ return " ".join(parts)
72
+
73
+
74
+ def _render_nodes(nodes: tuple[CSSNode | str, ...]) -> str:
75
+ parts: list[str] = []
76
+ for node in nodes:
77
+ parts.append(node.render() if isinstance(node, CSSNode) else str(node))
78
+ return "\n".join(parts)
79
+
80
+
81
+ @dataclass(frozen=True, init=False)
82
+ class CSSInlineStyle(CSSNode):
83
+ """Render a sequence of declarations for use in a style attribute."""
84
+
85
+ declarations: tuple[CSSDeclaration | tuple[str, Any] | Mapping[str, Any], ...] = ()
86
+
87
+ def __init__(self, *declarations: CSSDeclaration | tuple[str, Any] | Mapping[str, Any]) -> None:
88
+ object.__setattr__(self, "declarations", declarations)
89
+
90
+ def render(self) -> str:
91
+ return _render_declarations(self.declarations)
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class CSSLink(CSSNode):
96
+ """Render a stylesheet link element."""
97
+
98
+ href: str
99
+ rel: str = "stylesheet"
100
+ attributes: Mapping[str, Any] = field(default_factory=dict)
101
+
102
+ def render(self) -> str:
103
+ return element("link", void=True, rel=self.rel, href=self.href, **self.attributes).render()
104
+
105
+
106
+ @dataclass(frozen=True, init=False)
107
+ class CSSRule(CSSNode):
108
+ """Render a CSS selector rule."""
109
+
110
+ selector: str
111
+ declarations: tuple[CSSDeclaration | tuple[str, Any] | Mapping[str, Any], ...] = ()
112
+
113
+ def __init__(self, selector: str, *declarations: CSSDeclaration | tuple[str, Any] | Mapping[str, Any]) -> None:
114
+ object.__setattr__(self, "selector", selector)
115
+ object.__setattr__(self, "declarations", declarations)
116
+
117
+ def render(self) -> str:
118
+ body = _render_declarations(self.declarations)
119
+ return f"{self.selector} {{ {body} }}" if body else f"{self.selector} {{}}"
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class CSSAtRule(CSSNode):
124
+ """Render a generic CSS at-rule."""
125
+
126
+ name: str
127
+ prelude: str = ""
128
+ body: tuple[CSSNode | str, ...] = ()
129
+ block: bool = True
130
+
131
+ def render(self) -> str:
132
+ name = _normalize_at_rule_name(self.name)
133
+ prelude = f" {self.prelude}" if self.prelude else ""
134
+ if not self.block:
135
+ return f"@{name}{prelude};"
136
+ body = _render_nodes(self.body)
137
+ return f"@{name}{prelude} {{ {body} }}" if body else f"@{name}{prelude} {{}}"
138
+
139
+
140
+ @dataclass(frozen=True, init=False)
141
+ class CSSImportRule(CSSAtRule):
142
+ """Render a CSS @import rule."""
143
+
144
+ def __init__(self, url: str, media: str = "") -> None:
145
+ prelude = f'url("{url}")'
146
+ if media:
147
+ prelude = f"{prelude} {media}"
148
+ object.__setattr__(self, "name", "import")
149
+ object.__setattr__(self, "prelude", prelude)
150
+ object.__setattr__(self, "body", ())
151
+ object.__setattr__(self, "block", False)
152
+
153
+
154
+ @dataclass(frozen=True, init=False)
155
+ class CSSMediaRule(CSSAtRule):
156
+ """Render a CSS @media block."""
157
+
158
+ def __init__(self, query: str, *rules: CSSNode | str) -> None:
159
+ object.__setattr__(self, "name", "media")
160
+ object.__setattr__(self, "prelude", query)
161
+ object.__setattr__(self, "body", rules)
162
+ object.__setattr__(self, "block", True)
163
+
164
+
165
+ @dataclass(frozen=True, init=False)
166
+ class CSSSupportsRule(CSSAtRule):
167
+ """Render a CSS @supports block."""
168
+
169
+ def __init__(self, condition: str, *rules: CSSNode | str) -> None:
170
+ object.__setattr__(self, "name", "supports")
171
+ object.__setattr__(self, "prelude", condition)
172
+ object.__setattr__(self, "body", rules)
173
+ object.__setattr__(self, "block", True)
174
+
175
+
176
+ @dataclass(frozen=True, init=False)
177
+ class CSSLayerRule(CSSAtRule):
178
+ """Render a CSS @layer block."""
179
+
180
+ def __init__(self, layer: str = "", *rules: CSSNode | str) -> None:
181
+ object.__setattr__(self, "name", "layer")
182
+ object.__setattr__(self, "prelude", layer)
183
+ object.__setattr__(self, "body", rules)
184
+ object.__setattr__(self, "block", True)
185
+
186
+
187
+ @dataclass(frozen=True, init=False)
188
+ class CSSKeyframe(CSSNode):
189
+ """Render a single frame inside a CSS keyframes rule."""
190
+
191
+ selector: str
192
+ declarations: tuple[CSSDeclaration | tuple[str, Any] | Mapping[str, Any], ...] = ()
193
+
194
+ def __init__(self, selector: str, *declarations: CSSDeclaration | tuple[str, Any] | Mapping[str, Any]) -> None:
195
+ object.__setattr__(self, "selector", selector)
196
+ object.__setattr__(self, "declarations", declarations)
197
+
198
+ def render(self) -> str:
199
+ body = _render_declarations(self.declarations)
200
+ return f"{self.selector} {{ {body} }}" if body else f"{self.selector} {{}}"
201
+
202
+
203
+ @dataclass(frozen=True)
204
+ class CSSKeyframesRule(CSSNode):
205
+ """Render a CSS @keyframes rule."""
206
+
207
+ name: str
208
+ frames: tuple[CSSKeyframe, ...] = ()
209
+
210
+ def render(self) -> str:
211
+ body = " ".join(frame.render() for frame in self.frames)
212
+ return f"@keyframes {self.name} {{ {body} }}" if body else f"@keyframes {self.name} {{}}"
213
+
214
+
215
+ @dataclass(frozen=True)
216
+ class CSSStyleElement(CSSNode):
217
+ """Render a complete <style> element."""
218
+
219
+ stylesheet: CSSNode | str
220
+
221
+ def render(self) -> str:
222
+ css = self.stylesheet.render() if isinstance(self.stylesheet, CSSNode) else self.stylesheet
223
+ return f"<style>{css}</style>"
224
+
225
+
226
+ BOOTSTRAP5_CSS_URL = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
227
+ TAILWIND_PLAY_CDN_URL = "https://cdn.tailwindcss.com"
228
+ GOOGLE_FONTS_PRECONNECT_URL = "https://fonts.googleapis.com"
229
+ GOOGLE_FONTS_STATIC_URL = "https://fonts.gstatic.com"
230
+
231
+
232
+ def bootstrap5_stylesheet(version: str = "5.3.3") -> CSSLink:
233
+ """Return the Bootstrap 5 stylesheet link tag."""
234
+
235
+ href = f"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/css/bootstrap.min.css"
236
+ return CSSLink(href=href)
237
+
238
+
239
+ def tailwind_script() -> JSScript:
240
+ """Return the Tailwind Play CDN script tag."""
241
+
242
+ return JSScript(src=TAILWIND_PLAY_CDN_URL)
243
+
244
+
245
+ def google_fonts_url(
246
+ *families: str,
247
+ weights: Sequence[int] = (400, 500, 700),
248
+ display: str = "swap",
249
+ text: str | None = None,
250
+ ) -> str:
251
+ """Build a Google Fonts CSS2 URL for the requested families and weights."""
252
+
253
+ if not families:
254
+ raise ValueError("At least one Google Fonts family is required")
255
+
256
+ family_parts: list[str] = []
257
+ weight_part = ";".join(str(weight) for weight in weights)
258
+ for family in families:
259
+ family_parts.append(f"family={quote_plus(family)}:wght@{weight_part}")
260
+
261
+ params = [*family_parts, f"display={quote_plus(display)}"]
262
+ if text is not None:
263
+ params.append(f"text={quote_plus(text)}")
264
+ return "https://fonts.googleapis.com/css2?" + "&".join(params)
265
+
266
+
267
+ def google_fonts_assets(
268
+ *families: str,
269
+ weights: Sequence[int] = (400, 500, 700),
270
+ display: str = "swap",
271
+ text: str | None = None,
272
+ ) -> tuple[CSSLink, CSSLink, CSSLink]:
273
+ """Return preconnect and stylesheet links for Google Fonts."""
274
+
275
+ href = google_fonts_url(*families, weights=weights, display=display, text=text)
276
+ return (
277
+ CSSLink(href=GOOGLE_FONTS_PRECONNECT_URL, rel="preconnect"),
278
+ CSSLink(href=GOOGLE_FONTS_STATIC_URL, rel="preconnect", attributes={"crossorigin": "anonymous"}),
279
+ CSSLink(href=href),
280
+ )
281
+
282
+
283
+ @dataclass
284
+ class CSSStyleSheet(CSSNode):
285
+ rules: list[CSSNode | str] = field(default_factory=list)
286
+
287
+ def add(self, *items: CSSNode | str) -> "CSSStyleSheet":
288
+ self.rules.extend(items)
289
+ return self
290
+
291
+ def add_comment(self, value: str) -> "CSSStyleSheet":
292
+ self.rules.append(CSSComment(value))
293
+ return self
294
+
295
+ def add_rule(
296
+ self,
297
+ selector: str,
298
+ *declarations: CSSDeclaration | tuple[str, Any] | Mapping[str, Any],
299
+ **keyword_declarations: Any,
300
+ ) -> "CSSStyleSheet":
301
+ combined_declarations = declarations + tuple(keyword_declarations.items())
302
+ self.rules.append(CSSRule(selector, *combined_declarations))
303
+ return self
304
+
305
+ def add_import(self, url: str, media: str = "") -> "CSSStyleSheet":
306
+ self.rules.append(CSSImportRule(url=url, media=media))
307
+ return self
308
+
309
+ def add_media(self, query: str, *rules: CSSNode | str) -> "CSSStyleSheet":
310
+ self.rules.append(CSSMediaRule(query, *rules))
311
+ return self
312
+
313
+ def add_supports(self, condition: str, *rules: CSSNode | str) -> "CSSStyleSheet":
314
+ self.rules.append(CSSSupportsRule(condition, *rules))
315
+ return self
316
+
317
+ def add_layer(self, layer: str = "", *rules: CSSNode | str) -> "CSSStyleSheet":
318
+ self.rules.append(CSSLayerRule(layer, *rules))
319
+ return self
320
+
321
+ def add_keyframes(self, name: str, *frames: CSSKeyframe) -> "CSSStyleSheet":
322
+ self.rules.append(CSSKeyframesRule(name=name, frames=frames))
323
+ return self
324
+
325
+ def add_raw(self, css: str) -> "CSSStyleSheet":
326
+ self.rules.append(css)
327
+ return self
328
+
329
+ def render(self) -> str:
330
+ parts: list[str] = []
331
+ for rule in self.rules:
332
+ parts.append(rule.render() if isinstance(rule, CSSNode) else rule)
333
+ return "\n".join(parts)
334
+
335
+
336
+ def style_tag(stylesheet: CSSNode | str) -> Raw:
337
+ """Render a stylesheet or CSS node into a raw <style> tag.
338
+
339
+ If *stylesheet* is already a :class:`CSSStyleElement` (which renders its
340
+ own ``<style>`` wrapper), return it as-is to avoid double-wrapping.
341
+ """
342
+
343
+ if isinstance(stylesheet, CSSStyleElement):
344
+ return raw(stylesheet.render())
345
+ css = stylesheet.render() if isinstance(stylesheet, CSSNode) else stylesheet
346
+ return raw(f"<style>{css}</style>")
347
+
348
+
349
+ def inline_style(
350
+ *declarations: CSSDeclaration | tuple[str, Any] | Mapping[str, Any],
351
+ **keyword_declarations: Any,
352
+ ) -> str:
353
+ """Render declarations for a style attribute."""
354
+
355
+ return CSSInlineStyle(*(declarations + tuple(keyword_declarations.items()))).render()
356
+
357
+
358
+ __all__ = [
359
+ "CSSAtRule",
360
+ "CSSComment",
361
+ "CSSDeclaration",
362
+ "CSSLink",
363
+ "CSSImportRule",
364
+ "CSSInlineStyle",
365
+ "CSSKeyframe",
366
+ "CSSKeyframesRule",
367
+ "CSSLayerRule",
368
+ "CSSMediaRule",
369
+ "CSSNode",
370
+ "CSSRule",
371
+ "CSSStyleElement",
372
+ "CSSStyleSheet",
373
+ "CSSSupportsRule",
374
+ "BOOTSTRAP5_CSS_URL",
375
+ "GOOGLE_FONTS_PRECONNECT_URL",
376
+ "GOOGLE_FONTS_STATIC_URL",
377
+ "TAILWIND_PLAY_CDN_URL",
378
+ "bootstrap5_stylesheet",
379
+ "google_fonts_assets",
380
+ "google_fonts_url",
381
+ "inline_style",
382
+ "tailwind_script",
383
+ "style_tag",
384
+ ]
html5/document.py ADDED
@@ -0,0 +1,166 @@
1
+ """HTML5 document primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from html import escape
7
+ from typing import Any, Mapping
8
+
9
+
10
+ def doctype() -> str:
11
+ """Return the HTML5 doctype string."""
12
+
13
+ return "<!doctype html>"
14
+
15
+
16
+ def _normalize_attr_name(name: str) -> str:
17
+ if name.endswith("_"):
18
+ name = name[:-1]
19
+ return name.replace("_", "-")
20
+
21
+
22
+ def _render_attributes(attributes: Mapping[str, Any]) -> str:
23
+ rendered: list[str] = []
24
+ for key, value in attributes.items():
25
+ if value is None or value is False:
26
+ continue
27
+ attribute_name = _normalize_attr_name(key)
28
+ if value is True:
29
+ rendered.append(attribute_name)
30
+ else:
31
+ rendered.append(f'{attribute_name}="{escape(str(value), quote=True)}"')
32
+ return (" " + " ".join(rendered)) if rendered else ""
33
+
34
+
35
+ class Node:
36
+ """Base class for HTML document nodes."""
37
+
38
+ def render(self) -> str:
39
+ raise NotImplementedError
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Text(Node):
44
+ """Render escaped text content."""
45
+
46
+ value: str
47
+
48
+ def render(self) -> str:
49
+ return escape(self.value)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Raw(Node):
54
+ """Render raw HTML content without escaping."""
55
+
56
+ value: str
57
+
58
+ def render(self) -> str:
59
+ return self.value
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Comment(Node):
64
+ """Render an HTML comment."""
65
+
66
+ value: str
67
+
68
+ def render(self) -> str:
69
+ return f"<!--{self.value}-->"
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class Doctype(Node):
74
+ """Render a doctype node."""
75
+
76
+ value: str = "html"
77
+
78
+ def render(self) -> str:
79
+ return f"<!doctype {self.value}>"
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class Element(Node):
84
+ """Render an HTML element with children and attributes."""
85
+
86
+ tag: str
87
+ children: tuple[Node | str, ...] = ()
88
+ attributes: Mapping[str, Any] = field(default_factory=dict)
89
+ void: bool = False
90
+
91
+ def render(self) -> str:
92
+ attributes = _render_attributes(self.attributes)
93
+ if self.void:
94
+ return f"<{self.tag}{attributes}>"
95
+
96
+ inner_html = "".join(_coerce_node(child).render() for child in self.children)
97
+ return f"<{self.tag}{attributes}>{inner_html}</{self.tag}>"
98
+
99
+
100
+ def _coerce_node(value: Node | str) -> Node:
101
+ return value if isinstance(value, Node) else Text(str(value))
102
+
103
+
104
+ def text(value: str) -> Text:
105
+ """Create an escaped text node."""
106
+
107
+ return Text(value)
108
+
109
+
110
+ def raw(value: str) -> Raw:
111
+ """Create a raw HTML node."""
112
+
113
+ return Raw(value)
114
+
115
+
116
+ def comment(value: str) -> Comment:
117
+ """Create an HTML comment node."""
118
+
119
+ return Comment(value)
120
+
121
+
122
+ def doctype_node(value: str = "html") -> Doctype:
123
+ """Create a doctype node."""
124
+
125
+ return Doctype(value)
126
+
127
+
128
+ def element(tag: str, *children: Node | str, void: bool = False, **attributes: Any) -> Element:
129
+ """Create an HTML element node."""
130
+
131
+ return Element(tag=tag, children=children, attributes=attributes, void=void)
132
+
133
+
134
+ @dataclass
135
+ class HtmlDocument:
136
+ """Represent a complete HTML document."""
137
+
138
+ title: str = ""
139
+ lang: str = "en"
140
+ head_nodes: list[Node | str] = field(default_factory=list)
141
+ body_nodes: list[Node | str] = field(default_factory=list)
142
+
143
+ def add_head(self, *nodes: Node | str) -> "HtmlDocument":
144
+ """Append nodes to the document head."""
145
+
146
+ self.head_nodes.extend(nodes)
147
+ return self
148
+
149
+ def add_body(self, *nodes: Node | str) -> "HtmlDocument":
150
+ """Append nodes to the document body."""
151
+
152
+ self.body_nodes.extend(nodes)
153
+ return self
154
+
155
+ def render(self) -> str:
156
+ """Render the full HTML document."""
157
+
158
+ head_children: list[Node | str] = [element("meta", charset="utf-8", void=True)]
159
+ if self.title:
160
+ head_children.append(element("title", self.title))
161
+ head_children.extend(self.head_nodes)
162
+
163
+ body = element("body", *self.body_nodes)
164
+ head = element("head", *head_children)
165
+ html = element("html", head, body, lang=self.lang)
166
+ return "\n".join([doctype(), html.render()])
html5/elements.py ADDED
@@ -0,0 +1,397 @@
1
+ """Generated HTML5 element classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .document import Element, Node
8
+
9
+
10
+ VOID_ELEMENTS = {
11
+ "area",
12
+ "base",
13
+ "br",
14
+ "col",
15
+ "embed",
16
+ "hr",
17
+ "img",
18
+ "input",
19
+ "link",
20
+ "meta",
21
+ "param",
22
+ "source",
23
+ "track",
24
+ "wbr",
25
+ }
26
+
27
+ HTML5_ELEMENTS = (
28
+ "a",
29
+ "abbr",
30
+ "address",
31
+ "area",
32
+ "article",
33
+ "aside",
34
+ "audio",
35
+ "b",
36
+ "base",
37
+ "bdi",
38
+ "bdo",
39
+ "blockquote",
40
+ "body",
41
+ "br",
42
+ "button",
43
+ "canvas",
44
+ "caption",
45
+ "cite",
46
+ "code",
47
+ "col",
48
+ "colgroup",
49
+ "data",
50
+ "datalist",
51
+ "dd",
52
+ "del",
53
+ "details",
54
+ "dfn",
55
+ "dialog",
56
+ "div",
57
+ "dl",
58
+ "dt",
59
+ "em",
60
+ "embed",
61
+ "fieldset",
62
+ "figcaption",
63
+ "figure",
64
+ "footer",
65
+ "form",
66
+ "h1",
67
+ "h2",
68
+ "h3",
69
+ "h4",
70
+ "h5",
71
+ "h6",
72
+ "head",
73
+ "header",
74
+ "hgroup",
75
+ "hr",
76
+ "html",
77
+ "i",
78
+ "iframe",
79
+ "img",
80
+ "input",
81
+ "ins",
82
+ "kbd",
83
+ "label",
84
+ "legend",
85
+ "li",
86
+ "link",
87
+ "main",
88
+ "map",
89
+ "mark",
90
+ "menu",
91
+ "meta",
92
+ "meter",
93
+ "nav",
94
+ "noscript",
95
+ "object",
96
+ "ol",
97
+ "optgroup",
98
+ "option",
99
+ "output",
100
+ "p",
101
+ "param",
102
+ "picture",
103
+ "pre",
104
+ "progress",
105
+ "q",
106
+ "rp",
107
+ "rt",
108
+ "ruby",
109
+ "s",
110
+ "samp",
111
+ "script",
112
+ "section",
113
+ "select",
114
+ "slot",
115
+ "small",
116
+ "source",
117
+ "span",
118
+ "strong",
119
+ "style",
120
+ "sub",
121
+ "summary",
122
+ "sup",
123
+ "table",
124
+ "tbody",
125
+ "td",
126
+ "template",
127
+ "textarea",
128
+ "tfoot",
129
+ "th",
130
+ "thead",
131
+ "time",
132
+ "title",
133
+ "tr",
134
+ "track",
135
+ "u",
136
+ "ul",
137
+ "var",
138
+ "video",
139
+ "wbr",
140
+ )
141
+
142
+
143
+ def _class_name(tag: str) -> str:
144
+ return tag.replace("-", " ").title().replace(" ", "")
145
+
146
+
147
+ def _build_init(tag: str, *, void: bool) -> Any:
148
+ def __init__(self, *children: Node | str, **attributes: Any) -> None:
149
+ if void and children:
150
+ raise ValueError(f"{tag} is a void HTML element and cannot have children")
151
+ Element.__init__(self, tag=tag, children=children, attributes=attributes, void=void)
152
+
153
+ return __init__
154
+
155
+
156
+ def _create_tag_class(tag: str) -> type[Element]:
157
+ cls = type(
158
+ _class_name(tag),
159
+ (Element,),
160
+ {
161
+ "__module__": __name__,
162
+ "__init__": _build_init(tag, void=tag in VOID_ELEMENTS),
163
+ },
164
+ )
165
+ cls.__doc__ = f"Render a <{tag}> HTML element."
166
+ return cls
167
+
168
+
169
+ A = _create_tag_class("a")
170
+ Abbr = _create_tag_class("abbr")
171
+ Address = _create_tag_class("address")
172
+ Area = _create_tag_class("area")
173
+ Article = _create_tag_class("article")
174
+ Aside = _create_tag_class("aside")
175
+ Audio = _create_tag_class("audio")
176
+ B = _create_tag_class("b")
177
+ Base = _create_tag_class("base")
178
+ Bdi = _create_tag_class("bdi")
179
+ Bdo = _create_tag_class("bdo")
180
+ Blockquote = _create_tag_class("blockquote")
181
+ Body = _create_tag_class("body")
182
+ Br = _create_tag_class("br")
183
+ Button = _create_tag_class("button")
184
+ Canvas = _create_tag_class("canvas")
185
+ Caption = _create_tag_class("caption")
186
+ Cite = _create_tag_class("cite")
187
+ Code = _create_tag_class("code")
188
+ Col = _create_tag_class("col")
189
+ Colgroup = _create_tag_class("colgroup")
190
+ Data = _create_tag_class("data")
191
+ Datalist = _create_tag_class("datalist")
192
+ Dd = _create_tag_class("dd")
193
+ Del = _create_tag_class("del")
194
+ Details = _create_tag_class("details")
195
+ Dfn = _create_tag_class("dfn")
196
+ Dialog = _create_tag_class("dialog")
197
+ Div = _create_tag_class("div")
198
+ Dl = _create_tag_class("dl")
199
+ Dt = _create_tag_class("dt")
200
+ Em = _create_tag_class("em")
201
+ Embed = _create_tag_class("embed")
202
+ Fieldset = _create_tag_class("fieldset")
203
+ Figcaption = _create_tag_class("figcaption")
204
+ Figure = _create_tag_class("figure")
205
+ Footer = _create_tag_class("footer")
206
+ Form = _create_tag_class("form")
207
+ H1 = _create_tag_class("h1")
208
+ H2 = _create_tag_class("h2")
209
+ H3 = _create_tag_class("h3")
210
+ H4 = _create_tag_class("h4")
211
+ H5 = _create_tag_class("h5")
212
+ H6 = _create_tag_class("h6")
213
+ Head = _create_tag_class("head")
214
+ Header = _create_tag_class("header")
215
+ Hgroup = _create_tag_class("hgroup")
216
+ Hr = _create_tag_class("hr")
217
+ Html = _create_tag_class("html")
218
+ I = _create_tag_class("i")
219
+ Iframe = _create_tag_class("iframe")
220
+ Img = _create_tag_class("img")
221
+ Input = _create_tag_class("input")
222
+ Ins = _create_tag_class("ins")
223
+ Kbd = _create_tag_class("kbd")
224
+ Label = _create_tag_class("label")
225
+ Legend = _create_tag_class("legend")
226
+ Li = _create_tag_class("li")
227
+ Link = _create_tag_class("link")
228
+ Main = _create_tag_class("main")
229
+ Map = _create_tag_class("map")
230
+ Mark = _create_tag_class("mark")
231
+ Menu = _create_tag_class("menu")
232
+ Meta = _create_tag_class("meta")
233
+ Meter = _create_tag_class("meter")
234
+ Nav = _create_tag_class("nav")
235
+ Noscript = _create_tag_class("noscript")
236
+ Object = _create_tag_class("object")
237
+ Ol = _create_tag_class("ol")
238
+ Optgroup = _create_tag_class("optgroup")
239
+ Option = _create_tag_class("option")
240
+ Output = _create_tag_class("output")
241
+ P = _create_tag_class("p")
242
+ Param = _create_tag_class("param")
243
+ Picture = _create_tag_class("picture")
244
+ Pre = _create_tag_class("pre")
245
+ Progress = _create_tag_class("progress")
246
+ Q = _create_tag_class("q")
247
+ Rp = _create_tag_class("rp")
248
+ Rt = _create_tag_class("rt")
249
+ Ruby = _create_tag_class("ruby")
250
+ S = _create_tag_class("s")
251
+ Samp = _create_tag_class("samp")
252
+ Script = _create_tag_class("script")
253
+ Section = _create_tag_class("section")
254
+ Select = _create_tag_class("select")
255
+ Slot = _create_tag_class("slot")
256
+ Small = _create_tag_class("small")
257
+ Source = _create_tag_class("source")
258
+ Span = _create_tag_class("span")
259
+ Strong = _create_tag_class("strong")
260
+ Style = _create_tag_class("style")
261
+ Sub = _create_tag_class("sub")
262
+ Summary = _create_tag_class("summary")
263
+ Sup = _create_tag_class("sup")
264
+ Table = _create_tag_class("table")
265
+ Tbody = _create_tag_class("tbody")
266
+ Td = _create_tag_class("td")
267
+ Template = _create_tag_class("template")
268
+ Textarea = _create_tag_class("textarea")
269
+ Tfoot = _create_tag_class("tfoot")
270
+ Th = _create_tag_class("th")
271
+ Thead = _create_tag_class("thead")
272
+ Time = _create_tag_class("time")
273
+ Title = _create_tag_class("title")
274
+ Tr = _create_tag_class("tr")
275
+ Track = _create_tag_class("track")
276
+ U = _create_tag_class("u")
277
+ Ul = _create_tag_class("ul")
278
+ Var = _create_tag_class("var")
279
+ Video = _create_tag_class("video")
280
+ Wbr = _create_tag_class("wbr")
281
+
282
+ __all__ = [
283
+ "VOID_ELEMENTS",
284
+ "HTML5_ELEMENTS",
285
+ "A",
286
+ "Abbr",
287
+ "Address",
288
+ "Area",
289
+ "Article",
290
+ "Aside",
291
+ "Audio",
292
+ "B",
293
+ "Base",
294
+ "Bdi",
295
+ "Bdo",
296
+ "Blockquote",
297
+ "Body",
298
+ "Br",
299
+ "Button",
300
+ "Canvas",
301
+ "Caption",
302
+ "Cite",
303
+ "Code",
304
+ "Col",
305
+ "Colgroup",
306
+ "Data",
307
+ "Datalist",
308
+ "Dd",
309
+ "Del",
310
+ "Details",
311
+ "Dfn",
312
+ "Dialog",
313
+ "Div",
314
+ "Dl",
315
+ "Dt",
316
+ "Em",
317
+ "Embed",
318
+ "Fieldset",
319
+ "Figcaption",
320
+ "Figure",
321
+ "Footer",
322
+ "Form",
323
+ "H1",
324
+ "H2",
325
+ "H3",
326
+ "H4",
327
+ "H5",
328
+ "H6",
329
+ "Head",
330
+ "Header",
331
+ "Hgroup",
332
+ "Hr",
333
+ "Html",
334
+ "I",
335
+ "Iframe",
336
+ "Img",
337
+ "Input",
338
+ "Ins",
339
+ "Kbd",
340
+ "Label",
341
+ "Legend",
342
+ "Li",
343
+ "Link",
344
+ "Main",
345
+ "Map",
346
+ "Mark",
347
+ "Menu",
348
+ "Meta",
349
+ "Meter",
350
+ "Nav",
351
+ "Noscript",
352
+ "Object",
353
+ "Ol",
354
+ "Optgroup",
355
+ "Option",
356
+ "Output",
357
+ "P",
358
+ "Param",
359
+ "Picture",
360
+ "Pre",
361
+ "Progress",
362
+ "Q",
363
+ "Rp",
364
+ "Rt",
365
+ "Ruby",
366
+ "S",
367
+ "Samp",
368
+ "Script",
369
+ "Section",
370
+ "Select",
371
+ "Slot",
372
+ "Small",
373
+ "Source",
374
+ "Span",
375
+ "Strong",
376
+ "Style",
377
+ "Sub",
378
+ "Summary",
379
+ "Sup",
380
+ "Table",
381
+ "Tbody",
382
+ "Td",
383
+ "Template",
384
+ "Textarea",
385
+ "Tfoot",
386
+ "Th",
387
+ "Thead",
388
+ "Time",
389
+ "Title",
390
+ "Tr",
391
+ "Track",
392
+ "U",
393
+ "Ul",
394
+ "Var",
395
+ "Video",
396
+ "Wbr",
397
+ ]
html5/js.py ADDED
@@ -0,0 +1,85 @@
1
+ """JavaScript helpers for HTML documents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Sequence
7
+
8
+ from .document import raw, element
9
+
10
+
11
+ class JSNode:
12
+ """Base class for JavaScript document nodes."""
13
+
14
+ def render(self) -> str:
15
+ raise NotImplementedError
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class JSScript(JSNode):
20
+ """Render an inline or external JavaScript script tag.
21
+
22
+ Set *src* for an external script, or *code* for an inline script.
23
+ Setting both raises :exc:`ValueError`.
24
+ """
25
+
26
+ src: str | None = None
27
+ code: str = ""
28
+ attributes: dict[str, Any] = field(default_factory=dict)
29
+
30
+ def __post_init__(self) -> None:
31
+ if self.src is not None and self.code:
32
+ raise ValueError(
33
+ "JSScript cannot set both 'src' and 'code'; use one or the other"
34
+ )
35
+
36
+ def render(self) -> str:
37
+ if self.src is not None:
38
+ return element("script", src=self.src, **self.attributes).render()
39
+ return element("script", raw(self.code), **self.attributes).render()
40
+
41
+
42
+ def javascript_script(code: str, **attributes: Any) -> JSScript:
43
+ """Create an inline script node."""
44
+ return JSScript(code=code, attributes=attributes)
45
+
46
+
47
+ def javascript_link(src: str, **attributes: Any) -> JSScript:
48
+ """Create an external script node."""
49
+ return JSScript(src=src, attributes=attributes)
50
+
51
+
52
+ def bootstrap5_bundle_script(version: str = "5.3.3") -> JSScript:
53
+ """Return the Bootstrap 5 bundle script tag."""
54
+ href = f"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js"
55
+ return javascript_link(href)
56
+
57
+
58
+ def google_charts_loader(version: str = "51") -> JSScript:
59
+ """Return the Google Charts loader script tag."""
60
+ src = f"https://www.gstatic.com/charts/loader.js?ver={version}"
61
+ return javascript_link(src)
62
+
63
+
64
+ def google_charts_package_loader(packages: Sequence[str], callback: str | None = None) -> JSScript:
65
+ """Return an inline Google Charts package loader snippet."""
66
+ package_list = ", ".join(f'"{package}"' for package in packages)
67
+ callback_line = f"google.charts.setOnLoadCallback({callback});" if callback else ""
68
+ code = (
69
+ "google.charts.load('current', {packages: ["
70
+ f"{package_list}"
71
+ "]});\n"
72
+ f"{callback_line}"
73
+ )
74
+ return javascript_script(code.strip())
75
+
76
+
77
+ __all__ = [
78
+ "JSNode",
79
+ "JSScript",
80
+ "bootstrap5_bundle_script",
81
+ "google_charts_loader",
82
+ "google_charts_package_loader",
83
+ "javascript_link",
84
+ "javascript_script",
85
+ ]
html5/version.py ADDED
@@ -0,0 +1,3 @@
1
+ """Package version information."""
2
+
3
+ __version__ = "0.3.0"
html5/writer.py ADDED
@@ -0,0 +1,48 @@
1
+ """Utilities for writing rendered HTML and CSS to disk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Protocol
8
+
9
+
10
+ class Renderable(Protocol):
11
+ def render(self) -> str:
12
+ raise NotImplementedError
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class MarkupWriter:
17
+ """Write rendered HTML and CSS content beneath a fixed root directory."""
18
+
19
+ root: Path = Path(".")
20
+ encoding: str = "utf-8"
21
+
22
+ def write_text(self, path: str | Path, content: str) -> Path:
23
+ """Write text content to a file below the configured root directory."""
24
+
25
+ root = self.root.resolve()
26
+ relative_path = Path(path)
27
+ if relative_path.is_absolute():
28
+ raise ValueError("path must be relative to the writer root")
29
+
30
+ destination = (root / relative_path).resolve()
31
+ if destination == root or root not in destination.parents:
32
+ raise ValueError("path must be a file inside the writer root, not the root itself")
33
+
34
+ destination.parent.mkdir(parents=True, exist_ok=True)
35
+ destination.write_text(content, encoding=self.encoding)
36
+ return destination
37
+
38
+ def write_html(self, path: str | Path, document: Renderable | str) -> Path:
39
+ """Write rendered HTML to disk."""
40
+
41
+ content = document if isinstance(document, str) else document.render()
42
+ return self.write_text(path, content)
43
+
44
+ def write_css(self, path: str | Path, stylesheet: Renderable | str) -> Path:
45
+ """Write rendered CSS to disk."""
46
+
47
+ content = stylesheet if isinstance(stylesheet, str) else stylesheet.render()
48
+ return self.write_text(path, content)
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: html5cc
3
+ Version: 0.3.0
4
+ Summary: A small Python module for building HTML5 documents and CSS3 stylesheets.
5
+ Author: Jason Miller
6
+ License: MIT
7
+ Project-URL: Homepage, https://jasonmiller-cc.github.io/html5/
8
+ Project-URL: Documentation, https://jasonmiller-cc.github.io/html5/
9
+ Project-URL: Source, https://github.com/jasonmiller-cc/html5
10
+ Project-URL: Changelog, https://github.com/jasonmiller-cc/html5/blob/main/CHANGELOG.md
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=8.0; extra == "test"
20
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
21
+ Provides-Extra: docs
22
+ Requires-Dist: furo>=2024.8.6; extra == "docs"
23
+ Requires-Dist: myst-parser>=4.0; extra == "docs"
24
+ Requires-Dist: sphinx>=7.4; extra == "docs"
25
+ Dynamic: license-file
26
+
27
+ # html5
28
+
29
+ Small Python helpers for building HTML5 documents, CSS, and JavaScript assets from Python.
30
+
31
+ Documentation is published on GitHub Pages at [jasonmiller-cc.github.io/html5](https://jasonmiller-cc.github.io/html5/).
32
+
33
+ The package ships with HTML5 element classes, CSS node classes, JavaScript helpers, CDN helpers for Bootstrap 5 and Tailwind, optimized Google Fonts helpers, a disk writer, and semantic version metadata.
34
+
35
+ ## Installation
36
+
37
+ `html5cc` is published to [PyPI](https://pypi.org/project/html5cc/).
38
+ The import name is still `html5`.
39
+
40
+ ```bash
41
+ pip install html5cc
42
+ ```
43
+
44
+ **requirements.txt**
45
+
46
+ ```text
47
+ html5cc==0.3.0
48
+ ```
49
+
50
+ **pyproject.toml (uv)**
51
+
52
+ ```toml
53
+ [project]
54
+ dependencies = ["html5cc>=0.3.0"]
55
+ ```
56
+
57
+ See the [Installation docs](https://jasonmiller-cc.github.io/html5/installation.html) for Poetry and other options.
58
+
59
+ ## Example
60
+
61
+ ```python
62
+ from html5 import (
63
+ CSSDeclaration,
64
+ CSSKeyframe,
65
+ CSSStyleSheet,
66
+ Div,
67
+ H1,
68
+ HtmlDocument,
69
+ Img,
70
+ MarkupWriter,
71
+ bootstrap5_bundle_script,
72
+ bootstrap5_stylesheet,
73
+ google_charts_loader,
74
+ google_charts_package_loader,
75
+ google_fonts_assets,
76
+ inline_style,
77
+ tailwind_script,
78
+ )
79
+
80
+ styles = CSSStyleSheet()
81
+ styles.add_comment("base styles")
82
+ styles.add_import("reset.css")
83
+ styles.add_rule("body", margin="0", font_family="system-ui")
84
+ styles.add_rule("h1", color="#1f2937")
85
+ styles.add_media("screen and (min-width: 40rem)", CSSDeclaration("color", "black"))
86
+ styles.add_keyframes("fade", CSSKeyframe("from", ("opacity", 0)), CSSKeyframe("to", ("opacity", 1)))
87
+
88
+ doc = (
89
+ HtmlDocument(title="Hello HTML5")
90
+ .add_head(*google_fonts_assets("Inter"))
91
+ .add_head(bootstrap5_stylesheet())
92
+ .add_head(tailwind_script())
93
+ .add_head(bootstrap5_bundle_script())
94
+ .add_head(google_charts_loader())
95
+ .add_body(Div(H1("Hello, world!"), Img(src="hero.png", style=inline_style(("border_radius", "8px")))))
96
+ )
97
+
98
+ print(doc.render())
99
+
100
+ chart_loader = google_charts_package_loader(["corechart", "table"], callback="drawChart")
101
+ writer = MarkupWriter()
102
+ writer.write_html("dist/index.html", doc)
103
+ writer.write_css("dist/site.css", styles)
104
+ writer.write_html("dist/charts.html", HtmlDocument(title="Charts").add_head(chart_loader))
105
+ ```
106
+
107
+ ## JavaScript Helpers
108
+
109
+ Use `javascript_link()` and `javascript_script()` for arbitrary script tags and inline code. The library also exposes `bootstrap5_bundle_script()` and Google Charts helpers so you can wire up common browser libraries without hand-building script tags.
110
+
111
+ ## Asset Helpers
112
+
113
+ Tailwind is injected with the official Play CDN script because that is the fastest supported browser-side setup for this library. Bootstrap 5 is injected as a CSS stylesheet from jsDelivr. Google Fonts are emitted as a preconnect pair plus a CSS2 stylesheet link so browsers can establish connections early and only download the fonts you ask for.
114
+
115
+ Common Tailwind class families that pair well with the helpers:
116
+
117
+ - Layout: `container`, `mx-auto`, `flex`, `grid`, `gap-*`, `items-center`, `justify-between`
118
+ - Spacing: `p-*`, `px-*`, `py-*`, `m-*`, `space-x-*`, `space-y-*`
119
+ - Typography: `text-*`, `font-*`, `leading-*`, `tracking-*`
120
+ - Surface: `bg-*`, `text-*`, `border-*`, `rounded-*`, `shadow-*`
121
+ - Responsive prefixes: `sm:`, `md:`, `lg:`, `xl:`, `2xl:`
122
+
123
+ Common Bootstrap 5 class families that pair well with the helpers:
124
+
125
+ - Layout and grid: `container`, `container-fluid`, `row`, `col`, `g-*`, `row-cols-*`
126
+ - Spacing and display: `m-*`, `p-*`, `d-flex`, `d-grid`, `gap-*`, `justify-content-*`, `align-items-*`
127
+ - Typography: `lead`, `fw-bold`, `fst-italic`, `text-muted`, `text-center`, `text-nowrap`
128
+ - Components: `btn`, `btn-primary`, `card`, `badge`, `alert`, `navbar`, `modal`
129
+ - Responsive prefixes: `sm`, `md`, `lg`, `xl`, `xxl`
130
+
131
+ Google Fonts examples:
132
+
133
+ - `google_fonts_assets("Inter")`
134
+ - `google_fonts_assets("Inter", "Open Sans")`
135
+ - `google_fonts_assets("Roboto", weights=(400, 700), text="Hello world")`
136
+
137
+ ## Layout
138
+
139
+ - `src/html5/document.py` contains the HTML5 document and element primitives.
140
+ - `src/html5/elements.py` contains generated classes for standard HTML5 tags.
141
+ - `src/html5/css.py` contains CSS node classes for comments, declarations, rules, at-rules, keyframes, inline styles, and `<style>` generation.
142
+ - `src/html5/js.py` contains JavaScript helpers for inline scripts, external scripts, Bootstrap 5, and Google Charts.
143
+ - `src/html5/writer.py` contains a disk writer for rendered HTML and CSS outputs.
144
+ - `src/html5/version.py` stores the semantic version string used by packaging.
145
+
146
+ ## Versioning
147
+
148
+ This project uses semantic versioning. The current package version is `0.3.0`, and the release history is documented in [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,12 @@
1
+ html5/__init__.py,sha256=5JPvAKio0VdXclTdSd6gGJh9krj_yfK-fomdVMzV5YQ,967
2
+ html5/css.py,sha256=Awd05kzIkg9wzibrwUWEPHRrpd44UTzGYvuZ_G6Nw6g,12206
3
+ html5/document.py,sha256=U0zuXlgQobg1nmjOemEsiarpchbV6zgjNYBMRW1sutw,4057
4
+ html5/elements.py,sha256=qq1hvTGm7UIKoGeSatGacEkbrpRtd5kMNzZA4x-FhDo,7697
5
+ html5/js.py,sha256=MFd6DPT1UsPwyy15zwg-V5gPSkt3WI0Hl_Qwq_pcDzo,2574
6
+ html5/version.py,sha256=ARHt_V5W60rkhA4dNqWOMwN6UASxrGycgPN13sngWbQ,58
7
+ html5/writer.py,sha256=Ul2e3A_3cfCsREnQcg_wtc1bXCMj6t24smscabyb3r0,1665
8
+ html5cc-0.3.0.dist-info/licenses/LICENSE,sha256=AcJ45gbn44D2lI-RgrcLGCkiDjy2XpC8JhGHH-id07k,1073
9
+ html5cc-0.3.0.dist-info/METADATA,sha256=G5JMlAx_WaD6bkk1VLUw1OGr4-T7bIehSntgdOOPRW4,5715
10
+ html5cc-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ html5cc-0.3.0.dist-info/top_level.txt,sha256=wGjDApZ61gkSmsTfbb0jalPII1ajGD__R7w7Ba5PAlw,6
12
+ html5cc-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Lee Miller
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.
@@ -0,0 +1 @@
1
+ html5