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.
Files changed (154) hide show
  1. {justhtml-3.3.0 → justhtml-3.4.0}/CHANGELOG.md +10 -0
  2. {justhtml-3.3.0 → justhtml-3.4.0}/PKG-INFO +1 -1
  3. {justhtml-3.3.0 → justhtml-3.4.0}/SECURITY.md +14 -0
  4. {justhtml-3.3.0 → justhtml-3.4.0}/docs/bleach-migration.md +44 -52
  5. {justhtml-3.3.0 → justhtml-3.4.0}/docs/cli.md +1 -1
  6. {justhtml-3.3.0 → justhtml-3.4.0}/docs/correctness.md +2 -2
  7. {justhtml-3.3.0 → justhtml-3.4.0}/docs/errors.md +5 -0
  8. {justhtml-3.3.0 → justhtml-3.4.0}/docs/fragments.md +5 -5
  9. {justhtml-3.3.0 → justhtml-3.4.0}/docs/html-cleaning.md +66 -96
  10. {justhtml-3.3.0 → justhtml-3.4.0}/docs/quickstart.md +3 -3
  11. {justhtml-3.3.0 → justhtml-3.4.0}/docs/streaming.md +1 -1
  12. {justhtml-3.3.0 → justhtml-3.4.0}/docs/text.md +1 -1
  13. {justhtml-3.3.0 → justhtml-3.4.0}/docs/transforms.md +5 -5
  14. {justhtml-3.3.0 → justhtml-3.4.0}/docs/unsafe-handling.md +2 -2
  15. {justhtml-3.3.0 → justhtml-3.4.0}/docs/url-cleaning.md +7 -7
  16. {justhtml-3.3.0 → justhtml-3.4.0}/pyproject.toml +1 -1
  17. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/entities.py +13 -6
  18. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/__init__.py +44 -17
  19. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/engine.py +101 -37
  20. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/policy.py +0 -8
  21. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/runtime.py +12 -6
  22. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/serializer/html.py +63 -18
  23. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/__init__.py +1 -0
  24. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/compile.py +3 -20
  25. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/linkify_core.py +3 -1
  26. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/runtime.py +31 -0
  27. {justhtml-3.3.0 → justhtml-3.4.0}/test-summary.txt +18 -11
  28. justhtml-3.4.0/tests/justhtml-tests/character_preprocessing.dat +99 -0
  29. justhtml-3.4.0/tests/justhtml-tests/document_shell_recovery.dat +57 -0
  30. justhtml-3.4.0/tests/justhtml-tests/foreign_content.dat +122 -0
  31. justhtml-3.4.0/tests/justhtml-tests/frameset_recovery.dat +50 -0
  32. justhtml-3.4.0/tests/justhtml-tests/head_noscript_recovery.dat +25 -0
  33. justhtml-3.4.0/tests/justhtml-tests/in_body_recovery.dat +12 -0
  34. justhtml-3.4.0/tests/justhtml-tests/select_recovery.dat +138 -0
  35. justhtml-3.4.0/tests/justhtml-tests/selectedcontent.dat +76 -0
  36. justhtml-3.4.0/tests/justhtml-tests/table_recovery.dat +74 -0
  37. justhtml-3.3.0/tests/justhtml-tests/tokenizer_edge_cases.test → justhtml-3.4.0/tests/justhtml-tests/tokenizer_states.test +1 -1
  38. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_docs_examples.py +1 -1
  39. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_entities.py +5 -0
  40. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_errors.py +28 -1
  41. justhtml-3.3.0/tests/test_parser_engine_coverage.py → justhtml-3.4.0/tests/test_parser_engine.py +70 -5
  42. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_sanitize.py +20 -0
  43. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_serialize.py +1 -0
  44. justhtml-3.3.0/tests/test_transforms_edge_cases.py → justhtml-3.4.0/tests/test_transform_runtime.py +30 -45
  45. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_transforms.py +40 -0
  46. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_transforms_sanitize_integration.py +16 -0
  47. justhtml-3.3.0/tests/justhtml-tests/branch_coverage.dat +0 -450
  48. justhtml-3.3.0/tests/justhtml-tests/treebuilder_coverage.dat +0 -254
  49. {justhtml-3.3.0 → justhtml-3.4.0}/.github/copilot-instructions.md +0 -0
  50. {justhtml-3.3.0 → justhtml-3.4.0}/.github/workflows/ci.yml +0 -0
  51. {justhtml-3.3.0 → justhtml-3.4.0}/.github/workflows/publish.yml +0 -0
  52. {justhtml-3.3.0 → justhtml-3.4.0}/.gitignore +0 -0
  53. {justhtml-3.3.0 → justhtml-3.4.0}/.pre-commit-config.yaml +0 -0
  54. {justhtml-3.3.0 → justhtml-3.4.0}/CODE_OF_CONDUCT.md +0 -0
  55. {justhtml-3.3.0 → justhtml-3.4.0}/CONTRIBUTING.md +0 -0
  56. {justhtml-3.3.0 → justhtml-3.4.0}/LICENSE +0 -0
  57. {justhtml-3.3.0 → justhtml-3.4.0}/Makefile +0 -0
  58. {justhtml-3.3.0 → justhtml-3.4.0}/README.md +0 -0
  59. {justhtml-3.3.0 → justhtml-3.4.0}/assets/justhtml-readme-explainer.png +0 -0
  60. {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/chromium_parser_diff.py +0 -0
  61. {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/correctness.py +0 -0
  62. {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/fuzz.py +0 -0
  63. {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/html5lib_engine_diff.py +0 -0
  64. {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/parser_engine_gate.py +0 -0
  65. {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/performance.py +0 -0
  66. {justhtml-3.3.0 → justhtml-3.4.0}/benchmarks/profile.py +0 -0
  67. {justhtml-3.3.0 → justhtml-3.4.0}/docs/_config.yml +0 -0
  68. {justhtml-3.3.0 → justhtml-3.4.0}/docs/_layouts/default.html +0 -0
  69. {justhtml-3.3.0 → justhtml-3.4.0}/docs/api.md +0 -0
  70. {justhtml-3.3.0 → justhtml-3.4.0}/docs/assets/search.js +0 -0
  71. {justhtml-3.3.0 → justhtml-3.4.0}/docs/building.md +0 -0
  72. {justhtml-3.3.0 → justhtml-3.4.0}/docs/comparison.md +0 -0
  73. {justhtml-3.3.0 → justhtml-3.4.0}/docs/encoding.md +0 -0
  74. {justhtml-3.3.0 → justhtml-3.4.0}/docs/index.md +0 -0
  75. {justhtml-3.3.0 → justhtml-3.4.0}/docs/linkify.md +0 -0
  76. {justhtml-3.3.0 → justhtml-3.4.0}/docs/migration-examples.md +0 -0
  77. {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/app.js +0 -0
  78. {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/index.html +0 -0
  79. {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/__init__.py +0 -0
  80. {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/install_latest_justhtml.py +0 -0
  81. {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/render.py +0 -0
  82. {justhtml-3.3.0 → justhtml-3.4.0}/docs/playground/py/use_local_repo.py +0 -0
  83. {justhtml-3.3.0 → justhtml-3.4.0}/docs/sanitization.md +0 -0
  84. {justhtml-3.3.0 → justhtml-3.4.0}/docs/selectors.md +0 -0
  85. {justhtml-3.3.0 → justhtml-3.4.0}/llms.txt +0 -0
  86. {justhtml-3.3.0 → justhtml-3.4.0}/run_tests.py +0 -0
  87. {justhtml-3.3.0 → justhtml-3.4.0}/scripts/release.py +0 -0
  88. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/__init__.py +0 -0
  89. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/__main__.py +0 -0
  90. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/__init__.py +0 -0
  91. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/constants.py +0 -0
  92. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/doctype.py +0 -0
  93. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/errors.py +0 -0
  94. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/rawtext.py +0 -0
  95. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/text.py +0 -0
  96. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/core/types.py +0 -0
  97. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/dom/__init__.py +0 -0
  98. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/dom/builder.py +0 -0
  99. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/context.py +0 -0
  100. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/encoding.py +0 -0
  101. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/options.py +0 -0
  102. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/scanner.py +0 -0
  103. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/parser/stream.py +0 -0
  104. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/py.typed +0 -0
  105. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/__init__.py +0 -0
  106. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/css.py +0 -0
  107. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/dom.py +0 -0
  108. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
  109. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/rawtext.py +0 -0
  110. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
  111. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/policy.py +0 -0
  112. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/spec.py +0 -0
  113. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/selector/__init__.py +0 -0
  114. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/selector/core.py +0 -0
  115. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/serializer/__init__.py +0 -0
  116. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/serializer/markdown.py +0 -0
  117. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/linkify.py +0 -0
  118. {justhtml-3.3.0 → justhtml-3.4.0}/src/justhtml/transforms/spec.py +0 -0
  119. {justhtml-3.3.0 → justhtml-3.4.0}/tests/README.md +0 -0
  120. {justhtml-3.3.0 → justhtml-3.4.0}/tests/__init__.py +0 -0
  121. {justhtml-3.3.0 → justhtml-3.4.0}/tests/data/wikipedia.html +0 -0
  122. {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/__init__.py +0 -0
  123. {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/encoding.py +0 -0
  124. {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/regressions.py +0 -0
  125. {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/reporter.py +0 -0
  126. {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/serializer.py +0 -0
  127. {justhtml-3.3.0 → justhtml-3.4.0}/tests/harness/tree.py +0 -0
  128. {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
  129. {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-tests/entities.test +0 -0
  130. /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
  131. {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
  132. /justhtml-3.3.0/tests/justhtml-tests/coverage_gaps.test → /justhtml-3.4.0/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
  133. {justhtml-3.3.0 → justhtml-3.4.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
  134. /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
  135. {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/LICENSE.txt +0 -0
  136. {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/README.md +0 -0
  137. {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/fixtures/links.txt +0 -0
  138. {justhtml-3.3.0 → justhtml-3.4.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
  139. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_builder.py +0 -0
  140. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_cli.py +0 -0
  141. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_encoding.py +0 -0
  142. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_linkify_internals.py +0 -0
  143. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_linkify_it.py +0 -0
  144. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_linkify_transform.py +0 -0
  145. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_node.py +0 -0
  146. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_parser_regressions.py +0 -0
  147. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_parser_tree.py +0 -0
  148. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_playground_local_repo_file_list.py +0 -0
  149. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_sanitize_integration.py +0 -0
  150. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_sanitize_transform.py +0 -0
  151. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_selector.py +0 -0
  152. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_stream.py +0 -0
  153. {justhtml-3.3.0 → justhtml-3.4.0}/tests/test_transforms_compiler.py +0 -0
  154. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: justhtml
3
- Version: 3.3.0
3
+ Version: 3.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
@@ -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 2023, Bleach’s maintainer announced that **Bleach is deprecated** (but will continue to receive security updates, new Python version support, and fixes for egregious bugs). See: https://github.com/mozilla/bleach/issues/698
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 covers common migration patterns.
11
+ This guide starts with Bleach's defaults, then builds up to a copyable compatibility wrapper for a gradual migration.
12
12
 
13
- ## Real-world migration pattern: add a local wrapper first
13
+ ## Start with the defaults
14
14
 
15
- A large application migration often works best in two steps:
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
- 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.
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
- That keeps most call-site changes mechanical while leaving the security policy in one place.
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
- Example wrapper:
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={"http", "https", "mailto", "tel"},
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={"http", "https", "mailto", "tel"},
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]] | None = None,
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 = True,
88
+ strip: bool = False,
78
89
  strip_comments: bool = True,
79
90
  ) -> str:
80
- attrs = dict(attributes or {})
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
- 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]`.
123
+ Use the wrapper with Bleach-compatible defaults:
113
124
 
114
- ## Mental model differences
115
-
116
- - Bleach takes a string and returns a cleaned string.
117
- - JustHTML parses into a DOM and sanitizes by default at construction time:
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
- ## Equivalent of `bleach.clean(...)`
130
+ ## Customize the wrapper gradually
124
131
 
125
- A typical Bleach call:
132
+ Keep the call shape familiar while making the policy explicit at the call site:
126
133
 
127
134
  ```python
128
- import bleach
129
-
130
- clean = bleach.clean(
135
+ safe_html = clean(
131
136
  user_html,
132
- tags=["p", "b", "a"],
133
- attributes={"a": ["href"]},
134
- protocols=["http", "https"],
137
+ tags={"p", "b", "a"},
138
+ attributes={"a": {"href"}},
139
+ protocols={"http", "https"},
135
140
  strip=True,
136
141
  )
137
142
  ```
138
143
 
139
- In JustHTML you typically configure a `SanitizationPolicy`:
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
- ```python
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
- doc = JustHTML(user_html, fragment=True, policy=policy)
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
- Notes:
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
- - Prefer `fragment=True` for user-generated snippets. That avoids adding `<html>`, `<head>`, and `<body>` tags.
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
- Output:
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
- Output:
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
- Output:
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
- Output:
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
- Output:
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
- Output:
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
- Output:
146
+ <!-- justhtml: output -->
147
147
 
148
148
  ```html
149
149
  &lt;b&gt;not bold&lt;/b&gt;
@@ -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
- Output:
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
- Output:
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&amp;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&amp;b=2">ok</a></p>
64
38
  ```
65
39
 
66
- ## Disable sanitization
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>init_page_view_tracker()</script> <a href="javascript:track_pageview()">ok</a></p>'
74
- doc = JustHTML(user_html, fragment=True, sanitize=False)
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
- Output:
50
+ <!-- justhtml: output -->
80
51
 
81
- ```html
82
- <p>Hello <b>world</b> <script>init_page_view_tracker()</script> <a href="javascript:track_pageview()">ok</a></p>
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) Allow the `style` attribute for the relevant tag via `allowed_attributes`, and
122
- 2) Provide a non-empty allowlist via `allowed_css_properties`.
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
- To avoid "footgun" policies, you can start from the built-in preset `CSS_PRESET_TEXT`.
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=["p"],
133
- allowed_attributes={"*": [], "p": ["style"]},
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
- Output:
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
- You are encouraged to write your own `SanitizationPolicy`, and not rely on the default one. This makes it easier for future developers to understand what's being cleaned, without having to look it up in justhtml's documentation.
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
- Use this only when you trust the selectors and know which limit your workload needs to raise. The defaults are intentionally conservative for untrusted input.
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
- - `iframe`, `object`, `embed`
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
- For URL-related risks and controls, see [URL Cleaning](url-cleaning.md).
171
+ Only disable sanitization for HTML you fully trust:
179
172
 
180
173
  ```python
181
- from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
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&amp;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
- Output:
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
- Output:
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
- Output:
159
+ <!-- justhtml: output -->
160
160
 
161
161
  ```html
162
162
  Start: p
@@ -22,7 +22,7 @@ for event, data in stream(html):
22
22
  print(event, data)
23
23
  ```
24
24
 
25
- Output:
25
+ <!-- justhtml: output -->
26
26
  ```text
27
27
  start ('html', {})
28
28
  start ('body', {})
@@ -130,7 +130,7 @@ doc = JustHTML(html)
130
130
  print(doc.to_markdown())
131
131
  ```
132
132
 
133
- Output:
133
+ <!-- justhtml: output -->
134
134
 
135
135
  ````html
136
136
  # Title
@@ -153,7 +153,7 @@ print(doc.to_html(pretty=False))
153
153
  print(doc2.to_html(pretty=False))
154
154
  ```
155
155
 
156
- Output:
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
- Output:
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
- Output:
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
- Output:
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
- Output:
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
- Output:
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
- Output:
77
+ <!-- justhtml: output -->
78
78
 
79
79
  ```text
80
80
  Unsafe tag 'script' (dropped content)