justhtml 3.5.0__tar.gz → 3.6.1__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.1}/CHANGELOG.md +40 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/PKG-INFO +1 -1
- justhtml-3.6.1/project_ideas.md +47 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/pyproject.toml +1 -1
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/parser/engine.py +205 -57
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/parser/scanner.py +21 -12
- justhtml-3.6.1/tests/justhtml-tests/character_preprocessing.dat +252 -0
- justhtml-3.6.1/tests/justhtml-tests/document_shell_recovery.dat +366 -0
- justhtml-3.6.1/tests/justhtml-tests/foreign_content.dat +394 -0
- justhtml-3.6.1/tests/justhtml-tests/frameset_recovery.dat +229 -0
- justhtml-3.6.1/tests/justhtml-tests/head_noscript_recovery.dat +61 -0
- justhtml-3.6.1/tests/justhtml-tests/in_body_recovery.dat +175 -0
- justhtml-3.6.1/tests/justhtml-tests/raw_text_recovery.dat +166 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/select_recovery.dat +109 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/selectedcontent.dat +10 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/table_recovery.dat +107 -0
- justhtml-3.6.1/tests/justhtml-tests/template_recovery.dat +642 -0
- justhtml-3.6.1/tests/justhtml-tests/tokenizer_recovery.dat +238 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/xml_coercion.dat +24 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_parser_engine.py +18 -128
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_parser_regressions.py +0 -37
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_parser_tree.py +1 -372
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_stream.py +26 -1
- justhtml-3.5.0/tests/justhtml-tests/character_preprocessing.dat +0 -110
- justhtml-3.5.0/tests/justhtml-tests/document_shell_recovery.dat +0 -93
- justhtml-3.5.0/tests/justhtml-tests/foreign_content.dat +0 -135
- justhtml-3.5.0/tests/justhtml-tests/frameset_recovery.dat +0 -60
- justhtml-3.5.0/tests/justhtml-tests/head_noscript_recovery.dat +0 -25
- justhtml-3.5.0/tests/justhtml-tests/in_body_recovery.dat +0 -12
- justhtml-3.5.0/tests/justhtml-tests/template_recovery.dat +0 -294
- {justhtml-3.5.0 → justhtml-3.6.1}/.github/copilot-instructions.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/.github/workflows/ci.yml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/.github/workflows/publish.yml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/.gitignore +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/.pre-commit-config.yaml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/AGENTS.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/CONTRIBUTING.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/LICENSE +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/Makefile +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/PERFORMANCE.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/README.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/SECURITY.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/benchmarks/correctness.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/benchmarks/fuzz.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/benchmarks/performance.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/benchmarks/profile.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/_config.yml +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/_layouts/default.html +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/api.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/assets/search.js +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/bleach-migration.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/building.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/cli.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/comparison.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/correctness.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/encoding.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/errors.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/fragments.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/html-cleaning.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/index.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/linkify.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/migration-examples.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/playground/app.js +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/playground/index.html +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/playground/py/render.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/quickstart.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/sanitization.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/selectors.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/streaming.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/text.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/transforms.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/unsafe-handling.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/docs/url-cleaning.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/llms.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/run_tests.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/scripts/release.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/__main__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/entities.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/text.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/core/types.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/parser/stream.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/py.typed +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/serializer/html.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/transforms/runtime.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/test-summary.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/README.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/data/wikipedia.html +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/harness/__init__.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/harness/encoding.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/harness/regressions.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/harness/reporter.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/harness/serializer.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/harness/tree.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/tokenizer_states.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/linkify-it/README.md +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_builder.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_cli.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_docs_examples.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_encoding.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_entities.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_errors.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_linkify_it.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_node.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_sanitize.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_selector.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_serialize.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_transform_runtime.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_transforms.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.5.0 → justhtml-3.6.1}/tests/test_wikipedia.py +0 -0
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.6.1] - 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Keep processing instructions inside a scripting-disabled head `noscript`, and process them in body mode inside templates as required by the latest WHATWG parsing rules.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Move raw parser input/output regressions from Python unit tests into the shared tree-construction fixture format.
|
|
19
|
+
|
|
20
|
+
## [3.6.0] - 2026-07-13
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
Note: The below are all minor parsing differences found when diffing against Chromium output, that is not covered in WPT's html5 parsing tests.
|
|
24
|
+
|
|
25
|
+
- 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>`.
|
|
26
|
+
- 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.
|
|
27
|
+
- 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.
|
|
28
|
+
- 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.
|
|
29
|
+
- 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.
|
|
30
|
+
- 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.
|
|
31
|
+
- 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.
|
|
32
|
+
- 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.
|
|
33
|
+
- 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.
|
|
34
|
+
- 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.
|
|
35
|
+
- 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.
|
|
36
|
+
- 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.
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
- 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.
|
|
40
|
+
- 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.
|
|
41
|
+
- 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.
|
|
42
|
+
- 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`.
|
|
43
|
+
- 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>`.
|
|
44
|
+
- 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.
|
|
45
|
+
- 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.
|
|
46
|
+
- 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.
|
|
47
|
+
- 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.
|
|
48
|
+
- 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`.
|
|
49
|
+
|
|
10
50
|
## [3.5.0] - 2026-07-13
|
|
11
51
|
|
|
12
52
|
### 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:]
|
|
@@ -1226,8 +1253,32 @@ class ParseEngine:
|
|
|
1226
1253
|
if self._has_null and "\0" in data:
|
|
1227
1254
|
data = data.replace("\0", "\ufffd")
|
|
1228
1255
|
node = ProcessingInstruction(data=data)
|
|
1256
|
+
self._move_head_template_to_body_for_processing_instruction()
|
|
1229
1257
|
self._append_misc_node(node, source_pos)
|
|
1230
1258
|
|
|
1259
|
+
def _move_head_template_to_body_for_processing_instruction(self) -> None:
|
|
1260
|
+
if self._fragment or self._head is None or self._html is None:
|
|
1261
|
+
return
|
|
1262
|
+
stack = self._stack
|
|
1263
|
+
template_idx = self._open_template_index()
|
|
1264
|
+
if template_idx is None:
|
|
1265
|
+
return
|
|
1266
|
+
outer_template_idx = template_idx
|
|
1267
|
+
for idx in range(template_idx, 0, -1):
|
|
1268
|
+
candidate = stack[idx]
|
|
1269
|
+
if type(candidate) is Template and candidate.parent is self._head:
|
|
1270
|
+
outer_template_idx = idx
|
|
1271
|
+
template = stack[outer_template_idx]
|
|
1272
|
+
if type(template) is not Template or template.parent is not self._head:
|
|
1273
|
+
return
|
|
1274
|
+
head_children = self._head.children
|
|
1275
|
+
if head_children is not None: # pragma: no branch - Document head always owns a children list
|
|
1276
|
+
head_children.remove(template)
|
|
1277
|
+
self._append(self._body, template)
|
|
1278
|
+
self._stack = _CountingStack([self._doc, self._html, self._body, *stack[outer_template_idx:]]) # type: ignore[list-item]
|
|
1279
|
+
self._body_mode_seen = True
|
|
1280
|
+
self._after_head = False
|
|
1281
|
+
|
|
1231
1282
|
def _processing_instruction_data(self, content_start: int, content_end: int) -> str | None:
|
|
1232
1283
|
html = self._html_input
|
|
1233
1284
|
if content_start >= content_end or not _is_processing_instruction_name_start(html[content_start]):
|
|
@@ -1279,6 +1330,7 @@ class ParseEngine:
|
|
|
1279
1330
|
parent: Node
|
|
1280
1331
|
if self._current_parent() is self._head or (
|
|
1281
1332
|
not self._fragment
|
|
1333
|
+
and not self._after_head
|
|
1282
1334
|
and self._head is not None
|
|
1283
1335
|
and not self._body_mode_seen
|
|
1284
1336
|
and not self._body_has_content()
|
|
@@ -1604,7 +1656,7 @@ class ParseEngine:
|
|
|
1604
1656
|
append_text(html[lt:end] if gt == -1 else html[lt : gt + 1], lt)
|
|
1605
1657
|
pos = end if gt == -1 else gt + 1
|
|
1606
1658
|
continue
|
|
1607
|
-
if self._raw_mode and len(self._stack) > 1 and self._stack[-1].name in
|
|
1659
|
+
if self._raw_mode and len(self._stack) > 1 and self._stack[-1].name in self._rawtext_element_tags:
|
|
1608
1660
|
comment_end = end if gt == -1 else gt
|
|
1609
1661
|
append_text(html[lt:comment_end] if gt == -1 else html[lt : gt + 1], lt)
|
|
1610
1662
|
pos = end if gt == -1 else gt + 1
|
|
@@ -1931,7 +1983,14 @@ class ParseEngine:
|
|
|
1931
1983
|
self._insert_at(parent, position, node)
|
|
1932
1984
|
return
|
|
1933
1985
|
foster = None
|
|
1934
|
-
|
|
1986
|
+
# Foster parenting only applies in HTML table insertion modes. A
|
|
1987
|
+
# foreign element that merely shares a name with a table foster
|
|
1988
|
+
# target (e.g. an SVG or MathML "tr") keeps character data as its
|
|
1989
|
+
# own child instead.
|
|
1990
|
+
parent_is_table_foster = parent.name in self._table_foster_targets and getattr(
|
|
1991
|
+
parent, "namespace", None
|
|
1992
|
+
) in {None, "html", _PARSER_ONLY_NAMESPACE}
|
|
1993
|
+
if parent_is_table_foster:
|
|
1935
1994
|
is_table_space = text_is_space if text_is_space is not None else text.strip(_SPACE) == ""
|
|
1936
1995
|
if not is_table_space:
|
|
1937
1996
|
if pending_table_whitespace:
|
|
@@ -1961,7 +2020,7 @@ class ParseEngine:
|
|
|
1961
2020
|
node = self._new_text(text, source_pos) if self._track_node_locations else Text(text)
|
|
1962
2021
|
children.append(node)
|
|
1963
2022
|
node.parent = parent
|
|
1964
|
-
if
|
|
2023
|
+
if parent_is_table_foster and is_table_space:
|
|
1965
2024
|
self._foster_next_table_whitespace = pending_table_whitespace + len(text)
|
|
1966
2025
|
else:
|
|
1967
2026
|
foster_parent, position = foster
|
|
@@ -2002,9 +2061,15 @@ class ParseEngine:
|
|
|
2002
2061
|
else:
|
|
2003
2062
|
public_id = None
|
|
2004
2063
|
system_id = None
|
|
2005
|
-
|
|
2064
|
+
# Junk after the name with no PUBLIC/SYSTEM keyword lands in the
|
|
2065
|
+
# "after DOCTYPE name" state, which sets the force-quirks flag
|
|
2066
|
+
# (§13.2.5.55). The regex tolerates that trailing text, so detect
|
|
2067
|
+
# it here to reproduce the quirks-mode transition.
|
|
2068
|
+
trailing = raw[match.end() :].strip("\t\n\f\r ")
|
|
2069
|
+
bogus = kind is None and bool(trailing)
|
|
2070
|
+
doctype = Doctype(name, public_id, system_id, force_quirks=name is None or bogus)
|
|
2006
2071
|
doctype_error, self._quirks_mode = doctype_error_and_quirks(doctype, self._iframe_srcdoc)
|
|
2007
|
-
if doctype_error and self._collect_errors:
|
|
2072
|
+
if (doctype_error or bogus) and self._collect_errors:
|
|
2008
2073
|
self._emit_error(
|
|
2009
2074
|
"unknown-doctype",
|
|
2010
2075
|
doctype_end if gt != -1 else max(0, end - 1),
|
|
@@ -2297,8 +2362,21 @@ class ParseEngine:
|
|
|
2297
2362
|
self._body_mode_seen = True
|
|
2298
2363
|
|
|
2299
2364
|
if not self._fragment and name in {"html", "body"}:
|
|
2300
|
-
|
|
2301
|
-
|
|
2365
|
+
if self._find_open_html_index("select") is not None:
|
|
2366
|
+
# In the "in select" insertion mode, </html> and </body> are
|
|
2367
|
+
# unhandled end tags and must be ignored rather than switching to
|
|
2368
|
+
# the after-body/after-html modes. Any elements kept inside the
|
|
2369
|
+
# open select remain part of that insertion mode.
|
|
2370
|
+
return pos
|
|
2371
|
+
if (
|
|
2372
|
+
self._find_open_html_index("body") is not None
|
|
2373
|
+
and self._find_open_index_before_boundary("body", _DEFAULT_SCOPE_BOUNDARIES) is None
|
|
2374
|
+
):
|
|
2375
|
+
# With body on the open stack, </body> and </html> are ignored
|
|
2376
|
+
# unless body is in scope (§13.2.6.4.7); an open scope marker such
|
|
2377
|
+
# as marquee, object, or applet keeps it out of scope. When body
|
|
2378
|
+
# is not on the stack yet (in head/after head), fall through so
|
|
2379
|
+
# the head-to-body transition still runs.
|
|
2302
2380
|
return pos
|
|
2303
2381
|
self._in_colgroup = False
|
|
2304
2382
|
if self._head is not None and self._stack[-1] is self._head:
|
|
@@ -2369,11 +2447,16 @@ class ParseEngine:
|
|
|
2369
2447
|
self._form_element = None
|
|
2370
2448
|
if form_node is None:
|
|
2371
2449
|
return pos
|
|
2450
|
+
# §13.2.6.4.7: ignore </form> when the form element is not in scope,
|
|
2451
|
+
# e.g. behind an open <table>. Without this the form is removed from
|
|
2452
|
+
# the stack and following content escapes it.
|
|
2453
|
+
form_idx = self._find_open_index_before_boundary("form", _DEFAULT_SCOPE_BOUNDARIES)
|
|
2454
|
+
if form_idx is None or self._stack[form_idx] is not form_node:
|
|
2455
|
+
return pos
|
|
2456
|
+
# form_node is confirmed in scope on the stack and implied end tags
|
|
2457
|
+
# never pop a form, so the removal below always succeeds.
|
|
2372
2458
|
self._generate_implied_end_tags()
|
|
2373
|
-
|
|
2374
|
-
self._stack.remove(form_node)
|
|
2375
|
-
except ValueError:
|
|
2376
|
-
pass
|
|
2459
|
+
self._stack.remove(form_node)
|
|
2377
2460
|
return pos
|
|
2378
2461
|
select_idx = self._find_open_html_index("select")
|
|
2379
2462
|
if select_idx is not None and name not in {"optgroup", "option", "select", "selectedcontent", "template"}:
|
|
@@ -2494,7 +2577,7 @@ class ParseEngine:
|
|
|
2494
2577
|
elif name == "summary":
|
|
2495
2578
|
idx = self._find_open_index_before_boundary(name, _DEFAULT_SCOPE_BOUNDARIES)
|
|
2496
2579
|
elif self._template_modes:
|
|
2497
|
-
idx = self.
|
|
2580
|
+
idx = self._find_open_index_before_boundary(name, self._template_end_tag_boundaries(name, action))
|
|
2498
2581
|
elif name not in self._special_elements and (action is None or not action.p_closing):
|
|
2499
2582
|
idx = self._find_open_index_before_boundary(name, _GENERAL_END_TAG_BOUNDARIES)
|
|
2500
2583
|
else:
|
|
@@ -2671,12 +2754,9 @@ class ParseEngine:
|
|
|
2671
2754
|
self._close_until("select")
|
|
2672
2755
|
return pos
|
|
2673
2756
|
|
|
2674
|
-
if
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
"bgsound",
|
|
2678
|
-
"noframes",
|
|
2679
|
-
}: # pragma: no cover - compiled sanitizer drops parser-only template contents
|
|
2757
|
+
if (
|
|
2758
|
+
self._current_template_mode() == _TEMPLATE_MODE_INITIAL and name in _TEMPLATE_INITIAL_BODY_SWITCH_TAGS
|
|
2759
|
+
): # pragma: no cover - compiled sanitizer drops parser-only template contents
|
|
2680
2760
|
self._set_current_template_mode(_TEMPLATE_MODE_BODY) # pragma: no cover
|
|
2681
2761
|
|
|
2682
2762
|
if in_parser_only_template and self._current_template_mode() == _TEMPLATE_MODE_COLGROUP:
|
|
@@ -2981,6 +3061,9 @@ class ParseEngine:
|
|
|
2981
3061
|
action is not None
|
|
2982
3062
|
and allowed
|
|
2983
3063
|
and action.head_content
|
|
3064
|
+
# "after head" re-enters the head only for the metadata start
|
|
3065
|
+
# tags it lists; noscript is not among them and starts the body.
|
|
3066
|
+
and name != "noscript"
|
|
2984
3067
|
and self._head is not None
|
|
2985
3068
|
and not self._in_head_noscript
|
|
2986
3069
|
and not self._body_mode_seen
|
|
@@ -3088,12 +3171,7 @@ class ParseEngine:
|
|
|
3088
3171
|
):
|
|
3089
3172
|
self._close_html_until("select")
|
|
3090
3173
|
|
|
3091
|
-
if self._current_template_mode() == _TEMPLATE_MODE_INITIAL and name in
|
|
3092
|
-
"base",
|
|
3093
|
-
"basefont",
|
|
3094
|
-
"bgsound",
|
|
3095
|
-
"noframes",
|
|
3096
|
-
}:
|
|
3174
|
+
if self._current_template_mode() == _TEMPLATE_MODE_INITIAL and name in _TEMPLATE_INITIAL_BODY_SWITCH_TAGS:
|
|
3097
3175
|
self._set_current_template_mode(_TEMPLATE_MODE_BODY)
|
|
3098
3176
|
|
|
3099
3177
|
if html_text_parsing and self._current_template_mode() == _TEMPLATE_MODE_COLGROUP:
|
|
@@ -3202,7 +3280,14 @@ class ParseEngine:
|
|
|
3202
3280
|
)
|
|
3203
3281
|
return pos
|
|
3204
3282
|
|
|
3205
|
-
if
|
|
3283
|
+
if (
|
|
3284
|
+
action is not None
|
|
3285
|
+
and action.active_formatting
|
|
3286
|
+
and (html_text_parsing or self._is_foreign_breakout_start(name, attrs))
|
|
3287
|
+
):
|
|
3288
|
+
# A formatting element that breaks out of foreign content (e.g. <i>
|
|
3289
|
+
# inside <svg>) becomes an HTML formatting element and must join the
|
|
3290
|
+
# active formatting list so it is reconstructed later.
|
|
3206
3291
|
return self._parse_formatting_start(name, attrs, pos, tag_start=tag_start, tag_end=tag_end)
|
|
3207
3292
|
|
|
3208
3293
|
if html_text_parsing and name == "menuitem" and self._active_formatting_dirty:
|
|
@@ -3270,6 +3355,11 @@ class ParseEngine:
|
|
|
3270
3355
|
and not self._body_has_content()
|
|
3271
3356
|
):
|
|
3272
3357
|
parent = self._head
|
|
3358
|
+
elif not html_text_parsing and not self._is_foreign_breakout_start(name, attrs):
|
|
3359
|
+
# A non-breakout start tag inside foreign content is inserted as a
|
|
3360
|
+
# foreign element in the current namespace; the HTML in-body repair
|
|
3361
|
+
# (p-closing, heading/list handling, ...) must not run.
|
|
3362
|
+
parent = self._current_parent()
|
|
3273
3363
|
else:
|
|
3274
3364
|
self._repair_stack_for_start(name)
|
|
3275
3365
|
parent = self._current_parent()
|
|
@@ -3312,8 +3402,9 @@ class ParseEngine:
|
|
|
3312
3402
|
|
|
3313
3403
|
reconstruct_before_insert = (
|
|
3314
3404
|
name not in _TABLE_STRUCTURE_START_TAGS
|
|
3315
|
-
and name not in
|
|
3405
|
+
and name not in _INLINE_HEAD_VOID_START_TAGS
|
|
3316
3406
|
and name not in {"param", "source", "track"}
|
|
3407
|
+
and name not in _RUBY_START_TAGS
|
|
3317
3408
|
and name != "template"
|
|
3318
3409
|
and (action is None or not action.p_closing)
|
|
3319
3410
|
)
|
|
@@ -3484,9 +3575,11 @@ class ParseEngine:
|
|
|
3484
3575
|
"html",
|
|
3485
3576
|
}: # pragma: no branch - opposite edge requires invalid parser state
|
|
3486
3577
|
self._enter_template_mode() # pragma: no cover - unreachable after parser-state guards
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3578
|
+
# Active-formatting markers are pushed only for the HTML scope-marker
|
|
3579
|
+
# elements. A foreign element that merely shares a name (e.g. an SVG
|
|
3580
|
+
# or MathML "td") must not push one, or the stray marker would block
|
|
3581
|
+
# later reconstruction once the foreign element is popped.
|
|
3582
|
+
if is_html_namespace and (name in self._table_cell_tags or name in _ACTIVE_FORMATTING_MARKER_TAGS):
|
|
3490
3583
|
self._push_active_formatting_marker()
|
|
3491
3584
|
return node
|
|
3492
3585
|
|
|
@@ -3540,9 +3633,10 @@ class ParseEngine:
|
|
|
3540
3633
|
self._stack.append(node)
|
|
3541
3634
|
if type(node) is Template and node.namespace in {None, "html"}:
|
|
3542
3635
|
self._enter_template_mode()
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3636
|
+
# Only HTML scope-marker elements push an active-formatting marker; a
|
|
3637
|
+
# foreign element sharing a name (e.g. an SVG or MathML "td") must not,
|
|
3638
|
+
# or the stray marker blocks reconstruction once it is popped.
|
|
3639
|
+
if is_html_namespace and (name in self._table_cell_tags or name in _ACTIVE_FORMATTING_MARKER_TAGS):
|
|
3546
3640
|
self._push_active_formatting_marker()
|
|
3547
3641
|
return node
|
|
3548
3642
|
|
|
@@ -3558,6 +3652,25 @@ class ParseEngine:
|
|
|
3558
3652
|
attrs = self._prepare_foreign_attrs(namespace, attrs)
|
|
3559
3653
|
return name, attrs, namespace
|
|
3560
3654
|
|
|
3655
|
+
def _pop_foreign_for_breakout(self) -> None:
|
|
3656
|
+
# Pop the foreign-content elements a breakout start tag returns through,
|
|
3657
|
+
# stopping at the fragment context node and at integration points where
|
|
3658
|
+
# HTML content resumes.
|
|
3659
|
+
while (
|
|
3660
|
+
len(self._stack) > 1
|
|
3661
|
+
and self._stack[-1].namespace not in {None, "html", _PARSER_ONLY_NAMESPACE}
|
|
3662
|
+
and self._stack[-1] is not self._fragment_context_node
|
|
3663
|
+
and not self._is_html_integration_point(self._stack[-1])
|
|
3664
|
+
and not self._is_mathml_text_integration_point(self._stack[-1])
|
|
3665
|
+
):
|
|
3666
|
+
self._stack.pop()
|
|
3667
|
+
|
|
3668
|
+
def _is_foreign_breakout_start(self, name: str, attrs: dict[str, str | None]) -> bool:
|
|
3669
|
+
# Start tags that break out of foreign content back to HTML (§13.2.6.5).
|
|
3670
|
+
return name in FOREIGN_BREAKOUT_ELEMENTS or (
|
|
3671
|
+
name == "font" and any(attr.lower() in {"color", "face", "size"} for attr in attrs)
|
|
3672
|
+
)
|
|
3673
|
+
|
|
3561
3674
|
def _namespace_for_raw_start(self, name: str, attrs: dict[str, str | None]) -> str:
|
|
3562
3675
|
current = self._stack[-1] if self._stack else None
|
|
3563
3676
|
current_ns = getattr(current, "namespace", None)
|
|
@@ -3568,19 +3681,9 @@ class ParseEngine:
|
|
|
3568
3681
|
return "svg" if name == "svg" else "math" if name == "math" else "html"
|
|
3569
3682
|
if current_ns == "math" and current.name == "annotation-xml" and name == "svg":
|
|
3570
3683
|
return "svg"
|
|
3571
|
-
|
|
3572
|
-
name == "font" and any(attr.lower() in {"color", "face", "size"} for attr in attrs)
|
|
3573
|
-
)
|
|
3574
|
-
if not breaks_out:
|
|
3684
|
+
if not self._is_foreign_breakout_start(name, attrs):
|
|
3575
3685
|
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()
|
|
3686
|
+
self._pop_foreign_for_breakout()
|
|
3584
3687
|
|
|
3585
3688
|
if name == "svg":
|
|
3586
3689
|
return "svg"
|
|
@@ -3732,6 +3835,15 @@ class ParseEngine:
|
|
|
3732
3835
|
return None
|
|
3733
3836
|
return None
|
|
3734
3837
|
|
|
3838
|
+
def _template_end_tag_boundaries(self, name: str, action: TagAction | None) -> frozenset[str]:
|
|
3839
|
+
# Generic in-body end tags inside a template still obey scope: block and
|
|
3840
|
+
# special names close within the default scope, while other names stop at
|
|
3841
|
+
# any special element (§13.2.6.4.7 "any other end tag"). The template
|
|
3842
|
+
# element is a member of both sets, so the walk cannot escape it.
|
|
3843
|
+
if name in self._special_elements or (action is not None and action.p_closing):
|
|
3844
|
+
return _DEFAULT_SCOPE_BOUNDARIES
|
|
3845
|
+
return _GENERAL_END_TAG_BOUNDARIES
|
|
3846
|
+
|
|
3735
3847
|
def _find_open_heading_index(self) -> int | None:
|
|
3736
3848
|
stack = self._stack
|
|
3737
3849
|
for idx in range(len(stack) - 1, 0, -1): # pragma: no branch
|
|
@@ -3994,7 +4106,7 @@ class ParseEngine:
|
|
|
3994
4106
|
del self._stack[table_idx:]
|
|
3995
4107
|
self._insert_template_mode_element(name, attrs, self_closing)
|
|
3996
4108
|
return pos
|
|
3997
|
-
if
|
|
4109
|
+
if name in {
|
|
3998
4110
|
"caption",
|
|
3999
4111
|
"col",
|
|
4000
4112
|
"colgroup",
|
|
@@ -4005,7 +4117,12 @@ class ParseEngine:
|
|
|
4005
4117
|
"thead",
|
|
4006
4118
|
"tr",
|
|
4007
4119
|
}:
|
|
4008
|
-
|
|
4120
|
+
# Clear back to the table context before opening table structure.
|
|
4121
|
+
# In a template with no table element, that context is the
|
|
4122
|
+
# template contents, so an open caption is popped first.
|
|
4123
|
+
clear_idx = table_idx if table_idx is not None else template_idx
|
|
4124
|
+
if clear_idx is not None: # pragma: no branch - a template is always open in this mode
|
|
4125
|
+
del self._stack[clear_idx + 1 :]
|
|
4009
4126
|
if name in {"tbody", "tfoot", "thead"}:
|
|
4010
4127
|
self._set_current_template_mode(_TEMPLATE_MODE_TABLE_BODY)
|
|
4011
4128
|
self._insert_template_mode_element(name, attrs, self_closing)
|
|
@@ -4134,9 +4251,20 @@ class ParseEngine:
|
|
|
4134
4251
|
return False # pragma: no cover - unreachable after parser-state guards
|
|
4135
4252
|
if name in {"html", "head", "body"}:
|
|
4136
4253
|
return True
|
|
4254
|
+
if mode == _TEMPLATE_MODE_INITIAL:
|
|
4255
|
+
# "In template" handles only </template>; every other end tag with
|
|
4256
|
+
# nothing to close is a parse error that is ignored (§13.2.6.4.18),
|
|
4257
|
+
# including the </br> special case. An element left open in the
|
|
4258
|
+
# template (e.g. a scripting-disabled <noscript>) is still closeable
|
|
4259
|
+
# through the normal in-body handling.
|
|
4260
|
+
if name == "template":
|
|
4261
|
+
return False
|
|
4262
|
+
return self._find_open_index_before_boundary(name, _TEMPLATE_SCOPE_BOUNDARIES) is None
|
|
4137
4263
|
if mode == _TEMPLATE_MODE_CELL:
|
|
4138
4264
|
if name in self._table_cell_tags:
|
|
4139
|
-
|
|
4265
|
+
# Only the matching cell type closes; </th> with a <td> open (or
|
|
4266
|
+
# vice versa) leaves the cell open.
|
|
4267
|
+
return self._close_template_cell(name)
|
|
4140
4268
|
if name in {"table", "tbody", "tfoot", "thead", "tr"}:
|
|
4141
4269
|
if self._find_open_index_before_boundary(name, _TEMPLATE_SCOPE_BOUNDARIES) is None:
|
|
4142
4270
|
return True
|
|
@@ -4200,11 +4328,16 @@ class ParseEngine:
|
|
|
4200
4328
|
return False
|
|
4201
4329
|
return False # pragma: no cover - template scope always terminates the scan
|
|
4202
4330
|
|
|
4203
|
-
def _close_template_cell(self) -> bool:
|
|
4331
|
+
def _close_template_cell(self, name: str | None = None) -> bool:
|
|
4204
4332
|
stack = self._stack
|
|
4205
4333
|
for idx in range(len(stack) - 1, 0, -1):
|
|
4206
4334
|
node_name = stack[idx].name
|
|
4207
4335
|
if node_name in self._table_cell_tags:
|
|
4336
|
+
# A </td>/</th> end tag only closes the matching cell type; a
|
|
4337
|
+
# mismatched end tag (e.g. </th> for an open <td>) is left for
|
|
4338
|
+
# the generic handling, which ignores it.
|
|
4339
|
+
if name is not None and node_name != name:
|
|
4340
|
+
return False
|
|
4208
4341
|
self._mark_active_formatting_dirty()
|
|
4209
4342
|
self._clear_active_formatting_to_marker()
|
|
4210
4343
|
del stack[idx:]
|
|
@@ -4229,7 +4362,8 @@ class ParseEngine:
|
|
|
4229
4362
|
def _close_until_before_boundary(self, name: str, boundaries: frozenset[str]) -> bool:
|
|
4230
4363
|
stack = self._stack
|
|
4231
4364
|
for idx in range(len(stack) - 1, 0, -1):
|
|
4232
|
-
|
|
4365
|
+
node = stack[idx]
|
|
4366
|
+
node_name = getattr(node, "name", None)
|
|
4233
4367
|
if node_name == name:
|
|
4234
4368
|
if (
|
|
4235
4369
|
self._fragment_context_node is not None and stack[idx] is self._fragment_context_node
|
|
@@ -4238,7 +4372,11 @@ class ParseEngine:
|
|
|
4238
4372
|
self._mark_active_formatting_dirty()
|
|
4239
4373
|
del stack[idx:]
|
|
4240
4374
|
return True
|
|
4241
|
-
|
|
4375
|
+
# Scope boundaries are HTML elements. A foreign element that merely
|
|
4376
|
+
# shares a name with a boundary (e.g. an SVG or MathML "td") must not
|
|
4377
|
+
# stop the scan; callers guard this walk with an in-scope check that
|
|
4378
|
+
# already accounts for foreign integration points.
|
|
4379
|
+
if getattr(node, "namespace", None) in {None, "html", _PARSER_ONLY_NAMESPACE} and node_name in boundaries:
|
|
4242
4380
|
return False
|
|
4243
4381
|
return False
|
|
4244
4382
|
|
|
@@ -4301,9 +4439,14 @@ class ParseEngine:
|
|
|
4301
4439
|
self._stack.pop()
|
|
4302
4440
|
return
|
|
4303
4441
|
|
|
4304
|
-
if name in HEADING_ELEMENTS
|
|
4305
|
-
|
|
4306
|
-
|
|
4442
|
+
if name in HEADING_ELEMENTS:
|
|
4443
|
+
# Headings are foreign-content breakout elements; pop the foreign
|
|
4444
|
+
# ancestors first so the "current node is a heading" check runs
|
|
4445
|
+
# against the HTML element the breakout returns to (§13.2.6.5).
|
|
4446
|
+
self._pop_foreign_for_breakout()
|
|
4447
|
+
if getattr(self._stack[-1], "name", None) in HEADING_ELEMENTS:
|
|
4448
|
+
self._mark_active_formatting_dirty()
|
|
4449
|
+
self._stack.pop()
|
|
4307
4450
|
|
|
4308
4451
|
def _repair_table_for_start(self, name: str) -> None:
|
|
4309
4452
|
table_idx = self._find_open_index("table")
|
|
@@ -4427,7 +4570,12 @@ class ParseEngine:
|
|
|
4427
4570
|
self._refresh_active_formatting_dirty()
|
|
4428
4571
|
|
|
4429
4572
|
def _foster_parent_for(self, parent: Node, *, for_tag: str | None = None) -> tuple[Node, int] | None:
|
|
4430
|
-
|
|
4573
|
+
# Foster parenting only applies within an HTML table. A foreign element
|
|
4574
|
+
# sharing a name with a foster target (e.g. an SVG or MathML "tr") keeps
|
|
4575
|
+
# its children rather than fostering them out.
|
|
4576
|
+
if getattr(parent, "name", None) not in self._table_foster_targets or getattr(
|
|
4577
|
+
parent, "namespace", None
|
|
4578
|
+
) not in {None, "html", _PARSER_ONLY_NAMESPACE}:
|
|
4431
4579
|
return None
|
|
4432
4580
|
if (
|
|
4433
4581
|
for_tag is not None and for_tag in self._table_allowed_children
|
|
@@ -4481,7 +4629,7 @@ class ParseEngine:
|
|
|
4481
4629
|
|
|
4482
4630
|
def _find_script_start_marker(self, pos: int, end: int) -> int:
|
|
4483
4631
|
return _scanner.find_script_start_marker(
|
|
4484
|
-
self._html_input, self._lower_input, pos, end
|
|
4632
|
+
self._html_input, self._lower_input, pos, end, end
|
|
4485
4633
|
) # pragma: no cover - unreachable after parser-state guards
|
|
4486
4634
|
|
|
4487
4635
|
def _find_tag_end(self, pos: int, end: int) -> int:
|