justhtml 3.7.0__tar.gz → 3.9.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.7.0 → justhtml-3.9.0}/CHANGELOG.md +39 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/PERFORMANCE.md +25 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/PKG-INFO +8 -8
- {justhtml-3.7.0 → justhtml-3.9.0}/benchmarks/performance.py +10 -4
- {justhtml-3.7.0 → justhtml-3.9.0}/pyproject.toml +8 -8
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/constants.py +2 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/dom/__init__.py +180 -74
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/parser/__init__.py +24 -37
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/parser/engine.py +1262 -1020
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/parser/scanner.py +70 -10
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/parser/stream.py +5 -12
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/transforms/runtime.py +49 -3
- {justhtml-3.7.0 → justhtml-3.9.0}/test-summary.txt +21 -18
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_node.py +61 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_parser_engine.py +320 -19
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_parser_regressions.py +3 -3
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_parser_tree.py +9 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_stream.py +38 -7
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_transforms.py +186 -1
- {justhtml-3.7.0 → justhtml-3.9.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/.github/workflows/ci.yml +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/.github/workflows/publish.yml +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/.gitignore +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/.pre-commit-config.yaml +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/AGENTS.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/CONTRIBUTING.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/LICENSE +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/Makefile +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/README.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/SECURITY.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/benchmarks/correctness.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/benchmarks/profile.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/_config.yml +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/api.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/assets/search.js +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/bleach-migration.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/building.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/cli.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/comparison.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/correctness.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/encoding.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/errors.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/fragments.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/html-cleaning.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/index.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/linkify.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/migration-examples.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/playground/app.js +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/playground/index.html +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/quickstart.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/sanitization.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/selectors.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/streaming.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/text.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/transforms.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/unsafe-handling.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/docs/url-cleaning.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/llms.txt +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/project_ideas.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/run_tests.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/scripts/release.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/text.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/serializer/html.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/README.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/harness/tree.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/character_preprocessing.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/document_shell_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/foreign_content.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/frameset_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/head_noscript_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/in_body_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/raw_text_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/select_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/selectedcontent.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/table_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/template_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/tokenizer_recovery.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/tokenizer_states.test +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_builder.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_cli.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_docs_examples.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_encoding.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_entities.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_errors.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_sanitize.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_selector.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_serialize.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_transform_runtime.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.7.0 → justhtml-3.9.0}/tests/test_wikipedia.py +0 -0
|
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [3.9.0] - 2026-07-20
|
|
11
|
+
|
|
12
|
+
### Performance
|
|
13
|
+
|
|
14
|
+
Combined, the changes below reduce default-constructor parse time by about 9.5% relative to 3.8.0, on top of that release's 40% reduction from 3.7.1.
|
|
15
|
+
|
|
16
|
+
- Speed up the default `JustHTML()` constructor by about 3% on the web100k parser benchmark by moving the common text and compiled-attribute paths into their callers and avoiding full active-formatting bookkeeping for uncomplicated first entries.
|
|
17
|
+
- Reduce another 1.2% of default-constructor parse time by dispatching start and end tags before uncommon markup and removing redundant CDATA probes and regular-expression offset lookups while skipping dropped subtrees.
|
|
18
|
+
- Reduce default-constructor parse time by another 2% on a 1,000-document web100k sample by clearing stale foreign-context scan state, using C-level membership for ASCII tag dispatch, and inlining bounded case-insensitive raw-text matching.
|
|
19
|
+
- Reduce default-constructor parse time by another 3.6% on a 1,000-document web100k sample and shrink ordinary element nodes by one third by lazily allocating source-location metadata and bypassing adaptive stack bookkeeping for shallow end-tag mutations.
|
|
20
|
+
|
|
21
|
+
## [3.8.0] - 2026-07-19
|
|
22
|
+
|
|
23
|
+
### Performance
|
|
24
|
+
|
|
25
|
+
On the web100k parser benchmark (1,000 documents, three iterations), the default `JustHTML()` constructor now completes in 13.020 seconds rather than 21.648 seconds in 3.7.1: a 40% reduction in parse time, or about 1.7× higher throughput.
|
|
26
|
+
|
|
27
|
+
- Speed up the default `JustHTML()` constructor by compiling common start/end-tag and URL-sanitization decisions, and by folding only names that the parser encounters instead of copying and lowercasing the full input.
|
|
28
|
+
- Reduce tree-construction overhead by adaptively tracking open elements, removing completed formatting entries without leaving tombstones, and deferring formatting-attribute signatures until duplicate tracking actually needs them.
|
|
29
|
+
- Remove redundant constructor-path work for discarded end-tag attributes, canonical attribute names, shallow non-paragraph stack updates, active-formatting bookkeeping, and parser state that could never become active.
|
|
30
|
+
- Cut fixed default-constructor overhead by reusing built-in execution plans, skipping no-op finalization and error-list work, lazily allocating formatting indexes, and collapsing duplicate shell, mode, and end-of-input state.
|
|
31
|
+
- Collapse common start-tag and text insertion-mode checks into synchronized state flags, avoiding repeated condition ladders in the default `JustHTML()` constructor.
|
|
32
|
+
- Avoid open-elements membership scans for ordinary HTML insertions and batch large disallowed-wrapper projections instead of repeatedly searching and shifting the same child lists.
|
|
33
|
+
- Remove already-resolved sanitizer insertion, unknown-tag condition ladders, template-mode lookups, and wrapper-list copies from the default `JustHTML()` constructor path.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- (Severity: Low) Keep sanitization projection linear for large runs of disallowed sibling wrappers and avoid depth-proportional parent validation for nested wrappers. Previously, crafted wrapper-heavy HTML could make the default `JustHTML()` constructor take quadratic time.
|
|
38
|
+
|
|
39
|
+
## [3.7.1] - 2026-07-17
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Make `CollapseWhitespace(...)` trim text at block-sibling and line-break boundaries, including `<pre>`, `<hr>`, and `<br>`.
|
|
44
|
+
- Preserve visible spacing around closed dialogs and hidden elements while ignoring comments, processing instructions, empty text nodes, and native non-rendering elements when locating rendered siblings.
|
|
45
|
+
- Collapse whitespace across adjacent text runs exposed by transforms such as `Unwrap(...)` and `DropComments(...)` without merging the text nodes.
|
|
46
|
+
|
|
8
47
|
## [3.7.0] - 2026-07-15
|
|
9
48
|
|
|
10
49
|
### Fixed
|
|
@@ -22,6 +22,31 @@ Set `WEB100K_DIR` to use another location, or pass `--batches-dir` and
|
|
|
22
22
|
pip install -e ".[benchmark]"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
`html5-parser` (the Gumbo benchmark) and `lxml` must use the same libxml2
|
|
26
|
+
implementation. The `html5-parser` wheel uses the system libxml2, while a
|
|
27
|
+
current `lxml` wheel embeds a newer one. Remove that wheel and rebuild only
|
|
28
|
+
`lxml` against the local library:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip uninstall -y lxml
|
|
32
|
+
pip install --no-cache-dir --no-binary=lxml 'lxml==6.1.1'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Confirm that they agree before running the benchmark:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -c 'from lxml import etree; import html5_parser; print(etree.LIBXML_VERSION)'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If pip cannot build lxml, install your platform's libxml2 development package
|
|
42
|
+
and C compiler, then repeat the second command. Reinstalling the project extra
|
|
43
|
+
after this step is unnecessary and can reselect the binary lxml wheel.
|
|
44
|
+
|
|
45
|
+
The latest MarkupEver and TurboHTML wheels do not currently load on Python
|
|
46
|
+
3.15 pre-releases. On that interpreter they are reported as unavailable while
|
|
47
|
+
the remaining benchmark parsers, including Gumbo, still run. Use a supported
|
|
48
|
+
stable Python release when those comparisons are required.
|
|
49
|
+
|
|
25
50
|
## Measure the right pipeline
|
|
26
51
|
|
|
27
52
|
The benchmark has separate modes for parsing alone and parsing followed by
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: justhtml
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.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
|
|
@@ -35,14 +35,14 @@ Classifier: Operating System :: OS Independent
|
|
|
35
35
|
Classifier: Programming Language :: Python :: 3
|
|
36
36
|
Requires-Python: >=3.10
|
|
37
37
|
Provides-Extra: benchmark
|
|
38
|
-
Requires-Dist: beautifulsoup4; extra == 'benchmark'
|
|
39
|
-
Requires-Dist: html5-parser; extra == 'benchmark'
|
|
40
|
-
Requires-Dist: html5lib; extra == 'benchmark'
|
|
41
|
-
Requires-Dist: lxml; extra == 'benchmark'
|
|
42
|
-
Requires-Dist: markupever; extra == 'benchmark'
|
|
38
|
+
Requires-Dist: beautifulsoup4>=4.14.3; extra == 'benchmark'
|
|
39
|
+
Requires-Dist: html5-parser>=0.4.12; extra == 'benchmark'
|
|
40
|
+
Requires-Dist: html5lib>=1.1; extra == 'benchmark'
|
|
41
|
+
Requires-Dist: lxml>=6.1.1; extra == 'benchmark'
|
|
42
|
+
Requires-Dist: markupever>=0.3.4; extra == 'benchmark'
|
|
43
43
|
Requires-Dist: selectolax>=0.4.11; extra == 'benchmark'
|
|
44
|
-
Requires-Dist: turbohtml>=1.
|
|
45
|
-
Requires-Dist: zstandard; extra == 'benchmark'
|
|
44
|
+
Requires-Dist: turbohtml>=1.4.0; extra == 'benchmark'
|
|
45
|
+
Requires-Dist: zstandard>=0.25.0; extra == 'benchmark'
|
|
46
46
|
Provides-Extra: dev
|
|
47
47
|
Requires-Dist: build; extra == 'dev'
|
|
48
48
|
Requires-Dist: coverage; extra == 'dev'
|
|
@@ -454,6 +454,8 @@ def benchmark_gumbo(html_source, iterations=1):
|
|
|
454
454
|
import html5_parser
|
|
455
455
|
except ImportError:
|
|
456
456
|
return {"error": "html5-parser not installed (pip install html5-parser)"}
|
|
457
|
+
except RuntimeError as e:
|
|
458
|
+
return {"error": f"html5-parser unavailable ({e})"}
|
|
457
459
|
times = []
|
|
458
460
|
errors = 0
|
|
459
461
|
total_bytes = 0
|
|
@@ -493,8 +495,10 @@ def benchmark_markupever(html_source, iterations=1):
|
|
|
493
495
|
"""Benchmark markupever parser."""
|
|
494
496
|
try:
|
|
495
497
|
from markupever import parse
|
|
496
|
-
except ImportError:
|
|
497
|
-
return {"error": "markupever
|
|
498
|
+
except ImportError as e:
|
|
499
|
+
return {"error": f"markupever unavailable ({e})"}
|
|
500
|
+
except (RuntimeError, SystemError) as e:
|
|
501
|
+
return {"error": f"markupever unavailable ({e})"}
|
|
498
502
|
times = []
|
|
499
503
|
errors = 0
|
|
500
504
|
total_bytes = 0
|
|
@@ -534,8 +538,10 @@ def benchmark_turbohtml(html_source, iterations=1):
|
|
|
534
538
|
"""Benchmark TurboHTML parser."""
|
|
535
539
|
try:
|
|
536
540
|
from turbohtml import parse
|
|
537
|
-
except ImportError:
|
|
538
|
-
return {"error": "turbohtml
|
|
541
|
+
except ImportError as e:
|
|
542
|
+
return {"error": f"turbohtml unavailable ({e})"}
|
|
543
|
+
except (RuntimeError, SystemError) as e:
|
|
544
|
+
return {"error": f"turbohtml unavailable ({e})"}
|
|
539
545
|
times = []
|
|
540
546
|
errors = 0
|
|
541
547
|
total_bytes = 0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "justhtml"
|
|
3
3
|
authors = [{ name = "Emil Stenström", email = "emil@emilstenstrom.se" }]
|
|
4
|
-
version = "3.
|
|
4
|
+
version = "3.9.0"
|
|
5
5
|
description = "A pure Python HTML5 parser that just works."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = { file = "LICENSE" }
|
|
@@ -14,14 +14,14 @@ classifiers = [
|
|
|
14
14
|
|
|
15
15
|
[project.optional-dependencies]
|
|
16
16
|
benchmark = [
|
|
17
|
-
"zstandard",
|
|
18
|
-
"html5lib",
|
|
19
|
-
"lxml",
|
|
20
|
-
"beautifulsoup4",
|
|
17
|
+
"zstandard>=0.25.0",
|
|
18
|
+
"html5lib>=1.1",
|
|
19
|
+
"lxml>=6.1.1",
|
|
20
|
+
"beautifulsoup4>=4.14.3",
|
|
21
21
|
"selectolax>=0.4.11",
|
|
22
|
-
"html5-parser",
|
|
23
|
-
"markupever",
|
|
24
|
-
"turbohtml>=1.
|
|
22
|
+
"html5-parser>=0.4.12",
|
|
23
|
+
"markupever>=0.3.4",
|
|
24
|
+
"turbohtml>=1.4.0",
|
|
25
25
|
]
|
|
26
26
|
dev = [
|
|
27
27
|
"ruff==0.14.7",
|
|
@@ -230,6 +230,7 @@ WHITESPACE_TRIMMING_BLOCK_ELEMENTS: Final[frozenset[str]] = frozenset(
|
|
|
230
230
|
"header",
|
|
231
231
|
"hgroup",
|
|
232
232
|
"html",
|
|
233
|
+
"hr",
|
|
233
234
|
"li",
|
|
234
235
|
"listing",
|
|
235
236
|
"main",
|
|
@@ -237,6 +238,7 @@ WHITESPACE_TRIMMING_BLOCK_ELEMENTS: Final[frozenset[str]] = frozenset(
|
|
|
237
238
|
"nav",
|
|
238
239
|
"ol",
|
|
239
240
|
"p",
|
|
241
|
+
"pre",
|
|
240
242
|
"search",
|
|
241
243
|
"section",
|
|
242
244
|
"summary",
|
|
@@ -132,10 +132,7 @@ def _to_text_collect_block_chunks(node: NodeType, chunks: list[list[str]], strip
|
|
|
132
132
|
|
|
133
133
|
class Node:
|
|
134
134
|
__slots__ = (
|
|
135
|
-
"
|
|
136
|
-
"_origin_line",
|
|
137
|
-
"_origin_pos",
|
|
138
|
-
"_source_html",
|
|
135
|
+
"_metadata",
|
|
139
136
|
"attrs",
|
|
140
137
|
"children",
|
|
141
138
|
"data",
|
|
@@ -150,10 +147,7 @@ class Node:
|
|
|
150
147
|
children: list[Any] | None
|
|
151
148
|
data: str | Doctype | None
|
|
152
149
|
namespace: str | None
|
|
153
|
-
|
|
154
|
-
_origin_line: int | None
|
|
155
|
-
_origin_col: int | None
|
|
156
|
-
_source_html: str | None
|
|
150
|
+
_metadata: list[str | int | None] | None
|
|
157
151
|
|
|
158
152
|
def __init__(
|
|
159
153
|
self,
|
|
@@ -165,10 +159,7 @@ class Node:
|
|
|
165
159
|
self.name = name
|
|
166
160
|
self.parent = None
|
|
167
161
|
self.data = data
|
|
168
|
-
self.
|
|
169
|
-
self._origin_pos = None
|
|
170
|
-
self._origin_line = None
|
|
171
|
-
self._origin_col = None
|
|
162
|
+
self._metadata = None
|
|
172
163
|
|
|
173
164
|
if name.startswith("#") or name == "!doctype":
|
|
174
165
|
self.namespace = namespace
|
|
@@ -220,6 +211,66 @@ class Node:
|
|
|
220
211
|
node.parent = None
|
|
221
212
|
return old_parent, old_index
|
|
222
213
|
|
|
214
|
+
@property
|
|
215
|
+
def _source_html(self) -> str | None:
|
|
216
|
+
metadata = self._metadata
|
|
217
|
+
return None if metadata is None else cast("str | None", metadata[0])
|
|
218
|
+
|
|
219
|
+
@_source_html.setter
|
|
220
|
+
def _source_html(self, value: str | None) -> None:
|
|
221
|
+
metadata = self._metadata
|
|
222
|
+
if metadata is None:
|
|
223
|
+
if value is None:
|
|
224
|
+
return
|
|
225
|
+
metadata = [None] * 8
|
|
226
|
+
self._metadata = metadata
|
|
227
|
+
metadata[0] = value
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def _origin_pos(self) -> int | None:
|
|
231
|
+
metadata = self._metadata
|
|
232
|
+
return None if metadata is None else cast("int | None", metadata[1])
|
|
233
|
+
|
|
234
|
+
@_origin_pos.setter
|
|
235
|
+
def _origin_pos(self, value: int | None) -> None:
|
|
236
|
+
metadata = self._metadata
|
|
237
|
+
if metadata is None:
|
|
238
|
+
if value is None:
|
|
239
|
+
return
|
|
240
|
+
metadata = [None] * 8
|
|
241
|
+
self._metadata = metadata
|
|
242
|
+
metadata[1] = value
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def _origin_line(self) -> int | None:
|
|
246
|
+
metadata = self._metadata
|
|
247
|
+
return None if metadata is None else cast("int | None", metadata[2])
|
|
248
|
+
|
|
249
|
+
@_origin_line.setter
|
|
250
|
+
def _origin_line(self, value: int | None) -> None:
|
|
251
|
+
metadata = self._metadata
|
|
252
|
+
if metadata is None:
|
|
253
|
+
if value is None:
|
|
254
|
+
return
|
|
255
|
+
metadata = [None] * 8
|
|
256
|
+
self._metadata = metadata
|
|
257
|
+
metadata[2] = value
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def _origin_col(self) -> int | None:
|
|
261
|
+
metadata = self._metadata
|
|
262
|
+
return None if metadata is None else cast("int | None", metadata[3])
|
|
263
|
+
|
|
264
|
+
@_origin_col.setter
|
|
265
|
+
def _origin_col(self, value: int | None) -> None:
|
|
266
|
+
metadata = self._metadata
|
|
267
|
+
if metadata is None:
|
|
268
|
+
if value is None:
|
|
269
|
+
return
|
|
270
|
+
metadata = [None] * 8
|
|
271
|
+
self._metadata = metadata
|
|
272
|
+
metadata[3] = value
|
|
273
|
+
|
|
223
274
|
@property
|
|
224
275
|
def origin_offset(self) -> int | None:
|
|
225
276
|
"""Best-effort origin offset (0-indexed) in the source HTML, if known."""
|
|
@@ -429,10 +480,7 @@ class Node:
|
|
|
429
480
|
self.data,
|
|
430
481
|
self.namespace,
|
|
431
482
|
)
|
|
432
|
-
clone.
|
|
433
|
-
clone._origin_pos = self._origin_pos
|
|
434
|
-
clone._origin_line = self._origin_line
|
|
435
|
-
clone._origin_col = self._origin_col
|
|
483
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
436
484
|
if deep:
|
|
437
485
|
return _clone_subtree_iterative(self)
|
|
438
486
|
return clone
|
|
@@ -447,10 +495,7 @@ class Document(Node):
|
|
|
447
495
|
def clone_node(self, deep: bool = False, override_attrs: dict[str, str | None] | None = None) -> Document:
|
|
448
496
|
_ = override_attrs
|
|
449
497
|
clone = Document()
|
|
450
|
-
clone.
|
|
451
|
-
clone._origin_pos = self._origin_pos
|
|
452
|
-
clone._origin_line = self._origin_line
|
|
453
|
-
clone._origin_col = self._origin_col
|
|
498
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
454
499
|
if deep:
|
|
455
500
|
return cast("Document", _clone_subtree_iterative(self))
|
|
456
501
|
return clone
|
|
@@ -465,10 +510,7 @@ class DocumentFragment(Node):
|
|
|
465
510
|
def clone_node(self, deep: bool = False, override_attrs: dict[str, str | None] | None = None) -> DocumentFragment:
|
|
466
511
|
_ = override_attrs
|
|
467
512
|
clone = DocumentFragment()
|
|
468
|
-
clone.
|
|
469
|
-
clone._origin_pos = self._origin_pos
|
|
470
|
-
clone._origin_line = self._origin_line
|
|
471
|
-
clone._origin_col = self._origin_col
|
|
513
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
472
514
|
if deep:
|
|
473
515
|
return cast("DocumentFragment", _clone_subtree_iterative(self))
|
|
474
516
|
return clone
|
|
@@ -484,10 +526,7 @@ class Comment(Node):
|
|
|
484
526
|
_ = override_attrs
|
|
485
527
|
_ = deep
|
|
486
528
|
clone = Comment(self.data if isinstance(self.data, str) else None)
|
|
487
|
-
clone.
|
|
488
|
-
clone._origin_pos = self._origin_pos
|
|
489
|
-
clone._origin_line = self._origin_line
|
|
490
|
-
clone._origin_col = self._origin_col
|
|
529
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
491
530
|
return clone
|
|
492
531
|
|
|
493
532
|
|
|
@@ -503,31 +542,20 @@ class ProcessingInstruction(Node):
|
|
|
503
542
|
_ = override_attrs
|
|
504
543
|
_ = deep
|
|
505
544
|
clone = ProcessingInstruction(self.data if isinstance(self.data, str) else None)
|
|
506
|
-
clone.
|
|
507
|
-
clone._origin_pos = self._origin_pos
|
|
508
|
-
clone._origin_line = self._origin_line
|
|
509
|
-
clone._origin_col = self._origin_col
|
|
545
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
510
546
|
return clone
|
|
511
547
|
|
|
512
548
|
|
|
513
549
|
class Element(Node):
|
|
514
550
|
__slots__ = (
|
|
515
|
-
"_end_tag_end",
|
|
516
551
|
"_end_tag_present",
|
|
517
|
-
"_end_tag_start",
|
|
518
552
|
"_self_closing",
|
|
519
|
-
"_start_tag_end",
|
|
520
|
-
"_start_tag_start",
|
|
521
553
|
"template_content",
|
|
522
554
|
)
|
|
523
555
|
|
|
524
556
|
template_content: Node | None
|
|
525
557
|
children: list[Any]
|
|
526
558
|
attrs: dict[str, str | None]
|
|
527
|
-
_start_tag_start: int | None
|
|
528
|
-
_start_tag_end: int | None
|
|
529
|
-
_end_tag_start: int | None
|
|
530
|
-
_end_tag_end: int | None
|
|
531
559
|
_end_tag_present: bool
|
|
532
560
|
_self_closing: bool
|
|
533
561
|
|
|
@@ -539,28 +567,74 @@ class Element(Node):
|
|
|
539
567
|
self.children = []
|
|
540
568
|
self.attrs = attrs if attrs is not None else {}
|
|
541
569
|
self.template_content = None
|
|
542
|
-
self.
|
|
543
|
-
self._origin_pos = None
|
|
544
|
-
self._origin_line = None
|
|
545
|
-
self._origin_col = None
|
|
546
|
-
self._start_tag_start = None
|
|
547
|
-
self._start_tag_end = None
|
|
548
|
-
self._end_tag_start = None
|
|
549
|
-
self._end_tag_end = None
|
|
570
|
+
self._metadata = None
|
|
550
571
|
self._end_tag_present = False
|
|
551
572
|
self._self_closing = False
|
|
552
573
|
|
|
574
|
+
@property
|
|
575
|
+
def _start_tag_start(self) -> int | None:
|
|
576
|
+
metadata = self._metadata
|
|
577
|
+
return None if metadata is None else cast("int | None", metadata[4])
|
|
578
|
+
|
|
579
|
+
@_start_tag_start.setter
|
|
580
|
+
def _start_tag_start(self, value: int | None) -> None:
|
|
581
|
+
metadata = self._metadata
|
|
582
|
+
if metadata is None:
|
|
583
|
+
if value is None:
|
|
584
|
+
return
|
|
585
|
+
metadata = [None] * 8
|
|
586
|
+
self._metadata = metadata
|
|
587
|
+
metadata[4] = value
|
|
588
|
+
|
|
589
|
+
@property
|
|
590
|
+
def _start_tag_end(self) -> int | None:
|
|
591
|
+
metadata = self._metadata
|
|
592
|
+
return None if metadata is None else cast("int | None", metadata[5])
|
|
593
|
+
|
|
594
|
+
@_start_tag_end.setter
|
|
595
|
+
def _start_tag_end(self, value: int | None) -> None:
|
|
596
|
+
metadata = self._metadata
|
|
597
|
+
if metadata is None:
|
|
598
|
+
if value is None:
|
|
599
|
+
return
|
|
600
|
+
metadata = [None] * 8
|
|
601
|
+
self._metadata = metadata
|
|
602
|
+
metadata[5] = value
|
|
603
|
+
|
|
604
|
+
@property
|
|
605
|
+
def _end_tag_start(self) -> int | None:
|
|
606
|
+
metadata = self._metadata
|
|
607
|
+
return None if metadata is None else cast("int | None", metadata[6])
|
|
608
|
+
|
|
609
|
+
@_end_tag_start.setter
|
|
610
|
+
def _end_tag_start(self, value: int | None) -> None:
|
|
611
|
+
metadata = self._metadata
|
|
612
|
+
if metadata is None:
|
|
613
|
+
if value is None:
|
|
614
|
+
return
|
|
615
|
+
metadata = [None] * 8
|
|
616
|
+
self._metadata = metadata
|
|
617
|
+
metadata[6] = value
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def _end_tag_end(self) -> int | None:
|
|
621
|
+
metadata = self._metadata
|
|
622
|
+
return None if metadata is None else cast("int | None", metadata[7])
|
|
623
|
+
|
|
624
|
+
@_end_tag_end.setter
|
|
625
|
+
def _end_tag_end(self, value: int | None) -> None:
|
|
626
|
+
metadata = self._metadata
|
|
627
|
+
if metadata is None:
|
|
628
|
+
if value is None:
|
|
629
|
+
return
|
|
630
|
+
metadata = [None] * 8
|
|
631
|
+
self._metadata = metadata
|
|
632
|
+
metadata[7] = value
|
|
633
|
+
|
|
553
634
|
def clone_node(self, deep: bool = False, override_attrs: dict[str, str | None] | None = None) -> Element:
|
|
554
635
|
attrs = override_attrs.copy() if override_attrs is not None else (self.attrs.copy() if self.attrs else {})
|
|
555
636
|
clone = Element(self.name, attrs, self.namespace)
|
|
556
|
-
clone.
|
|
557
|
-
clone._origin_pos = self._origin_pos
|
|
558
|
-
clone._origin_line = self._origin_line
|
|
559
|
-
clone._origin_col = self._origin_col
|
|
560
|
-
clone._start_tag_start = self._start_tag_start
|
|
561
|
-
clone._start_tag_end = self._start_tag_end
|
|
562
|
-
clone._end_tag_start = self._end_tag_start
|
|
563
|
-
clone._end_tag_end = self._end_tag_end
|
|
637
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
564
638
|
clone._end_tag_present = self._end_tag_present
|
|
565
639
|
clone._self_closing = self._self_closing
|
|
566
640
|
if deep:
|
|
@@ -593,14 +667,7 @@ class Template(Element):
|
|
|
593
667
|
None,
|
|
594
668
|
self.namespace,
|
|
595
669
|
)
|
|
596
|
-
clone.
|
|
597
|
-
clone._origin_pos = self._origin_pos
|
|
598
|
-
clone._origin_line = self._origin_line
|
|
599
|
-
clone._origin_col = self._origin_col
|
|
600
|
-
clone._start_tag_start = self._start_tag_start
|
|
601
|
-
clone._start_tag_end = self._start_tag_end
|
|
602
|
-
clone._end_tag_start = self._end_tag_start
|
|
603
|
-
clone._end_tag_end = self._end_tag_end
|
|
670
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
604
671
|
clone._end_tag_present = self._end_tag_present
|
|
605
672
|
clone._self_closing = self._self_closing
|
|
606
673
|
if deep:
|
|
@@ -637,24 +704,65 @@ def _clone_subtree_iterative(root: Node) -> Node:
|
|
|
637
704
|
|
|
638
705
|
|
|
639
706
|
class Text:
|
|
640
|
-
__slots__ = ("
|
|
707
|
+
__slots__ = ("_metadata", "data", "name", "namespace", "parent")
|
|
641
708
|
|
|
642
709
|
data: str | None
|
|
643
710
|
name: str
|
|
644
711
|
namespace: None
|
|
645
712
|
parent: Node | None
|
|
646
|
-
|
|
647
|
-
_origin_line: int | None
|
|
648
|
-
_origin_col: int | None
|
|
713
|
+
_metadata: list[int | None] | None
|
|
649
714
|
|
|
650
715
|
def __init__(self, data: str | None) -> None:
|
|
651
716
|
self.data = data
|
|
652
717
|
self.parent = None
|
|
653
718
|
self.name = "#text"
|
|
654
719
|
self.namespace = None
|
|
655
|
-
self.
|
|
656
|
-
|
|
657
|
-
|
|
720
|
+
self._metadata = None
|
|
721
|
+
|
|
722
|
+
@property
|
|
723
|
+
def _origin_pos(self) -> int | None:
|
|
724
|
+
metadata = self._metadata
|
|
725
|
+
return None if metadata is None else metadata[0]
|
|
726
|
+
|
|
727
|
+
@_origin_pos.setter
|
|
728
|
+
def _origin_pos(self, value: int | None) -> None:
|
|
729
|
+
metadata = self._metadata
|
|
730
|
+
if metadata is None:
|
|
731
|
+
if value is None:
|
|
732
|
+
return
|
|
733
|
+
metadata = [None, None, None]
|
|
734
|
+
self._metadata = metadata
|
|
735
|
+
metadata[0] = value
|
|
736
|
+
|
|
737
|
+
@property
|
|
738
|
+
def _origin_line(self) -> int | None:
|
|
739
|
+
metadata = self._metadata
|
|
740
|
+
return None if metadata is None else metadata[1]
|
|
741
|
+
|
|
742
|
+
@_origin_line.setter
|
|
743
|
+
def _origin_line(self, value: int | None) -> None:
|
|
744
|
+
metadata = self._metadata
|
|
745
|
+
if metadata is None:
|
|
746
|
+
if value is None:
|
|
747
|
+
return
|
|
748
|
+
metadata = [None, None, None]
|
|
749
|
+
self._metadata = metadata
|
|
750
|
+
metadata[1] = value
|
|
751
|
+
|
|
752
|
+
@property
|
|
753
|
+
def _origin_col(self) -> int | None:
|
|
754
|
+
metadata = self._metadata
|
|
755
|
+
return None if metadata is None else metadata[2]
|
|
756
|
+
|
|
757
|
+
@_origin_col.setter
|
|
758
|
+
def _origin_col(self, value: int | None) -> None:
|
|
759
|
+
metadata = self._metadata
|
|
760
|
+
if metadata is None:
|
|
761
|
+
if value is None:
|
|
762
|
+
return
|
|
763
|
+
metadata = [None, None, None]
|
|
764
|
+
self._metadata = metadata
|
|
765
|
+
metadata[2] = value
|
|
658
766
|
|
|
659
767
|
@property
|
|
660
768
|
def origin_offset(self) -> int | None:
|
|
@@ -712,9 +820,7 @@ class Text:
|
|
|
712
820
|
def clone_node(self, deep: bool = False) -> Text:
|
|
713
821
|
_ = deep
|
|
714
822
|
clone = Text(self.data)
|
|
715
|
-
clone.
|
|
716
|
-
clone._origin_line = self._origin_line
|
|
717
|
-
clone._origin_col = self._origin_col
|
|
823
|
+
clone._metadata = self._metadata.copy() if self._metadata is not None else None
|
|
718
824
|
return clone
|
|
719
825
|
|
|
720
826
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from justhtml.core.types import ParseError
|
|
8
8
|
from justhtml.dom import Document, DocumentFragment, Node, QueryMatch, Text
|
|
@@ -20,7 +20,6 @@ from .engine import (
|
|
|
20
20
|
ParseEngine,
|
|
21
21
|
can_compile_engine_plan,
|
|
22
22
|
compile_default_engine_plan,
|
|
23
|
-
compile_engine_plan,
|
|
24
23
|
compile_raw_engine_plan,
|
|
25
24
|
)
|
|
26
25
|
|
|
@@ -150,7 +149,9 @@ class JustHTML:
|
|
|
150
149
|
self.encoding = None
|
|
151
150
|
|
|
152
151
|
html_str: str
|
|
153
|
-
if
|
|
152
|
+
if type(html) is str:
|
|
153
|
+
html_str = html
|
|
154
|
+
elif isinstance(html, (Node, Text)):
|
|
154
155
|
html_for_serialization = html
|
|
155
156
|
if sanitize_enabled or has_sanitize_transform or has_harden_rawtext_transform:
|
|
156
157
|
from justhtml.sanitizer import ( # noqa: PLC0415
|
|
@@ -179,40 +180,28 @@ class JustHTML:
|
|
|
179
180
|
# Node location tracking is opt-in to avoid slowing down the common case.
|
|
180
181
|
should_collect = collect_errors or strict
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
xml_coercion = False
|
|
192
|
-
emit_bogus_markup_as_text = False
|
|
193
|
-
discard_bom = bool(getattr(_parser_opts, "discard_bom", True))
|
|
194
|
-
if discard_bom and html_str.startswith("\ufeff"):
|
|
195
|
-
html_str = html_str[1:]
|
|
196
|
-
if _parser_opts is not None:
|
|
197
|
-
xml_coercion = bool(getattr(_parser_opts, "xml_coercion", False))
|
|
198
|
-
emit_bogus_markup_as_text = bool(getattr(_parser_opts, "emit_bogus_markup_as_text", False))
|
|
183
|
+
if _parser_opts is None:
|
|
184
|
+
discard_bom = True
|
|
185
|
+
xml_coercion = False
|
|
186
|
+
emit_bogus_markup_as_text = False
|
|
187
|
+
else:
|
|
188
|
+
discard_bom = _parser_opts.discard_bom
|
|
189
|
+
xml_coercion = _parser_opts.xml_coercion
|
|
190
|
+
emit_bogus_markup_as_text = _parser_opts.emit_bogus_markup_as_text
|
|
199
191
|
if emit_bogus_markup_as_text:
|
|
200
192
|
track_tag_spans = True
|
|
193
|
+
if discard_bom and html_str.startswith("\ufeff"):
|
|
194
|
+
html_str = html_str[1:]
|
|
201
195
|
|
|
202
196
|
use_compiled_safe_engine = (
|
|
203
|
-
|
|
197
|
+
sanitize_enabled
|
|
198
|
+
and not transforms
|
|
199
|
+
and (policy is None or can_compile_engine_plan(policy, fragment=fragment))
|
|
204
200
|
)
|
|
205
201
|
if use_compiled_safe_engine:
|
|
206
|
-
|
|
207
|
-
engine_plan = compile_default_engine_plan(fragment=fragment, scripting_enabled=scripting_enabled)
|
|
208
|
-
else:
|
|
209
|
-
engine_plan = compile_engine_plan(
|
|
210
|
-
policy=cast("SanitizationPolicy", engine_policy),
|
|
211
|
-
fragment=fragment,
|
|
212
|
-
scripting_enabled=scripting_enabled,
|
|
213
|
-
)
|
|
202
|
+
engine_plan = compile_default_engine_plan(fragment, scripting_enabled)
|
|
214
203
|
else:
|
|
215
|
-
engine_plan = compile_raw_engine_plan(fragment
|
|
204
|
+
engine_plan = compile_raw_engine_plan(fragment, scripting_enabled)
|
|
216
205
|
|
|
217
206
|
engine = ParseEngine(
|
|
218
207
|
html_str,
|
|
@@ -223,14 +212,14 @@ class JustHTML:
|
|
|
223
212
|
collect_errors=should_collect,
|
|
224
213
|
max_errors=max_errors,
|
|
225
214
|
iframe_srcdoc=iframe_srcdoc,
|
|
226
|
-
track_node_locations=
|
|
227
|
-
track_tag_spans=
|
|
215
|
+
track_node_locations=track_node_locations,
|
|
216
|
+
track_tag_spans=track_node_locations or track_tag_spans,
|
|
228
217
|
emit_bogus_markup_as_text=emit_bogus_markup_as_text,
|
|
229
218
|
xml_coercion=xml_coercion,
|
|
230
219
|
)
|
|
231
220
|
self.root = engine.parse()
|
|
232
221
|
|
|
233
|
-
|
|
222
|
+
errors = engine.errors
|
|
234
223
|
if not use_compiled_safe_engine:
|
|
235
224
|
transform_errors = self._apply_constructor_transforms(
|
|
236
225
|
transforms=transforms,
|
|
@@ -239,11 +228,9 @@ class JustHTML:
|
|
|
239
228
|
has_sanitize_transform=has_sanitize_transform,
|
|
240
229
|
max_errors=max_errors - len(engine.errors),
|
|
241
230
|
)
|
|
231
|
+
errors = engine.errors + transform_errors if should_collect else transform_errors
|
|
242
232
|
|
|
243
|
-
if should_collect
|
|
244
|
-
self.errors = self._sorted_errors(engine.errors + transform_errors)
|
|
245
|
-
else:
|
|
246
|
-
self.errors = transform_errors
|
|
233
|
+
self.errors = self._sorted_errors(errors) if should_collect else errors
|
|
247
234
|
if strict and self.errors:
|
|
248
235
|
raise StrictModeError(self.errors[0])
|
|
249
236
|
|