justhtml 3.4.0__tar.gz → 3.6.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.4.0 → justhtml-3.6.0}/.github/workflows/ci.yml +5 -7
- {justhtml-3.4.0 → justhtml-3.6.0}/.github/workflows/publish.yml +2 -2
- {justhtml-3.4.0 → justhtml-3.6.0}/.pre-commit-config.yaml +1 -1
- justhtml-3.6.0/AGENTS.md +10 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/CHANGELOG.md +62 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/CONTRIBUTING.md +40 -4
- justhtml-3.6.0/PERFORMANCE.md +84 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/PKG-INFO +2 -2
- {justhtml-3.4.0 → justhtml-3.6.0}/SECURITY.md +11 -1
- {justhtml-3.4.0 → justhtml-3.6.0}/benchmarks/correctness.py +117 -2
- {justhtml-3.4.0 → justhtml-3.6.0}/benchmarks/performance.py +57 -146
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/comparison.md +20 -16
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/correctness.md +3 -17
- justhtml-3.6.0/project_ideas.md +47 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/pyproject.toml +2 -2
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/parser/engine.py +290 -75
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/parser/scanner.py +21 -12
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/character_preprocessing.dat +11 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/document_shell_recovery.dat +49 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/foreign_content.dat +154 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/frameset_recovery.dat +10 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/head_noscript_recovery.dat +12 -0
- justhtml-3.6.0/tests/justhtml-tests/in_body_recovery.dat +66 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/select_recovery.dat +57 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/table_recovery.dat +14 -0
- justhtml-3.6.0/tests/justhtml-tests/template_recovery.dat +392 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_parser_engine.py +81 -3
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_sanitize.py +52 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_stream.py +26 -1
- justhtml-3.4.0/tests/justhtml-tests/in_body_recovery.dat +0 -12
- {justhtml-3.4.0 → justhtml-3.6.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/.gitignore +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/LICENSE +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/Makefile +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/README.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/benchmarks/profile.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/_config.yml +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/api.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/assets/search.js +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/bleach-migration.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/building.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/cli.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/encoding.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/errors.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/fragments.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/html-cleaning.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/index.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/linkify.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/migration-examples.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/playground/app.js +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/playground/index.html +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/quickstart.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/sanitization.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/selectors.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/streaming.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/text.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/transforms.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/unsafe-handling.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/docs/url-cleaning.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/llms.txt +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/run_tests.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/scripts/release.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/text.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/parser/stream.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/serializer/html.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/transforms/runtime.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/test-summary.txt +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/README.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/harness/tree.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/selectedcontent.dat +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/tokenizer_states.test +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_builder.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_cli.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_docs_examples.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_encoding.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_entities.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_errors.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_node.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_parser_regressions.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_parser_tree.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_selector.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_serialize.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_transform_runtime.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_transforms.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.4.0 → justhtml-3.6.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.6.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,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.6.0] - 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
Note: The below are all minor parsing differences found when diffing against Chromium output, that is not covered in WPT's html5 parsing tests.
|
|
14
|
+
|
|
15
|
+
- Add a formatting element that breaks out of foreign content (such as `<i>` inside `<svg>`) to the active formatting list, so it is reconstructed later. Previously, the breakout element was inserted but not tracked, so `<b><svg><i></b>z` dropped the reconstructed `<i>` around the trailing text and `<svg><code><h2></code>` lost the reconstructed `<code>`.
|
|
16
|
+
- Start the body for a `<noscript>` start tag in the "after head" insertion mode instead of re-entering the head. The after-head metadata list does not include noscript, so markup such as `</head><noscript>` now places the element in the body.
|
|
17
|
+
- Hand an end tag to the HTML rules when the foreign-content end-tag walk reaches a matching HTML element, rather than popping the foreign elements above it. Previously, `<form><math></form>` popped the `<math>` along with the form instead of splicing only the form out of the stack.
|
|
18
|
+
- Clear back to the template contents before opening table structure in a template with no table element, so an open caption is closed. Previously, `<template><caption><tr>` nested the row inside the caption instead of making it a sibling.
|
|
19
|
+
- Switch the "in template" mode into the body for a non-head raw-text or RCDATA start tag (`iframe`, `noembed`, `xmp`, `textarea`), which are dispatched before the template-mode handler. Previously, a following table-structure tag such as the `<td>` in `<template><xmp></xmp><td>` created a spurious cell instead of being dropped.
|
|
20
|
+
- Ignore a `</td>` or `</th>` end tag in a template cell when the matching cell type is not in table scope. Previously, `<template><td></th>x` closed the `<td>` on the mismatched `</th>`, so the following text became a sibling instead of staying in the cell.
|
|
21
|
+
- Insert a non-breakout HTML-named start tag inside foreign content as a foreign element without running HTML in-body recovery. Foreign elements sharing a name with an HTML scope boundary or table foster target (an SVG or MathML `td`, `tr`, `caption`, ...) no longer stop scope walks or foster their children out. Previously, markup such as `<p><svg><article>`, `<p><math><td><listing>`, and `<table><math><tr><d>` broke out of or escaped the foreign subtree.
|
|
22
|
+
- Ignore `</body>` and `</html>` whenever a select is open, not only when the select holds just options. Previously, an element retained inside the select (as in `<select><o></body><!--x-->`) let the end tag switch to the after-body mode, moving following content out of the select.
|
|
23
|
+
- Close a raw-text or RCDATA element when its end tag name is terminated by `/` or a space even without a closing `>`. Previously, `<style></style/x` kept the unterminated end tag as text and left the element open.
|
|
24
|
+
- Do not reconstruct the active formatting elements for `rb`, `rp`, `rt`, or `rtc` start tags. Previously, `<div><tt></div><rt>` wrapped the `<rt>` in a stale `<tt>` instead of inserting it directly.
|
|
25
|
+
- Ignore `</form>` when the form element is not in scope, such as behind an open `<table>`. Previously, `<form><table></form><table>` removed the form from the stack, so the following table became a sibling of the form instead of nesting inside it.
|
|
26
|
+
- Respect special-element blocking for generic end tags inside a template. Previously, `<template><dialog><object></dialog>e` closed the dialog through the open `<object>` instead of ignoring the end tag, so following text escaped the object.
|
|
27
|
+
- Place a comment after an explicit `</head>` between the head and body even when the head contains a template. Previously, `<head><template></template></head><!--c-->` kept the comment inside the head.
|
|
28
|
+
- Process in-body `base`, `link`, and `meta` start tags with the in-head rules without reconstructing the active formatting elements. Previously, markup such as `<p><u><section><meta>` could wrongly wrap the metadata element in a stale formatting element instead of inserting it directly.
|
|
29
|
+
- Check the script-data-double-escape-start terminator against the real following character. Previously, a `<script` immediately followed by `</script>` inside a script (for example `<script><!--<script</script>`) entered the double-escaped state and swallowed the closing tag, leaving the script element unclosed.
|
|
30
|
+
- Pop foreign-content ancestors before applying the heading start-tag rule so a heading that breaks out of MathML or SVG closes an open heading. Previously, markup such as `<h1><math><h3>` nested the second heading inside the first instead of making it a sibling.
|
|
31
|
+
- Ignore `</html>` and `</body>` end tags in the "in select" insertion mode instead of switching to the after-body/after-html modes. Previously, markup such as `<select></html><!--c-->` moved trailing comments out of the still-open select to the document level.
|
|
32
|
+
- Restrict character foster parenting to HTML table elements. Previously, a foreign SVG or MathML element that merely shared a name with a table foster target (such as an SVG `<tr>`) had following text foster-parented out of the foreign subtree instead of kept as its child, as in `<table><svg><tr>n`.
|
|
33
|
+
- Force quirks mode when a doctype has trailing junk after its name without a PUBLIC or SYSTEM keyword. Previously, `<!DOCTYPE html foo>` was treated as no-quirks, so a following `<table>` incorrectly closed an open `<p>`.
|
|
34
|
+
- Ignore end tags other than `</template>` in the "in template" insertion mode. Previously, the `</br>` special case still ran, so `<template></br>` created a stray `<br>` inside the template contents.
|
|
35
|
+
- Let table-scoped end tags follow the foreign-content end-tag algorithm when the current node is a MathML or SVG element. Previously, an end tag such as `</th>` inside foreign content was routed to HTML table recovery, so `<svg><th><o></th>0` left the foreign cell open and misplaced the following text.
|
|
36
|
+
- Ignore `</body>` and `</html>` when no body element is in scope. Previously, an open scope marker such as `<marquee>`, `<object>`, or `<applet>` did not prevent the switch to the after-body mode, so following content such as a comment in `<marquee></body><!--x-->` was placed outside the still-open element.
|
|
37
|
+
- Close an escaped script comment on `<!-->`, whose `<!--` dashes already count toward the `-->`. Previously, `<script><!--><script></script>` treated the following `<script>` as a double-escape start and swallowed the closing tag, leaving the script element unclosed.
|
|
38
|
+
- Push active-formatting markers only for HTML scope-marker elements. A foreign element sharing a name such as an SVG or MathML `<td>` no longer pushes a marker that would linger after the element is popped and block later formatting reconstruction, as in `<p><a><math><td></td><listing>U`.
|
|
39
|
+
|
|
40
|
+
## [3.5.0] - 2026-07-13
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Add contributor guidance for documentation examples, security and availability reviews, releases, and a new `PERFORMANCE.md` workflow for measuring and testing performance improvements.
|
|
44
|
+
- 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.
|
|
45
|
+
- Run CI against the latest WPT tree-construction fixtures rather than a pinned revision.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
Note: The below are all minor parsing differences found when diffing against Chromium output, that is not covered in WPT's html5 parsing tests.
|
|
49
|
+
|
|
50
|
+
- 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>`.
|
|
51
|
+
- 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.
|
|
52
|
+
- 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.
|
|
53
|
+
- 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.
|
|
54
|
+
- Keep comments following implied head metadata in the head. Previously, `<meta><!--...-->` could place the comment before the document element.
|
|
55
|
+
- 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.
|
|
56
|
+
- 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.
|
|
57
|
+
- Keep form controls in template contents when a template appears inside `<select>`. Previously, an `<input>` could prematurely close the outer select.
|
|
58
|
+
- Keep initial-template recovery in the head insertion mode after `</template>`. Previously, following head whitespace could be discarded.
|
|
59
|
+
- Match the latest WPT processing-instruction tree expectations, including empty data fields and the `<?#` bogus-comment case.
|
|
60
|
+
- Close an open template `<colgroup>` before processing following script and style tags. Previously, those tags could be incorrectly inserted inside the column group.
|
|
61
|
+
- Recover table-body start tags correctly inside templates. Previously, a following `<colgroup>` could be discarded instead of closing the current table body.
|
|
62
|
+
- Close template table bodies before handling `</table>`. Previously, following content could remain incorrectly nested inside `<tbody>`.
|
|
63
|
+
- Close template rows before handling `</table>`. Previously, following nested templates could remain incorrectly nested inside `<tr>`.
|
|
64
|
+
- Ignore nested table starts in template table modes that have no open table. Previously, malformed input could create a spurious empty `<table>`.
|
|
65
|
+
- Resume template body recovery after `<title>`. Previously, invalid table-structure tags following a template title could create incorrect table nodes.
|
|
66
|
+
- Resume template body recovery after base and noframes head-content tags. Previously, invalid following table-structure tags could create incorrect table nodes.
|
|
67
|
+
- Apply phantom paragraph recovery inside templates. Previously, an unmatched `</p>` could be incorrectly ignored.
|
|
68
|
+
- Keep unmatched `</p>` ignored in the template initial insertion mode.
|
|
69
|
+
- Keep template table-body recovery scoped to an open table. Previously, a nested table token could close and duplicate synthetic table bodies.
|
|
70
|
+
- Preserve open template tables when closing a select. Previously, following option content could be placed outside the select.
|
|
71
|
+
|
|
10
72
|
## [3.4.0] - 2026-07-11
|
|
11
73
|
|
|
12
74
|
### Security
|
|
@@ -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.6.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'
|
|
@@ -26,6 +26,11 @@ bytes, and HTML-derived values supplied to parsing, sanitization, and
|
|
|
26
26
|
serialization APIs. The promises below apply when callers use the documented
|
|
27
27
|
safe defaults or a deliberately safe policy.
|
|
28
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
|
+
|
|
29
34
|
Library configuration is trusted application code. This includes custom
|
|
30
35
|
sanitization policies, transform specifications and callbacks, selectors,
|
|
31
36
|
glob patterns, CLI flags, and any policy or transform values assembled from
|
|
@@ -56,7 +61,8 @@ Promise:
|
|
|
56
61
|
- JustHTML decodes bytes using HTML-style encoding detection and rejects UTF-7.
|
|
57
62
|
- JustHTML parses malformed HTML into a DOM using browser-style HTML5 recovery.
|
|
58
63
|
- The parser handles entity and character-reference edge cases.
|
|
59
|
-
- 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.
|
|
60
66
|
- JustHTML reports parse errors as data unless strict mode is enabled.
|
|
61
67
|
- Fragment parsing and full-document parsing keep their different assumptions
|
|
62
68
|
explicit.
|
|
@@ -140,6 +146,8 @@ Promise:
|
|
|
140
146
|
- Serialization rejects unsafe element and attribute names created through
|
|
141
147
|
custom DOM changes.
|
|
142
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.
|
|
143
151
|
- `to_text()` returns text content rather than markup.
|
|
144
152
|
- `to_markdown()` escapes Markdown-sensitive text by default.
|
|
145
153
|
- `to_markdown()` escapes line-start Markdown markers that could change block
|
|
@@ -179,6 +187,8 @@ Promise:
|
|
|
179
187
|
sanitization step after later transforms.
|
|
180
188
|
- Built-in URL-aware transforms reuse sanitizer URL cleaning where they can.
|
|
181
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.
|
|
182
192
|
|
|
183
193
|
Out of scope:
|
|
184
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
|
|