justhtml 3.5.0__tar.gz → 3.6.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-3.5.0 → justhtml-3.6.0}/CHANGELOG.md +30 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/PKG-INFO +1 -1
- justhtml-3.6.0/project_ideas.md +47 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/pyproject.toml +1 -1
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/parser/engine.py +180 -56
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/parser/scanner.py +21 -12
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/document_shell_recovery.dat +13 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/foreign_content.dat +141 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/head_noscript_recovery.dat +12 -0
- justhtml-3.6.0/tests/justhtml-tests/in_body_recovery.dat +66 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/select_recovery.dat +43 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/template_recovery.dat +98 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_parser_engine.py +18 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_stream.py +26 -1
- justhtml-3.5.0/tests/justhtml-tests/in_body_recovery.dat +0 -12
- {justhtml-3.5.0 → justhtml-3.6.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/.github/workflows/ci.yml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/.github/workflows/publish.yml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/.gitignore +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/.pre-commit-config.yaml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/AGENTS.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/CONTRIBUTING.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/LICENSE +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/Makefile +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/PERFORMANCE.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/README.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/SECURITY.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/benchmarks/correctness.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/benchmarks/performance.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/benchmarks/profile.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/_config.yml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/api.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/assets/search.js +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/bleach-migration.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/building.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/cli.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/comparison.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/correctness.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/encoding.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/errors.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/fragments.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/html-cleaning.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/index.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/linkify.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/migration-examples.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/playground/app.js +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/playground/index.html +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/quickstart.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/sanitization.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/selectors.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/streaming.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/text.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/transforms.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/unsafe-handling.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/docs/url-cleaning.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/llms.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/run_tests.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/scripts/release.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/text.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/parser/stream.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/serializer/html.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/transforms/runtime.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/test-summary.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/README.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/harness/tree.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/character_preprocessing.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/frameset_recovery.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/selectedcontent.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/table_recovery.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/tokenizer_states.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_builder.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_cli.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_docs_examples.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_encoding.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_entities.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_errors.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_node.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_parser_regressions.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_parser_tree.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_sanitize.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_selector.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_serialize.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_transform_runtime.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_transforms.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.0}/tests/test_wikipedia.py +0 -0
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.6.0] - 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
Note: The below are all minor parsing differences found when diffing against Chromium output, that is not covered in WPT's html5 parsing tests.
|
|
14
|
+
|
|
15
|
+
- Add a formatting element that breaks out of foreign content (such as `<i>` inside `<svg>`) to the active formatting list, so it is reconstructed later. Previously, the breakout element was inserted but not tracked, so `<b><svg><i></b>z` dropped the reconstructed `<i>` around the trailing text and `<svg><code><h2></code>` lost the reconstructed `<code>`.
|
|
16
|
+
- Start the body for a `<noscript>` start tag in the "after head" insertion mode instead of re-entering the head. The after-head metadata list does not include noscript, so markup such as `</head><noscript>` now places the element in the body.
|
|
17
|
+
- Hand an end tag to the HTML rules when the foreign-content end-tag walk reaches a matching HTML element, rather than popping the foreign elements above it. Previously, `<form><math></form>` popped the `<math>` along with the form instead of splicing only the form out of the stack.
|
|
18
|
+
- Clear back to the template contents before opening table structure in a template with no table element, so an open caption is closed. Previously, `<template><caption><tr>` nested the row inside the caption instead of making it a sibling.
|
|
19
|
+
- Switch the "in template" mode into the body for a non-head raw-text or RCDATA start tag (`iframe`, `noembed`, `xmp`, `textarea`), which are dispatched before the template-mode handler. Previously, a following table-structure tag such as the `<td>` in `<template><xmp></xmp><td>` created a spurious cell instead of being dropped.
|
|
20
|
+
- Ignore a `</td>` or `</th>` end tag in a template cell when the matching cell type is not in table scope. Previously, `<template><td></th>x` closed the `<td>` on the mismatched `</th>`, so the following text became a sibling instead of staying in the cell.
|
|
21
|
+
- Insert a non-breakout HTML-named start tag inside foreign content as a foreign element without running HTML in-body recovery. Foreign elements sharing a name with an HTML scope boundary or table foster target (an SVG or MathML `td`, `tr`, `caption`, ...) no longer stop scope walks or foster their children out. Previously, markup such as `<p><svg><article>`, `<p><math><td><listing>`, and `<table><math><tr><d>` broke out of or escaped the foreign subtree.
|
|
22
|
+
- Ignore `</body>` and `</html>` whenever a select is open, not only when the select holds just options. Previously, an element retained inside the select (as in `<select><o></body><!--x-->`) let the end tag switch to the after-body mode, moving following content out of the select.
|
|
23
|
+
- Close a raw-text or RCDATA element when its end tag name is terminated by `/` or a space even without a closing `>`. Previously, `<style></style/x` kept the unterminated end tag as text and left the element open.
|
|
24
|
+
- Do not reconstruct the active formatting elements for `rb`, `rp`, `rt`, or `rtc` start tags. Previously, `<div><tt></div><rt>` wrapped the `<rt>` in a stale `<tt>` instead of inserting it directly.
|
|
25
|
+
- Ignore `</form>` when the form element is not in scope, such as behind an open `<table>`. Previously, `<form><table></form><table>` removed the form from the stack, so the following table became a sibling of the form instead of nesting inside it.
|
|
26
|
+
- Respect special-element blocking for generic end tags inside a template. Previously, `<template><dialog><object></dialog>e` closed the dialog through the open `<object>` instead of ignoring the end tag, so following text escaped the object.
|
|
27
|
+
- Place a comment after an explicit `</head>` between the head and body even when the head contains a template. Previously, `<head><template></template></head><!--c-->` kept the comment inside the head.
|
|
28
|
+
- Process in-body `base`, `link`, and `meta` start tags with the in-head rules without reconstructing the active formatting elements. Previously, markup such as `<p><u><section><meta>` could wrongly wrap the metadata element in a stale formatting element instead of inserting it directly.
|
|
29
|
+
- Check the script-data-double-escape-start terminator against the real following character. Previously, a `<script` immediately followed by `</script>` inside a script (for example `<script><!--<script</script>`) entered the double-escaped state and swallowed the closing tag, leaving the script element unclosed.
|
|
30
|
+
- Pop foreign-content ancestors before applying the heading start-tag rule so a heading that breaks out of MathML or SVG closes an open heading. Previously, markup such as `<h1><math><h3>` nested the second heading inside the first instead of making it a sibling.
|
|
31
|
+
- Ignore `</html>` and `</body>` end tags in the "in select" insertion mode instead of switching to the after-body/after-html modes. Previously, markup such as `<select></html><!--c-->` moved trailing comments out of the still-open select to the document level.
|
|
32
|
+
- Restrict character foster parenting to HTML table elements. Previously, a foreign SVG or MathML element that merely shared a name with a table foster target (such as an SVG `<tr>`) had following text foster-parented out of the foreign subtree instead of kept as its child, as in `<table><svg><tr>n`.
|
|
33
|
+
- Force quirks mode when a doctype has trailing junk after its name without a PUBLIC or SYSTEM keyword. Previously, `<!DOCTYPE html foo>` was treated as no-quirks, so a following `<table>` incorrectly closed an open `<p>`.
|
|
34
|
+
- Ignore end tags other than `</template>` in the "in template" insertion mode. Previously, the `</br>` special case still ran, so `<template></br>` created a stray `<br>` inside the template contents.
|
|
35
|
+
- Let table-scoped end tags follow the foreign-content end-tag algorithm when the current node is a MathML or SVG element. Previously, an end tag such as `</th>` inside foreign content was routed to HTML table recovery, so `<svg><th><o></th>0` left the foreign cell open and misplaced the following text.
|
|
36
|
+
- Ignore `</body>` and `</html>` when no body element is in scope. Previously, an open scope marker such as `<marquee>`, `<object>`, or `<applet>` did not prevent the switch to the after-body mode, so following content such as a comment in `<marquee></body><!--x-->` was placed outside the still-open element.
|
|
37
|
+
- Close an escaped script comment on `<!-->`, whose `<!--` dashes already count toward the `-->`. Previously, `<script><!--><script></script>` treated the following `<script>` as a double-escape start and swallowed the closing tag, leaving the script element unclosed.
|
|
38
|
+
- Push active-formatting markers only for HTML scope-marker elements. A foreign element sharing a name such as an SVG or MathML `<td>` no longer pushes a marker that would linger after the element is popped and block later formatting reconstruction, as in `<p><a><math><td></td><listing>U`.
|
|
39
|
+
|
|
10
40
|
## [3.5.0] - 2026-07-13
|
|
11
41
|
|
|
12
42
|
### Changed
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Project Ideas
|
|
2
|
+
|
|
3
|
+
## Treebuilder Refactors
|
|
4
|
+
|
|
5
|
+
### Open element stack helper
|
|
6
|
+
|
|
7
|
+
Replace direct `open_elements` list mutation with an `OpenElementStack` helper. The helper would own push, pop, remove, replace, and truncate operations, and it would keep cached counters for common stack queries such as open `<p>` elements, template presence, and scope-relevant names.
|
|
8
|
+
|
|
9
|
+
Expected value: high maintainability, safer future performance caches, and fewer hidden invariants around `_note_open_element_pushed()` and `_note_open_element_removed()`.
|
|
10
|
+
|
|
11
|
+
Performance requirement: no meaningful regression on the general `web100k` benchmark.
|
|
12
|
+
|
|
13
|
+
### Active formatting list helper
|
|
14
|
+
|
|
15
|
+
Move active formatting marker and entry operations into an `ActiveFormattingList` helper. It would own marker handling, duplicate detection, lookup by name, lookup by node, removal, and clear-to-marker behavior.
|
|
16
|
+
|
|
17
|
+
Expected value: lower adoption-agency complexity and safer future optimizations around active formatting reconstruction.
|
|
18
|
+
|
|
19
|
+
### Declarative insertion-mode dispatch
|
|
20
|
+
|
|
21
|
+
Generate or centralize start/end tag dispatch tables for major insertion modes, especially `IN_BODY`, table modes, and select mode. Keep fallback behavior explicit so tag priority remains easy to audit.
|
|
22
|
+
|
|
23
|
+
Expected value: possible hot-path speedup and clearer mode behavior. Needs before/after benchmark validation because dispatch overhead may vary.
|
|
24
|
+
|
|
25
|
+
### Split insertion modes into smaller modules
|
|
26
|
+
|
|
27
|
+
Break `modes.py` into mode-group modules such as head, body, table, select, template, and foreign-content handling while keeping the public `TreeBuilder` API unchanged.
|
|
28
|
+
|
|
29
|
+
Expected value: large maintainability improvement and easier auditing. Performance should be neutral if done mechanically.
|
|
30
|
+
|
|
31
|
+
### Stack index helpers
|
|
32
|
+
|
|
33
|
+
Centralize repeated reverse stack scans into helpers such as `_find_open_element_index()`, `_find_last_open_element_index()`, and `_truncate_open_elements_from()`. Add caching only where benchmarks prove it helps.
|
|
34
|
+
|
|
35
|
+
Expected value: clearer code first, optional targeted performance improvements later.
|
|
36
|
+
|
|
37
|
+
### Separate parsing decisions from DOM mutation
|
|
38
|
+
|
|
39
|
+
Create a tighter mutation API for element insertion, text insertion, foster parenting, child movement, and stack popping. Keep mode handlers focused on parser decisions.
|
|
40
|
+
|
|
41
|
+
Expected value: maintainability and auditability. Runtime effect must be measured because extra calls could slow hot paths.
|
|
42
|
+
|
|
43
|
+
### Narrow internal node types
|
|
44
|
+
|
|
45
|
+
Replace some `Any` usage around stack nodes and active formatting entries with protocols or narrower internal types.
|
|
46
|
+
|
|
47
|
+
Expected value: safer refactors and better mypy coverage. Runtime effect should be neutral.
|
|
@@ -138,6 +138,15 @@ _P_CLOSING_START_TAGS = {
|
|
|
138
138
|
_HEAD_NOSCRIPT_ALLOWED_START_TAGS = {"basefont", "bgsound", "link", "meta", "noframes", "style"}
|
|
139
139
|
_HEAD_NOSCRIPT_VOID_START_TAGS = {"basefont", "bgsound"}
|
|
140
140
|
_HEAD_ONLY_VOID_START_TAGS = {"basefont", "bgsound"}
|
|
141
|
+
# In "in body", base/basefont/bgsound/link/meta are processed with the "in head"
|
|
142
|
+
# rules (§13.2.6.4.7), which insert-and-pop without reconstructing the active
|
|
143
|
+
# formatting elements. Reconstructing here would wrongly wrap them (and their
|
|
144
|
+
# following siblings) in a stale formatting element.
|
|
145
|
+
_INLINE_HEAD_VOID_START_TAGS = {"base", "basefont", "bgsound", "link", "meta"}
|
|
146
|
+
# The in-body rb/rp/rt/rtc rules (§13.2.6.4.7) only generate implied end tags
|
|
147
|
+
# and insert; unlike "any other start tag" they do not reconstruct the active
|
|
148
|
+
# formatting elements.
|
|
149
|
+
_RUBY_START_TAGS = {"rb", "rp", "rt", "rtc"}
|
|
141
150
|
_HTML_VOID_COMPAT_TAGS = {"basefont", "bgsound", "frame", "keygen"}
|
|
142
151
|
_DEFINITION_SCOPE_BOUNDARIES = frozenset(DEFINITION_SCOPE_TERMINATORS)
|
|
143
152
|
_LIST_ITEM_SCOPE_BOUNDARIES = frozenset(LIST_ITEM_SCOPE_TERMINATORS)
|
|
@@ -160,6 +169,20 @@ _TEMPLATE_MODE_COLGROUP = "colgroup"
|
|
|
160
169
|
_TEMPLATE_TABLE_CONTEXT_START_TAGS = {"caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"}
|
|
161
170
|
_TEMPLATE_TABLE_BODY_IGNORED_START_TAGS = {"caption", "col", "colgroup", "table"}
|
|
162
171
|
_TEMPLATE_ROW_STRUCTURE_START_TAGS = {"caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr", "table"}
|
|
172
|
+
# Start tags that move the "in template" mode into the body without going through
|
|
173
|
+
# _handle_template_mode_start: void head elements handled inline plus the raw
|
|
174
|
+
# text / RCDATA elements that are not head content (iframe, noembed, xmp,
|
|
175
|
+
# textarea), which are dispatched before the template-mode handler runs.
|
|
176
|
+
_TEMPLATE_INITIAL_BODY_SWITCH_TAGS = {
|
|
177
|
+
"base",
|
|
178
|
+
"basefont",
|
|
179
|
+
"bgsound",
|
|
180
|
+
"noframes",
|
|
181
|
+
"iframe",
|
|
182
|
+
"noembed",
|
|
183
|
+
"xmp",
|
|
184
|
+
"textarea",
|
|
185
|
+
}
|
|
163
186
|
_FRAMESET_BODY_OK_TAGS = {"div", "figure", "p", "param", "source", "track"} | FORMATTING_ELEMENTS
|
|
164
187
|
_FRAMESET_BLOCKING_START_TAGS = {
|
|
165
188
|
"applet",
|
|
@@ -931,8 +954,6 @@ class ParseEngine:
|
|
|
931
954
|
return False
|
|
932
955
|
if name in {"br", "p"}:
|
|
933
956
|
return False
|
|
934
|
-
if name in _TABLE_SCOPED_END_TAGS:
|
|
935
|
-
return False
|
|
936
957
|
|
|
937
958
|
crossed_integration_point = False
|
|
938
959
|
for idx in range(len(stack) - 1, 0, -1):
|
|
@@ -942,6 +963,12 @@ class ParseEngine:
|
|
|
942
963
|
return True
|
|
943
964
|
if self._fragment_context_node is not None and node is self._fragment_context_node:
|
|
944
965
|
return True
|
|
966
|
+
if node.namespace in {None, "html", _PARSER_ONLY_NAMESPACE}:
|
|
967
|
+
# The foreign-content walk reached a matching HTML element
|
|
968
|
+
# (§13.2.6.5 steps 6-7): hand the token to the HTML end-tag
|
|
969
|
+
# rules, which may splice a form or run adoption, rather than
|
|
970
|
+
# popping the foreign elements sitting above it.
|
|
971
|
+
return False
|
|
945
972
|
if self._track_tag_spans:
|
|
946
973
|
self._set_end_span(node, name, tag_start, tag_end)
|
|
947
974
|
del stack[idx:]
|
|
@@ -1279,6 +1306,7 @@ class ParseEngine:
|
|
|
1279
1306
|
parent: Node
|
|
1280
1307
|
if self._current_parent() is self._head or (
|
|
1281
1308
|
not self._fragment
|
|
1309
|
+
and not self._after_head
|
|
1282
1310
|
and self._head is not None
|
|
1283
1311
|
and not self._body_mode_seen
|
|
1284
1312
|
and not self._body_has_content()
|
|
@@ -1931,7 +1959,14 @@ class ParseEngine:
|
|
|
1931
1959
|
self._insert_at(parent, position, node)
|
|
1932
1960
|
return
|
|
1933
1961
|
foster = None
|
|
1934
|
-
|
|
1962
|
+
# Foster parenting only applies in HTML table insertion modes. A
|
|
1963
|
+
# foreign element that merely shares a name with a table foster
|
|
1964
|
+
# target (e.g. an SVG or MathML "tr") keeps character data as its
|
|
1965
|
+
# own child instead.
|
|
1966
|
+
parent_is_table_foster = parent.name in self._table_foster_targets and getattr(
|
|
1967
|
+
parent, "namespace", None
|
|
1968
|
+
) in {None, "html", _PARSER_ONLY_NAMESPACE}
|
|
1969
|
+
if parent_is_table_foster:
|
|
1935
1970
|
is_table_space = text_is_space if text_is_space is not None else text.strip(_SPACE) == ""
|
|
1936
1971
|
if not is_table_space:
|
|
1937
1972
|
if pending_table_whitespace:
|
|
@@ -1961,7 +1996,7 @@ class ParseEngine:
|
|
|
1961
1996
|
node = self._new_text(text, source_pos) if self._track_node_locations else Text(text)
|
|
1962
1997
|
children.append(node)
|
|
1963
1998
|
node.parent = parent
|
|
1964
|
-
if
|
|
1999
|
+
if parent_is_table_foster and is_table_space:
|
|
1965
2000
|
self._foster_next_table_whitespace = pending_table_whitespace + len(text)
|
|
1966
2001
|
else:
|
|
1967
2002
|
foster_parent, position = foster
|
|
@@ -2002,9 +2037,15 @@ class ParseEngine:
|
|
|
2002
2037
|
else:
|
|
2003
2038
|
public_id = None
|
|
2004
2039
|
system_id = None
|
|
2005
|
-
|
|
2040
|
+
# Junk after the name with no PUBLIC/SYSTEM keyword lands in the
|
|
2041
|
+
# "after DOCTYPE name" state, which sets the force-quirks flag
|
|
2042
|
+
# (§13.2.5.55). The regex tolerates that trailing text, so detect
|
|
2043
|
+
# it here to reproduce the quirks-mode transition.
|
|
2044
|
+
trailing = raw[match.end() :].strip("\t\n\f\r ")
|
|
2045
|
+
bogus = kind is None and bool(trailing)
|
|
2046
|
+
doctype = Doctype(name, public_id, system_id, force_quirks=name is None or bogus)
|
|
2006
2047
|
doctype_error, self._quirks_mode = doctype_error_and_quirks(doctype, self._iframe_srcdoc)
|
|
2007
|
-
if doctype_error and self._collect_errors:
|
|
2048
|
+
if (doctype_error or bogus) and self._collect_errors:
|
|
2008
2049
|
self._emit_error(
|
|
2009
2050
|
"unknown-doctype",
|
|
2010
2051
|
doctype_end if gt != -1 else max(0, end - 1),
|
|
@@ -2297,8 +2338,21 @@ class ParseEngine:
|
|
|
2297
2338
|
self._body_mode_seen = True
|
|
2298
2339
|
|
|
2299
2340
|
if not self._fragment and name in {"html", "body"}:
|
|
2300
|
-
|
|
2301
|
-
|
|
2341
|
+
if self._find_open_html_index("select") is not None:
|
|
2342
|
+
# In the "in select" insertion mode, </html> and </body> are
|
|
2343
|
+
# unhandled end tags and must be ignored rather than switching to
|
|
2344
|
+
# the after-body/after-html modes. Any elements kept inside the
|
|
2345
|
+
# open select remain part of that insertion mode.
|
|
2346
|
+
return pos
|
|
2347
|
+
if (
|
|
2348
|
+
self._find_open_html_index("body") is not None
|
|
2349
|
+
and self._find_open_index_before_boundary("body", _DEFAULT_SCOPE_BOUNDARIES) is None
|
|
2350
|
+
):
|
|
2351
|
+
# With body on the open stack, </body> and </html> are ignored
|
|
2352
|
+
# unless body is in scope (§13.2.6.4.7); an open scope marker such
|
|
2353
|
+
# as marquee, object, or applet keeps it out of scope. When body
|
|
2354
|
+
# is not on the stack yet (in head/after head), fall through so
|
|
2355
|
+
# the head-to-body transition still runs.
|
|
2302
2356
|
return pos
|
|
2303
2357
|
self._in_colgroup = False
|
|
2304
2358
|
if self._head is not None and self._stack[-1] is self._head:
|
|
@@ -2369,11 +2423,16 @@ class ParseEngine:
|
|
|
2369
2423
|
self._form_element = None
|
|
2370
2424
|
if form_node is None:
|
|
2371
2425
|
return pos
|
|
2426
|
+
# §13.2.6.4.7: ignore </form> when the form element is not in scope,
|
|
2427
|
+
# e.g. behind an open <table>. Without this the form is removed from
|
|
2428
|
+
# the stack and following content escapes it.
|
|
2429
|
+
form_idx = self._find_open_index_before_boundary("form", _DEFAULT_SCOPE_BOUNDARIES)
|
|
2430
|
+
if form_idx is None or self._stack[form_idx] is not form_node:
|
|
2431
|
+
return pos
|
|
2432
|
+
# form_node is confirmed in scope on the stack and implied end tags
|
|
2433
|
+
# never pop a form, so the removal below always succeeds.
|
|
2372
2434
|
self._generate_implied_end_tags()
|
|
2373
|
-
|
|
2374
|
-
self._stack.remove(form_node)
|
|
2375
|
-
except ValueError:
|
|
2376
|
-
pass
|
|
2435
|
+
self._stack.remove(form_node)
|
|
2377
2436
|
return pos
|
|
2378
2437
|
select_idx = self._find_open_html_index("select")
|
|
2379
2438
|
if select_idx is not None and name not in {"optgroup", "option", "select", "selectedcontent", "template"}:
|
|
@@ -2494,7 +2553,7 @@ class ParseEngine:
|
|
|
2494
2553
|
elif name == "summary":
|
|
2495
2554
|
idx = self._find_open_index_before_boundary(name, _DEFAULT_SCOPE_BOUNDARIES)
|
|
2496
2555
|
elif self._template_modes:
|
|
2497
|
-
idx = self.
|
|
2556
|
+
idx = self._find_open_index_before_boundary(name, self._template_end_tag_boundaries(name, action))
|
|
2498
2557
|
elif name not in self._special_elements and (action is None or not action.p_closing):
|
|
2499
2558
|
idx = self._find_open_index_before_boundary(name, _GENERAL_END_TAG_BOUNDARIES)
|
|
2500
2559
|
else:
|
|
@@ -2671,12 +2730,9 @@ class ParseEngine:
|
|
|
2671
2730
|
self._close_until("select")
|
|
2672
2731
|
return pos
|
|
2673
2732
|
|
|
2674
|
-
if
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
"bgsound",
|
|
2678
|
-
"noframes",
|
|
2679
|
-
}: # pragma: no cover - compiled sanitizer drops parser-only template contents
|
|
2733
|
+
if (
|
|
2734
|
+
self._current_template_mode() == _TEMPLATE_MODE_INITIAL and name in _TEMPLATE_INITIAL_BODY_SWITCH_TAGS
|
|
2735
|
+
): # pragma: no cover - compiled sanitizer drops parser-only template contents
|
|
2680
2736
|
self._set_current_template_mode(_TEMPLATE_MODE_BODY) # pragma: no cover
|
|
2681
2737
|
|
|
2682
2738
|
if in_parser_only_template and self._current_template_mode() == _TEMPLATE_MODE_COLGROUP:
|
|
@@ -2981,6 +3037,9 @@ class ParseEngine:
|
|
|
2981
3037
|
action is not None
|
|
2982
3038
|
and allowed
|
|
2983
3039
|
and action.head_content
|
|
3040
|
+
# "after head" re-enters the head only for the metadata start
|
|
3041
|
+
# tags it lists; noscript is not among them and starts the body.
|
|
3042
|
+
and name != "noscript"
|
|
2984
3043
|
and self._head is not None
|
|
2985
3044
|
and not self._in_head_noscript
|
|
2986
3045
|
and not self._body_mode_seen
|
|
@@ -3088,12 +3147,7 @@ class ParseEngine:
|
|
|
3088
3147
|
):
|
|
3089
3148
|
self._close_html_until("select")
|
|
3090
3149
|
|
|
3091
|
-
if self._current_template_mode() == _TEMPLATE_MODE_INITIAL and name in
|
|
3092
|
-
"base",
|
|
3093
|
-
"basefont",
|
|
3094
|
-
"bgsound",
|
|
3095
|
-
"noframes",
|
|
3096
|
-
}:
|
|
3150
|
+
if self._current_template_mode() == _TEMPLATE_MODE_INITIAL and name in _TEMPLATE_INITIAL_BODY_SWITCH_TAGS:
|
|
3097
3151
|
self._set_current_template_mode(_TEMPLATE_MODE_BODY)
|
|
3098
3152
|
|
|
3099
3153
|
if html_text_parsing and self._current_template_mode() == _TEMPLATE_MODE_COLGROUP:
|
|
@@ -3202,7 +3256,14 @@ class ParseEngine:
|
|
|
3202
3256
|
)
|
|
3203
3257
|
return pos
|
|
3204
3258
|
|
|
3205
|
-
if
|
|
3259
|
+
if (
|
|
3260
|
+
action is not None
|
|
3261
|
+
and action.active_formatting
|
|
3262
|
+
and (html_text_parsing or self._is_foreign_breakout_start(name, attrs))
|
|
3263
|
+
):
|
|
3264
|
+
# A formatting element that breaks out of foreign content (e.g. <i>
|
|
3265
|
+
# inside <svg>) becomes an HTML formatting element and must join the
|
|
3266
|
+
# active formatting list so it is reconstructed later.
|
|
3206
3267
|
return self._parse_formatting_start(name, attrs, pos, tag_start=tag_start, tag_end=tag_end)
|
|
3207
3268
|
|
|
3208
3269
|
if html_text_parsing and name == "menuitem" and self._active_formatting_dirty:
|
|
@@ -3270,6 +3331,11 @@ class ParseEngine:
|
|
|
3270
3331
|
and not self._body_has_content()
|
|
3271
3332
|
):
|
|
3272
3333
|
parent = self._head
|
|
3334
|
+
elif not html_text_parsing and not self._is_foreign_breakout_start(name, attrs):
|
|
3335
|
+
# A non-breakout start tag inside foreign content is inserted as a
|
|
3336
|
+
# foreign element in the current namespace; the HTML in-body repair
|
|
3337
|
+
# (p-closing, heading/list handling, ...) must not run.
|
|
3338
|
+
parent = self._current_parent()
|
|
3273
3339
|
else:
|
|
3274
3340
|
self._repair_stack_for_start(name)
|
|
3275
3341
|
parent = self._current_parent()
|
|
@@ -3312,8 +3378,9 @@ class ParseEngine:
|
|
|
3312
3378
|
|
|
3313
3379
|
reconstruct_before_insert = (
|
|
3314
3380
|
name not in _TABLE_STRUCTURE_START_TAGS
|
|
3315
|
-
and name not in
|
|
3381
|
+
and name not in _INLINE_HEAD_VOID_START_TAGS
|
|
3316
3382
|
and name not in {"param", "source", "track"}
|
|
3383
|
+
and name not in _RUBY_START_TAGS
|
|
3317
3384
|
and name != "template"
|
|
3318
3385
|
and (action is None or not action.p_closing)
|
|
3319
3386
|
)
|
|
@@ -3484,9 +3551,11 @@ class ParseEngine:
|
|
|
3484
3551
|
"html",
|
|
3485
3552
|
}: # pragma: no branch - opposite edge requires invalid parser state
|
|
3486
3553
|
self._enter_template_mode() # pragma: no cover - unreachable after parser-state guards
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3554
|
+
# Active-formatting markers are pushed only for the HTML scope-marker
|
|
3555
|
+
# elements. A foreign element that merely shares a name (e.g. an SVG
|
|
3556
|
+
# or MathML "td") must not push one, or the stray marker would block
|
|
3557
|
+
# later reconstruction once the foreign element is popped.
|
|
3558
|
+
if is_html_namespace and (name in self._table_cell_tags or name in _ACTIVE_FORMATTING_MARKER_TAGS):
|
|
3490
3559
|
self._push_active_formatting_marker()
|
|
3491
3560
|
return node
|
|
3492
3561
|
|
|
@@ -3540,9 +3609,10 @@ class ParseEngine:
|
|
|
3540
3609
|
self._stack.append(node)
|
|
3541
3610
|
if type(node) is Template and node.namespace in {None, "html"}:
|
|
3542
3611
|
self._enter_template_mode()
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3612
|
+
# Only HTML scope-marker elements push an active-formatting marker; a
|
|
3613
|
+
# foreign element sharing a name (e.g. an SVG or MathML "td") must not,
|
|
3614
|
+
# or the stray marker blocks reconstruction once it is popped.
|
|
3615
|
+
if is_html_namespace and (name in self._table_cell_tags or name in _ACTIVE_FORMATTING_MARKER_TAGS):
|
|
3546
3616
|
self._push_active_formatting_marker()
|
|
3547
3617
|
return node
|
|
3548
3618
|
|
|
@@ -3558,6 +3628,25 @@ class ParseEngine:
|
|
|
3558
3628
|
attrs = self._prepare_foreign_attrs(namespace, attrs)
|
|
3559
3629
|
return name, attrs, namespace
|
|
3560
3630
|
|
|
3631
|
+
def _pop_foreign_for_breakout(self) -> None:
|
|
3632
|
+
# Pop the foreign-content elements a breakout start tag returns through,
|
|
3633
|
+
# stopping at the fragment context node and at integration points where
|
|
3634
|
+
# HTML content resumes.
|
|
3635
|
+
while (
|
|
3636
|
+
len(self._stack) > 1
|
|
3637
|
+
and self._stack[-1].namespace not in {None, "html", _PARSER_ONLY_NAMESPACE}
|
|
3638
|
+
and self._stack[-1] is not self._fragment_context_node
|
|
3639
|
+
and not self._is_html_integration_point(self._stack[-1])
|
|
3640
|
+
and not self._is_mathml_text_integration_point(self._stack[-1])
|
|
3641
|
+
):
|
|
3642
|
+
self._stack.pop()
|
|
3643
|
+
|
|
3644
|
+
def _is_foreign_breakout_start(self, name: str, attrs: dict[str, str | None]) -> bool:
|
|
3645
|
+
# Start tags that break out of foreign content back to HTML (§13.2.6.5).
|
|
3646
|
+
return name in FOREIGN_BREAKOUT_ELEMENTS or (
|
|
3647
|
+
name == "font" and any(attr.lower() in {"color", "face", "size"} for attr in attrs)
|
|
3648
|
+
)
|
|
3649
|
+
|
|
3561
3650
|
def _namespace_for_raw_start(self, name: str, attrs: dict[str, str | None]) -> str:
|
|
3562
3651
|
current = self._stack[-1] if self._stack else None
|
|
3563
3652
|
current_ns = getattr(current, "namespace", None)
|
|
@@ -3568,19 +3657,9 @@ class ParseEngine:
|
|
|
3568
3657
|
return "svg" if name == "svg" else "math" if name == "math" else "html"
|
|
3569
3658
|
if current_ns == "math" and current.name == "annotation-xml" and name == "svg":
|
|
3570
3659
|
return "svg"
|
|
3571
|
-
|
|
3572
|
-
name == "font" and any(attr.lower() in {"color", "face", "size"} for attr in attrs)
|
|
3573
|
-
)
|
|
3574
|
-
if not breaks_out:
|
|
3660
|
+
if not self._is_foreign_breakout_start(name, attrs):
|
|
3575
3661
|
return current_ns or "html"
|
|
3576
|
-
|
|
3577
|
-
len(self._stack) > 1
|
|
3578
|
-
and self._stack[-1].namespace not in {None, "html", _PARSER_ONLY_NAMESPACE}
|
|
3579
|
-
and self._stack[-1] is not self._fragment_context_node
|
|
3580
|
-
and not self._is_html_integration_point(self._stack[-1])
|
|
3581
|
-
and not self._is_mathml_text_integration_point(self._stack[-1])
|
|
3582
|
-
):
|
|
3583
|
-
self._stack.pop()
|
|
3662
|
+
self._pop_foreign_for_breakout()
|
|
3584
3663
|
|
|
3585
3664
|
if name == "svg":
|
|
3586
3665
|
return "svg"
|
|
@@ -3732,6 +3811,15 @@ class ParseEngine:
|
|
|
3732
3811
|
return None
|
|
3733
3812
|
return None
|
|
3734
3813
|
|
|
3814
|
+
def _template_end_tag_boundaries(self, name: str, action: TagAction | None) -> frozenset[str]:
|
|
3815
|
+
# Generic in-body end tags inside a template still obey scope: block and
|
|
3816
|
+
# special names close within the default scope, while other names stop at
|
|
3817
|
+
# any special element (§13.2.6.4.7 "any other end tag"). The template
|
|
3818
|
+
# element is a member of both sets, so the walk cannot escape it.
|
|
3819
|
+
if name in self._special_elements or (action is not None and action.p_closing):
|
|
3820
|
+
return _DEFAULT_SCOPE_BOUNDARIES
|
|
3821
|
+
return _GENERAL_END_TAG_BOUNDARIES
|
|
3822
|
+
|
|
3735
3823
|
def _find_open_heading_index(self) -> int | None:
|
|
3736
3824
|
stack = self._stack
|
|
3737
3825
|
for idx in range(len(stack) - 1, 0, -1): # pragma: no branch
|
|
@@ -3994,7 +4082,7 @@ class ParseEngine:
|
|
|
3994
4082
|
del self._stack[table_idx:]
|
|
3995
4083
|
self._insert_template_mode_element(name, attrs, self_closing)
|
|
3996
4084
|
return pos
|
|
3997
|
-
if
|
|
4085
|
+
if name in {
|
|
3998
4086
|
"caption",
|
|
3999
4087
|
"col",
|
|
4000
4088
|
"colgroup",
|
|
@@ -4005,7 +4093,12 @@ class ParseEngine:
|
|
|
4005
4093
|
"thead",
|
|
4006
4094
|
"tr",
|
|
4007
4095
|
}:
|
|
4008
|
-
|
|
4096
|
+
# Clear back to the table context before opening table structure.
|
|
4097
|
+
# In a template with no table element, that context is the
|
|
4098
|
+
# template contents, so an open caption is popped first.
|
|
4099
|
+
clear_idx = table_idx if table_idx is not None else template_idx
|
|
4100
|
+
if clear_idx is not None: # pragma: no branch - a template is always open in this mode
|
|
4101
|
+
del self._stack[clear_idx + 1 :]
|
|
4009
4102
|
if name in {"tbody", "tfoot", "thead"}:
|
|
4010
4103
|
self._set_current_template_mode(_TEMPLATE_MODE_TABLE_BODY)
|
|
4011
4104
|
self._insert_template_mode_element(name, attrs, self_closing)
|
|
@@ -4134,9 +4227,20 @@ class ParseEngine:
|
|
|
4134
4227
|
return False # pragma: no cover - unreachable after parser-state guards
|
|
4135
4228
|
if name in {"html", "head", "body"}:
|
|
4136
4229
|
return True
|
|
4230
|
+
if mode == _TEMPLATE_MODE_INITIAL:
|
|
4231
|
+
# "In template" handles only </template>; every other end tag with
|
|
4232
|
+
# nothing to close is a parse error that is ignored (§13.2.6.4.18),
|
|
4233
|
+
# including the </br> special case. An element left open in the
|
|
4234
|
+
# template (e.g. a scripting-disabled <noscript>) is still closeable
|
|
4235
|
+
# through the normal in-body handling.
|
|
4236
|
+
if name == "template":
|
|
4237
|
+
return False
|
|
4238
|
+
return self._find_open_index_before_boundary(name, _TEMPLATE_SCOPE_BOUNDARIES) is None
|
|
4137
4239
|
if mode == _TEMPLATE_MODE_CELL:
|
|
4138
4240
|
if name in self._table_cell_tags:
|
|
4139
|
-
|
|
4241
|
+
# Only the matching cell type closes; </th> with a <td> open (or
|
|
4242
|
+
# vice versa) leaves the cell open.
|
|
4243
|
+
return self._close_template_cell(name)
|
|
4140
4244
|
if name in {"table", "tbody", "tfoot", "thead", "tr"}:
|
|
4141
4245
|
if self._find_open_index_before_boundary(name, _TEMPLATE_SCOPE_BOUNDARIES) is None:
|
|
4142
4246
|
return True
|
|
@@ -4200,11 +4304,16 @@ class ParseEngine:
|
|
|
4200
4304
|
return False
|
|
4201
4305
|
return False # pragma: no cover - template scope always terminates the scan
|
|
4202
4306
|
|
|
4203
|
-
def _close_template_cell(self) -> bool:
|
|
4307
|
+
def _close_template_cell(self, name: str | None = None) -> bool:
|
|
4204
4308
|
stack = self._stack
|
|
4205
4309
|
for idx in range(len(stack) - 1, 0, -1):
|
|
4206
4310
|
node_name = stack[idx].name
|
|
4207
4311
|
if node_name in self._table_cell_tags:
|
|
4312
|
+
# A </td>/</th> end tag only closes the matching cell type; a
|
|
4313
|
+
# mismatched end tag (e.g. </th> for an open <td>) is left for
|
|
4314
|
+
# the generic handling, which ignores it.
|
|
4315
|
+
if name is not None and node_name != name:
|
|
4316
|
+
return False
|
|
4208
4317
|
self._mark_active_formatting_dirty()
|
|
4209
4318
|
self._clear_active_formatting_to_marker()
|
|
4210
4319
|
del stack[idx:]
|
|
@@ -4229,7 +4338,8 @@ class ParseEngine:
|
|
|
4229
4338
|
def _close_until_before_boundary(self, name: str, boundaries: frozenset[str]) -> bool:
|
|
4230
4339
|
stack = self._stack
|
|
4231
4340
|
for idx in range(len(stack) - 1, 0, -1):
|
|
4232
|
-
|
|
4341
|
+
node = stack[idx]
|
|
4342
|
+
node_name = getattr(node, "name", None)
|
|
4233
4343
|
if node_name == name:
|
|
4234
4344
|
if (
|
|
4235
4345
|
self._fragment_context_node is not None and stack[idx] is self._fragment_context_node
|
|
@@ -4238,7 +4348,11 @@ class ParseEngine:
|
|
|
4238
4348
|
self._mark_active_formatting_dirty()
|
|
4239
4349
|
del stack[idx:]
|
|
4240
4350
|
return True
|
|
4241
|
-
|
|
4351
|
+
# Scope boundaries are HTML elements. A foreign element that merely
|
|
4352
|
+
# shares a name with a boundary (e.g. an SVG or MathML "td") must not
|
|
4353
|
+
# stop the scan; callers guard this walk with an in-scope check that
|
|
4354
|
+
# already accounts for foreign integration points.
|
|
4355
|
+
if getattr(node, "namespace", None) in {None, "html", _PARSER_ONLY_NAMESPACE} and node_name in boundaries:
|
|
4242
4356
|
return False
|
|
4243
4357
|
return False
|
|
4244
4358
|
|
|
@@ -4301,9 +4415,14 @@ class ParseEngine:
|
|
|
4301
4415
|
self._stack.pop()
|
|
4302
4416
|
return
|
|
4303
4417
|
|
|
4304
|
-
if name in HEADING_ELEMENTS
|
|
4305
|
-
|
|
4306
|
-
|
|
4418
|
+
if name in HEADING_ELEMENTS:
|
|
4419
|
+
# Headings are foreign-content breakout elements; pop the foreign
|
|
4420
|
+
# ancestors first so the "current node is a heading" check runs
|
|
4421
|
+
# against the HTML element the breakout returns to (§13.2.6.5).
|
|
4422
|
+
self._pop_foreign_for_breakout()
|
|
4423
|
+
if getattr(self._stack[-1], "name", None) in HEADING_ELEMENTS:
|
|
4424
|
+
self._mark_active_formatting_dirty()
|
|
4425
|
+
self._stack.pop()
|
|
4307
4426
|
|
|
4308
4427
|
def _repair_table_for_start(self, name: str) -> None:
|
|
4309
4428
|
table_idx = self._find_open_index("table")
|
|
@@ -4427,7 +4546,12 @@ class ParseEngine:
|
|
|
4427
4546
|
self._refresh_active_formatting_dirty()
|
|
4428
4547
|
|
|
4429
4548
|
def _foster_parent_for(self, parent: Node, *, for_tag: str | None = None) -> tuple[Node, int] | None:
|
|
4430
|
-
|
|
4549
|
+
# Foster parenting only applies within an HTML table. A foreign element
|
|
4550
|
+
# sharing a name with a foster target (e.g. an SVG or MathML "tr") keeps
|
|
4551
|
+
# its children rather than fostering them out.
|
|
4552
|
+
if getattr(parent, "name", None) not in self._table_foster_targets or getattr(
|
|
4553
|
+
parent, "namespace", None
|
|
4554
|
+
) not in {None, "html", _PARSER_ONLY_NAMESPACE}:
|
|
4431
4555
|
return None
|
|
4432
4556
|
if (
|
|
4433
4557
|
for_tag is not None and for_tag in self._table_allowed_children
|
|
@@ -4481,7 +4605,7 @@ class ParseEngine:
|
|
|
4481
4605
|
|
|
4482
4606
|
def _find_script_start_marker(self, pos: int, end: int) -> int:
|
|
4483
4607
|
return _scanner.find_script_start_marker(
|
|
4484
|
-
self._html_input, self._lower_input, pos, end
|
|
4608
|
+
self._html_input, self._lower_input, pos, end, end
|
|
4485
4609
|
) # pragma: no cover - unreachable after parser-state guards
|
|
4486
4610
|
|
|
4487
4611
|
def _find_tag_end(self, pos: int, end: int) -> int:
|
|
@@ -38,7 +38,11 @@ def find_rawtext_end_tag(html: str, lower: str, name: str, pos: int, end: int) -
|
|
|
38
38
|
continue
|
|
39
39
|
tag_end = find_tag_end(html, after_name, end)
|
|
40
40
|
if tag_end == -1:
|
|
41
|
-
if after_name < end
|
|
41
|
+
if after_name < end:
|
|
42
|
+
# A space or "/" terminated the end-tag name but no ">" follows.
|
|
43
|
+
# At EOF the end tag is still emitted (its attributes dropped), so
|
|
44
|
+
# the raw text ends here. Only a bare "</name" at EOF, with no
|
|
45
|
+
# terminator, stays raw text.
|
|
42
46
|
return close, end
|
|
43
47
|
search = after_name
|
|
44
48
|
continue
|
|
@@ -59,10 +63,14 @@ def find_script_end_tag(html: str, lower: str, pos: int, end: int) -> tuple[int
|
|
|
59
63
|
if comment_start == -1 or close < comment_start:
|
|
60
64
|
return close, next_pos
|
|
61
65
|
escaped = True
|
|
62
|
-
|
|
66
|
+
# After "<!--" the tokenizer is in the script-data-escaped-dash-dash
|
|
67
|
+
# state, so the two trailing dashes already count toward a closing
|
|
68
|
+
# "-->". Resume the comment-end search at those dashes so "<!-->"
|
|
69
|
+
# leaves the escaped state instead of being treated as still open.
|
|
70
|
+
search = comment_start + 2
|
|
63
71
|
continue
|
|
64
72
|
|
|
65
|
-
script_start = find_script_start_marker(html, lower, search, close) if not double_escaped else -1
|
|
73
|
+
script_start = find_script_start_marker(html, lower, search, close, end) if not double_escaped else -1
|
|
66
74
|
comment_end = lower.find("-->", search, close)
|
|
67
75
|
if (
|
|
68
76
|
comment_end != -1
|
|
@@ -85,20 +93,21 @@ def find_script_end_tag(html: str, lower: str, pos: int, end: int) -> tuple[int
|
|
|
85
93
|
double_escaped = False
|
|
86
94
|
search = close + 8
|
|
87
95
|
continue
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if after_name >= end or html[after_name] in TAG_END_NAME_STOP:
|
|
92
|
-
double_escaped = False
|
|
93
|
-
search = after_name
|
|
94
|
-
continue
|
|
96
|
+
# No earlier terminated "</script" can precede `close`: find_rawtext_end_tag
|
|
97
|
+
# already returns the first terminated end tag, so the region between
|
|
98
|
+
# `search` and `close` holds only non-terminated occurrences.
|
|
95
99
|
double_escaped = False
|
|
96
100
|
search = next_pos
|
|
97
101
|
continue
|
|
98
102
|
return close, next_pos
|
|
99
103
|
|
|
100
104
|
|
|
101
|
-
def find_script_start_marker(html: str, lower: str, pos: int, end: int) -> int:
|
|
105
|
+
def find_script_start_marker(html: str, lower: str, pos: int, end: int, text_end: int) -> int:
|
|
106
|
+
# `end` bounds where a "<script" marker may start; `text_end` bounds the real
|
|
107
|
+
# input so the script-data-double-escape-start terminator is checked against
|
|
108
|
+
# the actual following character. When these differ, a marker whose name ends
|
|
109
|
+
# exactly at `end` still has a following character (e.g. the "<" of a trailing
|
|
110
|
+
# "</script>") that must be inspected rather than treated as end-of-input.
|
|
102
111
|
search = pos
|
|
103
112
|
needle = "<script"
|
|
104
113
|
needle_len = len(needle)
|
|
@@ -107,6 +116,6 @@ def find_script_start_marker(html: str, lower: str, pos: int, end: int) -> int:
|
|
|
107
116
|
if start == -1:
|
|
108
117
|
return -1
|
|
109
118
|
after_name = start + needle_len
|
|
110
|
-
if after_name >=
|
|
119
|
+
if after_name >= text_end or html[after_name] in TAG_END_NAME_STOP:
|
|
111
120
|
return start
|
|
112
121
|
search = after_name
|