epub-translator 0.1.1__py3-none-any.whl → 0.1.4__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.
- epub_translator/__init__.py +9 -2
- epub_translator/data/fill.jinja +143 -38
- epub_translator/epub/__init__.py +1 -1
- epub_translator/epub/metadata.py +122 -0
- epub_translator/epub/spines.py +3 -2
- epub_translator/epub/zip.py +11 -9
- epub_translator/epub_transcode.py +108 -0
- epub_translator/llm/__init__.py +1 -0
- epub_translator/llm/context.py +109 -0
- epub_translator/llm/core.py +32 -113
- epub_translator/llm/executor.py +25 -31
- epub_translator/llm/increasable.py +1 -1
- epub_translator/llm/types.py +0 -3
- epub_translator/punctuation.py +34 -0
- epub_translator/segment/__init__.py +26 -0
- epub_translator/segment/block_segment.py +124 -0
- epub_translator/segment/common.py +29 -0
- epub_translator/segment/inline_segment.py +356 -0
- epub_translator/{xml_translator → segment}/text_segment.py +7 -72
- epub_translator/segment/utils.py +43 -0
- epub_translator/translator.py +152 -184
- epub_translator/utils.py +33 -0
- epub_translator/xml/__init__.py +3 -0
- epub_translator/xml/const.py +1 -0
- epub_translator/xml/deduplication.py +3 -3
- epub_translator/xml/inline.py +67 -0
- epub_translator/xml/self_closing.py +182 -0
- epub_translator/xml/utils.py +42 -0
- epub_translator/xml/xml.py +7 -0
- epub_translator/xml/xml_like.py +8 -33
- epub_translator/xml_interrupter.py +165 -0
- epub_translator/xml_translator/__init__.py +3 -3
- epub_translator/xml_translator/callbacks.py +34 -0
- epub_translator/xml_translator/{const.py → common.py} +0 -1
- epub_translator/xml_translator/hill_climbing.py +104 -0
- epub_translator/xml_translator/stream_mapper.py +253 -0
- epub_translator/xml_translator/submitter.py +352 -91
- epub_translator/xml_translator/translator.py +182 -114
- epub_translator/xml_translator/validation.py +458 -0
- {epub_translator-0.1.1.dist-info → epub_translator-0.1.4.dist-info}/METADATA +134 -21
- epub_translator-0.1.4.dist-info/RECORD +68 -0
- epub_translator/epub/placeholder.py +0 -53
- epub_translator/iter_sync.py +0 -24
- epub_translator/xml_translator/fill.py +0 -128
- epub_translator/xml_translator/format.py +0 -282
- epub_translator/xml_translator/fragmented.py +0 -125
- epub_translator/xml_translator/group.py +0 -183
- epub_translator/xml_translator/progressive_locking.py +0 -256
- epub_translator/xml_translator/utils.py +0 -29
- epub_translator-0.1.1.dist-info/RECORD +0 -58
- {epub_translator-0.1.1.dist-info → epub_translator-0.1.4.dist-info}/LICENSE +0 -0
- {epub_translator-0.1.1.dist-info → epub_translator-0.1.4.dist-info}/WHEEL +0 -0
epub_translator/translator.py
CHANGED
|
@@ -1,214 +1,182 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
1
|
+
from collections.abc import Callable, Generator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
from importlib.metadata import version as get_package_version
|
|
5
|
+
from os import PathLike
|
|
2
6
|
from pathlib import Path
|
|
3
|
-
from xml.etree.ElementTree import Element
|
|
4
7
|
|
|
5
|
-
from .epub import
|
|
6
|
-
|
|
8
|
+
from .epub import (
|
|
9
|
+
Zip,
|
|
10
|
+
read_metadata,
|
|
11
|
+
read_toc,
|
|
12
|
+
search_spine_paths,
|
|
13
|
+
write_metadata,
|
|
14
|
+
write_toc,
|
|
15
|
+
)
|
|
16
|
+
from .epub_transcode import decode_metadata, decode_toc_list, encode_metadata, encode_toc_list
|
|
7
17
|
from .llm import LLM
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
18
|
+
from .punctuation import unwrap_french_quotes
|
|
19
|
+
from .xml import XMLLikeNode, deduplicate_ids_in_element, find_first
|
|
20
|
+
from .xml_interrupter import XMLInterrupter
|
|
21
|
+
from .xml_translator import FillFailedEvent, SubmitKind, TranslationTask, XMLTranslator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _ElementType(Enum):
|
|
25
|
+
TOC = auto()
|
|
26
|
+
METADATA = auto()
|
|
27
|
+
CHAPTER = auto()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class _ElementContext:
|
|
32
|
+
element_type: _ElementType
|
|
33
|
+
chapter_data: tuple[Path, XMLLikeNode] | None = None
|
|
10
34
|
|
|
11
35
|
|
|
12
36
|
def translate(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
target_path: Path,
|
|
37
|
+
source_path: PathLike | str,
|
|
38
|
+
target_path: PathLike | str,
|
|
16
39
|
target_language: str,
|
|
40
|
+
submit: SubmitKind,
|
|
17
41
|
user_prompt: str | None = None,
|
|
18
42
|
max_retries: int = 5,
|
|
19
43
|
max_group_tokens: int = 1200,
|
|
44
|
+
llm: LLM | None = None,
|
|
45
|
+
translation_llm: LLM | None = None,
|
|
46
|
+
fill_llm: LLM | None = None,
|
|
20
47
|
on_progress: Callable[[float], None] | None = None,
|
|
48
|
+
on_fill_failed: Callable[[FillFailedEvent], None] | None = None,
|
|
21
49
|
) -> None:
|
|
50
|
+
translation_llm = translation_llm or llm
|
|
51
|
+
fill_llm = fill_llm or llm
|
|
52
|
+
if translation_llm is None:
|
|
53
|
+
raise ValueError("Either translation_llm or llm must be provided")
|
|
54
|
+
if fill_llm is None:
|
|
55
|
+
raise ValueError("Either fill_llm or llm must be provided")
|
|
56
|
+
|
|
22
57
|
translator = XMLTranslator(
|
|
23
|
-
|
|
58
|
+
translation_llm=translation_llm,
|
|
59
|
+
fill_llm=fill_llm,
|
|
24
60
|
target_language=target_language,
|
|
25
61
|
user_prompt=user_prompt,
|
|
26
62
|
ignore_translated_error=False,
|
|
27
63
|
max_retries=max_retries,
|
|
28
64
|
max_fill_displaying_errors=10,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
max_group_tokens=max_group_tokens,
|
|
32
|
-
),
|
|
65
|
+
max_group_tokens=max_group_tokens,
|
|
66
|
+
cache_seed_content=f"{_get_version()}:{target_language}",
|
|
33
67
|
)
|
|
34
|
-
with Zip(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
total_chapters =
|
|
42
|
-
|
|
43
|
-
|
|
68
|
+
with Zip(
|
|
69
|
+
source_path=Path(source_path).resolve(),
|
|
70
|
+
target_path=Path(target_path).resolve(),
|
|
71
|
+
) as zip:
|
|
72
|
+
# mimetype should be the first file in the EPUB ZIP
|
|
73
|
+
zip.migrate(Path("mimetype"))
|
|
74
|
+
|
|
75
|
+
total_chapters = sum(1 for _, _ in search_spine_paths(zip))
|
|
76
|
+
toc_list = read_toc(zip)
|
|
77
|
+
metadata_fields = read_metadata(zip)
|
|
78
|
+
|
|
79
|
+
# Calculate weights: TOC (5%), Metadata (5%), Chapters (90%)
|
|
80
|
+
toc_has_items = len(toc_list) > 0
|
|
81
|
+
metadata_has_items = len(metadata_fields) > 0
|
|
82
|
+
total_items = (1 if toc_has_items else 0) + (1 if metadata_has_items else 0) + total_chapters
|
|
83
|
+
|
|
84
|
+
if total_items == 0:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
interrupter = XMLInterrupter()
|
|
88
|
+
toc_weight = 0.05 if toc_has_items else 0
|
|
89
|
+
metadata_weight = 0.05 if metadata_has_items else 0
|
|
90
|
+
chapters_weight = 1.0 - toc_weight - metadata_weight
|
|
91
|
+
progress_per_chapter = chapters_weight / total_chapters if total_chapters > 0 else 0
|
|
44
92
|
current_progress = 0.0
|
|
45
93
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Translate chapters
|
|
59
|
-
processed_chapters = 0
|
|
60
|
-
for element, text_segments, (chapter_path, xml, placeholder) in translator.translate_to_text_segments(
|
|
61
|
-
items=_search_chapter_items(zip),
|
|
94
|
+
for translated_elem, context in translator.translate_elements(
|
|
95
|
+
interrupt_source_text_segments=interrupter.interrupt_source_text_segments,
|
|
96
|
+
interrupt_translated_text_segments=interrupter.interrupt_translated_text_segments,
|
|
97
|
+
interrupt_block_element=interrupter.interrupt_block_element,
|
|
98
|
+
on_fill_failed=on_fill_failed,
|
|
99
|
+
tasks=_generate_tasks_from_book(
|
|
100
|
+
zip=zip,
|
|
101
|
+
toc_list=toc_list,
|
|
102
|
+
metadata_fields=metadata_fields,
|
|
103
|
+
submit=submit,
|
|
104
|
+
),
|
|
62
105
|
):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def fill_titles(items):
|
|
119
|
-
nonlocal title_index
|
|
120
|
-
for item in items:
|
|
121
|
-
item.title = translated_titles[title_index]
|
|
122
|
-
title_index += 1
|
|
123
|
-
if item.children:
|
|
124
|
-
fill_titles(item.children)
|
|
125
|
-
|
|
126
|
-
fill_titles(toc_list)
|
|
127
|
-
|
|
128
|
-
# Write back the translated TOC
|
|
129
|
-
write_toc(zip, toc_list)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _translate_metadata(translator: XMLTranslator, zip: Zip):
|
|
133
|
-
"""Translate metadata fields in OPF file."""
|
|
134
|
-
opf_path = find_opf_path(zip)
|
|
135
|
-
|
|
136
|
-
with zip.read(opf_path) as f:
|
|
137
|
-
xml = XMLLikeNode(f)
|
|
138
|
-
|
|
139
|
-
# Find metadata element
|
|
140
|
-
metadata_elem = None
|
|
141
|
-
for child in xml.element:
|
|
142
|
-
if child.tag.endswith("metadata"):
|
|
143
|
-
metadata_elem = child
|
|
144
|
-
break
|
|
145
|
-
|
|
146
|
-
if metadata_elem is None:
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
# Collect metadata fields to translate
|
|
150
|
-
# Skip fields that should not be translated
|
|
151
|
-
skip_fields = {
|
|
152
|
-
"language",
|
|
153
|
-
"identifier",
|
|
154
|
-
"date",
|
|
155
|
-
"meta",
|
|
156
|
-
"contributor", # Usually technical information
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
fields_to_translate: list[tuple[Element, str]] = []
|
|
160
|
-
|
|
161
|
-
for elem in metadata_elem:
|
|
162
|
-
# Get tag name without namespace
|
|
163
|
-
tag_name = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
|
|
164
|
-
|
|
165
|
-
# Check if element has text content and should be translated
|
|
166
|
-
if elem.text and elem.text.strip() and tag_name not in skip_fields:
|
|
167
|
-
fields_to_translate.append((elem, elem.text.strip()))
|
|
168
|
-
|
|
169
|
-
if not fields_to_translate:
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
# Create XML elements for translation
|
|
173
|
-
elements_to_translate = Element("metadata")
|
|
174
|
-
elements_to_translate.extend(_create_text_element(text) for _, text in fields_to_translate)
|
|
175
|
-
|
|
176
|
-
# Translate all metadata at once
|
|
177
|
-
translated_element = translator.translate_to_element(elements_to_translate)
|
|
178
|
-
|
|
179
|
-
# Fill back translated texts
|
|
180
|
-
from builtins import zip as builtin_zip
|
|
181
|
-
|
|
182
|
-
for (elem, _), translated_elem in builtin_zip(fields_to_translate, translated_element, strict=True):
|
|
183
|
-
if translated_elem is not None:
|
|
184
|
-
translated_text = plain_text(translated_elem)
|
|
185
|
-
if translated_text:
|
|
186
|
-
elem.text = translated_text
|
|
187
|
-
|
|
188
|
-
# Write back the modified OPF file
|
|
189
|
-
with zip.replace(opf_path) as f:
|
|
190
|
-
xml.save(f)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _count_chapters(zip: Zip) -> int:
|
|
194
|
-
"""Count total chapters without loading content (lightweight)."""
|
|
195
|
-
return sum(1 for _ in search_spine_paths(zip))
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _search_chapter_items(zip: Zip):
|
|
199
|
-
for chapter_path in search_spine_paths(zip):
|
|
106
|
+
if context.element_type == _ElementType.TOC:
|
|
107
|
+
translated_elem = unwrap_french_quotes(translated_elem)
|
|
108
|
+
decoded_toc = decode_toc_list(translated_elem)
|
|
109
|
+
write_toc(zip, decoded_toc)
|
|
110
|
+
|
|
111
|
+
current_progress += toc_weight
|
|
112
|
+
if on_progress:
|
|
113
|
+
on_progress(current_progress)
|
|
114
|
+
|
|
115
|
+
elif context.element_type == _ElementType.METADATA:
|
|
116
|
+
translated_elem = unwrap_french_quotes(translated_elem)
|
|
117
|
+
decoded_metadata = decode_metadata(translated_elem)
|
|
118
|
+
write_metadata(zip, decoded_metadata)
|
|
119
|
+
|
|
120
|
+
current_progress += metadata_weight
|
|
121
|
+
if on_progress:
|
|
122
|
+
on_progress(current_progress)
|
|
123
|
+
|
|
124
|
+
elif context.element_type == _ElementType.CHAPTER:
|
|
125
|
+
if context.chapter_data is not None:
|
|
126
|
+
chapter_path, xml = context.chapter_data
|
|
127
|
+
deduplicate_ids_in_element(xml.element)
|
|
128
|
+
with zip.replace(chapter_path) as target_file:
|
|
129
|
+
xml.save(target_file)
|
|
130
|
+
|
|
131
|
+
current_progress += progress_per_chapter
|
|
132
|
+
if on_progress:
|
|
133
|
+
on_progress(current_progress)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _generate_tasks_from_book(
|
|
137
|
+
zip: Zip,
|
|
138
|
+
toc_list: list,
|
|
139
|
+
metadata_fields: list,
|
|
140
|
+
submit: SubmitKind,
|
|
141
|
+
) -> Generator[TranslationTask[_ElementContext], None, None]:
|
|
142
|
+
head_submit = submit
|
|
143
|
+
if head_submit == SubmitKind.APPEND_BLOCK:
|
|
144
|
+
head_submit = SubmitKind.APPEND_TEXT
|
|
145
|
+
|
|
146
|
+
if toc_list:
|
|
147
|
+
yield TranslationTask(
|
|
148
|
+
element=encode_toc_list(toc_list),
|
|
149
|
+
action=head_submit,
|
|
150
|
+
payload=_ElementContext(element_type=_ElementType.TOC),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if metadata_fields:
|
|
154
|
+
yield TranslationTask(
|
|
155
|
+
element=encode_metadata(metadata_fields),
|
|
156
|
+
action=head_submit,
|
|
157
|
+
payload=_ElementContext(element_type=_ElementType.METADATA),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
for chapter_path, media_type in search_spine_paths(zip):
|
|
200
161
|
with zip.read(chapter_path) as chapter_file:
|
|
201
162
|
xml = XMLLikeNode(
|
|
202
163
|
file=chapter_file,
|
|
203
|
-
is_html_like=
|
|
164
|
+
is_html_like=(media_type == "text/html"),
|
|
204
165
|
)
|
|
205
166
|
body_element = find_first(xml.element, "body")
|
|
206
167
|
if body_element is not None:
|
|
207
|
-
|
|
208
|
-
|
|
168
|
+
yield TranslationTask(
|
|
169
|
+
element=body_element,
|
|
170
|
+
action=submit,
|
|
171
|
+
payload=_ElementContext(
|
|
172
|
+
element_type=_ElementType.CHAPTER,
|
|
173
|
+
chapter_data=(chapter_path, xml),
|
|
174
|
+
),
|
|
175
|
+
)
|
|
209
176
|
|
|
210
177
|
|
|
211
|
-
def
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
178
|
+
def _get_version() -> str:
|
|
179
|
+
try:
|
|
180
|
+
return get_package_version("epub-translator")
|
|
181
|
+
except Exception:
|
|
182
|
+
return "development"
|
epub_translator/utils.py
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
K = TypeVar("K")
|
|
6
|
+
T = TypeVar("T")
|
|
2
7
|
|
|
3
8
|
_WHITESPACE_PATTERN = re.compile(r"\s+")
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
def normalize_whitespace(text: str) -> str:
|
|
7
12
|
return _WHITESPACE_PATTERN.sub(" ", text)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_the_same(elements: Iterable[T]) -> bool:
|
|
16
|
+
iterator = iter(elements)
|
|
17
|
+
try:
|
|
18
|
+
first_element = next(iterator)
|
|
19
|
+
except StopIteration:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
for element in iterator:
|
|
23
|
+
if element != first_element:
|
|
24
|
+
return False
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def nest(items: Iterable[tuple[K, T]]) -> dict[K, list[T]]:
|
|
29
|
+
nested_dict: dict[K, list[T]] = {}
|
|
30
|
+
for key, value in items:
|
|
31
|
+
ensure_list(nested_dict, key).append(value)
|
|
32
|
+
return nested_dict
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ensure_list(target: dict[K, list[T]], key: K) -> list[T]:
|
|
36
|
+
value = target.get(key, None)
|
|
37
|
+
if value is None:
|
|
38
|
+
value = []
|
|
39
|
+
target[key] = value
|
|
40
|
+
return value
|
epub_translator/xml/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ID_KEY: str = "id"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from xml.etree.ElementTree import Element
|
|
2
2
|
|
|
3
|
+
from .const import ID_KEY
|
|
3
4
|
from .xml import iter_with_stack
|
|
4
5
|
|
|
5
|
-
_ID_KEY = "id"
|
|
6
6
|
_SUFFIX = "__translated"
|
|
7
7
|
|
|
8
8
|
|
|
@@ -11,9 +11,9 @@ def deduplicate_ids_in_element(element: Element) -> Element:
|
|
|
11
11
|
original_id_count: dict[str, int] = {}
|
|
12
12
|
|
|
13
13
|
for _, sub_element in iter_with_stack(element):
|
|
14
|
-
if
|
|
14
|
+
if ID_KEY not in sub_element.attrib:
|
|
15
15
|
continue
|
|
16
|
-
original_id = sub_element.attrib[
|
|
16
|
+
original_id = sub_element.attrib[ID_KEY]
|
|
17
17
|
|
|
18
18
|
if original_id not in seen_ids:
|
|
19
19
|
seen_ids.add(original_id)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# HTML inline-level elements
|
|
2
|
+
# Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
|
|
3
|
+
# Reference: https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content
|
|
4
|
+
_HTML_INLINE_TAGS = frozenset(
|
|
5
|
+
(
|
|
6
|
+
# Inline text semantics
|
|
7
|
+
"a",
|
|
8
|
+
"abbr",
|
|
9
|
+
"b",
|
|
10
|
+
"bdi",
|
|
11
|
+
"bdo",
|
|
12
|
+
"br",
|
|
13
|
+
"cite",
|
|
14
|
+
"code",
|
|
15
|
+
"data",
|
|
16
|
+
"dfn",
|
|
17
|
+
"em",
|
|
18
|
+
"i",
|
|
19
|
+
"kbd",
|
|
20
|
+
"mark",
|
|
21
|
+
"q",
|
|
22
|
+
"rp",
|
|
23
|
+
"rt",
|
|
24
|
+
"ruby",
|
|
25
|
+
"s",
|
|
26
|
+
"samp",
|
|
27
|
+
"small",
|
|
28
|
+
"span",
|
|
29
|
+
"strong",
|
|
30
|
+
"sub",
|
|
31
|
+
"sup",
|
|
32
|
+
"time",
|
|
33
|
+
"u",
|
|
34
|
+
"var",
|
|
35
|
+
"wbr",
|
|
36
|
+
# Image and multimedia
|
|
37
|
+
"img",
|
|
38
|
+
"svg",
|
|
39
|
+
"canvas",
|
|
40
|
+
"audio",
|
|
41
|
+
"video",
|
|
42
|
+
"map",
|
|
43
|
+
"area",
|
|
44
|
+
# Form elements
|
|
45
|
+
"input",
|
|
46
|
+
"button",
|
|
47
|
+
"select",
|
|
48
|
+
"textarea",
|
|
49
|
+
"label",
|
|
50
|
+
"output",
|
|
51
|
+
"progress",
|
|
52
|
+
"meter",
|
|
53
|
+
# Embedded content
|
|
54
|
+
"iframe",
|
|
55
|
+
"embed",
|
|
56
|
+
"object",
|
|
57
|
+
# Other inline elements
|
|
58
|
+
"script",
|
|
59
|
+
"del",
|
|
60
|
+
"ins",
|
|
61
|
+
"slot",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_inline_tag(tag: str) -> bool:
|
|
67
|
+
return tag.lower() in _HTML_INLINE_TAGS
|