justhtml 3.3.0__tar.gz → 3.5.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.3.0 → justhtml-3.5.0}/.github/workflows/ci.yml +5 -7
- {justhtml-3.3.0 → justhtml-3.5.0}/.github/workflows/publish.yml +2 -2
- {justhtml-3.3.0 → justhtml-3.5.0}/.pre-commit-config.yaml +1 -1
- justhtml-3.5.0/AGENTS.md +10 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/CHANGELOG.md +42 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/CONTRIBUTING.md +40 -4
- justhtml-3.5.0/PERFORMANCE.md +84 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/PKG-INFO +2 -2
- {justhtml-3.3.0 → justhtml-3.5.0}/SECURITY.md +25 -1
- {justhtml-3.3.0 → justhtml-3.5.0}/benchmarks/correctness.py +117 -2
- {justhtml-3.3.0 → justhtml-3.5.0}/benchmarks/performance.py +57 -146
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/bleach-migration.md +44 -52
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/cli.md +1 -1
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/comparison.md +20 -16
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/correctness.md +5 -19
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/errors.md +5 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/fragments.md +5 -5
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/html-cleaning.md +66 -96
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/quickstart.md +3 -3
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/streaming.md +1 -1
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/text.md +1 -1
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/transforms.md +5 -5
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/unsafe-handling.md +2 -2
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/url-cleaning.md +7 -7
- {justhtml-3.3.0 → justhtml-3.5.0}/pyproject.toml +2 -2
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/entities.py +13 -6
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/parser/__init__.py +44 -17
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/parser/engine.py +223 -68
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/policy.py +0 -8
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/url/runtime.py +12 -6
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/serializer/html.py +63 -18
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/transforms/__init__.py +1 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/transforms/compile.py +3 -20
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/transforms/linkify_core.py +3 -1
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/transforms/runtime.py +31 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/test-summary.txt +18 -11
- justhtml-3.5.0/tests/justhtml-tests/character_preprocessing.dat +110 -0
- justhtml-3.5.0/tests/justhtml-tests/document_shell_recovery.dat +93 -0
- justhtml-3.5.0/tests/justhtml-tests/foreign_content.dat +135 -0
- justhtml-3.5.0/tests/justhtml-tests/frameset_recovery.dat +60 -0
- justhtml-3.5.0/tests/justhtml-tests/head_noscript_recovery.dat +25 -0
- justhtml-3.5.0/tests/justhtml-tests/in_body_recovery.dat +12 -0
- justhtml-3.5.0/tests/justhtml-tests/select_recovery.dat +152 -0
- justhtml-3.5.0/tests/justhtml-tests/selectedcontent.dat +76 -0
- justhtml-3.5.0/tests/justhtml-tests/table_recovery.dat +88 -0
- justhtml-3.5.0/tests/justhtml-tests/template_recovery.dat +294 -0
- justhtml-3.3.0/tests/justhtml-tests/tokenizer_edge_cases.test → justhtml-3.5.0/tests/justhtml-tests/tokenizer_states.test +1 -1
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_docs_examples.py +1 -1
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_entities.py +5 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_errors.py +28 -1
- justhtml-3.3.0/tests/test_parser_engine_coverage.py → justhtml-3.5.0/tests/test_parser_engine.py +133 -8
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_sanitize.py +72 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_serialize.py +1 -0
- justhtml-3.3.0/tests/test_transforms_edge_cases.py → justhtml-3.5.0/tests/test_transform_runtime.py +30 -45
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_transforms.py +40 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_transforms_sanitize_integration.py +16 -0
- justhtml-3.3.0/tests/justhtml-tests/branch_coverage.dat +0 -450
- justhtml-3.3.0/tests/justhtml-tests/treebuilder_coverage.dat +0 -254
- {justhtml-3.3.0 → justhtml-3.5.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/.gitignore +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/LICENSE +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/Makefile +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/README.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/benchmarks/profile.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/_config.yml +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/api.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/assets/search.js +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/building.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/encoding.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/index.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/linkify.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/migration-examples.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/playground/app.js +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/playground/index.html +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/sanitization.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/docs/selectors.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/llms.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/run_tests.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/scripts/release.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/text.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/parser/scanner.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/parser/stream.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/README.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/harness/tree.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/justhtml-tests/entities.test +0 -0
- /justhtml-3.3.0/tests/justhtml-tests/empty_stack_edge_cases.dat → /justhtml-3.5.0/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- /justhtml-3.3.0/tests/justhtml-tests/coverage_gaps.test → /justhtml-3.5.0/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- /justhtml-3.3.0/tests/justhtml-tests/xml_coercion_coverage.test → /justhtml-3.5.0/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_builder.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_cli.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_encoding.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_node.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_parser_regressions.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_parser_tree.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_selector.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_stream.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.3.0 → justhtml-3.5.0}/tests/test_wikipedia.py +0 -0
|
@@ -20,10 +20,10 @@ jobs:
|
|
|
20
20
|
PYTHONHASHSEED: "0"
|
|
21
21
|
|
|
22
22
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
23
|
+
- uses: actions/checkout@v5
|
|
24
24
|
|
|
25
25
|
- name: Set up Python ${{ matrix.python-version }}
|
|
26
|
-
uses: actions/setup-python@
|
|
26
|
+
uses: actions/setup-python@v6
|
|
27
27
|
with:
|
|
28
28
|
python-version: ${{ matrix.python-version }}
|
|
29
29
|
cache: "pip"
|
|
@@ -45,8 +45,6 @@ jobs:
|
|
|
45
45
|
run: |
|
|
46
46
|
cd ..
|
|
47
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
48
|
git -C wpt sparse-checkout set html/syntax/parsing/resources
|
|
51
49
|
git -C wpt rev-parse HEAD
|
|
52
50
|
|
|
@@ -84,15 +82,15 @@ jobs:
|
|
|
84
82
|
test-pyodide:
|
|
85
83
|
runs-on: ubuntu-latest
|
|
86
84
|
steps:
|
|
87
|
-
- uses: actions/checkout@
|
|
85
|
+
- uses: actions/checkout@v5
|
|
88
86
|
|
|
89
87
|
- name: Set up Node.js
|
|
90
|
-
uses: actions/setup-node@
|
|
88
|
+
uses: actions/setup-node@v5
|
|
91
89
|
with:
|
|
92
90
|
node-version: "20"
|
|
93
91
|
|
|
94
92
|
- name: Set up Python
|
|
95
|
-
uses: actions/setup-python@
|
|
93
|
+
uses: actions/setup-python@v6
|
|
96
94
|
with:
|
|
97
95
|
python-version: "3.12"
|
|
98
96
|
|
|
@@ -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 && coverage report'
|
|
36
|
+
entry: bash -c 'coverage run run_tests.py --no-write-summary && coverage report'
|
|
37
37
|
language: system
|
|
38
38
|
pass_filenames: false
|
|
39
39
|
always_run: true
|
justhtml-3.5.0/AGENTS.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Agent Contribution Guide
|
|
2
|
+
|
|
3
|
+
Read these files before making changes:
|
|
4
|
+
|
|
5
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) for development, tests, documentation
|
|
6
|
+
examples, changelog, and release expectations.
|
|
7
|
+
- [SECURITY.md](SECURITY.md) for the untrusted-HTML threat model and security
|
|
8
|
+
boundaries.
|
|
9
|
+
- [PERFORMANCE.md](PERFORMANCE.md) for parser-speed benchmarking and
|
|
10
|
+
optimization work.
|
|
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.5.0] - 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Add contributor guidance for documentation examples, security and availability reviews, releases, and a new `PERFORMANCE.md` workflow for measuring and testing performance improvements.
|
|
14
|
+
- Add TurboHTML to the correctness benchmarks, and refresh the comparison guide with current Python-parser and browser-engine scores. Many dropped because of the new tests around processing instructions.
|
|
15
|
+
- Run CI against the latest WPT tree-construction fixtures rather than a pinned revision.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
Note: The below are all minor parsing differences found when diffing against Chromium output, that is not covered in WPT's html5 parsing tests.
|
|
19
|
+
|
|
20
|
+
- Ignore null-plus-whitespace character runs in the post-doctype document shell before creating the body. Previously, removing the null after classification could leave a spurious newline in `<body>`.
|
|
21
|
+
- Ignore late `<head>` start tags after body text even when the fast text path has not marked body mode explicitly. Previously, following metadata could be moved from the body back into the head.
|
|
22
|
+
- Limit doctype tokenization whitespace to the HTML ASCII whitespace set. Previously, non-ASCII spaces such as NBSP could be discarded instead of becoming part of the doctype name.
|
|
23
|
+
- Preserve the `frameset-ok` state across empty active-formatting elements. Previously, markup such as `<i><frameset>` could retain an empty body instead of replacing it with the frameset.
|
|
24
|
+
- Keep comments following implied head metadata in the head. Previously, `<meta><!--...-->` could place the comment before the document element.
|
|
25
|
+
- Exit column-group mode before reprocessing malformed end tags. Previously, `<table><colgroup></p>` could leave a paragraph inside the colgroup instead of foster-parenting it before the table.
|
|
26
|
+
- Keep foreign elements named `colgroup` out of HTML table recovery. Previously, text following SVG or MathML `<colgroup>` could become a sibling instead of its child.
|
|
27
|
+
- Keep form controls in template contents when a template appears inside `<select>`. Previously, an `<input>` could prematurely close the outer select.
|
|
28
|
+
- Keep initial-template recovery in the head insertion mode after `</template>`. Previously, following head whitespace could be discarded.
|
|
29
|
+
- Match the latest WPT processing-instruction tree expectations, including empty data fields and the `<?#` bogus-comment case.
|
|
30
|
+
- Close an open template `<colgroup>` before processing following script and style tags. Previously, those tags could be incorrectly inserted inside the column group.
|
|
31
|
+
- Recover table-body start tags correctly inside templates. Previously, a following `<colgroup>` could be discarded instead of closing the current table body.
|
|
32
|
+
- Close template table bodies before handling `</table>`. Previously, following content could remain incorrectly nested inside `<tbody>`.
|
|
33
|
+
- Close template rows before handling `</table>`. Previously, following nested templates could remain incorrectly nested inside `<tr>`.
|
|
34
|
+
- Ignore nested table starts in template table modes that have no open table. Previously, malformed input could create a spurious empty `<table>`.
|
|
35
|
+
- Resume template body recovery after `<title>`. Previously, invalid table-structure tags following a template title could create incorrect table nodes.
|
|
36
|
+
- Resume template body recovery after base and noframes head-content tags. Previously, invalid following table-structure tags could create incorrect table nodes.
|
|
37
|
+
- Apply phantom paragraph recovery inside templates. Previously, an unmatched `</p>` could be incorrectly ignored.
|
|
38
|
+
- Keep unmatched `</p>` ignored in the template initial insertion mode.
|
|
39
|
+
- Keep template table-body recovery scoped to an open table. Previously, a nested table token could close and duplicate synthetic table bodies.
|
|
40
|
+
- Preserve open template tables when closing a select. Previously, following option content could be placed outside the select.
|
|
41
|
+
|
|
42
|
+
## [3.4.0] - 2026-07-11
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
- (Severity: Low) Bound named character-reference lookup to the finite HTML entity tables. Previously, a malformed `&` followed by a long alphanumeric run caused repeated shrinking-prefix allocations and lookups, making parsing quadratic and allowing denial of service before sanitization.
|
|
46
|
+
- (Severity: Low) Cap diagnostics retained by `JustHTML(...)` with the new `max_errors` option (default: 1000), including parser, sanitizer, raw-text, and transform findings. Previously, input containing many independently malformed characters or unsafe constructs could allocate an unbounded number of `ParseError` objects.
|
|
47
|
+
- (Severity: Low) Index active-formatting entries by signature within each HTML5 Noah's Ark marker segment, retiring superseded entries lazily and compacting in bounded batches. Previously, repeated fourth occurrences across many formatting signatures rescanned and shifted the active-formatting list, allowing quadratic parsing of crafted HTML.
|
|
48
|
+
- (Severity: Low) Avoid unbounded integer conversion for untrusted `srcset` width descriptors, URL host validation, and `Linkify(...)` URL ports. Previously, inputs with more decimal digits than Python permits in `int()` could raise an uncaught `ValueError` during sanitization or linkification.
|
|
49
|
+
- (Severity: Low) Bound pretty-print indentation and avoid repeatedly copying nested serialized output. Previously, serializing deeply nested untrusted HTML with the default `to_html()` settings could consume quadratic time and memory.
|
|
50
|
+
- (Severity: Low) Compact sibling lists when applying a tag-only `Drop(...)` transform. Previously, dropping many matching untrusted sibling elements shifted the remaining list after every removal, making transform execution quadratic.
|
|
51
|
+
|
|
10
52
|
## [3.3.0] - 2026-07-04
|
|
11
53
|
|
|
12
54
|
### Changed
|
|
@@ -49,6 +49,28 @@ python -c 'from justhtml import JustHTML, to_test_format; print(to_test_format(J
|
|
|
49
49
|
|
|
50
50
|
**Coverage is required to be 100%.** All new code must be fully tested.
|
|
51
51
|
|
|
52
|
+
### Documentation examples
|
|
53
|
+
|
|
54
|
+
Runnable documentation examples use an explicit output marker. Place it between
|
|
55
|
+
a self-contained Python fence and the expected-output fence:
|
|
56
|
+
|
|
57
|
+
````text
|
|
58
|
+
```python
|
|
59
|
+
from justhtml import JustHTML
|
|
60
|
+
print(JustHTML("<p>x</p>", fragment=True).to_html())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
<!-- justhtml: output -->
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<p>x</p>
|
|
67
|
+
```
|
|
68
|
+
````
|
|
69
|
+
|
|
70
|
+
The docs test executes each marked Python block independently and compares its
|
|
71
|
+
stdout with the following fence. Keep ordinary HTML fences unmarked so they
|
|
72
|
+
remain explanatory examples rather than tests.
|
|
73
|
+
|
|
52
74
|
## Pre-commit Hooks
|
|
53
75
|
|
|
54
76
|
Pre-commit runs automatically on every commit and checks:
|
|
@@ -85,12 +107,25 @@ After making changes, verify performance impact:
|
|
|
85
107
|
|
|
86
108
|
```bash
|
|
87
109
|
# Quick benchmark
|
|
88
|
-
python benchmarks/performance.py --
|
|
110
|
+
python benchmarks/performance.py --parsers justhtml --iterations 1
|
|
89
111
|
|
|
90
112
|
# Profile hotspots
|
|
91
113
|
python benchmarks/profile.py
|
|
92
114
|
```
|
|
93
115
|
|
|
116
|
+
For a parser-speed improvement, measure the relevant pipeline on the web100k
|
|
117
|
+
corpus before and after the change; see [PERFORMANCE.md](PERFORMANCE.md). For
|
|
118
|
+
an availability fix, add a regression that exercises the affected hostile-input
|
|
119
|
+
shape and verify that growth is linear or otherwise bounded.
|
|
120
|
+
|
|
121
|
+
## Releases
|
|
122
|
+
|
|
123
|
+
Use `python scripts/release.py --version X.Y.Z --yes` for releases. The script
|
|
124
|
+
runs release checks, bumps `pyproject.toml`, commits, tags, pushes, and creates
|
|
125
|
+
the GitHub release. Before invoking it, ensure the worktree is clean and move
|
|
126
|
+
the relevant Unreleased changelog entries into a dated `## [X.Y.Z]` section;
|
|
127
|
+
the helper requires that section to exist.
|
|
128
|
+
|
|
94
129
|
## Architecture Notes
|
|
95
130
|
|
|
96
131
|
- **Parser engine** (`src/justhtml/parser/engine.py`): Plan-driven tokenization, tree construction, and sanitizer projection
|
|
@@ -109,8 +144,9 @@ Golden rules:
|
|
|
109
144
|
|
|
110
145
|
1. Fork the repository
|
|
111
146
|
2. Create a feature branch
|
|
112
|
-
3.
|
|
113
|
-
4.
|
|
114
|
-
5.
|
|
147
|
+
3. Add an entry under the relevant section of `CHANGELOG.md`
|
|
148
|
+
4. Make your changes with tests
|
|
149
|
+
5. Ensure pre-commit passes
|
|
150
|
+
6. Submit a pull request
|
|
115
151
|
|
|
116
152
|
Questions? Open an issue on GitHub. For security vulnerabilities, please see our [Security Policy](SECURITY.md).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Performance Guide
|
|
2
|
+
|
|
3
|
+
This guide is for improving JustHTML's parsing speed on real-world HTML while
|
|
4
|
+
preserving its HTML5 recovery and sanitization behavior.
|
|
5
|
+
|
|
6
|
+
## Benchmark data and setup
|
|
7
|
+
|
|
8
|
+
The main benchmark uses the [web100k](https://github.com/EmilStenstrom/web100k)
|
|
9
|
+
corpus. By default it expects the dataset beside this repository:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
../web100k/
|
|
13
|
+
├── html.dict
|
|
14
|
+
└── batches/
|
|
15
|
+
└── web100k-batch-001.tar.zst
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Set `WEB100K_DIR` to use another location, or pass `--batches-dir` and
|
|
19
|
+
`--dict` explicitly. Install the benchmark dependencies first:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install -e ".[benchmark]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Measure the right pipeline
|
|
26
|
+
|
|
27
|
+
The benchmark has separate modes for parsing alone and parsing followed by
|
|
28
|
+
serialization. Start with the narrowest mode that covers the change:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Parser throughput on 100 real-world documents, without RSS sampling noise.
|
|
32
|
+
python benchmarks/performance.py --parsers justhtml --iterations 5
|
|
33
|
+
|
|
34
|
+
# Parse plus default HTML serialization.
|
|
35
|
+
python benchmarks/performance.py --parsers justhtml_to_html --iterations 5
|
|
36
|
+
|
|
37
|
+
# Compare selected installed parsers on a larger corpus sample.
|
|
38
|
+
python benchmarks/performance.py --parsers justhtml html5lib lxml --limit 1000 --iterations 3
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use `--all-batches` for a corpus-wide measurement, `--batch PATH` for one
|
|
42
|
+
archive, or `--downloaded DIR` for a directory of decompressed-source files.
|
|
43
|
+
The benchmark reports total throughput and per-document timing. Compare the
|
|
44
|
+
same command before and after a change on the same machine; do not compare
|
|
45
|
+
absolute timings across machines.
|
|
46
|
+
|
|
47
|
+
## Find the hot path
|
|
48
|
+
|
|
49
|
+
Profile a representative web100k batch before optimizing:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python benchmarks/profile.py --mode parse
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Available modes are `parse`, `fragment`, `compact-html`, `pretty-html`, and
|
|
56
|
+
`text`. Use the mode closest to the public operation you are improving. Focus
|
|
57
|
+
on functions that dominate cumulative work across many documents, not an
|
|
58
|
+
isolated slow malformed sample.
|
|
59
|
+
|
|
60
|
+
## Make a speed improvement
|
|
61
|
+
|
|
62
|
+
- Preserve parser and sanitizer semantics. Run the parser differential suite
|
|
63
|
+
before relying on a throughput result.
|
|
64
|
+
- Optimize work on the normal path: token scanning, tree construction,
|
|
65
|
+
attribute projection, and common serialization cases.
|
|
66
|
+
- Prefer direct local data access, reused compiled plans, and fewer temporary
|
|
67
|
+
allocations in per-character and per-node loops.
|
|
68
|
+
- Measure realistic HTML before and after the change. Include a focused
|
|
69
|
+
microbenchmark only when it explains the real-world result.
|
|
70
|
+
- Keep benchmarks honest: warm up once, run multiple iterations, and report
|
|
71
|
+
the exact command, corpus size, and pipeline mode in the pull request.
|
|
72
|
+
|
|
73
|
+
## Validate and document
|
|
74
|
+
|
|
75
|
+
Add correctness tests for behavior affected by an optimization. Run the full
|
|
76
|
+
project gate before submitting:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pre-commit run --all-files
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Update `CHANGELOG.md` with a `Performance` entry that states the affected
|
|
83
|
+
pipeline and the avoided work. If the change also prevents disproportionate
|
|
84
|
+
resource use from hostile input, add a `Security` entry as well.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: justhtml
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.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
|
|
@@ -40,8 +40,8 @@ Requires-Dist: html5-parser; extra == 'benchmark'
|
|
|
40
40
|
Requires-Dist: html5lib; extra == 'benchmark'
|
|
41
41
|
Requires-Dist: lxml; extra == 'benchmark'
|
|
42
42
|
Requires-Dist: markupever; extra == 'benchmark'
|
|
43
|
-
Requires-Dist: psutil; extra == 'benchmark'
|
|
44
43
|
Requires-Dist: selectolax>=0.4.8; extra == 'benchmark'
|
|
44
|
+
Requires-Dist: turbohtml>=1.3.1; extra == 'benchmark'
|
|
45
45
|
Requires-Dist: zstandard; extra == 'benchmark'
|
|
46
46
|
Provides-Extra: dev
|
|
47
47
|
Requires-Dist: build; extra == 'dev'
|
|
@@ -19,6 +19,25 @@
|
|
|
19
19
|
| 1.x | :x: |
|
|
20
20
|
| < 1.0 | :x: |
|
|
21
21
|
|
|
22
|
+
## Threat Model
|
|
23
|
+
|
|
24
|
+
JustHTML's primary security boundary is **untrusted HTML input**: markup,
|
|
25
|
+
bytes, and HTML-derived values supplied to parsing, sanitization, and
|
|
26
|
+
serialization APIs. The promises below apply when callers use the documented
|
|
27
|
+
safe defaults or a deliberately safe policy.
|
|
28
|
+
|
|
29
|
+
Availability is part of that boundary. Small hostile inputs must not trigger
|
|
30
|
+
disproportionate work or uncaught conversion errors while parsing, sanitizing,
|
|
31
|
+
transforming, or serializing. Applications still need their own limits for
|
|
32
|
+
total input size, execution time, and memory use.
|
|
33
|
+
|
|
34
|
+
Library configuration is trusted application code. This includes custom
|
|
35
|
+
sanitization policies, transform specifications and callbacks, selectors,
|
|
36
|
+
glob patterns, CLI flags, and any policy or transform values assembled from
|
|
37
|
+
configuration. Applications that accept those values from untrusted users must
|
|
38
|
+
validate and constrain them before passing them to JustHTML; transform APIs are
|
|
39
|
+
not a sandbox.
|
|
40
|
+
|
|
22
41
|
## Security Domains
|
|
23
42
|
|
|
24
43
|
This section describes the security areas we use when reviewing `justhtml`.
|
|
@@ -42,7 +61,8 @@ Promise:
|
|
|
42
61
|
- JustHTML decodes bytes using HTML-style encoding detection and rejects UTF-7.
|
|
43
62
|
- JustHTML parses malformed HTML into a DOM using browser-style HTML5 recovery.
|
|
44
63
|
- The parser handles entity and character-reference edge cases.
|
|
45
|
-
- The parser avoids known small-input patterns that can cause excessive work
|
|
64
|
+
- The parser avoids known small-input patterns that can cause excessive work or
|
|
65
|
+
uncaught conversion errors.
|
|
46
66
|
- JustHTML reports parse errors as data unless strict mode is enabled.
|
|
47
67
|
- Fragment parsing and full-document parsing keep their different assumptions
|
|
48
68
|
explicit.
|
|
@@ -126,6 +146,8 @@ Promise:
|
|
|
126
146
|
- Serialization rejects unsafe element and attribute names created through
|
|
127
147
|
custom DOM changes.
|
|
128
148
|
- Serialization neutralizes rawtext end-tag sequences.
|
|
149
|
+
- Pretty serialization bounds indentation and avoids repeatedly copying nested
|
|
150
|
+
output, so deeply nested input does not cause disproportionate work.
|
|
129
151
|
- `to_text()` returns text content rather than markup.
|
|
130
152
|
- `to_markdown()` escapes Markdown-sensitive text by default.
|
|
131
153
|
- `to_markdown()` escapes line-start Markdown markers that could change block
|
|
@@ -165,6 +187,8 @@ Promise:
|
|
|
165
187
|
sanitization step after later transforms.
|
|
166
188
|
- Built-in URL-aware transforms reuse sanitizer URL cleaning where they can.
|
|
167
189
|
- Linkification operates on DOM text nodes rather than raw HTML strings.
|
|
190
|
+
- Built-in structural transforms avoid repeated sibling-list shifting for
|
|
191
|
+
common bulk operations on untrusted documents.
|
|
168
192
|
|
|
169
193
|
Out of scope:
|
|
170
194
|
- Safety of arbitrary user callbacks passed to transform APIs.
|
|
@@ -22,7 +22,17 @@ from justhtml.parser.context import FragmentContext
|
|
|
22
22
|
from justhtml.serializer import to_test_format
|
|
23
23
|
|
|
24
24
|
# Available parsers
|
|
25
|
-
PARSERS = [
|
|
25
|
+
PARSERS = [
|
|
26
|
+
"justhtml",
|
|
27
|
+
"html5lib",
|
|
28
|
+
"html5_parser",
|
|
29
|
+
"lxml",
|
|
30
|
+
"bs4",
|
|
31
|
+
"html.parser",
|
|
32
|
+
"selectolax",
|
|
33
|
+
"markupever",
|
|
34
|
+
"turbohtml",
|
|
35
|
+
]
|
|
26
36
|
|
|
27
37
|
|
|
28
38
|
class Status(Enum):
|
|
@@ -61,6 +71,7 @@ PARSER_CAPABILITIES = {
|
|
|
61
71
|
"html.parser": ParserCapabilities(),
|
|
62
72
|
"selectolax": ParserCapabilities(fragment_context=True, foreign_fragment_context=True),
|
|
63
73
|
"markupever": ParserCapabilities(),
|
|
74
|
+
"turbohtml": ParserCapabilities(fragment_context=True, foreign_fragment_context=True),
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
|
|
@@ -146,6 +157,15 @@ def check_parser_available(parser_name):
|
|
|
146
157
|
return True
|
|
147
158
|
except ImportError:
|
|
148
159
|
return False
|
|
160
|
+
if parser_name == "turbohtml":
|
|
161
|
+
try:
|
|
162
|
+
from turbohtml import parse, parse_fragment
|
|
163
|
+
|
|
164
|
+
parse("<p></p>")
|
|
165
|
+
parse_fragment("<p></p>", "div")
|
|
166
|
+
return True
|
|
167
|
+
except (ImportError, TypeError, ValueError, AttributeError):
|
|
168
|
+
return False
|
|
149
169
|
return False
|
|
150
170
|
|
|
151
171
|
|
|
@@ -174,6 +194,33 @@ def parse_dat_file(path):
|
|
|
174
194
|
return tests
|
|
175
195
|
|
|
176
196
|
|
|
197
|
+
def decode_escapes(text):
|
|
198
|
+
"""Decode the \"\\xXX\" and \"\\uXXXX\" sequences used by tree fixtures."""
|
|
199
|
+
if "\\x" not in text and "\\u" not in text:
|
|
200
|
+
return text
|
|
201
|
+
|
|
202
|
+
result = []
|
|
203
|
+
i = 0
|
|
204
|
+
while i < len(text):
|
|
205
|
+
if text[i : i + 2] == "\\x" and i + 3 < len(text):
|
|
206
|
+
try:
|
|
207
|
+
result.append(chr(int(text[i + 2 : i + 4], 16)))
|
|
208
|
+
i += 4
|
|
209
|
+
continue
|
|
210
|
+
except ValueError:
|
|
211
|
+
pass
|
|
212
|
+
elif text[i : i + 2] == "\\u" and i + 5 < len(text):
|
|
213
|
+
try:
|
|
214
|
+
result.append(chr(int(text[i + 2 : i + 6], 16)))
|
|
215
|
+
i += 6
|
|
216
|
+
continue
|
|
217
|
+
except ValueError:
|
|
218
|
+
pass
|
|
219
|
+
result.append(text[i])
|
|
220
|
+
i += 1
|
|
221
|
+
return "".join(result)
|
|
222
|
+
|
|
223
|
+
|
|
177
224
|
def parse_single_test(lines):
|
|
178
225
|
"""Parse a single test from lines."""
|
|
179
226
|
data = []
|
|
@@ -209,7 +256,7 @@ def parse_single_test(lines):
|
|
|
209
256
|
|
|
210
257
|
if data or document:
|
|
211
258
|
return {
|
|
212
|
-
"data": "\n".join(data),
|
|
259
|
+
"data": decode_escapes("\n".join(data)),
|
|
213
260
|
"document": "\n".join(document),
|
|
214
261
|
"fragment_context": fragment_context,
|
|
215
262
|
"script_directive": script_directive,
|
|
@@ -502,6 +549,23 @@ def run_test_markupever(html, fragment_context, expected, xml_coercion=False, if
|
|
|
502
549
|
return _error(e)
|
|
503
550
|
|
|
504
551
|
|
|
552
|
+
def run_test_turbohtml(html, fragment_context, expected, xml_coercion=False, iframe_srcdoc=False):
|
|
553
|
+
"""Run a single test with TurboHTML's public DOM API."""
|
|
554
|
+
from turbohtml import parse, parse_fragment
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
if fragment_context:
|
|
558
|
+
namespace, tag_name = fragment_context
|
|
559
|
+
context = f"{namespace} {tag_name}" if namespace else tag_name
|
|
560
|
+
node = parse_fragment(html, context)
|
|
561
|
+
actual = _turbohtml_to_test_format(node.children, 0)
|
|
562
|
+
else:
|
|
563
|
+
actual = _turbohtml_to_test_format(parse(html).children, 0)
|
|
564
|
+
return _compare_result(expected, actual)
|
|
565
|
+
except Exception as e:
|
|
566
|
+
return _error(e)
|
|
567
|
+
|
|
568
|
+
|
|
505
569
|
# =============================================================================
|
|
506
570
|
# Test format conversion helpers
|
|
507
571
|
# =============================================================================
|
|
@@ -839,6 +903,56 @@ def _markupever_to_test_format(nodes):
|
|
|
839
903
|
return "".join(line for node in nodes for line in process(node, 0))
|
|
840
904
|
|
|
841
905
|
|
|
906
|
+
def _turbohtml_to_test_format(nodes, indent):
|
|
907
|
+
"""Convert TurboHTML's public node objects to html5lib test format."""
|
|
908
|
+
from turbohtml import Comment, Doctype, Element, Namespace, Text
|
|
909
|
+
|
|
910
|
+
def qualified_name(namespace, name):
|
|
911
|
+
if namespace is Namespace.SVG:
|
|
912
|
+
return f"svg {name}"
|
|
913
|
+
if namespace is Namespace.MATHML:
|
|
914
|
+
return f"math {name}"
|
|
915
|
+
return name
|
|
916
|
+
|
|
917
|
+
def attribute_name(name):
|
|
918
|
+
if name.startswith("xlink:"):
|
|
919
|
+
return f"xlink {name.removeprefix('xlink:')}"
|
|
920
|
+
if name.startswith("xml:"):
|
|
921
|
+
return f"xml {name.removeprefix('xml:')}"
|
|
922
|
+
if name.startswith("xmlns:"):
|
|
923
|
+
return f"xmlns {name.removeprefix('xmlns:')}"
|
|
924
|
+
return name
|
|
925
|
+
|
|
926
|
+
def process(node, node_indent):
|
|
927
|
+
prefix = " " * node_indent
|
|
928
|
+
if isinstance(node, Doctype):
|
|
929
|
+
if node.public_id is not None or node.system_id is not None:
|
|
930
|
+
public_id = node.public_id or ""
|
|
931
|
+
system_id = node.system_id or ""
|
|
932
|
+
return [f'| <!DOCTYPE {node.name} "{public_id}" "{system_id}">']
|
|
933
|
+
return [f"| <!DOCTYPE {node.name}>"]
|
|
934
|
+
if isinstance(node, Comment):
|
|
935
|
+
return [f"| {prefix}<!-- {node.data} -->"]
|
|
936
|
+
if isinstance(node, Text):
|
|
937
|
+
return [f'| {prefix}"{node.text}"']
|
|
938
|
+
if not isinstance(node, Element):
|
|
939
|
+
return [line for child in node.children for line in process(child, node_indent)]
|
|
940
|
+
|
|
941
|
+
lines = [f"| {prefix}<{qualified_name(node.namespace, node.tag)}>"]
|
|
942
|
+
for name, value in sorted(node.attrs.items()):
|
|
943
|
+
lines.append(f'| {prefix} {attribute_name(name)}="{value}"')
|
|
944
|
+
|
|
945
|
+
if node.tag == "template" and node.namespace is Namespace.HTML:
|
|
946
|
+
lines.append(f"| {prefix} content")
|
|
947
|
+
content = node.children[0]
|
|
948
|
+
lines.extend(line for child in content.children for line in process(child, node_indent + 4))
|
|
949
|
+
else:
|
|
950
|
+
lines.extend(line for child in node.children for line in process(child, node_indent + 2))
|
|
951
|
+
return lines
|
|
952
|
+
|
|
953
|
+
return "\n".join(line for node in nodes for line in process(node, indent))
|
|
954
|
+
|
|
955
|
+
|
|
842
956
|
# Parser dispatch
|
|
843
957
|
PARSER_RUNNERS = {
|
|
844
958
|
"justhtml": run_test_justhtml,
|
|
@@ -849,6 +963,7 @@ PARSER_RUNNERS = {
|
|
|
849
963
|
"html.parser": run_test_html_parser,
|
|
850
964
|
"selectolax": run_test_selectolax,
|
|
851
965
|
"markupever": run_test_markupever,
|
|
966
|
+
"turbohtml": run_test_turbohtml,
|
|
852
967
|
}
|
|
853
968
|
|
|
854
969
|
|