justhtml 3.3.0__tar.gz → 3.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-3.3.0 → justhtml-3.4.0}/CHANGELOG.md +10 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/PKG-INFO +1 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/SECURITY.md +14 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/bleach-migration.md +44 -52
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/cli.md +1 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/correctness.md +2 -2
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/errors.md +5 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/fragments.md +5 -5
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/html-cleaning.md +66 -96
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/quickstart.md +3 -3
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/streaming.md +1 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/text.md +1 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/transforms.md +5 -5
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/unsafe-handling.md +2 -2
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/url-cleaning.md +7 -7
- {justhtml-3.3.0 → justhtml-3.4.0}/pyproject.toml +1 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/entities.py +13 -6
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/__init__.py +44 -17
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/engine.py +101 -37
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/policy.py +0 -8
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/runtime.py +12 -6
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/serializer/html.py +63 -18
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/__init__.py +1 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/compile.py +3 -20
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/linkify_core.py +3 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/runtime.py +31 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/test-summary.txt +18 -11
- justhtml-3.4.0/tests/justhtml-tests/character_preprocessing.dat +99 -0
- justhtml-3.4.0/tests/justhtml-tests/document_shell_recovery.dat +57 -0
- justhtml-3.4.0/tests/justhtml-tests/foreign_content.dat +122 -0
- justhtml-3.4.0/tests/justhtml-tests/frameset_recovery.dat +50 -0
- justhtml-3.4.0/tests/justhtml-tests/head_noscript_recovery.dat +25 -0
- justhtml-3.4.0/tests/justhtml-tests/in_body_recovery.dat +12 -0
- justhtml-3.4.0/tests/justhtml-tests/select_recovery.dat +138 -0
- justhtml-3.4.0/tests/justhtml-tests/selectedcontent.dat +76 -0
- justhtml-3.4.0/tests/justhtml-tests/table_recovery.dat +74 -0
- justhtml-3.3.0/tests/justhtml-tests/tokenizer_edge_cases.test → justhtml-3.4.0/tests/justhtml-tests/tokenizer_states.test +1 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_docs_examples.py +1 -1
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_entities.py +5 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_errors.py +28 -1
- justhtml-3.3.0/tests/test_parser_engine_coverage.py → justhtml-3.4.0/tests/test_parser_engine.py +70 -5
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_sanitize.py +20 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_serialize.py +1 -0
- justhtml-3.3.0/tests/test_transforms_edge_cases.py → justhtml-3.4.0/tests/test_transform_runtime.py +30 -45
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_transforms.py +40 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_transforms_sanitize_integration.py +16 -0
- justhtml-3.3.0/tests/justhtml-tests/branch_coverage.dat +0 -450
- justhtml-3.3.0/tests/justhtml-tests/treebuilder_coverage.dat +0 -254
- {justhtml-3.3.0 → justhtml-3.4.0}/.github/copilot-instructions.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/.github/workflows/ci.yml +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/.github/workflows/publish.yml +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/.gitignore +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/.pre-commit-config.yaml +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/CONTRIBUTING.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/LICENSE +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/Makefile +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/README.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/correctness.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/performance.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/profile.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/_config.yml +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/api.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/assets/search.js +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/building.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/comparison.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/encoding.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/index.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/linkify.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/migration-examples.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/app.js +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/index.html +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/sanitization.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/docs/selectors.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/llms.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/run_tests.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/scripts/release.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/text.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/scanner.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/stream.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/README.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/tree.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-tests/entities.test +0 -0
- /justhtml-3.3.0/tests/justhtml-tests/empty_stack_edge_cases.dat → /justhtml-3.4.0/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- /justhtml-3.3.0/tests/justhtml-tests/coverage_gaps.test → /justhtml-3.4.0/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- /justhtml-3.3.0/tests/justhtml-tests/xml_coercion_coverage.test → /justhtml-3.4.0/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_builder.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_cli.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_encoding.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_node.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_parser_regressions.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_parser_tree.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_selector.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_stream.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_wikipedia.py +0 -0
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.4.0] - 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- (Severity: Low) Bound named character-reference lookup to the finite HTML entity tables. Previously, a malformed `&` followed by a long alphanumeric run caused repeated shrinking-prefix allocations and lookups, making parsing quadratic and allowing denial of service before sanitization.
|
|
14
|
+
- (Severity: Low) Cap diagnostics retained by `JustHTML(...)` with the new `max_errors` option (default: 1000), including parser, sanitizer, raw-text, and transform findings. Previously, input containing many independently malformed characters or unsafe constructs could allocate an unbounded number of `ParseError` objects.
|
|
15
|
+
- (Severity: Low) Index active-formatting entries by signature within each HTML5 Noah's Ark marker segment, retiring superseded entries lazily and compacting in bounded batches. Previously, repeated fourth occurrences across many formatting signatures rescanned and shifted the active-formatting list, allowing quadratic parsing of crafted HTML.
|
|
16
|
+
- (Severity: Low) Avoid unbounded integer conversion for untrusted `srcset` width descriptors, URL host validation, and `Linkify(...)` URL ports. Previously, inputs with more decimal digits than Python permits in `int()` could raise an uncaught `ValueError` during sanitization or linkification.
|
|
17
|
+
- (Severity: Low) Bound pretty-print indentation and avoid repeatedly copying nested serialized output. Previously, serializing deeply nested untrusted HTML with the default `to_html()` settings could consume quadratic time and memory.
|
|
18
|
+
- (Severity: Low) Compact sibling lists when applying a tag-only `Drop(...)` transform. Previously, dropping many matching untrusted sibling elements shifted the remaining list after every removal, making transform execution quadratic.
|
|
19
|
+
|
|
10
20
|
## [3.3.0] - 2026-07-04
|
|
11
21
|
|
|
12
22
|
### Changed
|
|
@@ -19,6 +19,20 @@
|
|
|
19
19
|
| 1.x | :x: |
|
|
20
20
|
| < 1.0 | :x: |
|
|
21
21
|
|
|
22
|
+
## Threat Model
|
|
23
|
+
|
|
24
|
+
JustHTML's primary security boundary is **untrusted HTML input**: markup,
|
|
25
|
+
bytes, and HTML-derived values supplied to parsing, sanitization, and
|
|
26
|
+
serialization APIs. The promises below apply when callers use the documented
|
|
27
|
+
safe defaults or a deliberately safe policy.
|
|
28
|
+
|
|
29
|
+
Library configuration is trusted application code. This includes custom
|
|
30
|
+
sanitization policies, transform specifications and callbacks, selectors,
|
|
31
|
+
glob patterns, CLI flags, and any policy or transform values assembled from
|
|
32
|
+
configuration. Applications that accept those values from untrusted users must
|
|
33
|
+
validate and constrain them before passing them to JustHTML; transform APIs are
|
|
34
|
+
not a sandbox.
|
|
35
|
+
|
|
22
36
|
## Security Domains
|
|
23
37
|
|
|
24
38
|
This section describes the security areas we use when reviewing `justhtml`.
|
|
@@ -6,20 +6,19 @@ JustHTML’s sanitization and transform pipeline were heavily inspired by Bleach
|
|
|
6
6
|
|
|
7
7
|
Bleach has helped a lot of projects ship safer HTML over the years, and a lot of that is thanks to the hard work of [@willkg](https://github.com/willkg) building and maintaining it.
|
|
8
8
|
|
|
9
|
-
In
|
|
9
|
+
In June 2026, Bleach’s maintainer [announced Bleach’s final release](https://bluesock.org/~willkg/blog/dev/bleach_6_4_0_final_release.html). The announcement identifies JustHTML as an easy migration path and explicitly recommends swapping Bleach out for it.
|
|
10
10
|
|
|
11
|
-
This guide
|
|
11
|
+
This guide starts with Bleach's defaults, then builds up to a copyable compatibility wrapper for a gradual migration.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Start with the defaults
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Bleach and JustHTML are both safe by default, but their default policies are intentionally different. Do not replace `bleach.clean(html)` with `JustHTML(html).to_html()` and expect byte-for-byte identical output.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
2. Migrate call sites from `bleach.clean(...)` / `bleach.linkify(...)` to that wrapper.
|
|
17
|
+
Bleach's default allowlist keeps a small set of formatting tags such as `b`, `em`, `i`, `a`, `code`, and lists. JustHTML's default policy allows a broader set of ordinary document tags and applies its own URL policy.
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
The wrapper below reproduces Bleach 6.x's default tags, attributes, and protocols without making Bleach a runtime dependency. It is a compatibility baseline, not a recommended final policy.
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
## Copyable `clean(...)` compatibility wrapper
|
|
23
22
|
|
|
24
23
|
```python
|
|
25
24
|
import re
|
|
@@ -40,10 +39,21 @@ URL_LIKE_ATTRS = {
|
|
|
40
39
|
"ping",
|
|
41
40
|
}
|
|
42
41
|
|
|
42
|
+
BLEACH_DEFAULT_TAGS = frozenset(
|
|
43
|
+
{"a", "abbr", "acronym", "b", "blockquote", "code", "em", "i", "li", "ol", "strong", "ul"}
|
|
44
|
+
)
|
|
45
|
+
BLEACH_DEFAULT_ATTRIBUTES = {
|
|
46
|
+
"a": {"href", "title"},
|
|
47
|
+
"abbr": {"title"},
|
|
48
|
+
"acronym": {"title"},
|
|
49
|
+
}
|
|
50
|
+
BLEACH_DEFAULT_PROTOCOLS = frozenset({"http", "https", "mailto"})
|
|
51
|
+
|
|
43
52
|
|
|
44
53
|
def build_url_policy(
|
|
45
54
|
allowed_tags: Collection[str],
|
|
46
55
|
allowed_attributes: Mapping[str, Collection[str]],
|
|
56
|
+
protocols: Collection[str],
|
|
47
57
|
) -> UrlPolicy:
|
|
48
58
|
rules = {}
|
|
49
59
|
|
|
@@ -52,7 +62,7 @@ def build_url_policy(
|
|
|
52
62
|
for attr in global_attrs:
|
|
53
63
|
if attr in URL_LIKE_ATTRS:
|
|
54
64
|
rules[(tag, attr)] = UrlRule(
|
|
55
|
-
allowed_schemes=
|
|
65
|
+
allowed_schemes=protocols,
|
|
56
66
|
allow_relative=True,
|
|
57
67
|
)
|
|
58
68
|
|
|
@@ -62,7 +72,7 @@ def build_url_policy(
|
|
|
62
72
|
for attr in attrs:
|
|
63
73
|
if attr in URL_LIKE_ATTRS:
|
|
64
74
|
rules[(tag, attr)] = UrlRule(
|
|
65
|
-
allowed_schemes=
|
|
75
|
+
allowed_schemes=protocols,
|
|
66
76
|
allow_relative=True,
|
|
67
77
|
)
|
|
68
78
|
return UrlPolicy(allow_rules=rules)
|
|
@@ -71,13 +81,14 @@ def build_url_policy(
|
|
|
71
81
|
def clean(
|
|
72
82
|
html: str,
|
|
73
83
|
*,
|
|
74
|
-
tags: Collection[str] =
|
|
75
|
-
attributes: Mapping[str, Collection[str]]
|
|
84
|
+
tags: Collection[str] = BLEACH_DEFAULT_TAGS,
|
|
85
|
+
attributes: Mapping[str, Collection[str]] = BLEACH_DEFAULT_ATTRIBUTES,
|
|
86
|
+
protocols: Collection[str] = BLEACH_DEFAULT_PROTOCOLS,
|
|
76
87
|
css_properties: Collection[str] = (),
|
|
77
|
-
strip: bool =
|
|
88
|
+
strip: bool = False,
|
|
78
89
|
strip_comments: bool = True,
|
|
79
90
|
) -> str:
|
|
80
|
-
attrs =
|
|
91
|
+
attrs = {tag: set(values) for tag, values in attributes.items()}
|
|
81
92
|
disallowed_tag_handling = "unwrap" if strip else "escape"
|
|
82
93
|
|
|
83
94
|
# Bleach defaults to strip_comments=True. If you use escape mode
|
|
@@ -90,7 +101,7 @@ def clean(
|
|
|
90
101
|
allowed_tags=tags,
|
|
91
102
|
allowed_attributes=attrs,
|
|
92
103
|
allowed_css_properties=css_properties,
|
|
93
|
-
url_policy=build_url_policy(tags, attrs),
|
|
104
|
+
url_policy=build_url_policy(tags, attrs, protocols),
|
|
94
105
|
drop_comments=strip_comments,
|
|
95
106
|
disallowed_tag_handling=disallowed_tag_handling,
|
|
96
107
|
)
|
|
@@ -109,57 +120,38 @@ def linkify(text: str, *, nofollow: bool = False) -> str:
|
|
|
109
120
|
).to_html(pretty=False)
|
|
110
121
|
```
|
|
111
122
|
|
|
112
|
-
|
|
123
|
+
Use the wrapper with Bleach-compatible defaults:
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- `JustHTML(html)` sanitizes by default (`sanitize=True`).
|
|
119
|
-
- `JustHTML(html, sanitize=False)` disables sanitization (trusted input only).
|
|
120
|
-
|
|
121
|
-
JustHTML also supports constructor-time **transforms** (a DOM equivalent of Bleach/html5lib filter pipelines): see [Transforms](transforms.md).
|
|
125
|
+
```python
|
|
126
|
+
# Replace existing bleach.clean(user_html) calls with clean(user_html).
|
|
127
|
+
safe_html = clean(user_html)
|
|
128
|
+
```
|
|
122
129
|
|
|
123
|
-
##
|
|
130
|
+
## Customize the wrapper gradually
|
|
124
131
|
|
|
125
|
-
|
|
132
|
+
Keep the call shape familiar while making the policy explicit at the call site:
|
|
126
133
|
|
|
127
134
|
```python
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
clean = bleach.clean(
|
|
135
|
+
safe_html = clean(
|
|
131
136
|
user_html,
|
|
132
|
-
tags=
|
|
133
|
-
attributes={"a":
|
|
134
|
-
protocols=
|
|
137
|
+
tags={"p", "b", "a"},
|
|
138
|
+
attributes={"a": {"href"}},
|
|
139
|
+
protocols={"http", "https"},
|
|
135
140
|
strip=True,
|
|
136
141
|
)
|
|
137
142
|
```
|
|
138
143
|
|
|
139
|
-
|
|
144
|
+
`fragment=True` inside the wrapper keeps user-generated snippets free of `<html>`, `<head>`, and `<body>` wrappers. For URL rules with host restrictions, proxies, `srcset`, and remote-resource controls, extend the local `build_url_policy(...)` function; see [URL Cleaning](url-cleaning.md).
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
|
|
143
|
-
|
|
144
|
-
policy = SanitizationPolicy(
|
|
145
|
-
allowed_tags=["p", "b", "a"],
|
|
146
|
-
allowed_attributes={"*": [], "a": ["href"]},
|
|
147
|
-
url_policy=UrlPolicy(
|
|
148
|
-
default_handling="allow",
|
|
149
|
-
allow_rules={
|
|
150
|
-
("a", "href"): UrlRule(allowed_schemes=["http", "https"]),
|
|
151
|
-
},
|
|
152
|
-
),
|
|
153
|
-
)
|
|
146
|
+
## Intentional compatibility differences
|
|
154
147
|
|
|
155
|
-
|
|
156
|
-
clean = doc.to_html()
|
|
157
|
-
```
|
|
148
|
+
The wrapper matches Bleach's ordinary allowlist behavior, but it deliberately does not preserve every historical output detail:
|
|
158
149
|
|
|
159
|
-
|
|
150
|
+
- JustHTML drops the contents of dangerous containers such as `script` and `style`; Bleach with `strip=True` can leave their text content behind.
|
|
151
|
+
- JustHTML normalizes a protocol-relative URL such as `//example.com/x` to `https://example.com/x`; Bleach preserves the original spelling.
|
|
152
|
+
- JustHTML parses into a DOM and sanitizes at construction time. Add `Sanitize(...)` after later DOM edits if the tree needs to become safe again.
|
|
160
153
|
|
|
161
|
-
|
|
162
|
-
- JustHTML sanitizes *at construction time* by default. If you need to sanitize again after later DOM edits, add `Sanitize(...)` at the end of your transform pipeline (see [HTML Cleaning](html-cleaning.md)).
|
|
154
|
+
JustHTML also supports constructor-time transforms, which replace Bleach/html5lib filter pipelines; see [Transforms](transforms.md).
|
|
163
155
|
|
|
164
156
|
## Bleach filters → JustHTML transforms
|
|
165
157
|
|
|
@@ -124,7 +124,7 @@ justhtml page.html --selector "main" --format text --separator "" --no-strip
|
|
|
124
124
|
curl -s https://github.com/EmilStenstrom/justhtml/ | justhtml - --selector '.markdown-body' --format markdown | head -n 15
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
<!-- justhtml: output -->
|
|
128
128
|
|
|
129
129
|
```text
|
|
130
130
|
# JustHTML
|
|
@@ -91,7 +91,7 @@ python run_tests.py --suite encoding
|
|
|
91
91
|
python run_tests.py --suite unit
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
<!-- justhtml: output -->
|
|
95
95
|
```
|
|
96
96
|
PASSED: 3464/3464 passed (100.0%)
|
|
97
97
|
```
|
|
@@ -132,7 +132,7 @@ We generate random malformed HTML to find crashes and hangs:
|
|
|
132
132
|
python benchmarks/fuzz.py -n 3000000
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
<!-- justhtml: output -->
|
|
136
136
|
```
|
|
137
137
|
============================================================
|
|
138
138
|
FUZZING RESULTS: justhtml
|
|
@@ -23,6 +23,11 @@ for error in doc.errors:
|
|
|
23
23
|
`doc.errors` is ordered by source position (line, column), with unknown
|
|
24
24
|
positions appearing last.
|
|
25
25
|
|
|
26
|
+
At most 1,000 diagnostics are retained by default, including parser,
|
|
27
|
+
sanitizer, and transform findings produced during construction. Set a different
|
|
28
|
+
positive `max_errors` value when constructing `JustHTML` if your application
|
|
29
|
+
needs a different limit.
|
|
30
|
+
|
|
26
31
|
## Error Categories
|
|
27
32
|
|
|
28
33
|
Each error has a `category` field:
|
|
@@ -16,7 +16,7 @@ doc = JustHTML("<tr><td>cell</td></tr>")
|
|
|
16
16
|
print(doc.to_html(indent_size=4))
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
<!-- justhtml: output -->
|
|
20
20
|
|
|
21
21
|
```html
|
|
22
22
|
<html>
|
|
@@ -37,7 +37,7 @@ doc = JustHTML(html, fragment_context=ctx)
|
|
|
37
37
|
print(doc.to_html(indent_size=4))
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
<!-- justhtml: output -->
|
|
41
41
|
|
|
42
42
|
```html
|
|
43
43
|
<tr>
|
|
@@ -64,7 +64,7 @@ print(doc.to_html())
|
|
|
64
64
|
paragraphs = doc.query("p")
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
<!-- justhtml: output -->
|
|
68
68
|
|
|
69
69
|
```html
|
|
70
70
|
#document-fragment
|
|
@@ -143,7 +143,7 @@ doc = JustHTML("<b>not bold</b>", fragment_context=ctx)
|
|
|
143
143
|
print(doc.to_html())
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
<!-- justhtml: output -->
|
|
147
147
|
|
|
148
148
|
```html
|
|
149
149
|
<b>not bold</b>
|
|
@@ -227,7 +227,7 @@ dirty = '<p>Hello</p><script>alert("xss")</script><b>world</b>'
|
|
|
227
227
|
print(sanitize_fragment(dirty))
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
<!-- justhtml: output -->
|
|
231
231
|
|
|
232
232
|
```html
|
|
233
233
|
<p>Hello</p><b>world</b>
|
|
@@ -8,78 +8,49 @@ This page focuses on **HTML cleaning**: tags, attributes, and inline styles. For
|
|
|
8
8
|
|
|
9
9
|
On this page:
|
|
10
10
|
|
|
11
|
-
- [DOM vs construction](#important-dom-vs-construction)
|
|
12
11
|
- [Safe-by-default construction](#safe-by-default-construction)
|
|
13
|
-
- [Sanitizing a DOM directly](#sanitizing-a-dom-directly)
|
|
14
|
-
- [Disable sanitization](#disable-sanitization)
|
|
15
12
|
- [Default policy](#default-sanitization-policy)
|
|
16
|
-
- [Inline styles](#inline-styles-optional)
|
|
17
13
|
- [Custom policy](#use-a-custom-sanitization-policy)
|
|
14
|
+
- [Sanitizing a DOM directly](#sanitizing-a-dom-directly)
|
|
15
|
+
- [Inline styles](#inline-styles-optional)
|
|
16
|
+
- [Advanced policy options](#advanced-policy-options)
|
|
17
|
+
- [Disable sanitization](#disable-sanitization)
|
|
18
18
|
- [Reporting issues](#reporting-issues)
|
|
19
19
|
|
|
20
|
-
## Important: DOM vs construction
|
|
21
|
-
|
|
22
|
-
The parsed DOM is **sanitized by default** at construction time (`JustHTML(..., sanitize=True)`), and serialization is a pure output step.
|
|
23
|
-
|
|
24
|
-
If you want to sanitize **after** other transforms or after direct DOM edits, apply the `Sanitize(...)` transform to sanitize the in-memory tree. If your transform list already includes `Sanitize(...)`, that explicit position becomes the sanitize point; later transforms can reintroduce unsafe content.
|
|
25
|
-
|
|
26
20
|
## Safe-by-default construction
|
|
27
21
|
|
|
28
22
|
By default, construction removes all dangerous html:
|
|
29
23
|
|
|
24
|
+
### HTML output
|
|
25
|
+
|
|
30
26
|
```python
|
|
31
27
|
from justhtml import JustHTML
|
|
32
28
|
|
|
33
29
|
user_html = '<p>Hello <b>world</b> <script>alert(1)</script> <a href="javascript:alert(1)">bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>'
|
|
34
30
|
doc = JustHTML(user_html, fragment=True)
|
|
35
|
-
|
|
36
31
|
print(doc.to_html())
|
|
37
|
-
print()
|
|
38
|
-
print(doc.to_markdown())
|
|
39
32
|
```
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
<!-- justhtml: output -->
|
|
42
35
|
|
|
43
36
|
```html
|
|
44
37
|
<p>Hello <b>world</b> <a>bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>
|
|
45
|
-
|
|
46
|
-
Hello **world** [bad] [ok](https://example.com/?a=1&b=2)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Sanitizing the in-memory DOM
|
|
50
|
-
|
|
51
|
-
If you will be working with the DOM and want a clean slate to work from, add `Sanitize(...)` to your transform pipeline.
|
|
52
|
-
|
|
53
|
-
If you want explicit pass boundaries (advanced use), you can group transforms using [`Stage([...])`](transforms.md#advanced-stages).
|
|
54
|
-
|
|
55
|
-
```python
|
|
56
|
-
from justhtml import JustHTML, Sanitize
|
|
57
|
-
|
|
58
|
-
user_html = '<p>Hello <b>world</b> <script>alert(1)</script> <a href="javascript:alert(1)">bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>'
|
|
59
|
-
doc = JustHTML(user_html, fragment=True, transforms=[Sanitize()])
|
|
60
|
-
|
|
61
|
-
# The DOM is now sanitized in-memory.
|
|
62
|
-
print(doc.to_html(pretty=False))
|
|
63
|
-
# => <p>Hello <b>world</b> <a>bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>
|
|
64
38
|
```
|
|
65
39
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
If you want to (dangerously) disable sanitization, because you know that your trusted HTML can't contain malicious code:
|
|
40
|
+
### Markdown output
|
|
69
41
|
|
|
70
42
|
```python
|
|
71
43
|
from justhtml import JustHTML
|
|
72
44
|
|
|
73
|
-
user_html = '<p>Hello <b>world</b> <script>
|
|
74
|
-
doc = JustHTML(user_html, fragment=True
|
|
75
|
-
|
|
76
|
-
print(doc.to_html())
|
|
45
|
+
user_html = '<p>Hello <b>world</b> <script>alert(1)</script> <a href="javascript:alert(1)">bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>'
|
|
46
|
+
doc = JustHTML(user_html, fragment=True)
|
|
47
|
+
print(doc.to_markdown())
|
|
77
48
|
```
|
|
78
49
|
|
|
79
|
-
|
|
50
|
+
<!-- justhtml: output -->
|
|
80
51
|
|
|
81
|
-
```
|
|
82
|
-
|
|
52
|
+
```markdown
|
|
53
|
+
Hello **world** [bad] [ok](https://example.com/?a=1&b=2)
|
|
83
54
|
```
|
|
84
55
|
|
|
85
56
|
## Default sanitization policy
|
|
@@ -114,23 +85,62 @@ Default allowlists:
|
|
|
114
85
|
- `img`: `src`, `alt`, `title`, `width`, `height`, `loading`, `decoding`
|
|
115
86
|
- `th`/`td`: `colspan`, `rowspan`
|
|
116
87
|
|
|
88
|
+
## Use a custom sanitization policy
|
|
89
|
+
|
|
90
|
+
Start with the smallest policy that matches the HTML you want to accept. This makes the allowed output clear to future maintainers.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
|
|
94
|
+
|
|
95
|
+
policy = SanitizationPolicy(
|
|
96
|
+
allowed_tags={"p", "strong", "a"},
|
|
97
|
+
allowed_attributes={"a": {"href"}},
|
|
98
|
+
url_policy=UrlPolicy(
|
|
99
|
+
allow_rules={
|
|
100
|
+
("a", "href"): UrlRule(allowed_schemes={"https", "mailto"}),
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
safe_html = JustHTML(user_html, fragment=True, policy=policy).to_html()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The three settings work together:
|
|
109
|
+
|
|
110
|
+
- `allowed_tags` controls which elements remain.
|
|
111
|
+
- `allowed_attributes` controls which attribute names remain. An omitted tag has no allowed attributes, so you only need to list tags that accept attributes.
|
|
112
|
+
- URL-valued attributes such as `href` also need a `UrlRule`; allowing the attribute name alone is not enough.
|
|
113
|
+
|
|
114
|
+
For URL schemes, hosts, proxies, `srcset`, and other URL-specific controls, continue with [URL Cleaning](url-cleaning.md).
|
|
115
|
+
|
|
116
|
+
## Sanitizing the in-memory DOM
|
|
117
|
+
|
|
118
|
+
The parsed DOM is sanitized by default at construction time (`JustHTML(..., sanitize=True)`), and serialization is a pure output step.
|
|
119
|
+
|
|
120
|
+
If you want to sanitize after other transforms or after direct DOM edits, add `Sanitize(...)` at the point where the tree should become safe. Later transforms can reintroduce unsafe content. For explicit pass boundaries (advanced use), see [`Stage([...])`](transforms.md#advanced-stages).
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from justhtml import JustHTML, Sanitize
|
|
124
|
+
|
|
125
|
+
doc = JustHTML(user_html, fragment=True, transforms=[Sanitize()])
|
|
126
|
+
print(doc.to_html(pretty=False))
|
|
127
|
+
```
|
|
128
|
+
|
|
117
129
|
## Inline styles (optional)
|
|
118
130
|
|
|
119
131
|
Inline styles are disabled by default. To allow them you must:
|
|
120
132
|
|
|
121
|
-
1
|
|
122
|
-
2
|
|
123
|
-
|
|
124
|
-
Even then, JustHTML is conservative: it rejects declarations that look like they can load external resources (such as values containing `url(` or `image-set(`), as well as legacy constructs like `expression(`.
|
|
133
|
+
1. Allow the `style` attribute for the relevant tag via `allowed_attributes`.
|
|
134
|
+
2. Provide a non-empty allowlist via `allowed_css_properties`.
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
Even then, JustHTML rejects declarations that look like they can load external resources (such as values containing `url(` or `image-set(`), as well as legacy constructs like `expression(`. Start from the conservative `CSS_PRESET_TEXT` preset.
|
|
127
137
|
|
|
128
138
|
```python
|
|
129
139
|
from justhtml import CSS_PRESET_TEXT, JustHTML, SanitizationPolicy, UrlPolicy
|
|
130
140
|
|
|
131
141
|
policy = SanitizationPolicy(
|
|
132
|
-
allowed_tags=
|
|
133
|
-
allowed_attributes={"
|
|
142
|
+
allowed_tags={"p"},
|
|
143
|
+
allowed_attributes={"p": {"style"}},
|
|
134
144
|
url_policy=UrlPolicy(allow_rules={}),
|
|
135
145
|
allowed_css_properties=CSS_PRESET_TEXT | {"width"},
|
|
136
146
|
)
|
|
@@ -139,20 +149,9 @@ html = '<p style="color: red; background-image: url(https://evil.test/x); width:
|
|
|
139
149
|
print(JustHTML(html, policy=policy).to_html())
|
|
140
150
|
```
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
```html
|
|
145
|
-
<p style="color: red">Hi</p>
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Use a custom sanitization policy
|
|
152
|
+
## Advanced policy options
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
When expanding the default policy, prefer adding small, explicit allowlists.
|
|
154
|
-
|
|
155
|
-
Advanced sanitization pipelines can also tune selector resource limits:
|
|
154
|
+
Selector limits are for trusted pipelines that need to accept unusually large selectors or match budgets:
|
|
156
155
|
|
|
157
156
|
```python
|
|
158
157
|
from justhtml import SanitizationPolicy
|
|
@@ -165,43 +164,14 @@ policy = SanitizationPolicy(
|
|
|
165
164
|
)
|
|
166
165
|
```
|
|
167
166
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Treat these as a separate security review if you plan to allow them:
|
|
167
|
+
Treat policies that allow active content as a separate security review: `iframe`, `object`, `embed`, `meta`, `link`, `base`, form elements, and their active attributes are preserved when you explicitly allow them.
|
|
171
168
|
|
|
172
|
-
|
|
173
|
-
- `meta`, `link`, `base`
|
|
174
|
-
- form elements and submission-related attributes
|
|
175
|
-
|
|
176
|
-
If you explicitly allow these tags and their active attributes (for example `iframe[src]` or `object[data]`), JustHTML will preserve them according to your policy. That is an explicit opt-in to active embedded content, not something the sanitizer makes safe for you.
|
|
169
|
+
## Disable sanitization
|
|
177
170
|
|
|
178
|
-
|
|
171
|
+
Only disable sanitization for HTML you fully trust:
|
|
179
172
|
|
|
180
173
|
```python
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
user_html = '<p>Hello <b>world</b> <script>alert(1)</script> <a href="javascript:alert(1)">bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>'
|
|
184
|
-
|
|
185
|
-
policy = SanitizationPolicy(
|
|
186
|
-
allowed_tags=["p", "b", "a"],
|
|
187
|
-
allowed_attributes={"*": [], "a": ["href"]},
|
|
188
|
-
url_policy=UrlPolicy(
|
|
189
|
-
default_handling="allow",
|
|
190
|
-
allow_rules={
|
|
191
|
-
("a", "href"): UrlRule(allowed_schemes=["https"]),
|
|
192
|
-
}
|
|
193
|
-
),
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
doc = JustHTML(user_html, fragment=True)
|
|
197
|
-
doc = JustHTML(user_html, fragment=True, policy=policy)
|
|
198
|
-
print(doc.to_html())
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Output:
|
|
202
|
-
|
|
203
|
-
```html
|
|
204
|
-
<p>Hello <b>world</b> <a>bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>
|
|
174
|
+
doc = JustHTML(trusted_html, fragment=True, sanitize=False)
|
|
205
175
|
```
|
|
206
176
|
|
|
207
177
|
## Reporting issues
|
|
@@ -102,7 +102,7 @@ div = doc.query_one("#main")
|
|
|
102
102
|
print(div.to_html(indent_size=4))
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
<!-- justhtml: output -->
|
|
106
106
|
|
|
107
107
|
```html
|
|
108
108
|
<div id="main">
|
|
@@ -120,7 +120,7 @@ from justhtml import JustHTML
|
|
|
120
120
|
JustHTML("<!doctype html><p></div>", strict=True) # doctest: skip
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
<!-- justhtml: output -->
|
|
124
124
|
|
|
125
125
|
```text
|
|
126
126
|
Traceback (most recent call last):
|
|
@@ -156,7 +156,7 @@ for event, data in stream(html):
|
|
|
156
156
|
print(f"End: {data}")
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
<!-- justhtml: output -->
|
|
160
160
|
|
|
161
161
|
```html
|
|
162
162
|
Start: p
|
|
@@ -153,7 +153,7 @@ print(doc.to_html(pretty=False))
|
|
|
153
153
|
print(doc2.to_html(pretty=False))
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
<!-- justhtml: output -->
|
|
157
157
|
|
|
158
158
|
```html
|
|
159
159
|
<span>NEW </span><p>one</p><p>two</p>
|
|
@@ -357,7 +357,7 @@ doc = JustHTML(
|
|
|
357
357
|
print(doc.to_html(pretty=False))
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
<!-- justhtml: output -->
|
|
361
361
|
|
|
362
362
|
```html
|
|
363
363
|
<p><img src="/x"></p>
|
|
@@ -402,7 +402,7 @@ doc = JustHTML(
|
|
|
402
402
|
print(doc.to_html(pretty=False))
|
|
403
403
|
```
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
<!-- justhtml: output -->
|
|
406
406
|
|
|
407
407
|
```html
|
|
408
408
|
<p>Hello</p>
|
|
@@ -422,7 +422,7 @@ doc = JustHTML(
|
|
|
422
422
|
print(doc.to_html(pretty=False))
|
|
423
423
|
```
|
|
424
424
|
|
|
425
|
-
|
|
425
|
+
<!-- justhtml: output -->
|
|
426
426
|
|
|
427
427
|
```html
|
|
428
428
|
<p>One</p><p>Two</p>
|
|
@@ -442,7 +442,7 @@ doc = JustHTML(
|
|
|
442
442
|
print(doc.to_html(pretty=False))
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
<!-- justhtml: output -->
|
|
446
446
|
|
|
447
447
|
```html
|
|
448
448
|
<p><img src="/x"></p>
|
|
@@ -44,7 +44,7 @@ for e in doc.errors:
|
|
|
44
44
|
print(e.message)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
<!-- justhtml: output -->
|
|
48
48
|
|
|
49
49
|
```text
|
|
50
50
|
Unsafe tag 'script' (dropped content)
|
|
@@ -74,7 +74,7 @@ except UnsafeHtmlError as e:
|
|
|
74
74
|
print(e)
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
<!-- justhtml: output -->
|
|
78
78
|
|
|
79
79
|
```text
|
|
80
80
|
Unsafe tag 'script' (dropped content)
|