justhtml 3.0.0__tar.gz → 3.2.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.0.0 → justhtml-3.2.0}/.github/workflows/ci.yml +9 -8
- {justhtml-3.0.0 → justhtml-3.2.0}/CHANGELOG.md +30 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/CONTRIBUTING.md +3 -3
- {justhtml-3.0.0 → justhtml-3.2.0}/PKG-INFO +4 -4
- {justhtml-3.0.0 → justhtml-3.2.0}/README.md +3 -3
- {justhtml-3.0.0 → justhtml-3.2.0}/benchmarks/profile.py +34 -4
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/api.md +9 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/building.md +27 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/correctness.md +17 -14
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/playground/app.js +1 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/transforms.md +8 -3
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/url-cleaning.md +5 -2
- {justhtml-3.0.0 → justhtml-3.2.0}/pyproject.toml +1 -1
- {justhtml-3.0.0 → justhtml-3.2.0}/run_tests.py +13 -7
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/__init__.py +13 -1
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/core/constants.py +51 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/core/entities.py +10 -1
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/core/rawtext.py +2 -1
- justhtml-3.2.0/src/justhtml/core/text.py +30 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/dom/__init__.py +20 -1
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/parser/encoding.py +6 -5
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/parser/engine.py +207 -20
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/parser/stream.py +5 -1
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/selector/core.py +10 -7
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/serializer/html.py +57 -23
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/transforms/__init__.py +2 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/transforms/compile.py +7 -3
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/transforms/runtime.py +38 -3
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/transforms/spec.py +9 -2
- {justhtml-3.0.0 → justhtml-3.2.0}/test-summary.txt +14 -13
- justhtml-3.2.0/tests/README.md +44 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_encoding.py +19 -13
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_entities.py +11 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_errors.py +19 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_node.py +19 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_parser_engine_coverage.py +66 -5
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_parser_regressions.py +42 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_parser_tree.py +2 -4
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_sanitize.py +30 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_serialize.py +44 -1
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_stream.py +48 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_transforms.py +85 -0
- justhtml-3.0.0/tests/README.md +0 -40
- {justhtml-3.0.0 → justhtml-3.2.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/.github/workflows/publish.yml +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/.gitignore +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/.pre-commit-config.yaml +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/LICENSE +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/Makefile +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/SECURITY.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/benchmarks/correctness.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/benchmarks/performance.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/_config.yml +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/assets/search.js +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/bleach-migration.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/cli.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/comparison.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/encoding.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/errors.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/fragments.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/html-cleaning.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/index.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/linkify.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/migration-examples.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/parser-engine-findings.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/playground/index.html +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/quickstart.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/sanitization.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/selectors.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/streaming.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/text.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/docs/unsafe-handling.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/llms.txt +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/scripts/release.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/parser/scanner.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/harness/tree.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/branch_coverage.dat +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/coverage_gaps.test +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/empty_stack_edge_cases.dat +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/tokenizer_edge_cases.test +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/treebuilder_coverage.dat +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/justhtml-tests/xml_coercion_coverage.test +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_builder.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_cli.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_docs_examples.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_selector.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_transforms_edge_cases.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.0.0 → justhtml-3.2.0}/tests/test_wikipedia.py +0 -0
|
@@ -36,20 +36,21 @@ jobs:
|
|
|
36
36
|
- name: Checkout html5lib-tests
|
|
37
37
|
run: |
|
|
38
38
|
cd ..
|
|
39
|
-
git clone https://github.com/html5lib/html5lib-tests.git
|
|
39
|
+
git clone --no-checkout https://github.com/html5lib/html5lib-tests.git
|
|
40
|
+
git -C html5lib-tests fetch origin 9fb614afaa42ce8787840f057b32084308e76549
|
|
41
|
+
git -C html5lib-tests checkout 9fb614afaa42ce8787840f057b32084308e76549
|
|
40
42
|
git -C html5lib-tests rev-parse HEAD
|
|
41
43
|
|
|
42
|
-
- name: Setup test
|
|
44
|
+
- name: Setup test fixtures
|
|
43
45
|
run: |
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ln -s ../../html5lib-tests/encoding html5lib-tests-encoding
|
|
46
|
+
cp -a ../html5lib-tests/tokenizer tests/html5lib-tests-tokenizer
|
|
47
|
+
cp -a ../html5lib-tests/tree-construction tests/html5lib-tests-tree
|
|
48
|
+
cp -a ../html5lib-tests/serializer tests/html5lib-tests-serializer
|
|
49
|
+
cp -a ../html5lib-tests/encoding tests/html5lib-tests-encoding
|
|
49
50
|
|
|
50
51
|
- name: Run pre-commit
|
|
51
52
|
id: precommit
|
|
52
|
-
|
|
53
|
+
run: pre-commit run --show-diff-on-failure --color=always --all-files
|
|
53
54
|
env:
|
|
54
55
|
SKIP: mypy
|
|
55
56
|
|
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.2.0] - 2026-07-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Use the web platform html5 treebuilder tests from WPT for tree-construction compliance fixtures.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Preserve HTML processing-instruction nodes in unsanitized parsing while keeping default-safe sanitization comment-like and conservative, and neutralize `>` in serialized processing-instruction data so a hand-built DOM or a policy that preserves comment-like nodes cannot serialize a processing instruction that a browser parses as a bogus comment terminating early and exposing following markup.
|
|
17
|
+
- Keep raw-text, RCDATA, doctype, and foreign-content CDATA scans aligned after Unicode characters whose Python lowercase mapping changes length or becomes ASCII. Previously, content such as `İ` before a later `<script>` close tag could make the parser miss the real end tag and treat following markup as raw text.
|
|
18
|
+
|
|
19
|
+
### Performance
|
|
20
|
+
- Short-circuit open-elements-stack scope checks (`<p>`, `<li>`, `<dd>`/`<dt>`, table-cell scoping, and similar) when the target element isn't open anywhere on the stack, avoiding quadratic parse time for deeply nested common block elements such as `<div>`, `<table>`, `<section>`, and heading tags.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
- (Severity: Low) Reject numeric character references with more digits than any valid Unicode code point could have before converting them to an integer. Previously, a decimal reference such as `&#` followed by thousands of digits could crash parsing with an uncaught `ValueError` before sanitization, since Python limits decimal string-to-integer conversion length.
|
|
24
|
+
- (Severity: Moderate) Prioritize a byte-order mark over a caller-declared transport encoding when decoding byte input, matching the HTML encoding-sniffing algorithm. Previously, passing an explicit `encoding=` alongside untrusted bytes that began with a BOM for a different encoding could make JustHTML decode and sanitize content differently than a browser does, letting markup such as `<script>` pass through undetected while a browser executes it under the BOM-indicated encoding.
|
|
25
|
+
|
|
26
|
+
## [3.1.0] - 2026-07-02
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `CollapseWhitespace(...)` now trims leading and trailing whitespace at block-container edges by default. Pass `trim_blocks=False` to keep the previous html5lib-like behavior, or customize `block_tags=(...)` for project-specific block containers.
|
|
30
|
+
- Document that DOM insertion methods adopt existing nodes, and show how to clone nodes before appending when using parsed DOMs as templates.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- Apply explicit `UrlPolicy.allow_rules` to custom URL attributes in constructor sanitization and `DropUrlAttrs(...)`. Previously, rules such as `("el-custom", "data-url")` were ignored unless the attribute name was one of JustHTML's built-in URL sinks.
|
|
34
|
+
- Restore strict-mode reporting for unexpected `</p>` end tags after implicit paragraph closure.
|
|
35
|
+
|
|
36
|
+
### Performance
|
|
37
|
+
- Speed up pretty HTML serialization for direct text-only elements, repeated tag/attribute names, and common empty-attribute elements; reduce repeated selector name normalization and hot parser namespace checks; and fix the real-world profiler script when run directly.
|
|
38
|
+
- Speed up parser URL sanitization for common lowercase `http://` and `https://` attributes.
|
|
39
|
+
|
|
10
40
|
## [3.0.0] - 2026-06-21
|
|
11
41
|
|
|
12
42
|
JustHTML 3.0.0 ships a new parser core. The old tokenizer and tree-builder are now one integrated system, which makes the parser much faster while keeping browser-style HTML5 recovery and default-safe behavior intact. In practice, this release is mainly about getting materially better performance without asking most users to change how they use the library.
|
|
@@ -24,9 +24,9 @@ Thanks for considering contributing to JustHTML! This document explains how to s
|
|
|
24
24
|
|
|
25
25
|
## Running Tests
|
|
26
26
|
|
|
27
|
-
The test suite uses the
|
|
27
|
+
The test suite uses the web platform html5 treebuilder tests plus additional tests for selector functionality.
|
|
28
28
|
|
|
29
|
-
If you want to run the full
|
|
29
|
+
If you want to run the full external fixture suite locally, clone the web platform html5 treebuilder tests and `html5lib-tests` serializer/encoding fixtures next to this repository, then create the symlinks described in [tests/README.md](tests/README.md).
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
# Run all tests
|
|
@@ -58,7 +58,7 @@ Pre-commit runs automatically on every commit and checks:
|
|
|
58
58
|
- **Ruff check** - linting with auto-fix
|
|
59
59
|
- **Ruff format** - code formatting
|
|
60
60
|
- **Tests & Coverage** - full test suite with 100% coverage requirement
|
|
61
|
-
- **Parser Differential** - exact agreement with the reference parser path across scored
|
|
61
|
+
- **Parser Differential** - exact agreement with the reference parser path across scored web platform html5 treebuilder cases
|
|
62
62
|
|
|
63
63
|
Run manually:
|
|
64
64
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: justhtml
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: A pure Python HTML5 parser that just works.
|
|
5
5
|
Project-URL: Homepage, https://github.com/emilstenstrom/justhtml
|
|
6
6
|
Project-URL: Issues, https://github.com/emilstenstrom/justhtml/issues
|
|
@@ -82,7 +82,7 @@ Most Python HTML libraries optimize for one part of the problem.
|
|
|
82
82
|
|
|
83
83
|
JustHTML is for applications that want a boring, inspectable, pure-Python default:
|
|
84
84
|
|
|
85
|
-
- **Correct parsing:** browser-style HTML5 recovery, tested against the
|
|
85
|
+
- **Correct parsing:** browser-style HTML5 recovery, tested against the web platform html5 treebuilder tests.
|
|
86
86
|
- **Safe by default:** `JustHTML(html)` sanitizes before you query or serialize.
|
|
87
87
|
- **One DOM:** parse once, then sanitize, query, transform, serialize, extract text, or produce Markdown.
|
|
88
88
|
- **Easy deployment:** zero runtime dependencies, no compiler, works on PyPy and Pyodide.
|
|
@@ -164,10 +164,10 @@ justhtml index.html --selector "article" --format markdown
|
|
|
164
164
|
|
|
165
165
|
## Correctness
|
|
166
166
|
|
|
167
|
-
JustHTML is tested against the
|
|
167
|
+
JustHTML is tested against the web platform html5 treebuilder tests, plus html5lib serializer/encoding fixtures and project-specific sanitizer, selector, transform, CLI, and regression tests.
|
|
168
168
|
|
|
169
169
|
The current test suite enforces 100% combined line and branch coverage, including the parser engine.
|
|
170
|
-
The parser engine additionally requires exact agreement with the reference path across the
|
|
170
|
+
The parser engine additionally requires exact agreement with the reference path across the web platform html5 treebuilder suite.
|
|
171
171
|
See [Correctness Testing](docs/correctness.md) for details.
|
|
172
172
|
|
|
173
173
|
## Documentation
|
|
@@ -28,7 +28,7 @@ Most Python HTML libraries optimize for one part of the problem.
|
|
|
28
28
|
|
|
29
29
|
JustHTML is for applications that want a boring, inspectable, pure-Python default:
|
|
30
30
|
|
|
31
|
-
- **Correct parsing:** browser-style HTML5 recovery, tested against the
|
|
31
|
+
- **Correct parsing:** browser-style HTML5 recovery, tested against the web platform html5 treebuilder tests.
|
|
32
32
|
- **Safe by default:** `JustHTML(html)` sanitizes before you query or serialize.
|
|
33
33
|
- **One DOM:** parse once, then sanitize, query, transform, serialize, extract text, or produce Markdown.
|
|
34
34
|
- **Easy deployment:** zero runtime dependencies, no compiler, works on PyPy and Pyodide.
|
|
@@ -110,10 +110,10 @@ justhtml index.html --selector "article" --format markdown
|
|
|
110
110
|
|
|
111
111
|
## Correctness
|
|
112
112
|
|
|
113
|
-
JustHTML is tested against the
|
|
113
|
+
JustHTML is tested against the web platform html5 treebuilder tests, plus html5lib serializer/encoding fixtures and project-specific sanitizer, selector, transform, CLI, and regression tests.
|
|
114
114
|
|
|
115
115
|
The current test suite enforces 100% combined line and branch coverage, including the parser engine.
|
|
116
|
-
The parser engine additionally requires exact agreement with the reference path across the
|
|
116
|
+
The parser engine additionally requires exact agreement with the reference path across the web platform html5 treebuilder suite.
|
|
117
117
|
See [Correctness Testing](docs/correctness.md) for details.
|
|
118
118
|
|
|
119
119
|
## Documentation
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
"""Profile JustHTML on real-world HTML."""
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
-
import
|
|
5
|
+
import importlib
|
|
6
6
|
import pathlib
|
|
7
7
|
import pstats
|
|
8
|
+
import sys
|
|
8
9
|
import tarfile
|
|
9
10
|
|
|
10
11
|
import zstandard as zstd
|
|
@@ -49,15 +50,30 @@ def load_html_files(batch_path: pathlib.Path, dict_bytes: bytes, limit: int = 10
|
|
|
49
50
|
|
|
50
51
|
def parse_args() -> argparse.Namespace:
|
|
51
52
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--mode",
|
|
55
|
+
choices=["parse", "compact-html", "pretty-html", "text", "fragment"],
|
|
56
|
+
default="parse",
|
|
57
|
+
help="Pipeline to profile (default: parse)",
|
|
58
|
+
)
|
|
52
59
|
parser.add_argument(
|
|
53
60
|
"--to-html",
|
|
54
61
|
action="store_true",
|
|
55
|
-
help="
|
|
62
|
+
help="Deprecated alias for --mode pretty-html",
|
|
56
63
|
)
|
|
57
64
|
return parser.parse_args()
|
|
58
65
|
|
|
59
66
|
|
|
67
|
+
def remove_script_dir_from_import_path() -> None:
|
|
68
|
+
script_dir = str(pathlib.Path(__file__).resolve().parent)
|
|
69
|
+
if sys.path and sys.path[0] == script_dir:
|
|
70
|
+
sys.path.pop(0)
|
|
71
|
+
|
|
72
|
+
|
|
60
73
|
def main() -> None:
|
|
74
|
+
remove_script_dir_from_import_path()
|
|
75
|
+
cprofile = importlib.import_module("cProfile")
|
|
76
|
+
|
|
61
77
|
args = parse_args()
|
|
62
78
|
|
|
63
79
|
dict_bytes = load_dict(pathlib.Path("/home/emilstenstrom/Projects/web100k/html.dict"))
|
|
@@ -69,12 +85,26 @@ def main() -> None:
|
|
|
69
85
|
|
|
70
86
|
print(f"Loaded {len(html_files)} files")
|
|
71
87
|
|
|
72
|
-
|
|
88
|
+
mode = "pretty-html" if args.to_html else args.mode
|
|
89
|
+
|
|
90
|
+
profiler = cprofile.Profile()
|
|
73
91
|
profiler.enable()
|
|
74
92
|
|
|
75
93
|
for _filename, html in html_files:
|
|
94
|
+
if mode == "fragment":
|
|
95
|
+
parser = JustHTML(html, fragment=True)
|
|
96
|
+
_ = parser.root
|
|
97
|
+
continue
|
|
98
|
+
|
|
76
99
|
parser = JustHTML(html)
|
|
77
|
-
|
|
100
|
+
if mode == "compact-html":
|
|
101
|
+
_ = parser.to_html(pretty=False)
|
|
102
|
+
elif mode == "pretty-html":
|
|
103
|
+
_ = parser.to_html()
|
|
104
|
+
elif mode == "text":
|
|
105
|
+
_ = parser.to_text()
|
|
106
|
+
else:
|
|
107
|
+
_ = parser.root
|
|
78
108
|
|
|
79
109
|
profiler.disable()
|
|
80
110
|
|
|
@@ -408,10 +408,16 @@ When you build documents with `JustHTML(..., sanitize=True)` (the default), this
|
|
|
408
408
|
|
|
409
409
|
Append a child node to this node.
|
|
410
410
|
|
|
411
|
+
If `node` already has a parent, it is moved from that parent to this node. Use
|
|
412
|
+
`node.clone_node(deep=True)` before appending when you want to keep the source
|
|
413
|
+
DOM unchanged.
|
|
414
|
+
|
|
411
415
|
#### `insert_before(node, reference_node)`
|
|
412
416
|
|
|
413
417
|
Insert `node` before `reference_node` (or append if `reference_node` is `None`).
|
|
414
418
|
|
|
419
|
+
If `node` already has a parent, it is moved from that parent to this node.
|
|
420
|
+
|
|
415
421
|
#### `remove_child(node)`
|
|
416
422
|
|
|
417
423
|
Remove a direct child node.
|
|
@@ -420,6 +426,9 @@ Remove a direct child node.
|
|
|
420
426
|
|
|
421
427
|
Replace a direct child node with a new node.
|
|
422
428
|
|
|
429
|
+
If `new_node` already has a parent, it is moved from that parent before it
|
|
430
|
+
replaces `old_node`.
|
|
431
|
+
|
|
423
432
|
#### `clone_node(deep=False, override_attrs=None)`
|
|
424
433
|
|
|
425
434
|
Clone this node. If `deep=True`, children are cloned recursively.
|
|
@@ -435,6 +435,33 @@ lead = doc.query_one("p.lead")
|
|
|
435
435
|
lead.to_html(pretty=False) # => <p class="lead">Hello</p>
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
+
### Clone from template DOMs before appending
|
|
439
|
+
|
|
440
|
+
DOM insertion methods adopt existing nodes. If you append a node that already
|
|
441
|
+
belongs to another tree, it is removed from its old parent and moved into the new
|
|
442
|
+
tree.
|
|
443
|
+
|
|
444
|
+
When you use one parsed DOM as a template for another document, clone the source
|
|
445
|
+
node before appending it:
|
|
446
|
+
|
|
447
|
+
```python
|
|
448
|
+
from justhtml import JustHTML
|
|
449
|
+
from justhtml.dom.builder import element
|
|
450
|
+
|
|
451
|
+
template = JustHTML('<section><p class="lead">Hello</p></section>', fragment=True, sanitize=False)
|
|
452
|
+
output = JustHTML(element("main"), fragment=True, sanitize=False)
|
|
453
|
+
|
|
454
|
+
lead = template.query_one("p.lead")
|
|
455
|
+
assert lead is not None
|
|
456
|
+
main = output.query_one("main")
|
|
457
|
+
assert main is not None
|
|
458
|
+
|
|
459
|
+
main.append_child(lead.clone_node(deep=True))
|
|
460
|
+
|
|
461
|
+
template.to_html(pretty=False) # => <section><p class="lead">Hello</p></section>
|
|
462
|
+
output.to_html(pretty=False) # => <main><p class="lead">Hello</p></main>
|
|
463
|
+
```
|
|
464
|
+
|
|
438
465
|
## When Not to Use the Builder
|
|
439
466
|
|
|
440
467
|
Don’t use the builder just because it exists.
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
# Correctness Testing
|
|
4
4
|
|
|
5
|
-
JustHTML is
|
|
5
|
+
JustHTML is tested against the web platform html5 treebuilder tests. This page explains how we verify and maintain that compliance.
|
|
6
6
|
|
|
7
|
-
## The
|
|
7
|
+
## The Web Platform Tests
|
|
8
8
|
|
|
9
|
-
The [
|
|
9
|
+
The web platform html5 treebuilder tests live in [web-platform-tests/wpt](https://github.com/web-platform-tests/wpt/tree/master/html/syntax/parsing/resources). Serializer and encoding fixtures remain in [html5lib-tests](https://github.com/html5lib/html5lib-tests).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The external fixture inputs contain:
|
|
12
12
|
|
|
13
|
-
- **
|
|
13
|
+
- **61 treebuilder test files** - Testing how the parser builds the DOM tree
|
|
14
14
|
- **5 serializer fixture files** - Testing how token streams are serialized back to HTML
|
|
15
15
|
- **Encoding sniffing tests** - Testing BOM/meta charset/transport overrides and legacy fallbacks
|
|
16
|
-
- **1,
|
|
16
|
+
- **1,914 enabled treebuilder cases** - Covering edge cases, error recovery, and spec compliance
|
|
17
17
|
|
|
18
18
|
### What the Tests Cover
|
|
19
19
|
|
|
@@ -54,12 +54,11 @@ This tests the adoption agency algorithm - when `</b>` is encountered inside `<p
|
|
|
54
54
|
## Compliance Comparison
|
|
55
55
|
|
|
56
56
|
We run the same test suite against other Python parsers to compare compliance.
|
|
57
|
-
The cross-parser snapshot below used the 1,743 cases available when it was
|
|
58
|
-
recorded; the current JustHTML gate covers 1,791 enabled cases.
|
|
57
|
+
The cross-parser snapshot below used the 1,743 cases available when it was recorded.
|
|
59
58
|
|
|
60
59
|
| Parser | Tests Passed | Compliance | Notes |
|
|
61
60
|
|--------|-------------|------------|-------|
|
|
62
|
-
| **JustHTML** | 1743/1743 | **100%** | Full spec compliance in this comparison snapshot
|
|
61
|
+
| **JustHTML** | 1743/1743 | **100%** | Full spec compliance in this comparison snapshot |
|
|
63
62
|
| selectolax | 1743/1743 | 100% | C-based (Lexbor), fast and spec-compliant with dev `html5test` output API |
|
|
64
63
|
| markupever | 1545/1743 | 89% | Rust-based (html5ever), mostly correct |
|
|
65
64
|
| html5lib | 1496/1743 | 86% | Reference implementation, but incomplete |
|
|
@@ -68,9 +67,9 @@ recorded; the current JustHTML gate covers 1,791 enabled cases.
|
|
|
68
67
|
| html.parser | 6/1743 | <1% | Python stdlib, basic error recovery only |
|
|
69
68
|
| lxml | 5/1743 | <1% | XML-based, not HTML5 compliant |
|
|
70
69
|
|
|
71
|
-
*Run `python benchmarks/correctness.py` to reproduce these results. The selectolax score requires its dev `html5test` output and fragment-context APIs. These scores were refreshed against html5lib-tests `e446320`.*
|
|
70
|
+
*Run `python benchmarks/correctness.py` to reproduce these results. The selectolax score requires its dev `html5test` output and fragment-context APIs. These historical scores were refreshed against html5lib-tests `e446320`.*
|
|
72
71
|
|
|
73
|
-
These numbers come from a strict tree comparison against the expected output in the
|
|
72
|
+
These numbers come from a strict tree comparison against the expected output in the treebuilder fixtures available at the time (excluding `#script-on` / `#script-off` cases). Unsupported parser capabilities count as failures for this compliance table. The numbers will not match the `html5lib` project’s own reported totals, because `html5lib` runs the suite in multiple configurations and also has its own skip/xfail lists.
|
|
74
73
|
|
|
75
74
|
## Our Testing Strategy
|
|
76
75
|
|
|
@@ -123,7 +122,7 @@ PYTHONPATH=src python benchmarks/html5lib_engine_diff.py \
|
|
|
123
122
|
--fail-on-current-exceptions
|
|
124
123
|
```
|
|
125
124
|
|
|
126
|
-
This requires exact agreement with the reference parser path across every scored
|
|
125
|
+
This requires exact agreement with the reference parser path across every scored web platform html5 treebuilder case.
|
|
127
126
|
|
|
128
127
|
### 3. Fuzz Testing (millions of cases)
|
|
129
128
|
|
|
@@ -169,14 +168,18 @@ We maintain additional tests in `tests/justhtml-tests/` for:
|
|
|
169
168
|
### Quick Start
|
|
170
169
|
|
|
171
170
|
```bash
|
|
172
|
-
# Clone the test
|
|
171
|
+
# Clone the test suites (one-time setup)
|
|
172
|
+
cd ..
|
|
173
|
+
git clone --filter=blob:none --sparse https://github.com/web-platform-tests/wpt.git
|
|
174
|
+
cd wpt
|
|
175
|
+
git sparse-checkout set html/syntax/parsing/resources
|
|
173
176
|
cd ..
|
|
174
177
|
git clone https://github.com/html5lib/html5lib-tests.git
|
|
175
178
|
cd justhtml
|
|
176
179
|
|
|
177
180
|
# Create symlinks
|
|
178
181
|
cd tests
|
|
179
|
-
ln -s ../../
|
|
182
|
+
ln -s ../../wpt/html/syntax/parsing/resources html5lib-tests-tree
|
|
180
183
|
ln -s ../../html5lib-tests/serializer html5lib-tests-serializer
|
|
181
184
|
ln -s ../../html5lib-tests/encoding html5lib-tests-encoding
|
|
182
185
|
cd ..
|
|
@@ -268,11 +268,16 @@ Advanced building blocks (useful for policy-driven pipelines):
|
|
|
268
268
|
|
|
269
269
|
See [`Linkify(...)`](linkify.md) for full documentation and examples.
|
|
270
270
|
|
|
271
|
-
### `CollapseWhitespace(skip_tags=(...), enabled=True, callback=None, report=None)`
|
|
271
|
+
### `CollapseWhitespace(skip_tags=(...), trim_blocks=True, block_tags=(...), enabled=True, callback=None, report=None)`
|
|
272
272
|
|
|
273
273
|
Collapses runs of HTML whitespace characters in text nodes to a single space.
|
|
274
|
+
By default, it also trims leading and trailing whitespace at the edges of block
|
|
275
|
+
containers such as `<p>`, `<div>`, and `<li>`.
|
|
274
276
|
|
|
275
|
-
|
|
277
|
+
Pass `trim_blocks=False` for html5lib-like whitespace filtering without edge
|
|
278
|
+
trimming. Use `block_tags=(...)` to define which containers trim their edge
|
|
279
|
+
whitespace; `skip_tags` still wins and preserves whitespace inside matching
|
|
280
|
+
tags.
|
|
276
281
|
|
|
277
282
|
By default it skips `<pre>`, `<textarea>`, `<code>`, `<title>`, `<script>`, and `<style>`.
|
|
278
283
|
|
|
@@ -280,7 +285,7 @@ By default it skips `<pre>`, `<textarea>`, `<code>`, `<title>`, `<script>`, and
|
|
|
280
285
|
from justhtml import CollapseWhitespace, JustHTML
|
|
281
286
|
|
|
282
287
|
doc = JustHTML(
|
|
283
|
-
"<p
|
|
288
|
+
"<p>\n Hello \n\t world\n</p><pre>a b</pre>",
|
|
284
289
|
fragment=True,
|
|
285
290
|
transforms=[CollapseWhitespace()],
|
|
286
291
|
)
|
|
@@ -85,7 +85,8 @@ Output:
|
|
|
85
85
|
|
|
86
86
|
## How URL cleaning works (in order)
|
|
87
87
|
|
|
88
|
-
For a URL-like attribute (like `img[src]` or `a[href]`)
|
|
88
|
+
For a URL-like attribute (like `img[src]` or `a[href]`) or a custom attribute with an explicit
|
|
89
|
+
`UrlPolicy(allow_rules=...)` entry, JustHTML applies these steps:
|
|
89
90
|
|
|
90
91
|
1. The tag must be allowed by `SanitizationPolicy.allowed_tags`.
|
|
91
92
|
2. The attribute name must be allowed by `SanitizationPolicy.allowed_attributes`.
|
|
@@ -103,7 +104,9 @@ URL behavior is controlled by `UrlPolicy`:
|
|
|
103
104
|
- `default_handling`: the default post-validation action for URL-like attributes that match an allow rule and do not set `UrlRule.handling`.
|
|
104
105
|
- `default_allow_relative`: whether **relative** URLs (like `/path`, `./path`, `../path`, `?q`) are allowed by default.
|
|
105
106
|
|
|
106
|
-
For URL-like attributes that match an explicit `(tag, attr)`
|
|
107
|
+
For URL-like attributes, or custom attributes that match an explicit `(tag, attr)` or `("*", attr)` rule in
|
|
108
|
+
`allow_rules`, validated URLs use `UrlPolicy.default_handling` unless that specific `UrlRule` overrides it with
|
|
109
|
+
`handling=...`.
|
|
107
110
|
|
|
108
111
|
Note: URL validation is always enforced by `UrlRule`.
|
|
109
112
|
|
|
@@ -114,7 +114,7 @@ def parse_args():
|
|
|
114
114
|
"--check-errors",
|
|
115
115
|
action="store_true",
|
|
116
116
|
help=(
|
|
117
|
-
"Enable additional error validation. For
|
|
117
|
+
"Enable additional error validation. For web platform html5 treebuilder .dat tests, "
|
|
118
118
|
"validates the number of parse errors; for tests/justhtml-tests, validates the exact "
|
|
119
119
|
"ordered list of error codes."
|
|
120
120
|
"parse errors (code+line+col) when provided by the fixture."
|
|
@@ -295,7 +295,7 @@ def main():
|
|
|
295
295
|
run_encoding = suite in {"all", "encoding"}
|
|
296
296
|
run_unit = suite in {"all", "unit"}
|
|
297
297
|
|
|
298
|
-
# Check that
|
|
298
|
+
# Check that external fixture symlinks exist (only for the selected suites)
|
|
299
299
|
tree_tests = test_dir / "html5lib-tests-tree"
|
|
300
300
|
serializer_tests = test_dir / "html5lib-tests-serializer"
|
|
301
301
|
encoding_tests = test_dir / "html5lib-tests-encoding"
|
|
@@ -307,13 +307,19 @@ def main():
|
|
|
307
307
|
if run_encoding and not encoding_tests.exists():
|
|
308
308
|
missing.append(str(encoding_tests))
|
|
309
309
|
if len(missing) > 0:
|
|
310
|
-
print("ERROR:
|
|
310
|
+
print("ERROR: external test fixtures not found. Please create symlinks:", file=sys.stderr)
|
|
311
311
|
for path in missing:
|
|
312
312
|
print(f" {path}", file=sys.stderr)
|
|
313
|
-
print("\nTo set up, clone
|
|
314
|
-
print(" git clone https://github.com/html5lib/html5lib-tests.git ../html5lib-tests", file=sys.stderr)
|
|
313
|
+
print("\nTo set up, clone the fixture repositories and create symlinks:", file=sys.stderr)
|
|
315
314
|
if run_tree:
|
|
316
|
-
print(
|
|
315
|
+
print(
|
|
316
|
+
" git clone --filter=blob:none --sparse https://github.com/web-platform-tests/wpt.git ../wpt",
|
|
317
|
+
file=sys.stderr,
|
|
318
|
+
)
|
|
319
|
+
print(" (cd ../wpt && git sparse-checkout set html/syntax/parsing/resources)", file=sys.stderr)
|
|
320
|
+
print(" ln -s ../../wpt/html/syntax/parsing/resources tests/html5lib-tests-tree", file=sys.stderr)
|
|
321
|
+
if run_serializer or run_encoding:
|
|
322
|
+
print(" git clone https://github.com/html5lib/html5lib-tests.git ../html5lib-tests", file=sys.stderr)
|
|
317
323
|
if run_serializer:
|
|
318
324
|
print(" ln -s ../../html5lib-tests/serializer tests/html5lib-tests-serializer", file=sys.stderr)
|
|
319
325
|
if run_encoding:
|
|
@@ -343,7 +349,7 @@ def main():
|
|
|
343
349
|
combined_results.update(runner.file_results)
|
|
344
350
|
|
|
345
351
|
if run_justhtml_tree:
|
|
346
|
-
# Run JustHTML-specific
|
|
352
|
+
# Run JustHTML-specific treebuilder tests (custom .dat fixtures).
|
|
347
353
|
# These live outside the upstream html5lib-tests checkout.
|
|
348
354
|
justhtml_tree_tests = test_dir / "justhtml-tests"
|
|
349
355
|
justhtml_runner = TestRunner(justhtml_tree_tests, config)
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
from .core.types import ParseError
|
|
2
|
-
from .dom import
|
|
2
|
+
from .dom import (
|
|
3
|
+
Comment,
|
|
4
|
+
Document,
|
|
5
|
+
DocumentFragment,
|
|
6
|
+
Element,
|
|
7
|
+
Node,
|
|
8
|
+
NodeType,
|
|
9
|
+
ProcessingInstruction,
|
|
10
|
+
QueryMatch,
|
|
11
|
+
Template,
|
|
12
|
+
Text,
|
|
13
|
+
)
|
|
3
14
|
from .parser import JustHTML, StrictModeError
|
|
4
15
|
from .parser.stream import stream
|
|
5
16
|
from .sanitizer import (
|
|
@@ -70,6 +81,7 @@ __all__ = [
|
|
|
70
81
|
"Node",
|
|
71
82
|
"NodeType",
|
|
72
83
|
"ParseError",
|
|
84
|
+
"ProcessingInstruction",
|
|
73
85
|
"PruneEmpty",
|
|
74
86
|
"QueryMatch",
|
|
75
87
|
"SanitizationPolicy",
|
|
@@ -200,6 +200,57 @@ WHITESPACE_PRESERVING_ELEMENTS: Final[frozenset[str]] = frozenset(
|
|
|
200
200
|
}
|
|
201
201
|
)
|
|
202
202
|
|
|
203
|
+
WHITESPACE_TRIMMING_BLOCK_ELEMENTS: Final[frozenset[str]] = frozenset(
|
|
204
|
+
{
|
|
205
|
+
"address",
|
|
206
|
+
"article",
|
|
207
|
+
"aside",
|
|
208
|
+
"blockquote",
|
|
209
|
+
"body",
|
|
210
|
+
"caption",
|
|
211
|
+
"center",
|
|
212
|
+
"dd",
|
|
213
|
+
"details",
|
|
214
|
+
"dialog",
|
|
215
|
+
"dir",
|
|
216
|
+
"div",
|
|
217
|
+
"dl",
|
|
218
|
+
"dt",
|
|
219
|
+
"fieldset",
|
|
220
|
+
"figcaption",
|
|
221
|
+
"figure",
|
|
222
|
+
"footer",
|
|
223
|
+
"form",
|
|
224
|
+
"h1",
|
|
225
|
+
"h2",
|
|
226
|
+
"h3",
|
|
227
|
+
"h4",
|
|
228
|
+
"h5",
|
|
229
|
+
"h6",
|
|
230
|
+
"header",
|
|
231
|
+
"hgroup",
|
|
232
|
+
"html",
|
|
233
|
+
"li",
|
|
234
|
+
"listing",
|
|
235
|
+
"main",
|
|
236
|
+
"menu",
|
|
237
|
+
"nav",
|
|
238
|
+
"ol",
|
|
239
|
+
"p",
|
|
240
|
+
"search",
|
|
241
|
+
"section",
|
|
242
|
+
"summary",
|
|
243
|
+
"table",
|
|
244
|
+
"tbody",
|
|
245
|
+
"td",
|
|
246
|
+
"tfoot",
|
|
247
|
+
"th",
|
|
248
|
+
"thead",
|
|
249
|
+
"tr",
|
|
250
|
+
"ul",
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
203
254
|
FORMATTING_ELEMENTS = {
|
|
204
255
|
"a",
|
|
205
256
|
"b",
|
|
@@ -199,7 +199,16 @@ def decode_numeric_entity(
|
|
|
199
199
|
The decoded character, or None if invalid
|
|
200
200
|
"""
|
|
201
201
|
base = 16 if is_hex else 10
|
|
202
|
-
|
|
202
|
+
|
|
203
|
+
# A numeric character reference this long is already guaranteed to exceed
|
|
204
|
+
# the valid Unicode range, so treat it as out-of-range without calling
|
|
205
|
+
# int() on an attacker-controlled digit string long enough to exceed
|
|
206
|
+
# Python's integer-string conversion limit (see sys.set_int_max_str_digits).
|
|
207
|
+
digits = text.lstrip("0") or "0"
|
|
208
|
+
if len(digits) > 16:
|
|
209
|
+
return "\ufffd" # REPLACEMENT CHARACTER
|
|
210
|
+
|
|
211
|
+
codepoint = int(digits, base)
|
|
203
212
|
|
|
204
213
|
# Invalid ranges per HTML5 spec
|
|
205
214
|
if codepoint > 0x10FFFF:
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from .constants import HTML_SPACE_OR_TAG_END_CHARACTERS
|
|
4
|
+
from .text import ascii_lower
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def neutralize_rawtext_end_tag_sequences(text: str, tag_name: str) -> tuple[str, bool]:
|
|
7
8
|
if not text:
|
|
8
9
|
return text, False
|
|
9
10
|
|
|
10
|
-
lower_text = text
|
|
11
|
+
lower_text = ascii_lower(text)
|
|
11
12
|
needle = f"</{tag_name}"
|
|
12
13
|
needle_len = len(needle)
|
|
13
14
|
out: list[str] = []
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import string
|
|
4
|
+
|
|
5
|
+
_ASCII_LOWER_TABLE = str.maketrans(string.ascii_uppercase, string.ascii_lowercase)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ascii_lower(text: str) -> str:
|
|
9
|
+
"""Fold A-Z for ASCII marker searches that reuse original offsets.
|
|
10
|
+
|
|
11
|
+
Some parser and serializer paths search a folded copy of full text for
|
|
12
|
+
ASCII HTML markers such as ``</script``, ``<!doctype``, and ``<![cdata[``.
|
|
13
|
+
Match offsets are then reused against the original string, so the folded
|
|
14
|
+
copy must preserve length, and it must not invent ASCII marker characters
|
|
15
|
+
from non-ASCII text content. HTML markup matching is ASCII
|
|
16
|
+
case-insensitive; it is not Unicode case-insensitive.
|
|
17
|
+
|
|
18
|
+
``str.lower()`` is still the fast path when it is provably equivalent for
|
|
19
|
+
these searches. In Python's Unicode data, U+0130 LATIN CAPITAL LETTER I
|
|
20
|
+
WITH DOT ABOVE is the only character whose lowercase mapping changes
|
|
21
|
+
length, and U+212A KELVIN SIGN is the only non-ASCII character whose
|
|
22
|
+
lowercase mapping becomes an ASCII letter (``k``). Inputs containing either
|
|
23
|
+
shape take the strict A-Z translation path. That path deliberately keeps
|
|
24
|
+
U+212A as U+212A; it is not a tag-name fold.
|
|
25
|
+
"""
|
|
26
|
+
if text.isascii():
|
|
27
|
+
return text.lower()
|
|
28
|
+
if "\u0130" in text or "\u212a" in text:
|
|
29
|
+
return text.translate(_ASCII_LOWER_TABLE)
|
|
30
|
+
return text.lower()
|