justhtml 2.4.0__tar.gz → 3.0.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-2.4.0 → justhtml-3.0.0}/.github/copilot-instructions.md +10 -9
- {justhtml-2.4.0 → justhtml-3.0.0}/.pre-commit-config.yaml +6 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/CHANGELOG.md +27 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/CONTRIBUTING.md +7 -5
- {justhtml-2.4.0 → justhtml-3.0.0}/PKG-INFO +5 -3
- {justhtml-2.4.0 → justhtml-3.0.0}/README.md +4 -2
- justhtml-3.0.0/benchmarks/chromium_parser_diff.py +383 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/benchmarks/correctness.py +4 -4
- justhtml-3.0.0/benchmarks/html5lib_engine_diff.py +288 -0
- justhtml-3.0.0/benchmarks/parser_engine_gate.py +112 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/correctness.md +35 -33
- justhtml-3.0.0/docs/errors.md +123 -0
- justhtml-3.0.0/docs/parser-engine-findings.md +469 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/playground/app.js +5 -7
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/transforms.md +2 -2
- {justhtml-2.4.0 → justhtml-3.0.0}/pyproject.toml +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/run_tests.py +4 -20
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/__init__.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/core/constants.py +0 -1
- justhtml-2.4.0/src/justhtml/treebuilder/utils.py → justhtml-3.0.0/src/justhtml/core/doctype.py +1 -32
- justhtml-3.0.0/src/justhtml/core/errors.py +40 -0
- justhtml-2.4.0/src/justhtml/tokenizer/tokens.py → justhtml-3.0.0/src/justhtml/core/types.py +0 -75
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/dom/__init__.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/dom/builder.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/parser/__init__.py +193 -157
- justhtml-3.0.0/src/justhtml/parser/engine.py +5524 -0
- justhtml-3.0.0/src/justhtml/parser/options.py +33 -0
- justhtml-3.0.0/src/justhtml/parser/scanner.py +112 -0
- justhtml-3.0.0/src/justhtml/parser/stream.py +492 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/dom.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/policy.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/rawtext.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/serializer/html.py +11 -4
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/transforms/__init__.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/transforms/runtime.py +62 -5
- justhtml-3.0.0/test-summary.txt +101 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/README.md +0 -2
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/harness/tree.py +4 -4
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/branch_coverage.dat +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/treebuilder_coverage.dat +4 -4
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_cli.py +6 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_encoding.py +39 -0
- justhtml-3.0.0/tests/test_entities.py +47 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_errors.py +57 -94
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_node.py +11 -0
- justhtml-3.0.0/tests/test_parser_engine_coverage.py +1071 -0
- justhtml-3.0.0/tests/test_parser_regressions.py +81 -0
- justhtml-3.0.0/tests/test_parser_tree.py +955 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_sanitize.py +1 -1
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_serialize.py +17 -6
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_stream.py +136 -2
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_transforms.py +7 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_transforms_edge_cases.py +21 -0
- justhtml-2.4.0/docs/errors.md +0 -334
- justhtml-2.4.0/src/justhtml/core/errors.py +0 -154
- justhtml-2.4.0/src/justhtml/parser/stream.py +0 -206
- justhtml-2.4.0/src/justhtml/tokenizer/__init__.py +0 -20
- justhtml-2.4.0/src/justhtml/tokenizer/html.py +0 -3131
- justhtml-2.4.0/src/justhtml/treebuilder/__init__.py +0 -6
- justhtml-2.4.0/src/justhtml/treebuilder/core.py +0 -1551
- justhtml-2.4.0/src/justhtml/treebuilder/modes.py +0 -2169
- justhtml-2.4.0/test-summary.txt +0 -117
- justhtml-2.4.0/tests/harness/tokenizer.py +0 -363
- justhtml-2.4.0/tests/test_tokenizer.py +0 -577
- justhtml-2.4.0/tests/test_treebuilder.py +0 -804
- {justhtml-2.4.0 → justhtml-3.0.0}/.github/workflows/ci.yml +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/.github/workflows/publish.yml +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/.gitignore +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/LICENSE +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/Makefile +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/SECURITY.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/benchmarks/fuzz.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/benchmarks/performance.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/benchmarks/profile.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/_config.yml +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/_layouts/default.html +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/api.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/assets/search.js +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/bleach-migration.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/building.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/cli.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/comparison.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/encoding.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/fragments.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/html-cleaning.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/index.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/linkify.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/migration-examples.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/playground/index.html +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/playground/py/render.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/quickstart.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/sanitization.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/selectors.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/streaming.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/text.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/unsafe-handling.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/docs/url-cleaning.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/llms.txt +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/scripts/release.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/__main__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/py.typed +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/data/wikipedia.html +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/harness/__init__.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/harness/encoding.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/harness/regressions.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/harness/reporter.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/harness/serializer.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/coverage_gaps.test +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/empty_stack_edge_cases.dat +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/tokenizer_edge_cases.test +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/justhtml-tests/xml_coercion_coverage.test +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/linkify-it/README.md +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_builder.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_docs_examples.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_linkify_it.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_selector.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-2.4.0 → justhtml-3.0.0}/tests/test_wikipedia.py +0 -0
|
@@ -6,19 +6,20 @@
|
|
|
6
6
|
- Keep preambles to a single declarative sentence ("I'm scanning the repo and then drafting a minimal fix.") — no approval requests.
|
|
7
7
|
|
|
8
8
|
### Architecture Snapshot
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- Entities (`entities.py`): HTML5
|
|
13
|
-
- Constants (`constants.py`): HTML5 element categories, void elements, formatting elements,
|
|
9
|
+
- Parser engine (`src/justhtml/parser/engine.py`): Plan-driven tokenization, tree construction, sanitizer projection, and HTML5 recovery behavior.
|
|
10
|
+
- Parser scanner (`src/justhtml/parser/scanner.py`): Shared low-level scanning for tags, rawtext, and script end markers.
|
|
11
|
+
- DOM (`src/justhtml/dom/`): DOM-like node tree. Use `append_child()` / `insert_before()` for public tree operations.
|
|
12
|
+
- Entities (`src/justhtml/core/entities.py`): HTML5 named and numeric character-reference decoding.
|
|
13
|
+
- Constants (`src/justhtml/core/constants.py`): HTML5 element categories, void elements, formatting elements, and namespaces.
|
|
14
|
+
- Transforms (`src/justhtml/transforms/`): Compiled post-parse tree transformations.
|
|
14
15
|
|
|
15
16
|
### Golden Rules
|
|
16
17
|
1. **Spec compliance first**: Follow WHATWG HTML5 spec exactly. No heuristics, no shortcuts.
|
|
17
18
|
2. **No exceptions in hot paths**: Use deterministic control flow, not try/except for branching.
|
|
18
19
|
3. **No reflective probing**: No `hasattr`, `getattr`, or `delattr` - all data structures used are deterministic.
|
|
19
|
-
4. **Minimal allocations**: Reuse buffers
|
|
20
|
-
5. **
|
|
21
|
-
6. **State
|
|
20
|
+
4. **Minimal allocations**: Reuse buffers and avoid transient objects in parser hot paths.
|
|
21
|
+
5. **Node ownership**: Move nodes through DOM helpers and keep parent/child links consistent.
|
|
22
|
+
6. **State-machine clarity**: Keep parser recovery and insertion-mode transitions explicit.
|
|
22
23
|
7. **No test-specific code**: No references to test files in comments or code.
|
|
23
24
|
|
|
24
25
|
### Testing Workflow
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
- Debug calls: `self.debug()` / `parser.debug()` - no gating needed
|
|
76
77
|
|
|
77
78
|
### Performance Mindset
|
|
78
|
-
-
|
|
79
|
+
- Parser scanning and tree construction are hot paths: minimize allocations and avoid unnecessary string slicing
|
|
79
80
|
- Use `str.find()` for scanning, not regex when possible
|
|
80
81
|
- Reuse buffers: `text_buffer`, `current_tag_name`, etc.
|
|
81
82
|
- Infer state from structure (stacks, tree) instead of storing flags
|
|
@@ -37,3 +37,9 @@ repos:
|
|
|
37
37
|
language: system
|
|
38
38
|
pass_filenames: false
|
|
39
39
|
always_run: true
|
|
40
|
+
- id: parser-differential
|
|
41
|
+
name: Parser Differential
|
|
42
|
+
entry: bash -c 'PYTHONPATH=src python benchmarks/html5lib_engine_diff.py --fail-under-rate 1.0 --fail-on-current-exceptions'
|
|
43
|
+
language: system
|
|
44
|
+
pass_filenames: false
|
|
45
|
+
always_run: true
|
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.0.0] - 2026-06-21
|
|
11
|
+
|
|
12
|
+
JustHTML 3.0.0 ships a new parser core. The old tokenizer and tree-builder are now one integrated system, which makes the parser much faster while keeping browser-style HTML5 recovery and default-safe behavior intact. In practice, this release is mainly about getting materially better performance without asking most users to change how they use the library.
|
|
13
|
+
|
|
14
|
+
The breaking part is narrow: if you do not depend on `doc.errors` or on the exact diagnostic details raised by `strict=True`, this release should behave like a faster, more robust JustHTML. If you do depend on parser diagnostics, expect different error codes, counts, locations, and ordering from the old implementation.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Replace the legacy tokenizer/tree-builder split with one plan-driven parser engine shared across sanitized, unsanitized, fragment, streaming, diagnostic, and source-location modes. This is a new parser architecture, not just an optimization pass: scanning, tree construction, and default-safe decisions now run as one coordinated system.
|
|
18
|
+
- BREAKING: Rework parser diagnostics around a smaller, higher-value built-in error set. `collect_errors=True` and `strict=True` still surface parser problems, but callers should expect different error codes, counts, locations, and ordering than the old tokenizer/tree-builder pipeline.
|
|
19
|
+
|
|
20
|
+
### Performance
|
|
21
|
+
- Deliver roughly a **2x parsing speedup** from the new fused engine while preserving browser-style HTML5 recovery behavior. The hot path now avoids the old tokenizer-to-treebuilder handoff, compiles default sanitizer decisions into parser tag actions, and scans projected attributes directly while parsing.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Preserve HTML5 tree-construction behavior across foreign-content integration points, select/table recovery, framesets, plaintext formatting reconstruction, form-pointer handling, malformed comments, foreign attribute parsing, adoption-agency edge cases, and scope-repair bugs found through Chromium and html5lib differentials.
|
|
25
|
+
- Stabilize sanitization of malformed foreign-namespace rawtext structures so trailing HTML cannot be exposed by mutation-XSS reparsing.
|
|
26
|
+
|
|
27
|
+
### Breaking
|
|
28
|
+
- BREAKING: Error collection is no longer a compatibility surface for the legacy parser internals. Applications that assert on the exact contents of `doc.errors`, or on the specific exception details raised by `strict=True`, must update to the new diagnostic model. Other users should not need API changes for this release.
|
|
29
|
+
|
|
30
|
+
## [2.4.1] - 2026-06-21
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- Prevent the leading-line-feed exception for `<pre>`, `<listing>`, and `<textarea>` from leaking past intervening non-character tokens and removing a later newline.
|
|
34
|
+
- Apply paragraph closing and leading-line-feed handling to `<pre>` and `<listing>` inside customizable `<select>` content.
|
|
35
|
+
- Preserve U+000C FORM FEED in HTML text like browsers do, while retaining its intentional conversion to a space in XML-coercion mode.
|
|
36
|
+
|
|
10
37
|
## [2.4.0] - 2026-06-20
|
|
11
38
|
|
|
12
39
|
### Fixed
|
|
@@ -58,6 +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 html5lib tree cases
|
|
61
62
|
|
|
62
63
|
Run manually:
|
|
63
64
|
```bash
|
|
@@ -92,15 +93,16 @@ python benchmarks/profile.py
|
|
|
92
93
|
|
|
93
94
|
## Architecture Notes
|
|
94
95
|
|
|
95
|
-
- **
|
|
96
|
-
- **
|
|
97
|
-
- **
|
|
98
|
-
- **
|
|
96
|
+
- **Parser engine** (`src/justhtml/parser/engine.py`): Plan-driven tokenization, tree construction, and sanitizer projection
|
|
97
|
+
- **Parser scanner** (`src/justhtml/parser/scanner.py`): Shared low-level tag and rawtext scanning helpers
|
|
98
|
+
- **DOM** (`src/justhtml/dom/`): DOM-like node tree; use `append_child()` / `insert_before()` for public tree operations
|
|
99
|
+
- **Selectors** (`src/justhtml/selector/`): CSS selector parsing and matching
|
|
100
|
+
- **Transforms** (`src/justhtml/transforms/`): Compiled post-parse tree transformations
|
|
99
101
|
|
|
100
102
|
Golden rules:
|
|
101
103
|
1. Follow WHATWG HTML5 spec exactly
|
|
102
104
|
2. No exceptions in hot paths
|
|
103
|
-
3. Minimal allocations in
|
|
105
|
+
3. Minimal allocations in parser hot paths
|
|
104
106
|
4. No `hasattr`/`getattr`/`delattr` - all structures are deterministic
|
|
105
107
|
|
|
106
108
|
## Submitting Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: justhtml
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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
|
|
@@ -164,9 +164,11 @@ justhtml index.html --selector "article" --format markdown
|
|
|
164
164
|
|
|
165
165
|
## Correctness
|
|
166
166
|
|
|
167
|
-
JustHTML is tested against the official html5lib
|
|
167
|
+
JustHTML is tested against the official html5lib tree-construction, serializer, and encoding fixtures, plus project-specific sanitizer, selector, transform, CLI, and regression tests.
|
|
168
168
|
|
|
169
|
-
The current test
|
|
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 html5lib tree suite.
|
|
171
|
+
See [Correctness Testing](docs/correctness.md) for details.
|
|
170
172
|
|
|
171
173
|
## Documentation
|
|
172
174
|
|
|
@@ -110,9 +110,11 @@ justhtml index.html --selector "article" --format markdown
|
|
|
110
110
|
|
|
111
111
|
## Correctness
|
|
112
112
|
|
|
113
|
-
JustHTML is tested against the official html5lib
|
|
113
|
+
JustHTML is tested against the official html5lib tree-construction, serializer, and encoding fixtures, plus project-specific sanitizer, selector, transform, CLI, and regression tests.
|
|
114
114
|
|
|
115
|
-
The current test
|
|
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 html5lib tree suite.
|
|
117
|
+
See [Correctness Testing](docs/correctness.md) for details.
|
|
116
118
|
|
|
117
119
|
## Documentation
|
|
118
120
|
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare JustHTML parse trees with headless Chromium.
|
|
3
|
+
|
|
4
|
+
Run with an environment containing Playwright, for example:
|
|
5
|
+
|
|
6
|
+
PYENV_VERSION=justhtml-html5lib-tests-bench \
|
|
7
|
+
python benchmarks/chromium_parser_diff.py --generated 5000
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import importlib
|
|
14
|
+
import inspect
|
|
15
|
+
import json
|
|
16
|
+
import random
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
for _path in (str(_ROOT), str(_ROOT / "src")):
|
|
24
|
+
if _path not in sys.path:
|
|
25
|
+
sys.path.insert(0, _path)
|
|
26
|
+
|
|
27
|
+
from justhtml import JustHTML # noqa: E402
|
|
28
|
+
from justhtml.dom import Template # noqa: E402
|
|
29
|
+
|
|
30
|
+
_KNOWN_CHROMIUM_DIVERGENCES = frozenset(
|
|
31
|
+
{
|
|
32
|
+
"adoption01.dat:17",
|
|
33
|
+
"generated:4",
|
|
34
|
+
"generated:65",
|
|
35
|
+
"noscript01.dat:12",
|
|
36
|
+
"tests25.dat:7",
|
|
37
|
+
"tests_innerHTML_1.dat:75",
|
|
38
|
+
"webkit02.dat:11",
|
|
39
|
+
"webkit02.dat:15",
|
|
40
|
+
"webkit02.dat:16",
|
|
41
|
+
"webkit02.dat:17",
|
|
42
|
+
"generated:649",
|
|
43
|
+
"generated:705",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
_NON_ATOMIC_FUZZ_STRATEGIES = frozenset({"fuzz_attribute", "fuzz_nested_structure", "fuzz_open_tag"})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True)
|
|
50
|
+
class Case:
|
|
51
|
+
source: str
|
|
52
|
+
html: str
|
|
53
|
+
context_name: str | None = None
|
|
54
|
+
context_namespace: str | None = None
|
|
55
|
+
scripting_enabled: bool = True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _runner_config() -> dict[str, object]:
|
|
59
|
+
return {
|
|
60
|
+
"fail_fast": False,
|
|
61
|
+
"test_specs": [],
|
|
62
|
+
"quiet": True,
|
|
63
|
+
"write_summary": False,
|
|
64
|
+
"exclude_errors": None,
|
|
65
|
+
"exclude_files": None,
|
|
66
|
+
"exclude_html": None,
|
|
67
|
+
"filter_html": None,
|
|
68
|
+
"filter_errors": None,
|
|
69
|
+
"verbosity": 0,
|
|
70
|
+
"regressions": False,
|
|
71
|
+
"check_errors": False,
|
|
72
|
+
"suite": "tree",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _html5lib_cases(limit: int | None) -> list[Case]:
|
|
77
|
+
if limit == 0:
|
|
78
|
+
return []
|
|
79
|
+
tree_module = importlib.import_module("tests.harness.tree")
|
|
80
|
+
runner = tree_module.TestRunner(_ROOT / "tests/html5lib-tests-tree", _runner_config())
|
|
81
|
+
cases: list[Case] = []
|
|
82
|
+
for path, tests in runner.load_tests():
|
|
83
|
+
for index, test in enumerate(tests):
|
|
84
|
+
if test.script_directive == "script-on" or test.xml_coercion or test.iframe_srcdoc:
|
|
85
|
+
continue
|
|
86
|
+
context = test.fragment_context
|
|
87
|
+
cases.append(
|
|
88
|
+
Case(
|
|
89
|
+
source=f"{path.name}:{index}",
|
|
90
|
+
html=test.data,
|
|
91
|
+
context_name=context.tag_name if context else None,
|
|
92
|
+
context_namespace=context.namespace if context else None,
|
|
93
|
+
scripting_enabled=test.script_directive != "script-off",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
if limit is not None and len(cases) >= limit:
|
|
97
|
+
return cases
|
|
98
|
+
return cases
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _generated_cases(count: int, seed: int) -> list[Case]:
|
|
102
|
+
fuzz = importlib.import_module("benchmarks.fuzz")
|
|
103
|
+
random.seed(seed)
|
|
104
|
+
return [
|
|
105
|
+
Case(source=f"generated:{index}", html=fuzz.generate_fuzzed_html(), scripting_enabled=False)
|
|
106
|
+
for index in range(count)
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _atomic_generated_cases(count_per_strategy: int, seed: int) -> list[Case]:
|
|
111
|
+
fuzz = importlib.import_module("benchmarks.fuzz")
|
|
112
|
+
random.seed(seed)
|
|
113
|
+
strategies = []
|
|
114
|
+
for name, value in vars(fuzz).items():
|
|
115
|
+
if not name.startswith("fuzz_") or name in _NON_ATOMIC_FUZZ_STRATEGIES or not callable(value):
|
|
116
|
+
continue
|
|
117
|
+
signature = inspect.signature(value)
|
|
118
|
+
if any(
|
|
119
|
+
parameter.default is inspect.Parameter.empty
|
|
120
|
+
and parameter.kind in {inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD}
|
|
121
|
+
for parameter in signature.parameters.values()
|
|
122
|
+
):
|
|
123
|
+
continue
|
|
124
|
+
strategies.append((name, value))
|
|
125
|
+
|
|
126
|
+
cases = []
|
|
127
|
+
for name, strategy in sorted(strategies):
|
|
128
|
+
cases.extend(
|
|
129
|
+
[
|
|
130
|
+
Case(
|
|
131
|
+
source=f"atomic:{name}:{index}",
|
|
132
|
+
html=strategy(),
|
|
133
|
+
scripting_enabled=False,
|
|
134
|
+
)
|
|
135
|
+
for index in range(count_per_strategy)
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
return cases
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _merge_adjacent_text(nodes: list[list[Any]]) -> list[list[Any]]:
|
|
142
|
+
merged: list[list[Any]] = []
|
|
143
|
+
for node in nodes:
|
|
144
|
+
if node[0] == "text" and merged and merged[-1][0] == "text":
|
|
145
|
+
merged[-1][1] += node[1]
|
|
146
|
+
else:
|
|
147
|
+
merged.append(node)
|
|
148
|
+
return merged
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _canonical_node(node: Any) -> list[Any]:
|
|
152
|
+
name = node.name
|
|
153
|
+
if name == "#text":
|
|
154
|
+
return ["text", node.data or ""]
|
|
155
|
+
if name == "#comment":
|
|
156
|
+
return ["comment", node.data or ""]
|
|
157
|
+
if name == "!doctype":
|
|
158
|
+
doctype = node.data
|
|
159
|
+
return ["doctype", doctype.name or "", doctype.public_id or "", doctype.system_id or ""]
|
|
160
|
+
|
|
161
|
+
namespace = node.namespace or "html"
|
|
162
|
+
attrs = [[name, value or ""] for name, value in sorted((node.attrs or {}).items())]
|
|
163
|
+
children = _merge_adjacent_text([_canonical_node(child) for child in node.children or ()])
|
|
164
|
+
content = None
|
|
165
|
+
if type(node) is Template and node.template_content is not None:
|
|
166
|
+
content = _merge_adjacent_text([_canonical_node(child) for child in node.template_content.children])
|
|
167
|
+
return ["element", namespace, name, attrs, children, content]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _render_justhtml(case: Case) -> list[Any]:
|
|
171
|
+
context = None
|
|
172
|
+
if case.context_name is not None:
|
|
173
|
+
context_module = importlib.import_module("justhtml.parser.context")
|
|
174
|
+
context = context_module.FragmentContext(case.context_name, case.context_namespace)
|
|
175
|
+
document = JustHTML(
|
|
176
|
+
case.html,
|
|
177
|
+
fragment_context=context,
|
|
178
|
+
scripting_enabled=case.scripting_enabled,
|
|
179
|
+
sanitize=False,
|
|
180
|
+
)
|
|
181
|
+
return [_canonical_node(child) for child in document.root.children or ()]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _canonical_subtree_is_empty(node: list[Any]) -> bool:
|
|
185
|
+
if node[0] == "text":
|
|
186
|
+
return not node[1].strip()
|
|
187
|
+
if node[0] != "element":
|
|
188
|
+
return False
|
|
189
|
+
return all(_canonical_subtree_is_empty(child) for child in node[4])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _is_known_divergence(case: Case, actual: list[Any], expected: list[Any]) -> bool:
|
|
193
|
+
if case.source in _KNOWN_CHROMIUM_DIVERGENCES:
|
|
194
|
+
return True
|
|
195
|
+
if case.html.startswith("\ufeff"):
|
|
196
|
+
# DOMParser receives an already-decoded string and preserves this
|
|
197
|
+
# character. JustHTML applies its byte-stream-compatible BOM policy.
|
|
198
|
+
return True
|
|
199
|
+
if case.html in {"&#x;", "&#X;"}:
|
|
200
|
+
# The HTML Standard requires an absent-digit numeric reference to be
|
|
201
|
+
# flushed unchanged. Chromium currently emits U+FFFD for this input.
|
|
202
|
+
return True
|
|
203
|
+
if case.html == "	":
|
|
204
|
+
# Chromium DOMParser preserves this as a body text node even though the
|
|
205
|
+
# tree builder ignores the leading character before the html element.
|
|
206
|
+
return True
|
|
207
|
+
if case.html.startswith("<!D") and not case.html.lower().startswith("<!doctype"):
|
|
208
|
+
# Chromium drops an incomplete uppercase DOCTYPE prefix where the
|
|
209
|
+
# tokenizer rules instead produce a bogus comment.
|
|
210
|
+
return True
|
|
211
|
+
if "<frameset" in case.html.lower() and actual and expected:
|
|
212
|
+
actual_html = actual[-1]
|
|
213
|
+
expected_html = expected[-1]
|
|
214
|
+
if actual_html[:3] == ["element", "html", "html"] and expected_html[:3] == ["element", "html", "html"]:
|
|
215
|
+
actual_elements = [node for node in actual_html[4] if node[0] == "element"]
|
|
216
|
+
expected_elements = [node for node in expected_html[4] if node[0] == "element"]
|
|
217
|
+
if (
|
|
218
|
+
actual_elements
|
|
219
|
+
and expected_elements
|
|
220
|
+
and actual_elements[-1][2] == "body"
|
|
221
|
+
and expected_elements[-1][2] == "frameset"
|
|
222
|
+
and all(_canonical_subtree_is_empty(child) for child in actual_elements[-1][4])
|
|
223
|
+
):
|
|
224
|
+
# Chromium still accepts a frameset after recursively empty
|
|
225
|
+
# unknown elements. The standard marks frameset-ok false.
|
|
226
|
+
return True
|
|
227
|
+
return case.source.startswith(("atomic:fuzz_scope_terminators:", "atomic:fuzz_formatting_boundary:")) and (
|
|
228
|
+
"<button><button>" in case.html
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
_CHROMIUM_RENDER = r"""
|
|
233
|
+
(cases) => {
|
|
234
|
+
const namespaceUri = {
|
|
235
|
+
html: "http://www.w3.org/1999/xhtml",
|
|
236
|
+
svg: "http://www.w3.org/2000/svg",
|
|
237
|
+
math: "http://www.w3.org/1998/Math/MathML",
|
|
238
|
+
mathml: "http://www.w3.org/1998/Math/MathML",
|
|
239
|
+
};
|
|
240
|
+
const namespaceName = (uri) => {
|
|
241
|
+
if (!uri || uri === namespaceUri.html) return "html";
|
|
242
|
+
if (uri === namespaceUri.svg) return "svg";
|
|
243
|
+
if (uri === namespaceUri.math) return "math";
|
|
244
|
+
return uri;
|
|
245
|
+
};
|
|
246
|
+
const canonical = (node) => {
|
|
247
|
+
if (node.nodeType === Node.TEXT_NODE) return ["text", node.data];
|
|
248
|
+
if (node.nodeType === Node.COMMENT_NODE) return ["comment", node.data];
|
|
249
|
+
if (node.nodeType === Node.DOCUMENT_TYPE_NODE) {
|
|
250
|
+
return ["doctype", node.name || "", node.publicId, node.systemId];
|
|
251
|
+
}
|
|
252
|
+
const attrs = Array.from(node.attributes || [], (attr) => [attr.name, attr.value])
|
|
253
|
+
.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
254
|
+
const mergeAdjacentText = (nodes) => {
|
|
255
|
+
const merged = [];
|
|
256
|
+
for (const item of nodes) {
|
|
257
|
+
if (item[0] === "text" && merged.length && merged[merged.length - 1][0] === "text") {
|
|
258
|
+
merged[merged.length - 1][1] += item[1];
|
|
259
|
+
} else {
|
|
260
|
+
merged.push(item);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return merged;
|
|
264
|
+
};
|
|
265
|
+
const children = mergeAdjacentText(Array.from(node.childNodes, canonical));
|
|
266
|
+
const content = node.localName === "template" && node.namespaceURI === namespaceUri.html
|
|
267
|
+
? mergeAdjacentText(Array.from(node.content.childNodes, canonical))
|
|
268
|
+
: null;
|
|
269
|
+
return ["element", namespaceName(node.namespaceURI), node.localName, attrs, children, content];
|
|
270
|
+
};
|
|
271
|
+
return cases.map((item) => {
|
|
272
|
+
let nodes;
|
|
273
|
+
if (item.context_name === null) {
|
|
274
|
+
const document = new DOMParser().parseFromString(item.html, "text/html");
|
|
275
|
+
nodes = Array.from(document.childNodes);
|
|
276
|
+
} else {
|
|
277
|
+
const owner = document.implementation.createHTMLDocument("");
|
|
278
|
+
const namespace = namespaceUri[item.context_namespace || "html"] || item.context_namespace;
|
|
279
|
+
const context = owner.createElementNS(namespace, item.context_name);
|
|
280
|
+
context.innerHTML = item.html;
|
|
281
|
+
nodes = item.context_name.toLowerCase() === "template" && namespace === namespaceUri.html
|
|
282
|
+
? Array.from(context.content.childNodes)
|
|
283
|
+
: Array.from(context.childNodes);
|
|
284
|
+
}
|
|
285
|
+
return nodes.map(canonical);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _render_chromium(cases: list[Case], executable: str, chunk_size: int) -> list[list[Any]]:
|
|
292
|
+
try:
|
|
293
|
+
from playwright.sync_api import sync_playwright # noqa: PLC0415
|
|
294
|
+
except ImportError as exc: # pragma: no cover - depends on benchmark environment
|
|
295
|
+
raise SystemExit("Playwright is required for the Chromium differential") from exc
|
|
296
|
+
|
|
297
|
+
output: list[list[Any]] = []
|
|
298
|
+
with sync_playwright() as playwright:
|
|
299
|
+
browser = playwright.chromium.launch(executable_path=executable, headless=True)
|
|
300
|
+
page = browser.new_page()
|
|
301
|
+
for start in range(0, len(cases), chunk_size):
|
|
302
|
+
payload = [
|
|
303
|
+
{
|
|
304
|
+
"html": case.html,
|
|
305
|
+
"context_name": case.context_name,
|
|
306
|
+
"context_namespace": case.context_namespace,
|
|
307
|
+
}
|
|
308
|
+
for case in cases[start : start + chunk_size]
|
|
309
|
+
]
|
|
310
|
+
rendered = page.evaluate(f"(cases) => JSON.stringify(({_CHROMIUM_RENDER})(cases))", payload)
|
|
311
|
+
output.extend(json.loads(rendered))
|
|
312
|
+
browser.close()
|
|
313
|
+
return output
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def main() -> int:
|
|
317
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
318
|
+
parser.add_argument("--chrome", default="/usr/bin/google-chrome")
|
|
319
|
+
parser.add_argument("--html5lib-limit", type=int)
|
|
320
|
+
parser.add_argument("--generated", type=int, default=0)
|
|
321
|
+
parser.add_argument("--atomic-generated", type=int, default=0, help="Cases per individual fuzz strategy")
|
|
322
|
+
parser.add_argument("--seed", type=int, default=20260621)
|
|
323
|
+
parser.add_argument("--chunk-size", type=int, default=250)
|
|
324
|
+
parser.add_argument("--examples", type=int, default=20)
|
|
325
|
+
parser.add_argument("--json-output", type=Path)
|
|
326
|
+
args = parser.parse_args()
|
|
327
|
+
|
|
328
|
+
cases = _html5lib_cases(args.html5lib_limit)
|
|
329
|
+
cases.extend(_generated_cases(args.generated, args.seed))
|
|
330
|
+
cases.extend(_atomic_generated_cases(args.atomic_generated, args.seed))
|
|
331
|
+
chromium = _render_chromium(cases, args.chrome, args.chunk_size)
|
|
332
|
+
|
|
333
|
+
mismatches: list[dict[str, Any]] = []
|
|
334
|
+
known_divergences: list[dict[str, Any]] = []
|
|
335
|
+
exceptions: list[dict[str, str]] = []
|
|
336
|
+
for case, expected in zip(cases, chromium, strict=True):
|
|
337
|
+
try:
|
|
338
|
+
actual = _render_justhtml(case)
|
|
339
|
+
except Exception as exc: # noqa: BLE001 - differential must continue
|
|
340
|
+
exceptions.append({"source": case.source, "html": case.html, "exception": f"{type(exc).__name__}: {exc}"})
|
|
341
|
+
continue
|
|
342
|
+
if actual != expected:
|
|
343
|
+
mismatch = {
|
|
344
|
+
"source": case.source,
|
|
345
|
+
"html": case.html,
|
|
346
|
+
"context_name": case.context_name,
|
|
347
|
+
"context_namespace": case.context_namespace,
|
|
348
|
+
"justhtml": actual,
|
|
349
|
+
"chromium": expected,
|
|
350
|
+
}
|
|
351
|
+
(known_divergences if _is_known_divergence(case, actual, expected) else mismatches).append(mismatch)
|
|
352
|
+
|
|
353
|
+
print(f"chromium: {args.chrome}")
|
|
354
|
+
print(f"cases: {len(cases)}")
|
|
355
|
+
print(f"exact: {len(cases) - len(mismatches) - len(known_divergences) - len(exceptions)}")
|
|
356
|
+
print(f"known_chromium_divergences: {len(known_divergences)}")
|
|
357
|
+
print(f"mismatches: {len(mismatches)}")
|
|
358
|
+
print(f"exceptions: {len(exceptions)}")
|
|
359
|
+
for mismatch in mismatches[: args.examples]:
|
|
360
|
+
print(f"\n{mismatch['source']}: {mismatch['html']!r}")
|
|
361
|
+
print(f" justhtml: {json.dumps(mismatch['justhtml'], ensure_ascii=False)}")
|
|
362
|
+
print(f" chromium: {json.dumps(mismatch['chromium'], ensure_ascii=False)}")
|
|
363
|
+
for exception in exceptions[: args.examples]:
|
|
364
|
+
print(f"\n{exception['source']}: {exception['html']!r}")
|
|
365
|
+
print(f" exception: {exception['exception']}")
|
|
366
|
+
|
|
367
|
+
if args.json_output is not None:
|
|
368
|
+
args.json_output.write_text(
|
|
369
|
+
json.dumps(
|
|
370
|
+
{
|
|
371
|
+
"known_chromium_divergences": known_divergences,
|
|
372
|
+
"mismatches": mismatches,
|
|
373
|
+
"exceptions": exceptions,
|
|
374
|
+
},
|
|
375
|
+
ensure_ascii=False,
|
|
376
|
+
indent=2,
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
return int(bool(mismatches or exceptions))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__":
|
|
383
|
+
raise SystemExit(main())
|
|
@@ -230,22 +230,22 @@ def compare_outputs(expected, actual):
|
|
|
230
230
|
|
|
231
231
|
def run_test_justhtml(html, fragment_context, expected, xml_coercion=False, iframe_srcdoc=False):
|
|
232
232
|
"""Run a single test with JustHTML."""
|
|
233
|
-
from justhtml.
|
|
233
|
+
from justhtml.parser.options import ParserOptions
|
|
234
234
|
|
|
235
235
|
try:
|
|
236
|
-
opts =
|
|
236
|
+
opts = ParserOptions(xml_coercion=xml_coercion)
|
|
237
237
|
if fragment_context:
|
|
238
238
|
namespace, tag_name = fragment_context
|
|
239
239
|
ctx = FragmentContext(tag_name, namespace)
|
|
240
240
|
parser = JustHTML(
|
|
241
241
|
html,
|
|
242
242
|
fragment_context=ctx,
|
|
243
|
-
|
|
243
|
+
_parser_opts=opts,
|
|
244
244
|
iframe_srcdoc=iframe_srcdoc,
|
|
245
245
|
sanitize=False,
|
|
246
246
|
)
|
|
247
247
|
else:
|
|
248
|
-
parser = JustHTML(html,
|
|
248
|
+
parser = JustHTML(html, _parser_opts=opts, iframe_srcdoc=iframe_srcdoc, sanitize=False)
|
|
249
249
|
actual = to_test_format(parser.root)
|
|
250
250
|
return _compare_result(expected, actual)
|
|
251
251
|
except Exception as e:
|