justhtml 3.1.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.1.0 → justhtml-3.2.0}/.github/workflows/ci.yml +9 -8
- {justhtml-3.1.0 → justhtml-3.2.0}/CHANGELOG.md +16 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/CONTRIBUTING.md +3 -3
- {justhtml-3.1.0 → justhtml-3.2.0}/PKG-INFO +4 -4
- {justhtml-3.1.0 → justhtml-3.2.0}/README.md +3 -3
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/correctness.md +17 -14
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/playground/app.js +1 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/pyproject.toml +1 -1
- {justhtml-3.1.0 → justhtml-3.2.0}/run_tests.py +13 -7
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/__init__.py +13 -1
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/core/entities.py +10 -1
- {justhtml-3.1.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.1.0 → justhtml-3.2.0}/src/justhtml/dom/__init__.py +20 -1
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/parser/encoding.py +6 -5
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/parser/engine.py +179 -11
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/parser/stream.py +5 -1
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/serializer/html.py +24 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/transforms/runtime.py +1 -1
- {justhtml-3.1.0 → justhtml-3.2.0}/test-summary.txt +14 -13
- justhtml-3.2.0/tests/README.md +44 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_encoding.py +19 -13
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_entities.py +11 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_node.py +19 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_parser_engine_coverage.py +42 -3
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_parser_regressions.py +42 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_parser_tree.py +2 -4
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_serialize.py +33 -1
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_stream.py +48 -0
- justhtml-3.1.0/tests/README.md +0 -40
- {justhtml-3.1.0 → justhtml-3.2.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/.github/workflows/publish.yml +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/.gitignore +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/.pre-commit-config.yaml +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/LICENSE +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/Makefile +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/SECURITY.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/benchmarks/correctness.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/benchmarks/performance.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/benchmarks/profile.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/_config.yml +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/api.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/assets/search.js +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/bleach-migration.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/building.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/cli.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/comparison.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/encoding.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/errors.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/fragments.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/html-cleaning.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/index.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/linkify.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/migration-examples.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/parser-engine-findings.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/playground/index.html +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/quickstart.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/sanitization.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/selectors.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/streaming.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/text.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/transforms.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/unsafe-handling.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/docs/url-cleaning.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/llms.txt +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/scripts/release.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/parser/scanner.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/harness/tree.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/branch_coverage.dat +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/coverage_gaps.test +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/empty_stack_edge_cases.dat +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/tokenizer_edge_cases.test +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/treebuilder_coverage.dat +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/justhtml-tests/xml_coercion_coverage.test +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_builder.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_cli.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_docs_examples.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_errors.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_sanitize.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_selector.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_transforms.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_transforms_edge_cases.py +0 -0
- {justhtml-3.1.0 → justhtml-3.2.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.1.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,22 @@ 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
|
+
|
|
10
26
|
## [3.1.0] - 2026-07-02
|
|
11
27
|
|
|
12
28
|
### Changed
|
|
@@ -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,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 ..
|
|
@@ -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",
|
|
@@ -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()
|
|
@@ -172,7 +172,7 @@ class Node:
|
|
|
172
172
|
|
|
173
173
|
if name.startswith("#") or name == "!doctype":
|
|
174
174
|
self.namespace = namespace
|
|
175
|
-
if name
|
|
175
|
+
if name in {"#comment", "#processing-instruction"} or name == "!doctype":
|
|
176
176
|
self.children = None
|
|
177
177
|
self.attrs = None
|
|
178
178
|
else:
|
|
@@ -491,6 +491,25 @@ class Comment(Node):
|
|
|
491
491
|
return clone
|
|
492
492
|
|
|
493
493
|
|
|
494
|
+
class ProcessingInstruction(Node):
|
|
495
|
+
__slots__ = ()
|
|
496
|
+
|
|
497
|
+
def __init__(self, data: str | None = None) -> None:
|
|
498
|
+
super().__init__("#processing-instruction", data=data)
|
|
499
|
+
|
|
500
|
+
def clone_node(
|
|
501
|
+
self, deep: bool = False, override_attrs: dict[str, str | None] | None = None
|
|
502
|
+
) -> ProcessingInstruction:
|
|
503
|
+
_ = override_attrs
|
|
504
|
+
_ = deep
|
|
505
|
+
clone = ProcessingInstruction(self.data if isinstance(self.data, str) else None)
|
|
506
|
+
clone._source_html = self._source_html
|
|
507
|
+
clone._origin_pos = self._origin_pos
|
|
508
|
+
clone._origin_line = self._origin_line
|
|
509
|
+
clone._origin_col = self._origin_col
|
|
510
|
+
return clone
|
|
511
|
+
|
|
512
|
+
|
|
494
513
|
class Element(Node):
|
|
495
514
|
__slots__ = (
|
|
496
515
|
"_end_tag_end",
|
|
@@ -345,15 +345,16 @@ def _prescan_for_meta_charset(data: bytes) -> str | None:
|
|
|
345
345
|
|
|
346
346
|
|
|
347
347
|
def sniff_html_encoding(data: bytes, transport_encoding: str | None = None) -> tuple[str, int]:
|
|
348
|
-
#
|
|
349
|
-
transport
|
|
350
|
-
if transport:
|
|
351
|
-
return transport, 0
|
|
352
|
-
|
|
348
|
+
# Per the HTML encoding-sniffing algorithm, a byte-order mark takes
|
|
349
|
+
# priority over a transport-layer declared encoding.
|
|
353
350
|
bom_enc, bom_len = _sniff_bom(data)
|
|
354
351
|
if bom_enc:
|
|
355
352
|
return bom_enc, bom_len
|
|
356
353
|
|
|
354
|
+
transport = normalize_encoding_label(transport_encoding)
|
|
355
|
+
if transport:
|
|
356
|
+
return transport, 0
|
|
357
|
+
|
|
357
358
|
meta_enc = _prescan_for_meta_charset(data)
|
|
358
359
|
if meta_enc:
|
|
359
360
|
return meta_enc, 0
|