justhtml 2.3.0__tar.gz → 2.4.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.3.0 → justhtml-2.4.0}/CHANGELOG.md +5 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/PKG-INFO +3 -1
- {justhtml-2.3.0 → justhtml-2.4.0}/README.md +2 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/bleach-migration.md +117 -4
- {justhtml-2.3.0 → justhtml-2.4.0}/pyproject.toml +1 -1
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/treebuilder/modes.py +7 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_treebuilder.py +30 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/.github/copilot-instructions.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/.github/workflows/ci.yml +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/.github/workflows/publish.yml +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/.gitignore +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/.pre-commit-config.yaml +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/CONTRIBUTING.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/LICENSE +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/Makefile +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/SECURITY.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/benchmarks/correctness.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/benchmarks/fuzz.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/benchmarks/performance.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/benchmarks/profile.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/_config.yml +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/_layouts/default.html +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/api.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/assets/search.js +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/building.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/cli.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/comparison.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/correctness.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/encoding.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/errors.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/fragments.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/html-cleaning.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/index.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/linkify.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/migration-examples.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/playground/app.js +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/playground/index.html +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/playground/py/render.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/quickstart.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/sanitization.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/selectors.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/streaming.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/text.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/transforms.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/unsafe-handling.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/docs/url-cleaning.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/llms.txt +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/run_tests.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/scripts/release.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/__main__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/parser/stream.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/py.typed +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/serializer/html.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/tokenizer/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/tokenizer/html.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/tokenizer/tokens.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/transforms/runtime.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/treebuilder/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/treebuilder/core.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/src/justhtml/treebuilder/utils.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/test-summary.txt +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/README.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/data/wikipedia.html +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/harness/__init__.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/harness/encoding.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/harness/regressions.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/harness/reporter.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/harness/serializer.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/harness/tokenizer.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/harness/tree.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/branch_coverage.dat +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/coverage_gaps.test +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/empty_stack_edge_cases.dat +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/tokenizer_edge_cases.test +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/treebuilder_coverage.dat +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/justhtml-tests/xml_coercion_coverage.test +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/linkify-it/README.md +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_builder.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_cli.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_docs_examples.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_encoding.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_errors.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_linkify_it.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_node.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_sanitize.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_selector.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_serialize.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_stream.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_tokenizer.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_transforms.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_transforms_edge_cases.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-2.3.0 → justhtml-2.4.0}/tests/test_wikipedia.py +0 -0
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.0] - 2026-06-20
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Handle `<hr>` using the HTML in-body void-element rules in delegated insertion modes, preserving following content in table cells and captions, closing open paragraphs, and acknowledging self-closing syntax.
|
|
14
|
+
|
|
10
15
|
## [2.3.0] - 2026-06-12
|
|
11
16
|
|
|
12
17
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: justhtml
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -88,6 +88,8 @@ JustHTML is for applications that want a boring, inspectable, pure-Python defaul
|
|
|
88
88
|
- **Easy deployment:** zero runtime dependencies, no compiler, works on PyPy and Pyodide.
|
|
89
89
|
- **Honest tradeoff:** if you are parsing terabytes of trusted HTML, use a C/Rust parser. If you need reliable handling of untrusted or malformed HTML inside a Python app, use JustHTML.
|
|
90
90
|
|
|
91
|
+
Real-world signal: [Mozilla Support migrated from Bleach to JustHTML](https://github.com/mozilla/kitsune/pull/7236) in Kitsune, the Django application behind support.mozilla.org.
|
|
92
|
+
|
|
91
93
|
## Quick Start
|
|
92
94
|
|
|
93
95
|
```python
|
|
@@ -34,6 +34,8 @@ JustHTML is for applications that want a boring, inspectable, pure-Python defaul
|
|
|
34
34
|
- **Easy deployment:** zero runtime dependencies, no compiler, works on PyPy and Pyodide.
|
|
35
35
|
- **Honest tradeoff:** if you are parsing terabytes of trusted HTML, use a C/Rust parser. If you need reliable handling of untrusted or malformed HTML inside a Python app, use JustHTML.
|
|
36
36
|
|
|
37
|
+
Real-world signal: [Mozilla Support migrated from Bleach to JustHTML](https://github.com/mozilla/kitsune/pull/7236) in Kitsune, the Django application behind support.mozilla.org.
|
|
38
|
+
|
|
37
39
|
## Quick Start
|
|
38
40
|
|
|
39
41
|
```python
|
|
@@ -10,6 +10,107 @@ In 2023, Bleach’s maintainer announced that **Bleach is deprecated** (but will
|
|
|
10
10
|
|
|
11
11
|
This guide covers common migration patterns.
|
|
12
12
|
|
|
13
|
+
## Real-world migration pattern: add a local wrapper first
|
|
14
|
+
|
|
15
|
+
A large application migration often works best in two steps:
|
|
16
|
+
|
|
17
|
+
1. Add a small application-local `clean(...)` wrapper that accepts the Bleach-shaped options your codebase already uses.
|
|
18
|
+
2. Migrate call sites from `bleach.clean(...)` / `bleach.linkify(...)` to that wrapper.
|
|
19
|
+
|
|
20
|
+
That keeps most call-site changes mechanical while leaving the security policy in one place.
|
|
21
|
+
|
|
22
|
+
Example wrapper:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import re
|
|
26
|
+
from collections.abc import Collection, Mapping
|
|
27
|
+
|
|
28
|
+
from justhtml import JustHTML, Linkify, SanitizationPolicy, SetAttrs, UrlPolicy, UrlRule
|
|
29
|
+
|
|
30
|
+
URL_LIKE_ATTRS = {
|
|
31
|
+
"href",
|
|
32
|
+
"src",
|
|
33
|
+
"srcset",
|
|
34
|
+
"poster",
|
|
35
|
+
"action",
|
|
36
|
+
"formaction",
|
|
37
|
+
"data",
|
|
38
|
+
"cite",
|
|
39
|
+
"background",
|
|
40
|
+
"ping",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_url_policy(
|
|
45
|
+
allowed_tags: Collection[str],
|
|
46
|
+
allowed_attributes: Mapping[str, Collection[str]],
|
|
47
|
+
) -> UrlPolicy:
|
|
48
|
+
rules = {}
|
|
49
|
+
|
|
50
|
+
global_attrs = allowed_attributes.get("*", ())
|
|
51
|
+
for tag in allowed_tags:
|
|
52
|
+
for attr in global_attrs:
|
|
53
|
+
if attr in URL_LIKE_ATTRS:
|
|
54
|
+
rules[(tag, attr)] = UrlRule(
|
|
55
|
+
allowed_schemes={"http", "https", "mailto", "tel"},
|
|
56
|
+
allow_relative=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
for tag, attrs in allowed_attributes.items():
|
|
60
|
+
if tag == "*":
|
|
61
|
+
continue
|
|
62
|
+
for attr in attrs:
|
|
63
|
+
if attr in URL_LIKE_ATTRS:
|
|
64
|
+
rules[(tag, attr)] = UrlRule(
|
|
65
|
+
allowed_schemes={"http", "https", "mailto", "tel"},
|
|
66
|
+
allow_relative=True,
|
|
67
|
+
)
|
|
68
|
+
return UrlPolicy(allow_rules=rules)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def clean(
|
|
72
|
+
html: str,
|
|
73
|
+
*,
|
|
74
|
+
tags: Collection[str] = (),
|
|
75
|
+
attributes: Mapping[str, Collection[str]] | None = None,
|
|
76
|
+
css_properties: Collection[str] = (),
|
|
77
|
+
strip: bool = True,
|
|
78
|
+
strip_comments: bool = True,
|
|
79
|
+
) -> str:
|
|
80
|
+
attrs = dict(attributes or {})
|
|
81
|
+
disallowed_tag_handling = "unwrap" if strip else "escape"
|
|
82
|
+
|
|
83
|
+
# Bleach defaults to strip_comments=True. If you use escape mode
|
|
84
|
+
# (`strip=False`) and still want comments removed rather than displayed,
|
|
85
|
+
# remove comments before parsing.
|
|
86
|
+
if strip_comments and disallowed_tag_handling == "escape":
|
|
87
|
+
html = re.sub(r"<!--.*?-->", "", html, flags=re.DOTALL)
|
|
88
|
+
|
|
89
|
+
policy = SanitizationPolicy(
|
|
90
|
+
allowed_tags=tags,
|
|
91
|
+
allowed_attributes=attrs,
|
|
92
|
+
allowed_css_properties=css_properties,
|
|
93
|
+
url_policy=build_url_policy(tags, attrs),
|
|
94
|
+
drop_comments=strip_comments,
|
|
95
|
+
disallowed_tag_handling=disallowed_tag_handling,
|
|
96
|
+
)
|
|
97
|
+
return JustHTML(html, fragment=True, policy=policy).to_html(pretty=False)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def linkify(text: str, *, nofollow: bool = False) -> str:
|
|
101
|
+
transforms = [Linkify()]
|
|
102
|
+
if nofollow:
|
|
103
|
+
transforms.append(SetAttrs("a", rel="nofollow"))
|
|
104
|
+
return JustHTML(
|
|
105
|
+
text,
|
|
106
|
+
fragment=True,
|
|
107
|
+
sanitize=False,
|
|
108
|
+
transforms=transforms,
|
|
109
|
+
).to_html(pretty=False)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Treat this as a compatibility shim, not a universal policy. Tighten `build_url_policy(...)` for your application, especially for attributes that load remote resources such as `img[src]`.
|
|
113
|
+
|
|
13
114
|
## Mental model differences
|
|
14
115
|
|
|
15
116
|
- Bleach takes a string and returns a cleaned string.
|
|
@@ -97,6 +198,7 @@ print(doc.to_html(pretty=False))
|
|
|
97
198
|
Bleach’s `protocols=[...]` concept maps to JustHTML’s URL policy rules.
|
|
98
199
|
|
|
99
200
|
- Configure allowed schemes per attribute via `UrlRule(allowed_schemes=[...])`.
|
|
201
|
+
- URL-like attributes need explicit `(tag, attr)` allow rules even when the attribute itself is allowed.
|
|
100
202
|
- Under `sanitize=True`, URLs can be rewritten or stripped according to policy (see [URL Cleaning](url-cleaning.md)).
|
|
101
203
|
|
|
102
204
|
## What to do with “strip=True/False”
|
|
@@ -118,9 +220,20 @@ If you need to display untrusted HTML with no HTML in the output, prefer `to_tex
|
|
|
118
220
|
|
|
119
221
|
If you need additional structural cleanup beyond policy decisions, prefer doing it explicitly with transforms.
|
|
120
222
|
|
|
223
|
+
## Migration test expectations
|
|
224
|
+
|
|
225
|
+
Expect some output differences even when the security behavior is equivalent:
|
|
226
|
+
|
|
227
|
+
- Inline styles may be normalized, for example `vertical-align:top;` can serialize as `vertical-align: top`.
|
|
228
|
+
- Malformed HTML can serialize differently because JustHTML parses with its own HTML5 tree builder.
|
|
229
|
+
- `strip=False` maps to escaped disallowed tags. If your old Bleach call also relied on `strip_comments=True`, add an explicit test for comments.
|
|
230
|
+
- Linkification should be tested separately from sanitization when your old code used both `bleach.linkify(...)` and `bleach.clean(...)`.
|
|
231
|
+
|
|
121
232
|
## Suggested migration approach
|
|
122
233
|
|
|
123
|
-
1.
|
|
124
|
-
2.
|
|
125
|
-
3.
|
|
126
|
-
4. Add
|
|
234
|
+
1. Inventory existing `bleach.clean(...)`, `bleach.Cleaner(...)`, and `bleach.linkify(...)` call shapes.
|
|
235
|
+
2. For a large app, add a local compatibility wrapper so most call sites can move mechanically.
|
|
236
|
+
3. Start by using JustHTML’s default sanitizer (`JustHTML(...).to_html()`) for new code.
|
|
237
|
+
4. Add a `SanitizationPolicy` that matches your allowlist and URL requirements.
|
|
238
|
+
5. Add transforms to replace any Bleach filter/linkify behavior.
|
|
239
|
+
6. Add tests for your exact input corpus and output expectations (especially around URLs, allowed attributes, comments, and malformed HTML).
|
|
@@ -954,6 +954,12 @@ class TreeBuilderModesMixin:
|
|
|
954
954
|
self._insert_element(token, push=False)
|
|
955
955
|
return
|
|
956
956
|
|
|
957
|
+
def _handle_body_start_hr(self, token: Tag) -> None:
|
|
958
|
+
self._close_p_element()
|
|
959
|
+
self._insert_element(token, push=False)
|
|
960
|
+
self.frameset_ok = False
|
|
961
|
+
return
|
|
962
|
+
|
|
957
963
|
def _handle_body_start_input(self, token: Tag) -> None:
|
|
958
964
|
if (
|
|
959
965
|
self.fragment_context is not None
|
|
@@ -2054,6 +2060,7 @@ class TreeBuilderModesMixin:
|
|
|
2054
2060
|
"head": _handle_body_start_head,
|
|
2055
2061
|
"header": _handle_body_start_block_with_p,
|
|
2056
2062
|
"hgroup": _handle_body_start_block_with_p,
|
|
2063
|
+
"hr": _handle_body_start_hr,
|
|
2057
2064
|
"html": _handle_body_start_html,
|
|
2058
2065
|
"i": _handle_body_start_formatting,
|
|
2059
2066
|
"image": _handle_body_start_image,
|
|
@@ -220,6 +220,36 @@ class TestTreeBuilder(unittest.TestCase):
|
|
|
220
220
|
doc = JustHTML(html, fragment=True, sanitize=False)
|
|
221
221
|
self.assertEqual(doc.to_html(pretty=False), expected)
|
|
222
222
|
|
|
223
|
+
def test_hr_in_table_context_uses_in_body_void_element_rules(self) -> None:
|
|
224
|
+
cases = {
|
|
225
|
+
"<table><tr><td>before<hr>after</td></tr></table>": (
|
|
226
|
+
"<table><tbody><tr><td>before<hr>after</td></tr></tbody></table>"
|
|
227
|
+
),
|
|
228
|
+
"<table><tr><th>a<hr>b</th></tr></table>": ("<table><tbody><tr><th>a<hr>b</th></tr></tbody></table>"),
|
|
229
|
+
"<table><caption>a<hr>b</caption></table>": "<table><caption>a<hr>b</caption></table>",
|
|
230
|
+
"<table><tr><td><p>a<hr>b</td></tr></table>": (
|
|
231
|
+
"<table><tbody><tr><td><p>a</p><hr>b</td></tr></tbody></table>"
|
|
232
|
+
),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for html, expected in cases.items():
|
|
236
|
+
with self.subTest(html=html):
|
|
237
|
+
doc = JustHTML(html, fragment=True, sanitize=False)
|
|
238
|
+
self.assertEqual(doc.to_html(pretty=False), expected)
|
|
239
|
+
|
|
240
|
+
def test_self_closing_hr_in_table_context_is_acknowledged(self) -> None:
|
|
241
|
+
doc = JustHTML(
|
|
242
|
+
"<table><tr><td><hr/></td></tr></table>",
|
|
243
|
+
fragment=True,
|
|
244
|
+
sanitize=False,
|
|
245
|
+
collect_errors=True,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self.assertNotIn(
|
|
249
|
+
"non-void-html-element-start-tag-with-trailing-solidus",
|
|
250
|
+
[error.code for error in doc.errors],
|
|
251
|
+
)
|
|
252
|
+
|
|
223
253
|
def test_select_end_tags_create_p_and_close_custom_children_like_chromium(self) -> None:
|
|
224
254
|
cases = {
|
|
225
255
|
"<select>x</p>": "<select>x<p></p></select>",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|