justhtml 2.2.0__tar.gz → 2.3.0__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.
- {justhtml-2.2.0 → justhtml-2.3.0}/CHANGELOG.md +11 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/PKG-INFO +1 -1
- {justhtml-2.2.0 → justhtml-2.3.0}/pyproject.toml +1 -1
- {justhtml-2.2.0 → justhtml-2.3.0}/run_tests.py +13 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/parser/__init__.py +1 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/serializer/html.py +1 -1
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/treebuilder/core.py +149 -53
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/treebuilder/modes.py +23 -35
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_linkify_internals.py +6 -2
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_serialize.py +66 -7
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_treebuilder.py +138 -4
- justhtml-2.2.0/tests/test_precommit_coverage.py +0 -83
- {justhtml-2.2.0 → justhtml-2.3.0}/.github/copilot-instructions.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/.github/workflows/ci.yml +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/.github/workflows/publish.yml +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/.gitignore +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/.pre-commit-config.yaml +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/CONTRIBUTING.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/LICENSE +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/Makefile +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/README.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/SECURITY.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/benchmarks/correctness.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/benchmarks/fuzz.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/benchmarks/performance.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/benchmarks/profile.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/_config.yml +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/_layouts/default.html +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/api.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/assets/search.js +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/bleach-migration.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/building.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/cli.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/comparison.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/correctness.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/encoding.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/errors.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/fragments.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/html-cleaning.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/index.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/linkify.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/migration-examples.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/playground/app.js +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/playground/index.html +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/playground/py/render.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/quickstart.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/sanitization.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/selectors.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/streaming.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/text.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/transforms.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/unsafe-handling.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/docs/url-cleaning.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/llms.txt +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/scripts/release.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/__main__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/parser/stream.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/py.typed +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/tokenizer/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/tokenizer/html.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/tokenizer/tokens.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/transforms/runtime.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/treebuilder/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/src/justhtml/treebuilder/utils.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/test-summary.txt +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/README.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/data/wikipedia.html +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/harness/__init__.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/harness/encoding.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/harness/regressions.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/harness/reporter.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/harness/serializer.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/harness/tokenizer.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/harness/tree.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/branch_coverage.dat +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/coverage_gaps.test +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/empty_stack_edge_cases.dat +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/tokenizer_edge_cases.test +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/treebuilder_coverage.dat +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/justhtml-tests/xml_coercion_coverage.test +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/linkify-it/README.md +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_builder.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_cli.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_docs_examples.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_encoding.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_errors.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_linkify_it.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_node.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_sanitize.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_selector.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_stream.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_tokenizer.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_transforms.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_transforms_edge_cases.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-2.2.0 → justhtml-2.3.0}/tests/test_wikipedia.py +0 -0
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.0] - 2026-06-12
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Preserve pretty-printed paragraph line breaks when sanitization is disabled and comments are present in block containers.
|
|
14
|
+
|
|
15
|
+
### Performance
|
|
16
|
+
- Speed up tree construction scope checks by tracking open elements for common scoped element names.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Refactor tree-builder open-element bookkeeping and active-formatting entries without changing the public API.
|
|
20
|
+
|
|
10
21
|
## [2.2.0] - 2026-06-07
|
|
11
22
|
|
|
12
23
|
### Fixed
|
|
@@ -194,6 +194,7 @@ def _run_unit_tests(config):
|
|
|
194
194
|
self._test_counter = 0
|
|
195
195
|
self._test_to_index: dict[str, int] = {}
|
|
196
196
|
self.test_indices: list[tuple[str, int]] = []
|
|
197
|
+
self._failed_test_ids: set[str] = set()
|
|
197
198
|
|
|
198
199
|
def startTest(self, test): # noqa: N802 - unittest API
|
|
199
200
|
super().startTest(test)
|
|
@@ -211,10 +212,22 @@ def _run_unit_tests(config):
|
|
|
211
212
|
|
|
212
213
|
def addFailure(self, test, err): # noqa: N802 - unittest API
|
|
213
214
|
super().addFailure(test, err)
|
|
215
|
+
self._failed_test_ids.add(test.id())
|
|
214
216
|
self.test_indices.append(("fail", self._idx(test)))
|
|
215
217
|
|
|
216
218
|
def addError(self, test, err): # noqa: N802 - unittest API
|
|
217
219
|
super().addError(test, err)
|
|
220
|
+
self._failed_test_ids.add(test.id())
|
|
221
|
+
self.test_indices.append(("fail", self._idx(test)))
|
|
222
|
+
|
|
223
|
+
def addSubTest(self, test, subtest, err): # noqa: N802 - unittest API
|
|
224
|
+
super().addSubTest(test, subtest, err)
|
|
225
|
+
if err is None:
|
|
226
|
+
return
|
|
227
|
+
test_id = test.id()
|
|
228
|
+
if test_id in self._failed_test_ids:
|
|
229
|
+
return
|
|
230
|
+
self._failed_test_ids.add(test_id)
|
|
218
231
|
self.test_indices.append(("fail", self._idx(test)))
|
|
219
232
|
|
|
220
233
|
def addSkip(self, test, reason): # noqa: N802 - unittest API
|
|
@@ -156,6 +156,7 @@ class JustHTML:
|
|
|
156
156
|
iframe_srcdoc=iframe_srcdoc,
|
|
157
157
|
collect_errors=should_collect,
|
|
158
158
|
scripting_enabled=scripting_enabled,
|
|
159
|
+
track_node_locations=bool(track_node_locations),
|
|
159
160
|
track_tag_spans=track_tag_spans,
|
|
160
161
|
)
|
|
161
162
|
opts = _tokenizer_opts.copy() if _tokenizer_opts is not None else TokenizerOpts()
|
|
@@ -756,7 +756,7 @@ def _node_to_html(node: Any, indent: int = 0, indent_size: int = 2, *, in_pre: b
|
|
|
756
756
|
for child in children:
|
|
757
757
|
if child is None:
|
|
758
758
|
continue
|
|
759
|
-
if child.name == "#
|
|
759
|
+
if child.name == "#text" and (child.data or "").strip():
|
|
760
760
|
can_indent = False
|
|
761
761
|
break
|
|
762
762
|
|
|
@@ -54,6 +54,46 @@ class _SelectedContentWalkItem(NamedTuple):
|
|
|
54
54
|
in_datalist: bool
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
class _FormattingEntry:
|
|
58
|
+
__slots__ = ("attrs", "name", "node", "signature")
|
|
59
|
+
|
|
60
|
+
name: str
|
|
61
|
+
attrs: dict[str, str | None]
|
|
62
|
+
node: Any
|
|
63
|
+
signature: tuple[tuple[str, str], ...]
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
name: str,
|
|
68
|
+
attrs: dict[str, str | None],
|
|
69
|
+
node: Any,
|
|
70
|
+
signature: tuple[tuple[str, str], ...],
|
|
71
|
+
) -> None:
|
|
72
|
+
self.name = name
|
|
73
|
+
self.attrs = attrs
|
|
74
|
+
self.node = node
|
|
75
|
+
self.signature = signature
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_OPEN_ELEMENT_SCOPE_COUNT_NAMES = frozenset(
|
|
79
|
+
(
|
|
80
|
+
"button",
|
|
81
|
+
"caption",
|
|
82
|
+
"dd",
|
|
83
|
+
"dt",
|
|
84
|
+
"form",
|
|
85
|
+
"li",
|
|
86
|
+
"table",
|
|
87
|
+
"tbody",
|
|
88
|
+
"td",
|
|
89
|
+
"tfoot",
|
|
90
|
+
"th",
|
|
91
|
+
"thead",
|
|
92
|
+
"tr",
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
57
97
|
class TreeBuilder(TreeBuilderModesMixin):
|
|
58
98
|
__slots__ = (
|
|
59
99
|
"_body_end_handlers",
|
|
@@ -61,6 +101,8 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
61
101
|
"_body_token_handlers",
|
|
62
102
|
"_mode_handlers",
|
|
63
103
|
"_open_p_elements",
|
|
104
|
+
"_open_scope_name_counts",
|
|
105
|
+
"_open_template_elements",
|
|
64
106
|
"_pending_end_tag_end",
|
|
65
107
|
"_pending_end_tag_name",
|
|
66
108
|
"_pending_end_tag_start",
|
|
@@ -88,6 +130,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
88
130
|
"template_modes",
|
|
89
131
|
"tokenizer",
|
|
90
132
|
"tokenizer_state_override",
|
|
133
|
+
"track_node_locations",
|
|
91
134
|
"track_tag_spans",
|
|
92
135
|
)
|
|
93
136
|
|
|
@@ -98,6 +141,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
98
141
|
_pending_end_tag_name: str | None
|
|
99
142
|
_pending_end_tag_start: int | None
|
|
100
143
|
_pending_end_tag_end: int | None
|
|
144
|
+
track_node_locations: bool
|
|
101
145
|
track_tag_spans: bool
|
|
102
146
|
active_formatting: list[Any]
|
|
103
147
|
collect_errors: bool
|
|
@@ -115,6 +159,8 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
115
159
|
open_elements: list[Any]
|
|
116
160
|
original_mode: InsertionMode | None # type: ignore[assignment]
|
|
117
161
|
_open_p_elements: int
|
|
162
|
+
_open_scope_name_counts: dict[str, int]
|
|
163
|
+
_open_template_elements: int
|
|
118
164
|
_saw_select_element: bool
|
|
119
165
|
pending_table_text: list[str]
|
|
120
166
|
pending_table_text_should_error: bool
|
|
@@ -131,12 +177,14 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
131
177
|
iframe_srcdoc: bool = False,
|
|
132
178
|
collect_errors: bool = False,
|
|
133
179
|
scripting_enabled: bool = True,
|
|
180
|
+
track_node_locations: bool = False,
|
|
134
181
|
track_tag_spans: bool = False,
|
|
135
182
|
) -> None:
|
|
136
183
|
self.fragment_context = fragment_context
|
|
137
184
|
self.iframe_srcdoc = iframe_srcdoc
|
|
138
185
|
self.collect_errors = collect_errors
|
|
139
186
|
self.scripting_enabled = bool(scripting_enabled)
|
|
187
|
+
self.track_node_locations = bool(track_node_locations)
|
|
140
188
|
self.track_tag_spans = bool(track_tag_spans)
|
|
141
189
|
self.errors = []
|
|
142
190
|
self.tokenizer = None # Set by parser after tokenizer is created
|
|
@@ -150,6 +198,8 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
150
198
|
self.table_text_original_mode = None
|
|
151
199
|
self.open_elements = []
|
|
152
200
|
self._open_p_elements = 0
|
|
201
|
+
self._open_scope_name_counts = {}
|
|
202
|
+
self._open_template_elements = 0
|
|
153
203
|
self._saw_select_element = False
|
|
154
204
|
self._pending_end_tag_name = None
|
|
155
205
|
self._pending_end_tag_start = None
|
|
@@ -263,6 +313,8 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
263
313
|
) -> bool:
|
|
264
314
|
if target == "p" and self._open_p_elements == 0:
|
|
265
315
|
return False
|
|
316
|
+
if target in _OPEN_ELEMENT_SCOPE_COUNT_NAMES and self._open_scope_name_counts.get(target, 0) == 0:
|
|
317
|
+
return False
|
|
266
318
|
if terminators is None:
|
|
267
319
|
terminators = DEFAULT_SCOPE_TERMINATORS
|
|
268
320
|
open_elements = self.open_elements
|
|
@@ -294,17 +346,59 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
294
346
|
break
|
|
295
347
|
|
|
296
348
|
def _note_open_element_pushed(self, node: Any) -> None:
|
|
297
|
-
if node is
|
|
349
|
+
if node is None:
|
|
350
|
+
return
|
|
351
|
+
name = node.name
|
|
352
|
+
if name == "p":
|
|
298
353
|
self._open_p_elements += 1
|
|
354
|
+
elif name == "template":
|
|
355
|
+
self._open_template_elements += 1
|
|
356
|
+
elif name in _OPEN_ELEMENT_SCOPE_COUNT_NAMES:
|
|
357
|
+
self._open_scope_name_counts[name] = self._open_scope_name_counts.get(name, 0) + 1
|
|
299
358
|
|
|
300
359
|
def _note_open_element_removed(self, node: Any) -> None:
|
|
301
|
-
if node is
|
|
360
|
+
if node is None:
|
|
361
|
+
return
|
|
362
|
+
name = node.name
|
|
363
|
+
if name == "p":
|
|
302
364
|
self._open_p_elements -= 1
|
|
365
|
+
elif name == "template":
|
|
366
|
+
self._open_template_elements -= 1
|
|
367
|
+
elif name in _OPEN_ELEMENT_SCOPE_COUNT_NAMES:
|
|
368
|
+
count = self._open_scope_name_counts.get(name, 0)
|
|
369
|
+
if count <= 1:
|
|
370
|
+
self._open_scope_name_counts.pop(name, None)
|
|
371
|
+
else:
|
|
372
|
+
self._open_scope_name_counts[name] = count - 1
|
|
303
373
|
|
|
304
374
|
def _note_open_elements_removed(self, nodes: list[Any]) -> None:
|
|
305
375
|
for node in nodes:
|
|
306
376
|
self._note_open_element_removed(node)
|
|
307
377
|
|
|
378
|
+
def _has_template_on_stack(self) -> bool:
|
|
379
|
+
return self._open_template_elements > 0
|
|
380
|
+
|
|
381
|
+
def _delete_open_element_at(self, index: int) -> None:
|
|
382
|
+
self._note_open_element_removed(self.open_elements[index])
|
|
383
|
+
del self.open_elements[index]
|
|
384
|
+
|
|
385
|
+
def _replace_open_element_at(self, index: int, node: Any) -> None:
|
|
386
|
+
self._note_open_element_removed(self.open_elements[index])
|
|
387
|
+
self.open_elements[index] = node
|
|
388
|
+
self._note_open_element_pushed(node)
|
|
389
|
+
|
|
390
|
+
def _remove_open_element(self, node: Any) -> None:
|
|
391
|
+
self._note_open_element_removed(node)
|
|
392
|
+
self.open_elements.remove(node)
|
|
393
|
+
|
|
394
|
+
def _insert_open_element_at(self, index: int, node: Any) -> None:
|
|
395
|
+
self.open_elements.insert(index, node)
|
|
396
|
+
self._note_open_element_pushed(node)
|
|
397
|
+
|
|
398
|
+
def _truncate_open_elements_from(self, index: int) -> None:
|
|
399
|
+
self._note_open_elements_removed(self.open_elements[index:])
|
|
400
|
+
del self.open_elements[index:]
|
|
401
|
+
|
|
308
402
|
def _close_p_element(self) -> bool:
|
|
309
403
|
if self._has_element_in_button_scope("p"):
|
|
310
404
|
self._generate_implied_end_tags("p")
|
|
@@ -545,10 +639,11 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
545
639
|
|
|
546
640
|
def _append_comment_to_document(self, text: str) -> None:
|
|
547
641
|
node = Comment(data=text)
|
|
548
|
-
|
|
549
|
-
|
|
642
|
+
tokenizer = self.tokenizer
|
|
643
|
+
if self.track_node_locations and tokenizer is not None:
|
|
644
|
+
node._origin_pos = tokenizer.last_token_start_pos
|
|
550
645
|
if node._origin_pos is not None:
|
|
551
|
-
node._origin_line, node._origin_col =
|
|
646
|
+
node._origin_line, node._origin_col = tokenizer.location_at_pos(node._origin_pos)
|
|
552
647
|
self.document.append_child(node)
|
|
553
648
|
|
|
554
649
|
def _append_comment(self, text: str, parent: Any | None = None) -> None:
|
|
@@ -558,10 +653,11 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
558
653
|
if type(parent) is Template and parent.template_content:
|
|
559
654
|
parent = parent.template_content
|
|
560
655
|
node = Comment(data=text)
|
|
561
|
-
|
|
562
|
-
|
|
656
|
+
tokenizer = self.tokenizer
|
|
657
|
+
if self.track_node_locations and tokenizer is not None:
|
|
658
|
+
node._origin_pos = tokenizer.last_token_start_pos
|
|
563
659
|
if node._origin_pos is not None:
|
|
564
|
-
node._origin_line, node._origin_col =
|
|
660
|
+
node._origin_line, node._origin_col = tokenizer.location_at_pos(node._origin_pos)
|
|
565
661
|
parent.append_child(node)
|
|
566
662
|
|
|
567
663
|
def _append_text(self, text: str) -> None:
|
|
@@ -591,10 +687,11 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
591
687
|
return
|
|
592
688
|
|
|
593
689
|
node = Text(text)
|
|
594
|
-
|
|
595
|
-
|
|
690
|
+
tokenizer = self.tokenizer
|
|
691
|
+
if self.track_node_locations and tokenizer is not None:
|
|
692
|
+
node._origin_pos = tokenizer.last_token_start_pos
|
|
596
693
|
if node._origin_pos is not None:
|
|
597
|
-
node._origin_line, node._origin_col =
|
|
694
|
+
node._origin_line, node._origin_col = tokenizer.location_at_pos(node._origin_pos)
|
|
598
695
|
children.append(node)
|
|
599
696
|
node.parent = target
|
|
600
697
|
return
|
|
@@ -615,10 +712,11 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
615
712
|
return
|
|
616
713
|
|
|
617
714
|
node = Text(text)
|
|
618
|
-
|
|
619
|
-
|
|
715
|
+
tokenizer = self.tokenizer
|
|
716
|
+
if self.track_node_locations and tokenizer is not None:
|
|
717
|
+
node._origin_pos = tokenizer.last_token_start_pos
|
|
620
718
|
if node._origin_pos is not None:
|
|
621
|
-
node._origin_line, node._origin_col =
|
|
719
|
+
node._origin_line, node._origin_col = tokenizer.location_at_pos(node._origin_pos)
|
|
622
720
|
children = parent.children
|
|
623
721
|
if position < len(children):
|
|
624
722
|
children.insert(position, node)
|
|
@@ -662,10 +760,11 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
662
760
|
node._start_tag_end = tag.end_pos
|
|
663
761
|
node._self_closing = tag.self_closing
|
|
664
762
|
|
|
665
|
-
|
|
763
|
+
tokenizer = self.tokenizer
|
|
764
|
+
if self.track_node_locations and tokenizer is not None:
|
|
666
765
|
node._origin_pos = tag.start_pos
|
|
667
766
|
if node._origin_pos is not None:
|
|
668
|
-
node._origin_line, node._origin_col =
|
|
767
|
+
node._origin_line, node._origin_col = tokenizer.location_at_pos(node._origin_pos)
|
|
669
768
|
|
|
670
769
|
# Fast path for common case: not inserting from table
|
|
671
770
|
if not self.insert_from_table:
|
|
@@ -747,9 +846,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
747
846
|
node = self.open_elements[index]
|
|
748
847
|
if node is not None and node.name == name:
|
|
749
848
|
self._maybe_mark_end_tag(node)
|
|
750
|
-
|
|
751
|
-
self._note_open_elements_removed(removed)
|
|
752
|
-
del self.open_elements[index:]
|
|
849
|
+
self._truncate_open_elements_from(index)
|
|
753
850
|
return
|
|
754
851
|
index -= 1
|
|
755
852
|
|
|
@@ -772,9 +869,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
772
869
|
self._parse_error("end-tag-too-early")
|
|
773
870
|
self._maybe_mark_end_tag(node)
|
|
774
871
|
# Pop all elements from this node onwards
|
|
775
|
-
|
|
776
|
-
self._note_open_elements_removed(removed)
|
|
777
|
-
del self.open_elements[index:]
|
|
872
|
+
self._truncate_open_elements_from(index)
|
|
778
873
|
return
|
|
779
874
|
|
|
780
875
|
# If node is a special element, parse error and ignore the tag
|
|
@@ -796,8 +891,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
796
891
|
def _remove_from_open_elements(self, node: Any) -> None:
|
|
797
892
|
index = self.open_elements.index(node)
|
|
798
893
|
self._maybe_mark_end_tag(node)
|
|
799
|
-
self.
|
|
800
|
-
del self.open_elements[index]
|
|
894
|
+
self._delete_open_element_at(index)
|
|
801
895
|
|
|
802
896
|
def _is_special_element(self, node: Any) -> bool:
|
|
803
897
|
if node.namespace not in {None, "html"}:
|
|
@@ -809,14 +903,14 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
809
903
|
entry = self.active_formatting[index]
|
|
810
904
|
if entry is FORMAT_MARKER:
|
|
811
905
|
break
|
|
812
|
-
if entry
|
|
906
|
+
if entry.name == name:
|
|
813
907
|
return index
|
|
814
908
|
return None
|
|
815
909
|
|
|
816
910
|
def _find_active_formatting_index_by_node(self, node: Any) -> int | None:
|
|
817
911
|
for index in range(len(self.active_formatting) - 1, -1, -1):
|
|
818
912
|
entry = self.active_formatting[index]
|
|
819
|
-
if entry is not FORMAT_MARKER and entry
|
|
913
|
+
if entry is not FORMAT_MARKER and entry.node is node:
|
|
820
914
|
return index
|
|
821
915
|
return None
|
|
822
916
|
|
|
@@ -826,6 +920,18 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
826
920
|
def _attrs_signature(self, attrs: dict[str, str | None]) -> tuple[tuple[str, str], ...]:
|
|
827
921
|
if not attrs:
|
|
828
922
|
return ()
|
|
923
|
+
if len(attrs) == 1:
|
|
924
|
+
name, value = next(iter(attrs.items()))
|
|
925
|
+
return ((name, value or ""),)
|
|
926
|
+
if len(attrs) == 2:
|
|
927
|
+
iterator = iter(attrs.items())
|
|
928
|
+
name1, value1 = next(iterator)
|
|
929
|
+
name2, value2 = next(iterator)
|
|
930
|
+
item1 = (name1, value1 or "")
|
|
931
|
+
item2 = (name2, value2 or "")
|
|
932
|
+
if name1 <= name2:
|
|
933
|
+
return (item1, item2)
|
|
934
|
+
return (item2, item1)
|
|
829
935
|
items: list[tuple[str, str]] = []
|
|
830
936
|
for name, value in attrs.items():
|
|
831
937
|
items.append((name, value or ""))
|
|
@@ -839,8 +945,8 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
839
945
|
if entry is FORMAT_MARKER:
|
|
840
946
|
matches.clear()
|
|
841
947
|
continue
|
|
842
|
-
existing_signature = entry
|
|
843
|
-
if entry
|
|
948
|
+
existing_signature = entry.signature
|
|
949
|
+
if entry.name == name and existing_signature == signature:
|
|
844
950
|
matches.append(index)
|
|
845
951
|
if len(matches) >= 3:
|
|
846
952
|
return matches[0]
|
|
@@ -851,7 +957,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
851
957
|
entry = self.active_formatting[index]
|
|
852
958
|
if entry is FORMAT_MARKER:
|
|
853
959
|
break
|
|
854
|
-
if entry
|
|
960
|
+
if entry.name == name:
|
|
855
961
|
return True
|
|
856
962
|
return False
|
|
857
963
|
|
|
@@ -860,8 +966,8 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
860
966
|
entry = self.active_formatting[index]
|
|
861
967
|
if entry is FORMAT_MARKER:
|
|
862
968
|
break
|
|
863
|
-
if entry
|
|
864
|
-
return entry
|
|
969
|
+
if entry.name == "a":
|
|
970
|
+
return entry.node not in self.open_elements
|
|
865
971
|
return False
|
|
866
972
|
|
|
867
973
|
def _remove_last_active_formatting_by_name(self, name: str) -> None:
|
|
@@ -869,7 +975,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
869
975
|
entry = self.active_formatting[index]
|
|
870
976
|
if entry is FORMAT_MARKER:
|
|
871
977
|
break
|
|
872
|
-
if entry
|
|
978
|
+
if entry.name == name:
|
|
873
979
|
del self.active_formatting[index]
|
|
874
980
|
return
|
|
875
981
|
|
|
@@ -878,21 +984,13 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
878
984
|
if self.open_elements[index].name == name:
|
|
879
985
|
node = self.open_elements[index]
|
|
880
986
|
self._maybe_mark_end_tag(node)
|
|
881
|
-
self.
|
|
882
|
-
del self.open_elements[index]
|
|
987
|
+
self._delete_open_element_at(index)
|
|
883
988
|
return
|
|
884
989
|
|
|
885
990
|
def _append_active_formatting_entry(self, name: str, attrs: dict[str, str | None], node: Any) -> None:
|
|
886
991
|
entry_attrs = self._clone_attributes(attrs)
|
|
887
992
|
signature = self._attrs_signature(entry_attrs)
|
|
888
|
-
self.active_formatting.append(
|
|
889
|
-
{
|
|
890
|
-
"name": name,
|
|
891
|
-
"attrs": entry_attrs,
|
|
892
|
-
"node": node,
|
|
893
|
-
"signature": signature,
|
|
894
|
-
},
|
|
895
|
-
)
|
|
993
|
+
self.active_formatting.append(_FormattingEntry(name, entry_attrs, node, signature))
|
|
896
994
|
|
|
897
995
|
def _clear_active_formatting_up_to_marker(self) -> None:
|
|
898
996
|
while self.active_formatting:
|
|
@@ -911,7 +1009,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
911
1009
|
if not self.active_formatting:
|
|
912
1010
|
return
|
|
913
1011
|
last_entry = self.active_formatting[-1]
|
|
914
|
-
if last_entry is FORMAT_MARKER or last_entry
|
|
1012
|
+
if last_entry is FORMAT_MARKER or last_entry.node in self.open_elements:
|
|
915
1013
|
return
|
|
916
1014
|
|
|
917
1015
|
index = len(self.active_formatting) - 1
|
|
@@ -920,20 +1018,20 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
920
1018
|
if index < 0:
|
|
921
1019
|
break
|
|
922
1020
|
entry = self.active_formatting[index]
|
|
923
|
-
if entry is FORMAT_MARKER or entry
|
|
1021
|
+
if entry is FORMAT_MARKER or entry.node in self.open_elements:
|
|
924
1022
|
index += 1
|
|
925
1023
|
break
|
|
926
1024
|
if index < 0:
|
|
927
1025
|
index = 0
|
|
928
1026
|
while index < len(self.active_formatting):
|
|
929
1027
|
entry = self.active_formatting[index]
|
|
930
|
-
tag = Tag(Tag.START, entry
|
|
1028
|
+
tag = Tag(Tag.START, entry.name, self._clone_attributes(entry.attrs), False)
|
|
931
1029
|
new_node = self._insert_element(tag, push=True)
|
|
932
|
-
if self.
|
|
933
|
-
new_node._origin_pos = entry
|
|
934
|
-
new_node._origin_line = entry
|
|
935
|
-
new_node._origin_col = entry
|
|
936
|
-
entry
|
|
1030
|
+
if self.track_node_locations:
|
|
1031
|
+
new_node._origin_pos = entry.node.origin_offset
|
|
1032
|
+
new_node._origin_line = entry.node.origin_line
|
|
1033
|
+
new_node._origin_col = entry.node.origin_col
|
|
1034
|
+
entry.node = new_node
|
|
937
1035
|
index += 1
|
|
938
1036
|
|
|
939
1037
|
def _insert_node_at(self, parent: Any, index: int, node: Any) -> None:
|
|
@@ -1263,9 +1361,7 @@ class TreeBuilder(TreeBuilderModesMixin):
|
|
|
1263
1361
|
return ("reprocess", self.mode, token, True)
|
|
1264
1362
|
# Otherwise it's a foreign element - pop everything from this point up
|
|
1265
1363
|
self._maybe_mark_end_tag(node)
|
|
1266
|
-
|
|
1267
|
-
self._note_open_elements_removed(removed)
|
|
1268
|
-
del self.open_elements[idx:]
|
|
1364
|
+
self._truncate_open_elements_from(idx)
|
|
1269
1365
|
return None
|
|
1270
1366
|
|
|
1271
1367
|
# Per HTML5 spec: if first node doesn't match, it's a parse error
|
|
@@ -199,9 +199,7 @@ class TreeBuilderModesMixin:
|
|
|
199
199
|
self.template_modes.append(InsertionMode.IN_TEMPLATE)
|
|
200
200
|
return None
|
|
201
201
|
if token.kind == Tag.END and token.name == "template":
|
|
202
|
-
|
|
203
|
-
has_template = any(node is not None and node.name == "template" for node in self.open_elements)
|
|
204
|
-
if not has_template:
|
|
202
|
+
if not self._has_template_on_stack():
|
|
205
203
|
return None
|
|
206
204
|
self._generate_implied_end_tags()
|
|
207
205
|
self._pop_until_inclusive("template")
|
|
@@ -357,8 +355,7 @@ class TreeBuilderModesMixin:
|
|
|
357
355
|
self.mode = InsertionMode.IN_HEAD
|
|
358
356
|
return ("reprocess", InsertionMode.IN_HEAD, token)
|
|
359
357
|
if token.kind == Tag.END and token.name == "template":
|
|
360
|
-
|
|
361
|
-
if not has_template:
|
|
358
|
+
if not self._has_template_on_stack():
|
|
362
359
|
self._parse_error("unexpected-end-tag", tag_name=token.name)
|
|
363
360
|
return None
|
|
364
361
|
return self._mode_in_head(token)
|
|
@@ -617,7 +614,7 @@ class TreeBuilderModesMixin:
|
|
|
617
614
|
return
|
|
618
615
|
|
|
619
616
|
formatting_element_entry = self.active_formatting[formatting_element_index]
|
|
620
|
-
formatting_element = formatting_element_entry
|
|
617
|
+
formatting_element = formatting_element_entry.node
|
|
621
618
|
|
|
622
619
|
# 4. If formatting element is not in open elements
|
|
623
620
|
if formatting_element not in self.open_elements:
|
|
@@ -686,21 +683,18 @@ class TreeBuilderModesMixin:
|
|
|
686
683
|
node_formatting_index = None
|
|
687
684
|
|
|
688
685
|
if node_formatting_index is None:
|
|
689
|
-
self.
|
|
690
|
-
del self.open_elements[node_index]
|
|
686
|
+
self._delete_open_element_at(node_index)
|
|
691
687
|
continue
|
|
692
688
|
|
|
693
689
|
# 10.4 Replace entry with new element
|
|
694
690
|
entry = self.active_formatting[node_formatting_index]
|
|
695
|
-
new_element = self._create_element(entry
|
|
696
|
-
if self.
|
|
697
|
-
new_element._origin_pos = entry
|
|
698
|
-
new_element._origin_line = entry
|
|
699
|
-
new_element._origin_col = entry
|
|
700
|
-
entry
|
|
701
|
-
self.
|
|
702
|
-
self.open_elements[node_index] = new_element
|
|
703
|
-
self._note_open_element_pushed(new_element)
|
|
691
|
+
new_element = self._create_element(entry.name, entry.node.namespace, entry.attrs)
|
|
692
|
+
if self.track_node_locations:
|
|
693
|
+
new_element._origin_pos = entry.node.origin_offset
|
|
694
|
+
new_element._origin_line = entry.node.origin_line
|
|
695
|
+
new_element._origin_col = entry.node.origin_col
|
|
696
|
+
entry.node = new_element
|
|
697
|
+
self._replace_open_element_at(node_index, new_element)
|
|
704
698
|
node = new_element
|
|
705
699
|
|
|
706
700
|
# 10.5 If last node is furthest block, update bookmark
|
|
@@ -731,12 +725,12 @@ class TreeBuilderModesMixin:
|
|
|
731
725
|
|
|
732
726
|
# 12. Create new formatting element
|
|
733
727
|
entry = self.active_formatting[formatting_element_index]
|
|
734
|
-
new_formatting_element = self._create_element(entry
|
|
735
|
-
if self.
|
|
736
|
-
new_formatting_element._origin_pos = entry
|
|
737
|
-
new_formatting_element._origin_line = entry
|
|
738
|
-
new_formatting_element._origin_col = entry
|
|
739
|
-
entry
|
|
728
|
+
new_formatting_element = self._create_element(entry.name, entry.node.namespace, entry.attrs)
|
|
729
|
+
if self.track_node_locations:
|
|
730
|
+
new_formatting_element._origin_pos = entry.node.origin_offset
|
|
731
|
+
new_formatting_element._origin_line = entry.node.origin_line
|
|
732
|
+
new_formatting_element._origin_col = entry.node.origin_col
|
|
733
|
+
entry.node = new_formatting_element
|
|
740
734
|
|
|
741
735
|
# 13. Move children of furthest block
|
|
742
736
|
moved_children = furthest_block.children
|
|
@@ -754,11 +748,9 @@ class TreeBuilderModesMixin:
|
|
|
754
748
|
self.active_formatting.insert(bookmark, entry)
|
|
755
749
|
|
|
756
750
|
# 15. Remove formatting element from open elements and insert new one
|
|
757
|
-
self.
|
|
758
|
-
self.open_elements.remove(formatting_element)
|
|
751
|
+
self._remove_open_element(formatting_element)
|
|
759
752
|
furthest_block_index = self.open_elements.index(furthest_block)
|
|
760
|
-
self.
|
|
761
|
-
self._note_open_element_pushed(new_formatting_element)
|
|
753
|
+
self._insert_open_element_at(furthest_block_index + 1, new_formatting_element)
|
|
762
754
|
|
|
763
755
|
def _handle_body_start_a(self, token: Tag) -> None:
|
|
764
756
|
if self._has_active_formatting_entry("a"):
|
|
@@ -815,8 +807,7 @@ class TreeBuilderModesMixin:
|
|
|
815
807
|
return
|
|
816
808
|
body_elem = self.open_elements[body_index]
|
|
817
809
|
body_elem.parent.remove_child(body_elem)
|
|
818
|
-
self.
|
|
819
|
-
self.open_elements = self.open_elements[:body_index]
|
|
810
|
+
self._truncate_open_elements_from(body_index)
|
|
820
811
|
self._insert_element(token, push=True)
|
|
821
812
|
self.mode = InsertionMode.IN_FRAMESET
|
|
822
813
|
return
|
|
@@ -922,8 +913,7 @@ class TreeBuilderModesMixin:
|
|
|
922
913
|
return
|
|
923
914
|
|
|
924
915
|
def _handle_body_end_template(self, token: Tag) -> None:
|
|
925
|
-
|
|
926
|
-
if not has_template:
|
|
916
|
+
if not self._has_template_on_stack():
|
|
927
917
|
self._parse_error("unexpected-end-tag", tag_name=token.name)
|
|
928
918
|
return
|
|
929
919
|
self._generate_implied_end_tags()
|
|
@@ -1716,7 +1706,7 @@ class TreeBuilderModesMixin:
|
|
|
1716
1706
|
if fmt_index is None:
|
|
1717
1707
|
self._parse_error("unexpected-end-tag-in-select", tag_name=name)
|
|
1718
1708
|
return None
|
|
1719
|
-
target = self.active_formatting[fmt_index]
|
|
1709
|
+
target = self.active_formatting[fmt_index].node
|
|
1720
1710
|
if target in self.open_elements: # pragma: no branch
|
|
1721
1711
|
select_index = self.open_elements.index(select_node)
|
|
1722
1712
|
target_index = self.open_elements.index(target)
|
|
@@ -1816,9 +1806,7 @@ class TreeBuilderModesMixin:
|
|
|
1816
1806
|
}:
|
|
1817
1807
|
return self._mode_in_head(token)
|
|
1818
1808
|
if isinstance(token, EOFToken):
|
|
1819
|
-
|
|
1820
|
-
has_template = any(node is not None and node.name == "template" for node in self.open_elements)
|
|
1821
|
-
if not has_template:
|
|
1809
|
+
if not self._has_template_on_stack():
|
|
1822
1810
|
return None
|
|
1823
1811
|
# Parse error for EOF in template
|
|
1824
1812
|
self._parse_error("expected-closing-tag-but-got-eof", tag_name="template")
|
|
@@ -69,15 +69,19 @@ class TestLinkifyInternals(unittest.TestCase):
|
|
|
69
69
|
def test_punctuation_runs_do_not_trigger_quadratic_email_scan(self) -> None:
|
|
70
70
|
for ch in ('"', "-", ".", "!"):
|
|
71
71
|
with self.subTest(ch=ch):
|
|
72
|
+
text = ch * 20_000
|
|
73
|
+
find_links_with_config(text, LinkifyConfig())
|
|
72
74
|
start = perf_counter()
|
|
73
|
-
assert find_links_with_config(
|
|
75
|
+
assert find_links_with_config(text, LinkifyConfig()) == []
|
|
74
76
|
assert perf_counter() - start < 0.25
|
|
75
77
|
|
|
76
78
|
def test_unmatched_closing_brackets_do_not_trigger_quadratic_trimming(self) -> None:
|
|
77
79
|
for ch in (")", "]", "}", ">"):
|
|
78
80
|
with self.subTest(ch=ch):
|
|
81
|
+
text = "http://example.com/" + ch * 20_000
|
|
82
|
+
find_links_with_config(text, LinkifyConfig())
|
|
79
83
|
start = perf_counter()
|
|
80
|
-
out = find_links_with_config(
|
|
84
|
+
out = find_links_with_config(text, LinkifyConfig())
|
|
81
85
|
assert len(out) == 1
|
|
82
86
|
assert out[0].text == "http://example.com/"
|
|
83
87
|
assert perf_counter() - start < 0.25
|