domica-html 0.1.4__tar.gz
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.
- domica_html-0.1.4/LICENSE +21 -0
- domica_html-0.1.4/PKG-INFO +130 -0
- domica_html-0.1.4/README.md +101 -0
- domica_html-0.1.4/pyproject.toml +46 -0
- domica_html-0.1.4/setup.cfg +4 -0
- domica_html-0.1.4/src/domica_html/__init__.py +191 -0
- domica_html-0.1.4/src/domica_html/block.py +29 -0
- domica_html-0.1.4/src/domica_html/inctement.py +104 -0
- domica_html-0.1.4/src/domica_html/node.py +100 -0
- domica_html-0.1.4/src/domica_html/tags.py +231 -0
- domica_html-0.1.4/src/domica_html.egg-info/PKG-INFO +130 -0
- domica_html-0.1.4/src/domica_html.egg-info/SOURCES.txt +15 -0
- domica_html-0.1.4/src/domica_html.egg-info/dependency_links.txt +1 -0
- domica_html-0.1.4/src/domica_html.egg-info/requires.txt +4 -0
- domica_html-0.1.4/src/domica_html.egg-info/top_level.txt +1 -0
- domica_html-0.1.4/tests/test_api.py +28 -0
- domica_html-0.1.4/tests/test_rendering.py +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OPOLO
|
|
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,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: domica_html
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Declarative Python DSL for building HTML documents programmatically
|
|
5
|
+
Author-email: Opolo <nefris.me@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/opolonix/domica-html
|
|
8
|
+
Project-URL: Repository, https://github.com/opolonix/domica-html
|
|
9
|
+
Project-URL: Documentation, https://github.com/opolonix/domica-html#readme
|
|
10
|
+
Keywords: html,dsl,html-generator,python-html,dom
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
27
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
Библиотека для декларативного формирования HTML-структуры на Python.
|
|
31
|
+
|
|
32
|
+
Текущая ветка `0.1.x` рассматривается как финальная синхронная линия библиотеки. Дальнейшие крупные изменения, включая поддержку асинхронности, планируются в `0.2.0`.
|
|
33
|
+
|
|
34
|
+
## Установка
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install git+https://github.com/opolonix/domica-html.git@v0.1.5
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Для локальной разработки:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install -e .[dev]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Пример
|
|
47
|
+
```python
|
|
48
|
+
from domica_html import html, div
|
|
49
|
+
|
|
50
|
+
doc = html()
|
|
51
|
+
|
|
52
|
+
with doc:
|
|
53
|
+
div("hello world")
|
|
54
|
+
|
|
55
|
+
print(doc.render())
|
|
56
|
+
```
|
|
57
|
+
output:
|
|
58
|
+
```txt
|
|
59
|
+
<html>
|
|
60
|
+
<div>
|
|
61
|
+
hello world
|
|
62
|
+
</div>
|
|
63
|
+
</html>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Реализация собственного компонента
|
|
67
|
+
```python
|
|
68
|
+
from domica_html import html, div, inc, node_container, script, line
|
|
69
|
+
from contextvars import ContextVar
|
|
70
|
+
from collections import defaultdict
|
|
71
|
+
from typing import Type
|
|
72
|
+
|
|
73
|
+
external_tags: ContextVar[dict[Type, list["external_container"]]] = ContextVar("external_tags", default=defaultdict(list))
|
|
74
|
+
|
|
75
|
+
class external_container(node_container):
|
|
76
|
+
def __init__(self, *, anchor=False):
|
|
77
|
+
super().__init__(anchor=anchor)
|
|
78
|
+
if not anchor:
|
|
79
|
+
tags = external_tags.get()
|
|
80
|
+
tags[self.__class__].append(self)
|
|
81
|
+
|
|
82
|
+
def render(self):
|
|
83
|
+
tags = external_tags.get()
|
|
84
|
+
|
|
85
|
+
for child in tags[self.__class__]:
|
|
86
|
+
if child is self: continue
|
|
87
|
+
self.add_child(child)
|
|
88
|
+
|
|
89
|
+
with inc(indent=inc.indent-1):
|
|
90
|
+
return super().render()
|
|
91
|
+
|
|
92
|
+
class global_script(external_container): ...
|
|
93
|
+
|
|
94
|
+
doc = html()
|
|
95
|
+
|
|
96
|
+
with doc:
|
|
97
|
+
with div():
|
|
98
|
+
div("Hello world with some script", onclick="hello_on_click")
|
|
99
|
+
with global_script():
|
|
100
|
+
line("const hello_on_click = () => {")
|
|
101
|
+
line(inc.char, "alert('hello!');")
|
|
102
|
+
line("}")
|
|
103
|
+
|
|
104
|
+
with script():
|
|
105
|
+
global_script(anchor=True)
|
|
106
|
+
|
|
107
|
+
print(doc.render())
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
output:
|
|
111
|
+
```txt
|
|
112
|
+
<html>
|
|
113
|
+
<div>
|
|
114
|
+
<div onclick="hello_on_click">
|
|
115
|
+
Hello world with some script
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<script>
|
|
119
|
+
const hello_on_click = () => {
|
|
120
|
+
alert('hello!');
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
</html>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Релизы
|
|
127
|
+
|
|
128
|
+
- `0.1.5`: финальная стабильная синхронная версия ветки `0.1.x`
|
|
129
|
+
|
|
130
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Библиотека для декларативного формирования HTML-структуры на Python.
|
|
2
|
+
|
|
3
|
+
Текущая ветка `0.1.x` рассматривается как финальная синхронная линия библиотеки. Дальнейшие крупные изменения, включая поддержку асинхронности, планируются в `0.2.0`.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install git+https://github.com/opolonix/domica-html.git@v0.1.5
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Для локальной разработки:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e .[dev]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Пример
|
|
18
|
+
```python
|
|
19
|
+
from domica_html import html, div
|
|
20
|
+
|
|
21
|
+
doc = html()
|
|
22
|
+
|
|
23
|
+
with doc:
|
|
24
|
+
div("hello world")
|
|
25
|
+
|
|
26
|
+
print(doc.render())
|
|
27
|
+
```
|
|
28
|
+
output:
|
|
29
|
+
```txt
|
|
30
|
+
<html>
|
|
31
|
+
<div>
|
|
32
|
+
hello world
|
|
33
|
+
</div>
|
|
34
|
+
</html>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Реализация собственного компонента
|
|
38
|
+
```python
|
|
39
|
+
from domica_html import html, div, inc, node_container, script, line
|
|
40
|
+
from contextvars import ContextVar
|
|
41
|
+
from collections import defaultdict
|
|
42
|
+
from typing import Type
|
|
43
|
+
|
|
44
|
+
external_tags: ContextVar[dict[Type, list["external_container"]]] = ContextVar("external_tags", default=defaultdict(list))
|
|
45
|
+
|
|
46
|
+
class external_container(node_container):
|
|
47
|
+
def __init__(self, *, anchor=False):
|
|
48
|
+
super().__init__(anchor=anchor)
|
|
49
|
+
if not anchor:
|
|
50
|
+
tags = external_tags.get()
|
|
51
|
+
tags[self.__class__].append(self)
|
|
52
|
+
|
|
53
|
+
def render(self):
|
|
54
|
+
tags = external_tags.get()
|
|
55
|
+
|
|
56
|
+
for child in tags[self.__class__]:
|
|
57
|
+
if child is self: continue
|
|
58
|
+
self.add_child(child)
|
|
59
|
+
|
|
60
|
+
with inc(indent=inc.indent-1):
|
|
61
|
+
return super().render()
|
|
62
|
+
|
|
63
|
+
class global_script(external_container): ...
|
|
64
|
+
|
|
65
|
+
doc = html()
|
|
66
|
+
|
|
67
|
+
with doc:
|
|
68
|
+
with div():
|
|
69
|
+
div("Hello world with some script", onclick="hello_on_click")
|
|
70
|
+
with global_script():
|
|
71
|
+
line("const hello_on_click = () => {")
|
|
72
|
+
line(inc.char, "alert('hello!');")
|
|
73
|
+
line("}")
|
|
74
|
+
|
|
75
|
+
with script():
|
|
76
|
+
global_script(anchor=True)
|
|
77
|
+
|
|
78
|
+
print(doc.render())
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
output:
|
|
82
|
+
```txt
|
|
83
|
+
<html>
|
|
84
|
+
<div>
|
|
85
|
+
<div onclick="hello_on_click">
|
|
86
|
+
Hello world with some script
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<script>
|
|
90
|
+
const hello_on_click = () => {
|
|
91
|
+
alert('hello!');
|
|
92
|
+
}
|
|
93
|
+
</script>
|
|
94
|
+
</html>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Релизы
|
|
98
|
+
|
|
99
|
+
- `0.1.5`: финальная стабильная синхронная версия ветки `0.1.x`
|
|
100
|
+
|
|
101
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "domica_html"
|
|
7
|
+
version = "0.1.4"
|
|
8
|
+
description = "Declarative Python DSL for building HTML documents programmatically"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["html", "dsl", "html-generator", "python-html", "dom"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Opolo", email = "nefris.me@gmail.com" }
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: Text Processing :: Markup :: HTML",
|
|
28
|
+
]
|
|
29
|
+
dependencies = []
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"build>=1.2.2",
|
|
34
|
+
"twine>=6.1.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/opolonix/domica-html"
|
|
39
|
+
Repository = "https://github.com/opolonix/domica-html"
|
|
40
|
+
Documentation = "https://github.com/opolonix/domica-html#readme"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
package-dir = {"" = "src"}
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from .block import line, text
|
|
2
|
+
from .inctement import inc
|
|
3
|
+
from .node import node, node_container
|
|
4
|
+
from .tags import (
|
|
5
|
+
a,
|
|
6
|
+
abbr,
|
|
7
|
+
article,
|
|
8
|
+
aside,
|
|
9
|
+
attr_value,
|
|
10
|
+
audio,
|
|
11
|
+
b,
|
|
12
|
+
base,
|
|
13
|
+
body,
|
|
14
|
+
br,
|
|
15
|
+
button,
|
|
16
|
+
canvas,
|
|
17
|
+
caption,
|
|
18
|
+
cite,
|
|
19
|
+
code,
|
|
20
|
+
col,
|
|
21
|
+
colgroup,
|
|
22
|
+
datalist,
|
|
23
|
+
dd,
|
|
24
|
+
details,
|
|
25
|
+
dfn,
|
|
26
|
+
dialog,
|
|
27
|
+
div,
|
|
28
|
+
dl,
|
|
29
|
+
dt,
|
|
30
|
+
em,
|
|
31
|
+
fieldset,
|
|
32
|
+
figcaption,
|
|
33
|
+
figure,
|
|
34
|
+
footer,
|
|
35
|
+
form,
|
|
36
|
+
h1,
|
|
37
|
+
h2,
|
|
38
|
+
h3,
|
|
39
|
+
h4,
|
|
40
|
+
h5,
|
|
41
|
+
h6,
|
|
42
|
+
head,
|
|
43
|
+
header,
|
|
44
|
+
hr,
|
|
45
|
+
html,
|
|
46
|
+
html_tag,
|
|
47
|
+
i,
|
|
48
|
+
iframe,
|
|
49
|
+
img,
|
|
50
|
+
input,
|
|
51
|
+
kbd,
|
|
52
|
+
label,
|
|
53
|
+
li,
|
|
54
|
+
link,
|
|
55
|
+
main,
|
|
56
|
+
mark,
|
|
57
|
+
meta,
|
|
58
|
+
nav,
|
|
59
|
+
ol,
|
|
60
|
+
optgroup,
|
|
61
|
+
option,
|
|
62
|
+
p,
|
|
63
|
+
picture,
|
|
64
|
+
pre,
|
|
65
|
+
q,
|
|
66
|
+
samp,
|
|
67
|
+
script,
|
|
68
|
+
section,
|
|
69
|
+
select,
|
|
70
|
+
small,
|
|
71
|
+
source,
|
|
72
|
+
span,
|
|
73
|
+
strong,
|
|
74
|
+
style,
|
|
75
|
+
style_item,
|
|
76
|
+
summary,
|
|
77
|
+
svg,
|
|
78
|
+
table,
|
|
79
|
+
tbody,
|
|
80
|
+
td,
|
|
81
|
+
textarea,
|
|
82
|
+
tfoot,
|
|
83
|
+
th,
|
|
84
|
+
thead,
|
|
85
|
+
time,
|
|
86
|
+
title,
|
|
87
|
+
tr,
|
|
88
|
+
track,
|
|
89
|
+
u,
|
|
90
|
+
ul,
|
|
91
|
+
var,
|
|
92
|
+
video,
|
|
93
|
+
wbr,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"inc",
|
|
98
|
+
"text",
|
|
99
|
+
"line",
|
|
100
|
+
"node",
|
|
101
|
+
"node_container",
|
|
102
|
+
"attr_value",
|
|
103
|
+
"html_tag",
|
|
104
|
+
"html",
|
|
105
|
+
"head",
|
|
106
|
+
"body",
|
|
107
|
+
"div",
|
|
108
|
+
"script",
|
|
109
|
+
"style",
|
|
110
|
+
"select",
|
|
111
|
+
"option",
|
|
112
|
+
"pre",
|
|
113
|
+
"header",
|
|
114
|
+
"footer",
|
|
115
|
+
"main",
|
|
116
|
+
"section",
|
|
117
|
+
"article",
|
|
118
|
+
"aside",
|
|
119
|
+
"nav",
|
|
120
|
+
"figure",
|
|
121
|
+
"figcaption",
|
|
122
|
+
"details",
|
|
123
|
+
"summary",
|
|
124
|
+
"fieldset",
|
|
125
|
+
"form",
|
|
126
|
+
"dialog",
|
|
127
|
+
"ul",
|
|
128
|
+
"ol",
|
|
129
|
+
"li",
|
|
130
|
+
"dl",
|
|
131
|
+
"dt",
|
|
132
|
+
"dd",
|
|
133
|
+
"table",
|
|
134
|
+
"thead",
|
|
135
|
+
"tbody",
|
|
136
|
+
"tfoot",
|
|
137
|
+
"tr",
|
|
138
|
+
"th",
|
|
139
|
+
"td",
|
|
140
|
+
"caption",
|
|
141
|
+
"colgroup",
|
|
142
|
+
"col",
|
|
143
|
+
"img",
|
|
144
|
+
"audio",
|
|
145
|
+
"video",
|
|
146
|
+
"source",
|
|
147
|
+
"track",
|
|
148
|
+
"canvas",
|
|
149
|
+
"svg",
|
|
150
|
+
"picture",
|
|
151
|
+
"iframe",
|
|
152
|
+
"input",
|
|
153
|
+
"textarea",
|
|
154
|
+
"button",
|
|
155
|
+
"label",
|
|
156
|
+
"optgroup",
|
|
157
|
+
"datalist",
|
|
158
|
+
"h1",
|
|
159
|
+
"h2",
|
|
160
|
+
"h3",
|
|
161
|
+
"h4",
|
|
162
|
+
"h5",
|
|
163
|
+
"h6",
|
|
164
|
+
"p",
|
|
165
|
+
"a",
|
|
166
|
+
"span",
|
|
167
|
+
"strong",
|
|
168
|
+
"em",
|
|
169
|
+
"b",
|
|
170
|
+
"i",
|
|
171
|
+
"u",
|
|
172
|
+
"small",
|
|
173
|
+
"mark",
|
|
174
|
+
"abbr",
|
|
175
|
+
"code",
|
|
176
|
+
"var",
|
|
177
|
+
"samp",
|
|
178
|
+
"kbd",
|
|
179
|
+
"q",
|
|
180
|
+
"cite",
|
|
181
|
+
"dfn",
|
|
182
|
+
"time",
|
|
183
|
+
"title",
|
|
184
|
+
"meta",
|
|
185
|
+
"link",
|
|
186
|
+
"br",
|
|
187
|
+
"wbr",
|
|
188
|
+
"base",
|
|
189
|
+
"hr",
|
|
190
|
+
"style_item",
|
|
191
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from .node import node_container, node
|
|
4
|
+
from .inctement import inc
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class text(node_container):
|
|
8
|
+
indent_prefix: bool = False
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
*value: Union[str, node],
|
|
13
|
+
anchor = True
|
|
14
|
+
):
|
|
15
|
+
self.value = value
|
|
16
|
+
super().__init__(anchor)
|
|
17
|
+
|
|
18
|
+
def render(self):
|
|
19
|
+
content = self.value_sync(self.value)
|
|
20
|
+
if self.children:
|
|
21
|
+
with inc:
|
|
22
|
+
content += self.value_sync(self.children)
|
|
23
|
+
|
|
24
|
+
if self.indent_prefix:
|
|
25
|
+
return inc.enter_space + content
|
|
26
|
+
return content
|
|
27
|
+
|
|
28
|
+
class line(text):
|
|
29
|
+
indent_prefix: bool = True
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import contextvars
|
|
2
|
+
from typing import Callable, List, Union
|
|
3
|
+
|
|
4
|
+
increment_context: contextvars.ContextVar["IncrementContext"] = contextvars.ContextVar("dom_item_context_var_ind")
|
|
5
|
+
|
|
6
|
+
_UNSET = object()
|
|
7
|
+
|
|
8
|
+
def _str(value, refresh: Callable = None):
|
|
9
|
+
class r_str(str):
|
|
10
|
+
def re_render(_):
|
|
11
|
+
if not refresh: return str(_)
|
|
12
|
+
return str(refresh())
|
|
13
|
+
|
|
14
|
+
return r_str(value)
|
|
15
|
+
|
|
16
|
+
class IncrementContext:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._indent: List[int] = []
|
|
19
|
+
self._char: List[str] = []
|
|
20
|
+
self._is_set = False
|
|
21
|
+
|
|
22
|
+
def set(
|
|
23
|
+
self,
|
|
24
|
+
indent: Union[int, object] = _UNSET,
|
|
25
|
+
char: Union[str, object] = _UNSET,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._is_set = True
|
|
28
|
+
if indent is not _UNSET:
|
|
29
|
+
self._indent.append(indent)
|
|
30
|
+
elif self._indent:
|
|
31
|
+
self._indent.append(self._indent[-1])
|
|
32
|
+
|
|
33
|
+
if char is not _UNSET:
|
|
34
|
+
self._char.append(char)
|
|
35
|
+
elif self._char:
|
|
36
|
+
self._char.append(self._char[-1])
|
|
37
|
+
|
|
38
|
+
def inc(self) -> None:
|
|
39
|
+
if not self._is_set:
|
|
40
|
+
self.set(indent=self.indent + 1)
|
|
41
|
+
|
|
42
|
+
self._is_set = False
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def indent(self) -> int:
|
|
46
|
+
return self._indent[-1] if self._indent else 0
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def char(self) -> str:
|
|
50
|
+
return self._char[-1] if self._char else " "
|
|
51
|
+
|
|
52
|
+
def pop(self) -> bool:
|
|
53
|
+
if self._indent:
|
|
54
|
+
self._indent.pop()
|
|
55
|
+
if self._char:
|
|
56
|
+
self._char.pop()
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _increment:
|
|
61
|
+
@property
|
|
62
|
+
def indent(self) -> int:
|
|
63
|
+
return self.context.indent
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def char(self) -> str:
|
|
67
|
+
return _str(self.context.char, refresh=lambda: self.char)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def space(self) -> str:
|
|
71
|
+
return _str((self.char * self.indent) if self.char else "", refresh=lambda: self.space)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def context(self) -> IncrementContext:
|
|
75
|
+
ctx = increment_context.get(None)
|
|
76
|
+
if ctx is None:
|
|
77
|
+
ctx = IncrementContext()
|
|
78
|
+
increment_context.set(ctx)
|
|
79
|
+
return ctx
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def enter_space(self) -> str:
|
|
83
|
+
return ("\n" + (self.char * self.indent)) if self.char else ""
|
|
84
|
+
|
|
85
|
+
def __call__(
|
|
86
|
+
self,
|
|
87
|
+
indent: Union[int, object] = _UNSET,
|
|
88
|
+
char: Union[str, object] = _UNSET
|
|
89
|
+
) -> "_increment":
|
|
90
|
+
self.context.set(
|
|
91
|
+
indent=indent,
|
|
92
|
+
char=char
|
|
93
|
+
)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __enter__(self) -> "_increment":
|
|
97
|
+
self.context.inc()
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def __exit__(self, exc_type, exc_value, traceback) -> bool:
|
|
101
|
+
self.context.pop()
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
inc = _increment()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Optional, List, Protocol, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
|
|
5
|
+
item_context: contextvars.ContextVar[Optional[List["node_container"]]] = contextvars.ContextVar("item_context", default=None)
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
class node_container_rotocol(Protocol):
|
|
9
|
+
def add_child(self, child: "node") -> None: ...
|
|
10
|
+
def remove_child(self, child: "node") -> None: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class node_base:
|
|
14
|
+
@staticmethod
|
|
15
|
+
def render_item(value) -> str:
|
|
16
|
+
if isinstance(value, (list, tuple)):
|
|
17
|
+
return "".join([node_base.render_item(v) for v in value])
|
|
18
|
+
if isinstance(value, node):
|
|
19
|
+
return node_base.render_item(value.render())
|
|
20
|
+
if hasattr(value, "re_render") and callable((to_call := getattr(value, "re_render"))):
|
|
21
|
+
return node_base.render_item(to_call())
|
|
22
|
+
return str(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class node:
|
|
26
|
+
def __init__(self, anchor: bool = False):
|
|
27
|
+
self._parent: Optional["node_container_rotocol"] = None
|
|
28
|
+
|
|
29
|
+
if anchor:
|
|
30
|
+
parent = self.get_parent()
|
|
31
|
+
if parent:
|
|
32
|
+
parent.add_child(self)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def parent(self) -> Optional["node_container_rotocol"]:
|
|
36
|
+
return self._parent
|
|
37
|
+
|
|
38
|
+
def value_sync(self, value):
|
|
39
|
+
return node_base.render_item(value)
|
|
40
|
+
|
|
41
|
+
@parent.setter
|
|
42
|
+
def parent(self, value: Optional["node_container_rotocol"]):
|
|
43
|
+
self._parent = value
|
|
44
|
+
|
|
45
|
+
def get_parent(self) -> Optional["node_container_rotocol"]:
|
|
46
|
+
stack = item_context.get()
|
|
47
|
+
if stack is None:
|
|
48
|
+
return None
|
|
49
|
+
return stack[-1] if stack else None
|
|
50
|
+
|
|
51
|
+
def unpin_from_parent(self) -> Optional["node_container_rotocol"]:
|
|
52
|
+
old_parent = self._parent
|
|
53
|
+
if old_parent:
|
|
54
|
+
old_parent.remove_child(self)
|
|
55
|
+
return old_parent
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __enter__(self):
|
|
60
|
+
stack = item_context.get()
|
|
61
|
+
if stack is None:
|
|
62
|
+
stack = []
|
|
63
|
+
item_context.set(stack)
|
|
64
|
+
stack.append(self)
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
68
|
+
stack = item_context.get()
|
|
69
|
+
if stack is None:
|
|
70
|
+
return False
|
|
71
|
+
stack.pop(-1)
|
|
72
|
+
if not stack:
|
|
73
|
+
item_context.set(None)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def __str__(self):
|
|
77
|
+
return self.value_sync(self.render())
|
|
78
|
+
|
|
79
|
+
def render(self):
|
|
80
|
+
return "<node/>"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class node_container(node):
|
|
85
|
+
def __init__(self, anchor: bool = False):
|
|
86
|
+
super().__init__(anchor)
|
|
87
|
+
self.children: List[node] = []
|
|
88
|
+
|
|
89
|
+
def add_child(self, child: node):
|
|
90
|
+
child.unpin_from_parent()
|
|
91
|
+
self.children.append(child)
|
|
92
|
+
child.parent = self
|
|
93
|
+
|
|
94
|
+
def remove_child(self, child: node):
|
|
95
|
+
if child in self.children:
|
|
96
|
+
self.children.remove(child)
|
|
97
|
+
child.parent = None
|
|
98
|
+
|
|
99
|
+
def render(self):
|
|
100
|
+
return self.children
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from html import escape
|
|
2
|
+
|
|
3
|
+
from .node import node_container
|
|
4
|
+
from .inctement import inc
|
|
5
|
+
from typing import Iterable, Callable, Optional, Union
|
|
6
|
+
|
|
7
|
+
class attr_value(node_container):
|
|
8
|
+
def __init__(self, value):
|
|
9
|
+
self.value = value
|
|
10
|
+
super().__init__(anchor=False)
|
|
11
|
+
|
|
12
|
+
def render(self):
|
|
13
|
+
value = str(self.value)
|
|
14
|
+
return '"' + escape(value, quote=True) + '"'
|
|
15
|
+
|
|
16
|
+
class html_tag(node_container):
|
|
17
|
+
close_tag: bool = True
|
|
18
|
+
enter_space: bool = True
|
|
19
|
+
|
|
20
|
+
_replace_attr_name: Callable[["html_tag", str], str] = lambda _, s: s.removeprefix("_").replace("_", "-")
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
inner_text: Union[str, node_container] = "",
|
|
25
|
+
*,
|
|
26
|
+
_class: Optional[Iterable[Union[node_container, str]]] = None,
|
|
27
|
+
anchor = True,
|
|
28
|
+
**attrs
|
|
29
|
+
):
|
|
30
|
+
add = {}
|
|
31
|
+
if _class:
|
|
32
|
+
if isinstance(_class, str):
|
|
33
|
+
add["_class"] = _class
|
|
34
|
+
else:
|
|
35
|
+
_class = " ".join([str(_c) for _c in _class])
|
|
36
|
+
if _class:
|
|
37
|
+
add["_class"] = _class
|
|
38
|
+
|
|
39
|
+
self.attrs = {k: self.prepare_value(v) for k, v in (attrs | add).items()}
|
|
40
|
+
self.inner_text = self.prepare_value(inner_text)
|
|
41
|
+
|
|
42
|
+
super().__init__(anchor=anchor)
|
|
43
|
+
|
|
44
|
+
def prepare_value(self, value):
|
|
45
|
+
if isinstance(value, node_container):
|
|
46
|
+
value.unpin_from_parent()
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
def render(self):
|
|
50
|
+
kd = []
|
|
51
|
+
kd.append(inc.enter_space)
|
|
52
|
+
kd.append("<")
|
|
53
|
+
kd.append(self.__class__.__name__)
|
|
54
|
+
|
|
55
|
+
attrs_kb = []
|
|
56
|
+
for key, value in self.attrs.items():
|
|
57
|
+
if not isinstance(value, attr_value):
|
|
58
|
+
value = attr_value(value)
|
|
59
|
+
|
|
60
|
+
attrs_kb.append(" ")
|
|
61
|
+
attrs_kb.append(self._replace_attr_name(key) +"="+self.value_sync(value))
|
|
62
|
+
|
|
63
|
+
kd.append(attrs_kb)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
kd.append(">")
|
|
67
|
+
|
|
68
|
+
if self.close_tag:
|
|
69
|
+
kd_childs = []
|
|
70
|
+
with inc:
|
|
71
|
+
if (v := self.value_sync(self.inner_text)):
|
|
72
|
+
if self.enter_space and not v.startswith(inc.enter_space):
|
|
73
|
+
kd_childs.append(inc.enter_space)
|
|
74
|
+
kd_childs.append(v)
|
|
75
|
+
for child in self.children:
|
|
76
|
+
kd_childs.append(self.value_sync(child))
|
|
77
|
+
|
|
78
|
+
if kd_childs:
|
|
79
|
+
kd += kd_childs
|
|
80
|
+
|
|
81
|
+
if self.enter_space and kd_childs: kd.append(inc.enter_space)
|
|
82
|
+
|
|
83
|
+
kd.append("</")
|
|
84
|
+
kd.append(self.__class__.__name__)
|
|
85
|
+
kd.append(">")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return self.value_sync(kd)
|
|
89
|
+
|
|
90
|
+
class _inline(html_tag):
|
|
91
|
+
enter_space=False
|
|
92
|
+
|
|
93
|
+
class _open(html_tag):
|
|
94
|
+
close_tag=False
|
|
95
|
+
|
|
96
|
+
class html(html_tag): ...
|
|
97
|
+
class head(html_tag): ...
|
|
98
|
+
class body(html_tag): ...
|
|
99
|
+
class div(html_tag): ...
|
|
100
|
+
class script(html_tag): ...
|
|
101
|
+
class style(html_tag): ...
|
|
102
|
+
class select(html_tag): ...
|
|
103
|
+
class option(html_tag): ...
|
|
104
|
+
class pre(html_tag): ...
|
|
105
|
+
|
|
106
|
+
class header(html_tag): ...
|
|
107
|
+
class footer(html_tag): ...
|
|
108
|
+
class main(html_tag): ...
|
|
109
|
+
class section(html_tag): ...
|
|
110
|
+
class article(html_tag): ...
|
|
111
|
+
class aside(html_tag): ...
|
|
112
|
+
class nav(html_tag): ...
|
|
113
|
+
class figure(html_tag): ...
|
|
114
|
+
class figcaption(html_tag): ...
|
|
115
|
+
class details(html_tag): ...
|
|
116
|
+
class summary(html_tag): ...
|
|
117
|
+
class fieldset(html_tag): ...
|
|
118
|
+
class form(html_tag): ...
|
|
119
|
+
class dialog(html_tag): ...
|
|
120
|
+
|
|
121
|
+
class ul(html_tag): ...
|
|
122
|
+
class ol(html_tag): ...
|
|
123
|
+
class li(html_tag): ...
|
|
124
|
+
class dl(html_tag): ...
|
|
125
|
+
class dt(html_tag): ...
|
|
126
|
+
class dd(html_tag): ...
|
|
127
|
+
|
|
128
|
+
class table(html_tag): ...
|
|
129
|
+
class thead(html_tag): ...
|
|
130
|
+
class tbody(html_tag): ...
|
|
131
|
+
class tfoot(html_tag): ...
|
|
132
|
+
class tr(html_tag): ...
|
|
133
|
+
class th(html_tag): ...
|
|
134
|
+
class td(html_tag): ...
|
|
135
|
+
class caption(html_tag): ...
|
|
136
|
+
class colgroup(html_tag): ...
|
|
137
|
+
class col(html_tag): ...
|
|
138
|
+
|
|
139
|
+
class img(_open): ...
|
|
140
|
+
class audio(html_tag): ...
|
|
141
|
+
class video(html_tag): ...
|
|
142
|
+
class source(_open): ...
|
|
143
|
+
class track(_open): ...
|
|
144
|
+
class canvas(html_tag): ...
|
|
145
|
+
class svg(html_tag): ...
|
|
146
|
+
class picture(html_tag): ...
|
|
147
|
+
class iframe(html_tag): ...
|
|
148
|
+
|
|
149
|
+
class input(_open): ...
|
|
150
|
+
class textarea(html_tag): ...
|
|
151
|
+
class button(html_tag): ...
|
|
152
|
+
class label(_inline): ...
|
|
153
|
+
class optgroup(html_tag): ...
|
|
154
|
+
class datalist(html_tag): ...
|
|
155
|
+
|
|
156
|
+
class h1(_inline): ...
|
|
157
|
+
class h2(_inline): ...
|
|
158
|
+
class h3(_inline): ...
|
|
159
|
+
class h4(_inline): ...
|
|
160
|
+
class h5(_inline): ...
|
|
161
|
+
class h6(_inline): ...
|
|
162
|
+
class p(_inline): ...
|
|
163
|
+
class a(_inline): ...
|
|
164
|
+
class span(_inline): ...
|
|
165
|
+
class strong(_inline): ...
|
|
166
|
+
class em(_inline): ...
|
|
167
|
+
class b(_inline): ...
|
|
168
|
+
class i(_inline): ...
|
|
169
|
+
class u(_inline): ...
|
|
170
|
+
class small(_inline): ...
|
|
171
|
+
class mark(_inline): ...
|
|
172
|
+
class abbr(_inline): ...
|
|
173
|
+
class code(_inline): ...
|
|
174
|
+
class var(_inline): ...
|
|
175
|
+
class samp(_inline): ...
|
|
176
|
+
class kbd(_inline): ...
|
|
177
|
+
class q(_inline): ...
|
|
178
|
+
class cite(_inline): ...
|
|
179
|
+
class dfn(_inline): ...
|
|
180
|
+
class time(_inline): ...
|
|
181
|
+
class label(_inline): ...
|
|
182
|
+
class title(_inline): ...
|
|
183
|
+
|
|
184
|
+
class meta(_open): ...
|
|
185
|
+
class link(_open): ...
|
|
186
|
+
class br(_open): ...
|
|
187
|
+
class wbr(_open): ...
|
|
188
|
+
class img(_open): ...
|
|
189
|
+
class source(_open): ...
|
|
190
|
+
class track(_open): ...
|
|
191
|
+
class base(_open): ...
|
|
192
|
+
class hr(_open): ...
|
|
193
|
+
|
|
194
|
+
class style_item(node_container):
|
|
195
|
+
|
|
196
|
+
_replace_attr_name: Callable[[str], str] = lambda _, s: s.removeprefix("_").replace("_", "-")
|
|
197
|
+
|
|
198
|
+
def __init__(self, selector, *items: node_container, _anchor=True, **attrs):
|
|
199
|
+
super().__init__(anchor=_anchor)
|
|
200
|
+
self.selector = selector
|
|
201
|
+
self.attrs = attrs
|
|
202
|
+
|
|
203
|
+
for item in items:
|
|
204
|
+
if isinstance(item, node_container):
|
|
205
|
+
self.add_child(item)
|
|
206
|
+
|
|
207
|
+
def render(self):
|
|
208
|
+
kb = []
|
|
209
|
+
kb.append(inc.enter_space)
|
|
210
|
+
kb.append(str(self.selector))
|
|
211
|
+
kb.append(" {")
|
|
212
|
+
kb_childs = []
|
|
213
|
+
with inc:
|
|
214
|
+
space = inc.enter_space
|
|
215
|
+
for k,v in self.attrs.items():
|
|
216
|
+
render_key = str(k)
|
|
217
|
+
render_value = str(v)
|
|
218
|
+
kb_childs.append(space + self._replace_attr_name(render_key.removeprefix(space)))
|
|
219
|
+
kb_childs.append(": ")
|
|
220
|
+
kb_childs.append(render_value)
|
|
221
|
+
kb_childs.append(";")
|
|
222
|
+
|
|
223
|
+
for child in self.children:
|
|
224
|
+
render_child = str(child)
|
|
225
|
+
kb_childs.append(space + render_child.removeprefix(space))
|
|
226
|
+
if kb_childs:
|
|
227
|
+
kb += kb_childs
|
|
228
|
+
kb.append(inc.enter_space)
|
|
229
|
+
kb.append("}")
|
|
230
|
+
|
|
231
|
+
return "".join(kb)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: domica_html
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Declarative Python DSL for building HTML documents programmatically
|
|
5
|
+
Author-email: Opolo <nefris.me@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/opolonix/domica-html
|
|
8
|
+
Project-URL: Repository, https://github.com/opolonix/domica-html
|
|
9
|
+
Project-URL: Documentation, https://github.com/opolonix/domica-html#readme
|
|
10
|
+
Keywords: html,dsl,html-generator,python-html,dom
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
27
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
Библиотека для декларативного формирования HTML-структуры на Python.
|
|
31
|
+
|
|
32
|
+
Текущая ветка `0.1.x` рассматривается как финальная синхронная линия библиотеки. Дальнейшие крупные изменения, включая поддержку асинхронности, планируются в `0.2.0`.
|
|
33
|
+
|
|
34
|
+
## Установка
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install git+https://github.com/opolonix/domica-html.git@v0.1.5
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Для локальной разработки:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install -e .[dev]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Пример
|
|
47
|
+
```python
|
|
48
|
+
from domica_html import html, div
|
|
49
|
+
|
|
50
|
+
doc = html()
|
|
51
|
+
|
|
52
|
+
with doc:
|
|
53
|
+
div("hello world")
|
|
54
|
+
|
|
55
|
+
print(doc.render())
|
|
56
|
+
```
|
|
57
|
+
output:
|
|
58
|
+
```txt
|
|
59
|
+
<html>
|
|
60
|
+
<div>
|
|
61
|
+
hello world
|
|
62
|
+
</div>
|
|
63
|
+
</html>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Реализация собственного компонента
|
|
67
|
+
```python
|
|
68
|
+
from domica_html import html, div, inc, node_container, script, line
|
|
69
|
+
from contextvars import ContextVar
|
|
70
|
+
from collections import defaultdict
|
|
71
|
+
from typing import Type
|
|
72
|
+
|
|
73
|
+
external_tags: ContextVar[dict[Type, list["external_container"]]] = ContextVar("external_tags", default=defaultdict(list))
|
|
74
|
+
|
|
75
|
+
class external_container(node_container):
|
|
76
|
+
def __init__(self, *, anchor=False):
|
|
77
|
+
super().__init__(anchor=anchor)
|
|
78
|
+
if not anchor:
|
|
79
|
+
tags = external_tags.get()
|
|
80
|
+
tags[self.__class__].append(self)
|
|
81
|
+
|
|
82
|
+
def render(self):
|
|
83
|
+
tags = external_tags.get()
|
|
84
|
+
|
|
85
|
+
for child in tags[self.__class__]:
|
|
86
|
+
if child is self: continue
|
|
87
|
+
self.add_child(child)
|
|
88
|
+
|
|
89
|
+
with inc(indent=inc.indent-1):
|
|
90
|
+
return super().render()
|
|
91
|
+
|
|
92
|
+
class global_script(external_container): ...
|
|
93
|
+
|
|
94
|
+
doc = html()
|
|
95
|
+
|
|
96
|
+
with doc:
|
|
97
|
+
with div():
|
|
98
|
+
div("Hello world with some script", onclick="hello_on_click")
|
|
99
|
+
with global_script():
|
|
100
|
+
line("const hello_on_click = () => {")
|
|
101
|
+
line(inc.char, "alert('hello!');")
|
|
102
|
+
line("}")
|
|
103
|
+
|
|
104
|
+
with script():
|
|
105
|
+
global_script(anchor=True)
|
|
106
|
+
|
|
107
|
+
print(doc.render())
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
output:
|
|
111
|
+
```txt
|
|
112
|
+
<html>
|
|
113
|
+
<div>
|
|
114
|
+
<div onclick="hello_on_click">
|
|
115
|
+
Hello world with some script
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<script>
|
|
119
|
+
const hello_on_click = () => {
|
|
120
|
+
alert('hello!');
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
</html>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Релизы
|
|
127
|
+
|
|
128
|
+
- `0.1.5`: финальная стабильная синхронная версия ветки `0.1.x`
|
|
129
|
+
|
|
130
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/domica_html/__init__.py
|
|
5
|
+
src/domica_html/block.py
|
|
6
|
+
src/domica_html/inctement.py
|
|
7
|
+
src/domica_html/node.py
|
|
8
|
+
src/domica_html/tags.py
|
|
9
|
+
src/domica_html.egg-info/PKG-INFO
|
|
10
|
+
src/domica_html.egg-info/SOURCES.txt
|
|
11
|
+
src/domica_html.egg-info/dependency_links.txt
|
|
12
|
+
src/domica_html.egg-info/requires.txt
|
|
13
|
+
src/domica_html.egg-info/top_level.txt
|
|
14
|
+
tests/test_api.py
|
|
15
|
+
tests/test_rendering.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
domica_html
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
sys.path.insert(0, "src")
|
|
6
|
+
|
|
7
|
+
import domica_html
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PublicApiTests(unittest.TestCase):
|
|
11
|
+
def test_all_names_are_exported(self):
|
|
12
|
+
missing = [name for name in domica_html.__all__ if not hasattr(domica_html, name)]
|
|
13
|
+
self.assertEqual(missing, [])
|
|
14
|
+
|
|
15
|
+
def test_star_import_uses_public_api(self):
|
|
16
|
+
namespace = {}
|
|
17
|
+
exec("from domica_html import *", namespace)
|
|
18
|
+
|
|
19
|
+
for name in domica_html.__all__:
|
|
20
|
+
self.assertIn(name, namespace)
|
|
21
|
+
|
|
22
|
+
def test_expected_html_tags_are_available(self):
|
|
23
|
+
for name in ("header", "footer", "table", "img", "hr", "style_item"):
|
|
24
|
+
self.assertTrue(hasattr(domica_html, name), name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
unittest.main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
sys.path.insert(0, "src")
|
|
6
|
+
|
|
7
|
+
from domica_html import div, html, img, line, script, style, style_item
|
|
8
|
+
from domica_html.inctement import inc
|
|
9
|
+
from domica_html.node import item_context, node_container
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RenderingTests(unittest.TestCase):
|
|
13
|
+
def tearDown(self):
|
|
14
|
+
item_context.set(None)
|
|
15
|
+
inc.context._indent.clear()
|
|
16
|
+
inc.context._char.clear()
|
|
17
|
+
inc.context._is_set = False
|
|
18
|
+
|
|
19
|
+
def test_basic_document_render(self):
|
|
20
|
+
doc = html()
|
|
21
|
+
|
|
22
|
+
with doc:
|
|
23
|
+
div("hello world")
|
|
24
|
+
|
|
25
|
+
expected = "\n<html>\n <div>\n hello world\n </div>\n</html>"
|
|
26
|
+
self.assertEqual(doc.render(), expected)
|
|
27
|
+
|
|
28
|
+
def test_attribute_values_are_html_escaped(self):
|
|
29
|
+
rendered = div("x", title='a&b"<c>\'').render()
|
|
30
|
+
self.assertIn('title="a&b"<c>'"', rendered)
|
|
31
|
+
|
|
32
|
+
def test_open_tags_do_not_render_closing_tag(self):
|
|
33
|
+
rendered = img(src="logo.svg", alt='logo & "icon"').render()
|
|
34
|
+
self.assertEqual(rendered, '\n<img src="logo.svg" alt="logo & "icon"">')
|
|
35
|
+
|
|
36
|
+
def test_style_item_renders_css_block(self):
|
|
37
|
+
rendered = style(style_item(".card", color="red", font_size="14px")).render()
|
|
38
|
+
expected = (
|
|
39
|
+
"\n<style>\n"
|
|
40
|
+
" .card {\n"
|
|
41
|
+
" color: red;\n"
|
|
42
|
+
" font-size: 14px;\n"
|
|
43
|
+
" }\n"
|
|
44
|
+
"</style>"
|
|
45
|
+
)
|
|
46
|
+
self.assertEqual(rendered, expected)
|
|
47
|
+
|
|
48
|
+
def test_line_uses_current_indentation(self):
|
|
49
|
+
with script() as tag:
|
|
50
|
+
line("const x = 1;")
|
|
51
|
+
|
|
52
|
+
expected = "\n<script>\n const x = 1;\n</script>"
|
|
53
|
+
self.assertEqual(tag.render(), expected)
|
|
54
|
+
|
|
55
|
+
def test_context_stack_is_created_and_cleared(self):
|
|
56
|
+
self.assertIsNone(item_context.get())
|
|
57
|
+
|
|
58
|
+
container = node_container()
|
|
59
|
+
with container:
|
|
60
|
+
stack = item_context.get()
|
|
61
|
+
self.assertIsInstance(stack, list)
|
|
62
|
+
self.assertEqual(stack[-1], container)
|
|
63
|
+
|
|
64
|
+
self.assertIsNone(item_context.get())
|
|
65
|
+
|
|
66
|
+
def test_anchor_auto_attaches_to_current_parent(self):
|
|
67
|
+
parent = node_container()
|
|
68
|
+
|
|
69
|
+
with parent:
|
|
70
|
+
child = node_container(anchor=True)
|
|
71
|
+
|
|
72
|
+
self.assertIs(child.parent, parent)
|
|
73
|
+
self.assertEqual(parent.children, [child])
|
|
74
|
+
|
|
75
|
+
def test_unpin_from_parent_returns_previous_parent(self):
|
|
76
|
+
parent = node_container()
|
|
77
|
+
child = node_container()
|
|
78
|
+
parent.add_child(child)
|
|
79
|
+
|
|
80
|
+
old_parent = child.unpin_from_parent()
|
|
81
|
+
|
|
82
|
+
self.assertIs(old_parent, parent)
|
|
83
|
+
self.assertIsNone(child.parent)
|
|
84
|
+
self.assertEqual(parent.children, [])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|