justhtml 3.2.0__tar.gz → 3.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {justhtml-3.2.0 → justhtml-3.3.0}/.github/workflows/ci.yml +13 -3
- {justhtml-3.2.0 → justhtml-3.3.0}/.pre-commit-config.yaml +1 -1
- {justhtml-3.2.0 → justhtml-3.3.0}/CHANGELOG.md +17 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/PKG-INFO +1 -1
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/api.md +1 -1
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/linkify.md +27 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/sanitization.md +6 -4
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/streaming.md +13 -15
- {justhtml-3.2.0 → justhtml-3.3.0}/pyproject.toml +1 -1
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/dom/__init__.py +3 -3
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/parser/engine.py +44 -52
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/parser/stream.py +41 -13
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/dom.py +5 -1
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/serializer/html.py +19 -1
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/transforms/linkify_core.py +7 -1
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/transforms/runtime.py +32 -3
- {justhtml-3.2.0 → justhtml-3.3.0}/test-summary.txt +7 -7
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_linkify_internals.py +20 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_node.py +21 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_serialize.py +14 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_stream.py +14 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_transforms_edge_cases.py +74 -1
- justhtml-3.2.0/docs/parser-engine-findings.md +0 -469
- {justhtml-3.2.0 → justhtml-3.3.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/.github/workflows/publish.yml +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/.gitignore +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/CONTRIBUTING.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/LICENSE +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/Makefile +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/README.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/SECURITY.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/benchmarks/correctness.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/benchmarks/performance.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/benchmarks/profile.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/_config.yml +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/assets/search.js +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/bleach-migration.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/building.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/cli.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/comparison.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/correctness.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/encoding.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/errors.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/fragments.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/html-cleaning.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/index.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/migration-examples.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/playground/app.js +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/playground/index.html +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/quickstart.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/selectors.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/text.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/transforms.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/unsafe-handling.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/docs/url-cleaning.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/llms.txt +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/run_tests.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/scripts/release.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/text.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/parser/scanner.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/README.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/harness/tree.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/branch_coverage.dat +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/coverage_gaps.test +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/empty_stack_edge_cases.dat +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/tokenizer_edge_cases.test +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/treebuilder_coverage.dat +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/justhtml-tests/xml_coercion_coverage.test +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_builder.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_cli.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_docs_examples.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_encoding.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_entities.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_errors.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_parser_engine_coverage.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_parser_regressions.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_parser_tree.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_sanitize.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_selector.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_transforms.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.2.0 → justhtml-3.3.0}/tests/test_wikipedia.py +0 -0
|
@@ -41,15 +41,25 @@ jobs:
|
|
|
41
41
|
git -C html5lib-tests checkout 9fb614afaa42ce8787840f057b32084308e76549
|
|
42
42
|
git -C html5lib-tests rev-parse HEAD
|
|
43
43
|
|
|
44
|
+
- name: Checkout WPT tree-construction tests
|
|
45
|
+
run: |
|
|
46
|
+
cd ..
|
|
47
|
+
git clone --filter=blob:none --sparse https://github.com/web-platform-tests/wpt.git
|
|
48
|
+
git -C wpt fetch origin 75b089d70448e1da425ab8cf38971b6af41f3cea
|
|
49
|
+
git -C wpt checkout 75b089d70448e1da425ab8cf38971b6af41f3cea
|
|
50
|
+
git -C wpt sparse-checkout set html/syntax/parsing/resources
|
|
51
|
+
git -C wpt rev-parse HEAD
|
|
52
|
+
|
|
44
53
|
- name: Setup test fixtures
|
|
45
54
|
run: |
|
|
46
55
|
cp -a ../html5lib-tests/tokenizer tests/html5lib-tests-tokenizer
|
|
47
|
-
cp -a ../
|
|
56
|
+
cp -a ../wpt/html/syntax/parsing/resources tests/html5lib-tests-tree
|
|
48
57
|
cp -a ../html5lib-tests/serializer tests/html5lib-tests-serializer
|
|
49
58
|
cp -a ../html5lib-tests/encoding tests/html5lib-tests-encoding
|
|
50
59
|
|
|
51
60
|
- name: Run pre-commit
|
|
52
61
|
id: precommit
|
|
62
|
+
if: matrix.python-version == '3.14'
|
|
53
63
|
run: pre-commit run --show-diff-on-failure --color=always --all-files
|
|
54
64
|
env:
|
|
55
65
|
SKIP: mypy
|
|
@@ -61,14 +71,14 @@ jobs:
|
|
|
61
71
|
- name: Diagnose test failures (verbose)
|
|
62
72
|
if: always()
|
|
63
73
|
run: |
|
|
64
|
-
if [ "${{ steps.precommit.outcome }}"
|
|
74
|
+
if [ "${{ steps.precommit.outcome }}" != "failure" ] && [ "${{ steps.run_tests.outcome }}" = "success" ]; then
|
|
65
75
|
echo "No failures detected; skipping verbose rerun."
|
|
66
76
|
exit 0
|
|
67
77
|
fi
|
|
68
78
|
python run_tests.py -v --no-write-summary
|
|
69
79
|
|
|
70
80
|
- name: Run mypy
|
|
71
|
-
if: matrix.python-version == '3.
|
|
81
|
+
if: matrix.python-version == '3.14'
|
|
72
82
|
run: mypy
|
|
73
83
|
|
|
74
84
|
test-pyodide:
|
|
@@ -33,7 +33,7 @@ repos:
|
|
|
33
33
|
hooks:
|
|
34
34
|
- id: tests-coverage
|
|
35
35
|
name: Tests & Coverage
|
|
36
|
-
entry: bash -c 'coverage run run_tests.py
|
|
36
|
+
entry: bash -c 'coverage run run_tests.py && coverage report'
|
|
37
37
|
language: system
|
|
38
38
|
pass_filenames: false
|
|
39
39
|
always_run: true
|
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.3.0] - 2026-07-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Document that `Linkify(...)` must run before an explicit `Sanitize(...)` in a transform list, not after, and correct the `clean_url_value()` example to escape the returned value with `escape_attr_value()` before embedding it in HTML. Previously, the docs suggested orderings that silently skip URL-policy validation on Linkify-generated links, and separately claimed `clean_url_value()` output needed no further escaping for HTML attribute embedding.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Copy `override_attrs` when cloning a node instead of storing the caller's dict by reference. Previously, calling `clone_node(override_attrs=...)` more than once with the same dict (or holding onto that dict after cloning) produced clones that aliased the same attribute dictionary, so mutating one clone's attributes silently mutated another clone or the caller's own dict.
|
|
17
|
+
|
|
18
|
+
### Performance
|
|
19
|
+
- Avoid rescanning the entire open-elements stack for every unmatched end tag inside foreign (SVG/MathML) content in the streaming parser (`stream()`), by tracking open-element name counts the same way the main parser engine does. Previously, a run of unmatched end tags deep inside foreign content was quadratic in the depth of nesting.
|
|
20
|
+
- Bound `Linkify(...)`'s email-candidate pattern to RFC 5321's 64-octet local-part limit instead of an unbounded match. Previously, the greedy local-part match followed by a required (and often absent) `@` forced a full-length backtrack at every candidate start position, making long "@"-free runs of otherwise email-local-part-legal text (such as repeated dotted-numeric tokens) quadratic to scan.
|
|
21
|
+
- Share cached sibling/ancestor lookups across nodes when matching structural selectors (`:nth-child`, sibling combinators) in custom `EditAttrs(...)`/`SetAttrs(...)`-style transform pipelines, the same way `query()` already does. Previously, each node rebuilt this data from scratch, making structural-selector transforms quadratic over documents with many siblings. Sharing is only enabled when every transform in the pipeline is provably attribute-only; a pipeline that also uses `Edit(...)`, `Decide(...)`, `Drop(...)`, `Linkify(...)`, or similar structure-changing transforms is unaffected and keeps its previous (already correct) behavior.
|
|
22
|
+
- Access the parser's open-elements stack through a plain attribute again instead of a property, removing a Python-level function call from one of the hottest paths in tree construction.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
- (Severity: Low) HTML-escape a literal `&` left over after percent-encoding a URL for the `HTMLContext.URL` serialization context (used for `href`/`src`-style attributes). Previously, a URL containing a legacy (no-semicolon) named HTML entity immediately after a decoded `&` could be interpreted differently once embedded in an HTML attribute and re-parsed by a browser than what was validated.
|
|
26
|
+
|
|
10
27
|
## [3.2.0] - 2026-07-04
|
|
11
28
|
|
|
12
29
|
### Changed
|
|
@@ -602,7 +602,7 @@ To override the encoding for byte input, pass `encoding=...`.
|
|
|
602
602
|
| `"end"` | `tag_name` | Closing tag |
|
|
603
603
|
| `"text"` | `text_content` | Text content |
|
|
604
604
|
| `"comment"` | `comment_text` | HTML comment |
|
|
605
|
-
| `"doctype"` | `
|
|
605
|
+
| `"doctype"` | `(name, public_id, system_id)` | DOCTYPE declaration fields |
|
|
606
606
|
|
|
607
607
|
---
|
|
608
608
|
|
|
@@ -125,6 +125,33 @@ This matters for Linkify because sanitization policies can remove or rewrite att
|
|
|
125
125
|
- Schemes not allowed for `a[href]` are stripped (the `<a>` remains, but `href` is removed).
|
|
126
126
|
- Protocol-relative `//example.com` is resolved according to policy (default: `https://example.com`).
|
|
127
127
|
|
|
128
|
+
**Put `Linkify()` before `Sanitize()`, not after.** It is tempting to reach for `transforms=[Sanitize(policy), Linkify()]`, reasoning "sanitize first, then add links to the clean text." That order does the opposite of what it looks like: your explicit `Sanitize()` position becomes the sanitize point (see above), so the `<a href="...">` elements Linkify creates afterward are never checked against your `UrlPolicy`/`UrlRule` restrictions.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from justhtml import JustHTML, Linkify, Sanitize, SanitizationPolicy, UrlPolicy, UrlRule
|
|
132
|
+
|
|
133
|
+
# A policy that only allows https:// links on <a href>.
|
|
134
|
+
policy = SanitizationPolicy(
|
|
135
|
+
allowed_tags=["a"],
|
|
136
|
+
allowed_attributes={"a": ["href"]},
|
|
137
|
+
url_policy=UrlPolicy(allow_rules={("a", "href"): UrlRule(allowed_schemes={"https"})}),
|
|
138
|
+
)
|
|
139
|
+
text = "See http://example.com"
|
|
140
|
+
|
|
141
|
+
# Wrong: Linkify's generated <a href> is created after the sanitize point,
|
|
142
|
+
# so the https-only restriction never sees it.
|
|
143
|
+
doc = JustHTML(text, transforms=[Sanitize(policy), Linkify()])
|
|
144
|
+
print(doc.to_html(pretty=False))
|
|
145
|
+
# => See <a href="http://example.com">http://example.com</a>
|
|
146
|
+
|
|
147
|
+
# Right: Linkify runs first, so its output is covered by the same sanitize pass.
|
|
148
|
+
doc = JustHTML(text, transforms=[Linkify(), Sanitize(policy)])
|
|
149
|
+
print(doc.to_html(pretty=False))
|
|
150
|
+
# => See http://example.com
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If you don't need other transforms between them, the simplest safe option is to omit `Sanitize()` entirely and let `sanitize=True` (the default) append it automatically after `Linkify()`.
|
|
154
|
+
|
|
128
155
|
If you want Linkify output without any sanitization changes (trusted input only), use `sanitize=False` and avoid adding `Sanitize(...)` in transforms.
|
|
129
156
|
|
|
130
157
|
## Provenance
|
|
@@ -124,10 +124,12 @@ rule = UrlRule(allowed_schemes={"https", "mailto"})
|
|
|
124
124
|
cleaned_url = JustHTML.clean_url_value(value=user_url, url_rule=rule)
|
|
125
125
|
|
|
126
126
|
if cleaned_url:
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
|
|
127
|
+
# cleaned_url is a validated, percent-encoded URL, not an HTML-attribute-safe
|
|
128
|
+
# string: it deliberately keeps "&" literal so multi-parameter query strings
|
|
129
|
+
# stay usable as plain text (e.g. in a JS string). Escape it like any other
|
|
130
|
+
# attribute value before embedding it in HTML.
|
|
131
|
+
safe_url = JustHTML.escape_attr_value(cleaned_url)
|
|
132
|
+
# <a href="${safe_url}">
|
|
131
133
|
```
|
|
132
134
|
|
|
133
135
|
## Configuration
|
|
@@ -15,7 +15,6 @@ The streaming parser is:
|
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
17
|
from justhtml import stream
|
|
18
|
-
from pathlib import Path
|
|
19
18
|
|
|
20
19
|
html = "<html><body><p>Hello, world!</p></body></html>"
|
|
21
20
|
|
|
@@ -23,12 +22,24 @@ for event, data in stream(html):
|
|
|
23
22
|
print(event, data)
|
|
24
23
|
```
|
|
25
24
|
|
|
25
|
+
Output:
|
|
26
|
+
```text
|
|
27
|
+
start ('html', {})
|
|
28
|
+
start ('body', {})
|
|
29
|
+
start ('p', {})
|
|
30
|
+
text Hello, world!
|
|
31
|
+
end p
|
|
32
|
+
end body
|
|
33
|
+
end html
|
|
34
|
+
```
|
|
35
|
+
|
|
26
36
|
## Byte Input and Encodings
|
|
27
37
|
|
|
28
38
|
`stream()` also accepts bytes (`bytes`, `bytearray`, `memoryview`). In that case, the input is decoded using HTML encoding sniffing (including a `windows-1252` fallback for legacy documents).
|
|
29
39
|
|
|
30
40
|
```python
|
|
31
41
|
from justhtml import stream
|
|
42
|
+
from pathlib import Path
|
|
32
43
|
|
|
33
44
|
data = Path("page.html").read_bytes()
|
|
34
45
|
for event, data in stream(data):
|
|
@@ -48,19 +59,6 @@ for event, data in stream(data, encoding="utf-8"):
|
|
|
48
59
|
|
|
49
60
|
See [Encoding & Byte Input](encoding.md) for details.
|
|
50
61
|
|
|
51
|
-
Output:
|
|
52
|
-
```
|
|
53
|
-
start ('html', {})
|
|
54
|
-
start ('head', {})
|
|
55
|
-
end head
|
|
56
|
-
start ('body', {})
|
|
57
|
-
start ('p', {})
|
|
58
|
-
text Hello, world!
|
|
59
|
-
end p
|
|
60
|
-
end body
|
|
61
|
-
end html
|
|
62
|
-
```
|
|
63
|
-
|
|
64
62
|
## Events
|
|
65
63
|
|
|
66
64
|
| Event | Data Type | Description |
|
|
@@ -69,7 +67,7 @@ end html
|
|
|
69
67
|
| `"end"` | `tag_name` | Closing tag encountered |
|
|
70
68
|
| `"text"` | `str` | Text content |
|
|
71
69
|
| `"comment"` | `str` | HTML comment content |
|
|
72
|
-
| `"doctype"` | `
|
|
70
|
+
| `"doctype"` | `(name, public_id, system_id)` | DOCTYPE declaration fields |
|
|
73
71
|
|
|
74
72
|
## Examples
|
|
75
73
|
|
|
@@ -422,7 +422,7 @@ class Node:
|
|
|
422
422
|
Returns:
|
|
423
423
|
A new node that is a copy of this node.
|
|
424
424
|
"""
|
|
425
|
-
attrs = override_attrs if override_attrs is not None else (self.attrs.copy() if self.attrs else None)
|
|
425
|
+
attrs = override_attrs.copy() if override_attrs is not None else (self.attrs.copy() if self.attrs else None)
|
|
426
426
|
clone = Node(
|
|
427
427
|
self.name,
|
|
428
428
|
attrs,
|
|
@@ -551,7 +551,7 @@ class Element(Node):
|
|
|
551
551
|
self._self_closing = False
|
|
552
552
|
|
|
553
553
|
def clone_node(self, deep: bool = False, override_attrs: dict[str, str | None] | None = None) -> Element:
|
|
554
|
-
attrs = override_attrs if override_attrs is not None else (self.attrs.copy() if self.attrs else {})
|
|
554
|
+
attrs = override_attrs.copy() if override_attrs is not None else (self.attrs.copy() if self.attrs else {})
|
|
555
555
|
clone = Element(self.name, attrs, self.namespace)
|
|
556
556
|
clone._source_html = self._source_html
|
|
557
557
|
clone._origin_pos = self._origin_pos
|
|
@@ -586,7 +586,7 @@ class Template(Element):
|
|
|
586
586
|
self.template_content = None
|
|
587
587
|
|
|
588
588
|
def clone_node(self, deep: bool = False, override_attrs: dict[str, str | None] | None = None) -> Template:
|
|
589
|
-
attrs = override_attrs if override_attrs is not None else (self.attrs.copy() if self.attrs else {})
|
|
589
|
+
attrs = override_attrs.copy() if override_attrs is not None else (self.attrs.copy() if self.attrs else {})
|
|
590
590
|
clone = Template(
|
|
591
591
|
self.name,
|
|
592
592
|
attrs,
|
|
@@ -722,7 +722,7 @@ class ParseEngine:
|
|
|
722
722
|
"_rcdata_tags",
|
|
723
723
|
"_skip_escaped_comment_space",
|
|
724
724
|
"_special_elements",
|
|
725
|
-
"
|
|
725
|
+
"_stack",
|
|
726
726
|
"_strip_invisible_unicode",
|
|
727
727
|
"_table_allowed_children",
|
|
728
728
|
"_table_cell_tags",
|
|
@@ -847,20 +847,12 @@ class ParseEngine:
|
|
|
847
847
|
self._nodes_to_drop: list[Element] = []
|
|
848
848
|
self._nodes_to_unwrap: list[Element] = []
|
|
849
849
|
self._template_modes: list[str] = []
|
|
850
|
-
self.
|
|
850
|
+
self._stack: _CountingStack = _CountingStack()
|
|
851
851
|
|
|
852
852
|
@property
|
|
853
853
|
def errors(self) -> list[ParseError]:
|
|
854
854
|
return self._errors
|
|
855
855
|
|
|
856
|
-
@property
|
|
857
|
-
def _stack(self) -> _CountingStack:
|
|
858
|
-
return self._stack_impl
|
|
859
|
-
|
|
860
|
-
@_stack.setter
|
|
861
|
-
def _stack(self, value: list[Node]) -> None:
|
|
862
|
-
self._stack_impl = value if isinstance(value, _CountingStack) else _CountingStack(value)
|
|
863
|
-
|
|
864
856
|
def _line_col_at_pos(self, pos: int) -> tuple[int, int]:
|
|
865
857
|
if pos < 0: # pragma: no cover - parser offsets are non-negative
|
|
866
858
|
pos = 0
|
|
@@ -1104,7 +1096,7 @@ class ParseEngine:
|
|
|
1104
1096
|
html_context = self._fragment_context_namespace in {None, "html"}
|
|
1105
1097
|
if html_context and context_name in self._rcdata_tags:
|
|
1106
1098
|
self._body = root
|
|
1107
|
-
self._stack = [root]
|
|
1099
|
+
self._stack = _CountingStack([root])
|
|
1108
1100
|
text = self._clean_text(self._html_input, replace_null=True)
|
|
1109
1101
|
if context_name == "textarea" and text.startswith("\n"):
|
|
1110
1102
|
text = text[1:]
|
|
@@ -1114,7 +1106,7 @@ class ParseEngine:
|
|
|
1114
1106
|
|
|
1115
1107
|
if html_context and context_name in self._plaintext_tags:
|
|
1116
1108
|
self._body = root
|
|
1117
|
-
self._stack = [root]
|
|
1109
|
+
self._stack = _CountingStack([root])
|
|
1118
1110
|
self._append_raw_literal_text(self._html_input, 0)
|
|
1119
1111
|
return root
|
|
1120
1112
|
|
|
@@ -1124,7 +1116,7 @@ class ParseEngine:
|
|
|
1124
1116
|
or context_name in self._rawtext_element_tags
|
|
1125
1117
|
):
|
|
1126
1118
|
self._body = root
|
|
1127
|
-
self._stack = [root]
|
|
1119
|
+
self._stack = _CountingStack([root])
|
|
1128
1120
|
self._append_raw_literal_text(self._html_input, 0)
|
|
1129
1121
|
return root
|
|
1130
1122
|
|
|
@@ -1139,7 +1131,7 @@ class ParseEngine:
|
|
|
1139
1131
|
self._html = context
|
|
1140
1132
|
self._head = head
|
|
1141
1133
|
self._body = body
|
|
1142
|
-
self._stack = [root, context, body]
|
|
1134
|
+
self._stack = _CountingStack([root, context, body])
|
|
1143
1135
|
if not self._raw_mode:
|
|
1144
1136
|
if (
|
|
1145
1137
|
"head" not in self._allowed_tags
|
|
@@ -1154,10 +1146,10 @@ class ParseEngine:
|
|
|
1154
1146
|
self._append(root, context)
|
|
1155
1147
|
self._fragment_context_node = context
|
|
1156
1148
|
self._body = context
|
|
1157
|
-
self._stack = [root, context]
|
|
1149
|
+
self._stack = _CountingStack([root, context])
|
|
1158
1150
|
else:
|
|
1159
1151
|
self._body = root
|
|
1160
|
-
self._stack = [root]
|
|
1152
|
+
self._stack = _CountingStack([root])
|
|
1161
1153
|
else:
|
|
1162
1154
|
doc = Document()
|
|
1163
1155
|
if self._track_tag_spans:
|
|
@@ -1172,7 +1164,7 @@ class ParseEngine:
|
|
|
1172
1164
|
self._html = html_el
|
|
1173
1165
|
self._head = head
|
|
1174
1166
|
self._body = body
|
|
1175
|
-
self._stack = [doc, html_el, body]
|
|
1167
|
+
self._stack = _CountingStack([doc, html_el, body])
|
|
1176
1168
|
|
|
1177
1169
|
self._parse_range(0, self._length)
|
|
1178
1170
|
self._finish_document_shell()
|
|
@@ -1408,7 +1400,7 @@ class ParseEngine:
|
|
|
1408
1400
|
and not self._body_has_content()
|
|
1409
1401
|
and not self._has_open_parser_only_template()
|
|
1410
1402
|
):
|
|
1411
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
1403
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
1412
1404
|
self._after_head = False
|
|
1413
1405
|
self._explicit_head = True
|
|
1414
1406
|
else:
|
|
@@ -1818,7 +1810,7 @@ class ParseEngine:
|
|
|
1818
1810
|
if source_pos is not None: # pragma: no branch - opposite edge requires invalid parser state
|
|
1819
1811
|
source_pos += leading_len
|
|
1820
1812
|
raw = stripped
|
|
1821
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
1813
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
1822
1814
|
self._after_head = False
|
|
1823
1815
|
self._body_mode_seen = True
|
|
1824
1816
|
parent = self._body
|
|
@@ -1826,7 +1818,7 @@ class ParseEngine:
|
|
|
1826
1818
|
if raw_is_space is None: # pragma: no branch - opposite edge requires invalid parser state
|
|
1827
1819
|
raw_is_space = raw.strip(_SPACE) == ""
|
|
1828
1820
|
if not raw_is_space:
|
|
1829
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
1821
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
1830
1822
|
self._after_head = False
|
|
1831
1823
|
self._body_mode_seen = True
|
|
1832
1824
|
parent = self._body
|
|
@@ -1835,7 +1827,7 @@ class ParseEngine:
|
|
|
1835
1827
|
raw_is_space = raw.strip(_SPACE) == ""
|
|
1836
1828
|
if not raw_is_space:
|
|
1837
1829
|
if self._find_open_html_index("body") is None:
|
|
1838
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
1830
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
1839
1831
|
self._after_body = False
|
|
1840
1832
|
self._after_document = False
|
|
1841
1833
|
self._after_html = False
|
|
@@ -2068,9 +2060,9 @@ class ParseEngine:
|
|
|
2068
2060
|
if (
|
|
2069
2061
|
self._frameset_seen and not self._body_explicit
|
|
2070
2062
|
): # pragma: no cover - later state normalization restores body mode
|
|
2071
|
-
self._stack = [self._doc, self._html] # type: ignore[list-item]
|
|
2063
|
+
self._stack = _CountingStack([self._doc, self._html]) # type: ignore[list-item]
|
|
2072
2064
|
else:
|
|
2073
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2065
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2074
2066
|
self._after_head = False
|
|
2075
2067
|
self._body_mode_seen = True
|
|
2076
2068
|
return pos
|
|
@@ -2078,7 +2070,7 @@ class ParseEngine:
|
|
|
2078
2070
|
self._in_colgroup = False
|
|
2079
2071
|
if self._body_mode_seen and self._stack[-1] is not self._head:
|
|
2080
2072
|
return pos
|
|
2081
|
-
self._stack = [self._doc, self._html] # type: ignore[list-item]
|
|
2073
|
+
self._stack = _CountingStack([self._doc, self._html]) # type: ignore[list-item]
|
|
2082
2074
|
self._after_head = True
|
|
2083
2075
|
return pos
|
|
2084
2076
|
if name == "colgroup" and not self._parser_only_template_depth:
|
|
@@ -2119,7 +2111,7 @@ class ParseEngine:
|
|
|
2119
2111
|
and self._stack[-1] is self._head
|
|
2120
2112
|
and self._html is not None
|
|
2121
2113
|
):
|
|
2122
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2114
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2123
2115
|
self._after_head = False
|
|
2124
2116
|
self._body_mode_seen = True
|
|
2125
2117
|
if self._in_head_noscript:
|
|
@@ -2279,7 +2271,7 @@ class ParseEngine:
|
|
|
2279
2271
|
if (
|
|
2280
2272
|
self._find_open_html_index("body") is None
|
|
2281
2273
|
): # pragma: no cover - body-less after-document tags return earlier
|
|
2282
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2274
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2283
2275
|
self._after_body = False
|
|
2284
2276
|
self._after_document = False
|
|
2285
2277
|
self._after_html = False
|
|
@@ -2291,9 +2283,9 @@ class ParseEngine:
|
|
|
2291
2283
|
return pos
|
|
2292
2284
|
self._in_colgroup = False
|
|
2293
2285
|
if self._head is not None and self._stack[-1] is self._head:
|
|
2294
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2286
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2295
2287
|
if self._frameset_seen and not self._body_explicit:
|
|
2296
|
-
self._stack = [self._doc, self._html] # type: ignore[list-item] # pragma: no cover - frameset state keeps later body reconstruction unreachable here
|
|
2288
|
+
self._stack = _CountingStack([self._doc, self._html]) # type: ignore[list-item] # pragma: no cover - frameset state keeps later body reconstruction unreachable here
|
|
2297
2289
|
self._after_head = False
|
|
2298
2290
|
self._body_mode_seen = (
|
|
2299
2291
|
True # pragma: no cover - frameset end-tag paths return before re-entering body mode
|
|
@@ -2311,7 +2303,7 @@ class ParseEngine:
|
|
|
2311
2303
|
self._set_end_span(self._html, name, tag_start, tag_end)
|
|
2312
2304
|
return pos
|
|
2313
2305
|
if self._fragment and self._fragment_context_name == "html" and name == "html":
|
|
2314
|
-
self._stack = [self._doc]
|
|
2306
|
+
self._stack = _CountingStack([self._doc])
|
|
2315
2307
|
return pos
|
|
2316
2308
|
if not self._fragment and name == "head":
|
|
2317
2309
|
self._in_colgroup = False
|
|
@@ -2320,7 +2312,7 @@ class ParseEngine:
|
|
|
2320
2312
|
if self._head is not None: # pragma: no branch - opposite edge requires invalid parser state
|
|
2321
2313
|
if self._track_tag_spans:
|
|
2322
2314
|
self._set_end_span(self._head, name, tag_start, tag_end)
|
|
2323
|
-
self._stack = [self._doc, self._html] # type: ignore[list-item]
|
|
2315
|
+
self._stack = _CountingStack([self._doc, self._html]) # type: ignore[list-item]
|
|
2324
2316
|
self._after_head = True
|
|
2325
2317
|
return pos
|
|
2326
2318
|
if name == "colgroup" and not self._parser_only_template_depth:
|
|
@@ -2383,7 +2375,7 @@ class ParseEngine:
|
|
|
2383
2375
|
and self._stack[-1] is self._head
|
|
2384
2376
|
and self._html is not None
|
|
2385
2377
|
):
|
|
2386
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2378
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2387
2379
|
self._after_head = False
|
|
2388
2380
|
self._body_mode_seen = True
|
|
2389
2381
|
if self._in_head_noscript:
|
|
@@ -2551,7 +2543,7 @@ class ParseEngine:
|
|
|
2551
2543
|
if self._body_mode_seen:
|
|
2552
2544
|
return pos
|
|
2553
2545
|
if self._head is not None: # pragma: no branch - opposite edge requires invalid parser state
|
|
2554
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
2546
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
2555
2547
|
self._after_head = False
|
|
2556
2548
|
return pos
|
|
2557
2549
|
if name == "body":
|
|
@@ -2567,7 +2559,7 @@ class ParseEngine:
|
|
|
2567
2559
|
if self._body_mode_seen:
|
|
2568
2560
|
return pos
|
|
2569
2561
|
self._body_mode_seen = True
|
|
2570
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2562
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2571
2563
|
self._after_head = False
|
|
2572
2564
|
return pos
|
|
2573
2565
|
if not self._body_mode_seen and not (action.head_content if action is not None else False):
|
|
@@ -2581,7 +2573,7 @@ class ParseEngine:
|
|
|
2581
2573
|
and current_top is self._head
|
|
2582
2574
|
and not (action.head_content if action is not None else False)
|
|
2583
2575
|
):
|
|
2584
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2576
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2585
2577
|
self._after_head = False
|
|
2586
2578
|
self._body_mode_seen = True
|
|
2587
2579
|
current_top = self._body
|
|
@@ -2601,11 +2593,11 @@ class ParseEngine:
|
|
|
2601
2593
|
and not self._body_mode_seen
|
|
2602
2594
|
and not self._body_has_content()
|
|
2603
2595
|
):
|
|
2604
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
2596
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
2605
2597
|
self._after_head = False
|
|
2606
2598
|
current_top = self._head
|
|
2607
2599
|
else:
|
|
2608
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2600
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2609
2601
|
self._after_head = False
|
|
2610
2602
|
self._body_mode_seen = True
|
|
2611
2603
|
current_top = self._body
|
|
@@ -2644,7 +2636,7 @@ class ParseEngine:
|
|
|
2644
2636
|
|
|
2645
2637
|
if action is not None and action.drop_content:
|
|
2646
2638
|
if self._raw_head_text_parent(name) and self._head is not None:
|
|
2647
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
2639
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
2648
2640
|
return self._skip_rawtext(name, pos, end)
|
|
2649
2641
|
if action is not None and action.drop_subtree:
|
|
2650
2642
|
if action.allowed: # pragma: no branch - opposite edge requires invalid parser state
|
|
@@ -2823,7 +2815,7 @@ class ParseEngine:
|
|
|
2823
2815
|
in_template_content = bool(self._template_modes)
|
|
2824
2816
|
if not self._fragment and self._after_document and name != "html" and not self._frameset_seen:
|
|
2825
2817
|
if self._find_open_html_index("body") is None:
|
|
2826
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2818
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2827
2819
|
self._after_body = False
|
|
2828
2820
|
self._after_document = False
|
|
2829
2821
|
self._after_html = False
|
|
@@ -2873,7 +2865,7 @@ class ParseEngine:
|
|
|
2873
2865
|
self._head.attrs.setdefault(attr_name, attr_value)
|
|
2874
2866
|
self._set_origin(self._head, tag_start)
|
|
2875
2867
|
self._set_source_span(self._head, tag_start, tag_end)
|
|
2876
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
2868
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
2877
2869
|
self._after_head = False
|
|
2878
2870
|
return pos
|
|
2879
2871
|
if name == "body":
|
|
@@ -2891,7 +2883,7 @@ class ParseEngine:
|
|
|
2891
2883
|
if self._body_mode_seen:
|
|
2892
2884
|
return pos
|
|
2893
2885
|
self._body_mode_seen = True
|
|
2894
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2886
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2895
2887
|
self._after_head = False
|
|
2896
2888
|
return pos
|
|
2897
2889
|
if not self._body_mode_seen and not (action.head_content if action is not None else False):
|
|
@@ -2905,7 +2897,7 @@ class ParseEngine:
|
|
|
2905
2897
|
and current_top is self._head
|
|
2906
2898
|
and not (action.head_content if action is not None else False)
|
|
2907
2899
|
):
|
|
2908
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2900
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2909
2901
|
self._after_head = False
|
|
2910
2902
|
self._body_mode_seen = True
|
|
2911
2903
|
current_top = self._body
|
|
@@ -2926,11 +2918,11 @@ class ParseEngine:
|
|
|
2926
2918
|
and not self._body_mode_seen
|
|
2927
2919
|
and not self._body_has_content()
|
|
2928
2920
|
):
|
|
2929
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
2921
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
2930
2922
|
self._head_reentry = True
|
|
2931
2923
|
current_top = self._head
|
|
2932
2924
|
else:
|
|
2933
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
2925
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
2934
2926
|
self._after_head = False
|
|
2935
2927
|
self._body_mode_seen = True
|
|
2936
2928
|
current_top = self._body
|
|
@@ -3071,7 +3063,7 @@ class ParseEngine:
|
|
|
3071
3063
|
return self._parse_rawtext_element(name, attrs, self_closing, pos, end, tag_start, tag_end)
|
|
3072
3064
|
if action is not None and not raw_mode and action.drop_content:
|
|
3073
3065
|
if self._raw_head_text_parent(name) and self._head is not None:
|
|
3074
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
3066
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
3075
3067
|
return self._skip_rawtext(name, pos, end)
|
|
3076
3068
|
if action is not None and action.rawtext_as_text:
|
|
3077
3069
|
return self._parse_rawtext_as_text(name, pos, end)
|
|
@@ -3086,7 +3078,7 @@ class ParseEngine:
|
|
|
3086
3078
|
or (not self._body_explicit and not self._body_mode_seen and not self._body_has_content())
|
|
3087
3079
|
)
|
|
3088
3080
|
):
|
|
3089
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
3081
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
3090
3082
|
if raw_mode:
|
|
3091
3083
|
self._insert_sanitized_element(
|
|
3092
3084
|
name, attrs, self_closing, self._head, tag_start=tag_start, tag_end=tag_end
|
|
@@ -3149,7 +3141,7 @@ class ParseEngine:
|
|
|
3149
3141
|
if (
|
|
3150
3142
|
not self._fragment and self._html is not None
|
|
3151
3143
|
): # pragma: no branch - opposite edge requires invalid parser state
|
|
3152
|
-
self._stack = [self._doc, self._html]
|
|
3144
|
+
self._stack = _CountingStack([self._doc, self._html])
|
|
3153
3145
|
else:
|
|
3154
3146
|
for idx, stack_node in enumerate(
|
|
3155
3147
|
self._stack[1:], start=1
|
|
@@ -3677,7 +3669,7 @@ class ParseEngine:
|
|
|
3677
3669
|
self._in_head_noscript = False
|
|
3678
3670
|
if self._fragment or self._html is None: # pragma: no branch - opposite edge requires invalid parser state
|
|
3679
3671
|
return # pragma: no cover - unreachable after parser-state guards
|
|
3680
|
-
self._stack = [self._doc, self._html, self._body] # type: ignore[list-item]
|
|
3672
|
+
self._stack = _CountingStack([self._doc, self._html, self._body]) # type: ignore[list-item]
|
|
3681
3673
|
self._after_head = False
|
|
3682
3674
|
self._body_mode_seen = True
|
|
3683
3675
|
|
|
@@ -3687,7 +3679,7 @@ class ParseEngine:
|
|
|
3687
3679
|
if self._open_template_index() is not None: # pragma: no branch - reentry finishes after templates close
|
|
3688
3680
|
return # pragma: no cover - guarded by template close ordering
|
|
3689
3681
|
self._head_reentry = False
|
|
3690
|
-
self._stack = [self._doc, self._html]
|
|
3682
|
+
self._stack = _CountingStack([self._doc, self._html])
|
|
3691
3683
|
self._after_head = True
|
|
3692
3684
|
|
|
3693
3685
|
def _close_to_fragment_context(self) -> bool:
|
|
@@ -4468,7 +4460,7 @@ class ParseEngine:
|
|
|
4468
4460
|
self._set_end_span(node, name, close_start, pos)
|
|
4469
4461
|
self._stack.pop()
|
|
4470
4462
|
if parsed_in_initial_head and self._html is not None and self._head is not None:
|
|
4471
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
4463
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
4472
4464
|
self._finish_head_reentry()
|
|
4473
4465
|
return pos
|
|
4474
4466
|
|
|
@@ -4520,7 +4512,7 @@ class ParseEngine:
|
|
|
4520
4512
|
if self._stack and self._stack[-1] is node: # pragma: no branch - opposite edge requires invalid parser state
|
|
4521
4513
|
self._stack.pop()
|
|
4522
4514
|
if parsed_in_initial_head and self._html is not None and self._head is not None:
|
|
4523
|
-
self._stack = [self._doc, self._html, self._head] # type: ignore[list-item]
|
|
4515
|
+
self._stack = _CountingStack([self._doc, self._html, self._head]) # type: ignore[list-item]
|
|
4524
4516
|
self._finish_head_reentry()
|
|
4525
4517
|
return pos
|
|
4526
4518
|
|
|
@@ -5624,7 +5616,7 @@ class ParseEngine:
|
|
|
5624
5616
|
children.clear()
|
|
5625
5617
|
self._frameset_seen = True
|
|
5626
5618
|
self._mark_active_formatting_dirty()
|
|
5627
|
-
self._stack = [self._doc, self._html] # type: ignore[list-item]
|
|
5619
|
+
self._stack = _CountingStack([self._doc, self._html]) # type: ignore[list-item]
|
|
5628
5620
|
self._after_head = False
|
|
5629
5621
|
return True
|
|
5630
5622
|
|
|
@@ -5642,7 +5634,7 @@ class ParseEngine:
|
|
|
5642
5634
|
self._body, Element
|
|
5643
5635
|
): # pragma: no branch - opposite edge requires invalid parser state
|
|
5644
5636
|
self._remove_child(self._html, self._body)
|
|
5645
|
-
self._stack = [self._doc, self._html]
|
|
5637
|
+
self._stack = _CountingStack([self._doc, self._html])
|
|
5646
5638
|
self._frameset_seen = True
|
|
5647
5639
|
self._mark_active_formatting_dirty()
|
|
5648
5640
|
return True
|