justhtml 3.2.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 (155) hide show
  1. {justhtml-3.2.0 → justhtml-3.4.0}/.github/workflows/ci.yml +13 -3
  2. {justhtml-3.2.0 → justhtml-3.4.0}/.pre-commit-config.yaml +1 -1
  3. {justhtml-3.2.0 → justhtml-3.4.0}/CHANGELOG.md +27 -0
  4. {justhtml-3.2.0 → justhtml-3.4.0}/PKG-INFO +1 -1
  5. {justhtml-3.2.0 → justhtml-3.4.0}/SECURITY.md +14 -0
  6. {justhtml-3.2.0 → justhtml-3.4.0}/docs/api.md +1 -1
  7. {justhtml-3.2.0 → justhtml-3.4.0}/docs/bleach-migration.md +44 -52
  8. {justhtml-3.2.0 → justhtml-3.4.0}/docs/cli.md +1 -1
  9. {justhtml-3.2.0 → justhtml-3.4.0}/docs/correctness.md +2 -2
  10. {justhtml-3.2.0 → justhtml-3.4.0}/docs/errors.md +5 -0
  11. {justhtml-3.2.0 → justhtml-3.4.0}/docs/fragments.md +5 -5
  12. {justhtml-3.2.0 → justhtml-3.4.0}/docs/html-cleaning.md +66 -96
  13. {justhtml-3.2.0 → justhtml-3.4.0}/docs/linkify.md +27 -0
  14. {justhtml-3.2.0 → justhtml-3.4.0}/docs/quickstart.md +3 -3
  15. {justhtml-3.2.0 → justhtml-3.4.0}/docs/sanitization.md +6 -4
  16. {justhtml-3.2.0 → justhtml-3.4.0}/docs/streaming.md +13 -15
  17. {justhtml-3.2.0 → justhtml-3.4.0}/docs/text.md +1 -1
  18. {justhtml-3.2.0 → justhtml-3.4.0}/docs/transforms.md +5 -5
  19. {justhtml-3.2.0 → justhtml-3.4.0}/docs/unsafe-handling.md +2 -2
  20. {justhtml-3.2.0 → justhtml-3.4.0}/docs/url-cleaning.md +7 -7
  21. {justhtml-3.2.0 → justhtml-3.4.0}/pyproject.toml +1 -1
  22. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/entities.py +13 -6
  23. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/dom/__init__.py +3 -3
  24. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/parser/__init__.py +44 -17
  25. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/parser/engine.py +145 -89
  26. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/parser/stream.py +41 -13
  27. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/dom.py +5 -1
  28. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/policy.py +0 -8
  29. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/runtime.py +12 -6
  30. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/serializer/html.py +82 -19
  31. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/transforms/__init__.py +1 -0
  32. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/transforms/compile.py +3 -20
  33. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/transforms/linkify_core.py +10 -2
  34. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/transforms/runtime.py +63 -3
  35. {justhtml-3.2.0 → justhtml-3.4.0}/test-summary.txt +23 -16
  36. justhtml-3.4.0/tests/justhtml-tests/character_preprocessing.dat +99 -0
  37. justhtml-3.4.0/tests/justhtml-tests/document_shell_recovery.dat +57 -0
  38. justhtml-3.4.0/tests/justhtml-tests/foreign_content.dat +122 -0
  39. justhtml-3.4.0/tests/justhtml-tests/frameset_recovery.dat +50 -0
  40. justhtml-3.4.0/tests/justhtml-tests/head_noscript_recovery.dat +25 -0
  41. justhtml-3.4.0/tests/justhtml-tests/in_body_recovery.dat +12 -0
  42. justhtml-3.4.0/tests/justhtml-tests/select_recovery.dat +138 -0
  43. justhtml-3.4.0/tests/justhtml-tests/selectedcontent.dat +76 -0
  44. justhtml-3.4.0/tests/justhtml-tests/table_recovery.dat +74 -0
  45. justhtml-3.2.0/tests/justhtml-tests/tokenizer_edge_cases.test → justhtml-3.4.0/tests/justhtml-tests/tokenizer_states.test +1 -1
  46. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_docs_examples.py +1 -1
  47. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_entities.py +5 -0
  48. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_errors.py +28 -1
  49. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_linkify_internals.py +20 -0
  50. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_node.py +21 -0
  51. justhtml-3.2.0/tests/test_parser_engine_coverage.py → justhtml-3.4.0/tests/test_parser_engine.py +70 -5
  52. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_sanitize.py +20 -0
  53. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_serialize.py +15 -0
  54. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_stream.py +14 -0
  55. justhtml-3.2.0/tests/test_transforms_edge_cases.py → justhtml-3.4.0/tests/test_transform_runtime.py +104 -46
  56. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_transforms.py +40 -0
  57. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_transforms_sanitize_integration.py +16 -0
  58. justhtml-3.2.0/docs/parser-engine-findings.md +0 -469
  59. justhtml-3.2.0/tests/justhtml-tests/branch_coverage.dat +0 -450
  60. justhtml-3.2.0/tests/justhtml-tests/treebuilder_coverage.dat +0 -254
  61. {justhtml-3.2.0 → justhtml-3.4.0}/.github/copilot-instructions.md +0 -0
  62. {justhtml-3.2.0 → justhtml-3.4.0}/.github/workflows/publish.yml +0 -0
  63. {justhtml-3.2.0 → justhtml-3.4.0}/.gitignore +0 -0
  64. {justhtml-3.2.0 → justhtml-3.4.0}/CODE_OF_CONDUCT.md +0 -0
  65. {justhtml-3.2.0 → justhtml-3.4.0}/CONTRIBUTING.md +0 -0
  66. {justhtml-3.2.0 → justhtml-3.4.0}/LICENSE +0 -0
  67. {justhtml-3.2.0 → justhtml-3.4.0}/Makefile +0 -0
  68. {justhtml-3.2.0 → justhtml-3.4.0}/README.md +0 -0
  69. {justhtml-3.2.0 → justhtml-3.4.0}/assets/justhtml-readme-explainer.png +0 -0
  70. {justhtml-3.2.0 → justhtml-3.4.0}/benchmarks/chromium_parser_diff.py +0 -0
  71. {justhtml-3.2.0 → justhtml-3.4.0}/benchmarks/correctness.py +0 -0
  72. {justhtml-3.2.0 → justhtml-3.4.0}/benchmarks/fuzz.py +0 -0
  73. {justhtml-3.2.0 → justhtml-3.4.0}/benchmarks/html5lib_engine_diff.py +0 -0
  74. {justhtml-3.2.0 → justhtml-3.4.0}/benchmarks/parser_engine_gate.py +0 -0
  75. {justhtml-3.2.0 → justhtml-3.4.0}/benchmarks/performance.py +0 -0
  76. {justhtml-3.2.0 → justhtml-3.4.0}/benchmarks/profile.py +0 -0
  77. {justhtml-3.2.0 → justhtml-3.4.0}/docs/_config.yml +0 -0
  78. {justhtml-3.2.0 → justhtml-3.4.0}/docs/_layouts/default.html +0 -0
  79. {justhtml-3.2.0 → justhtml-3.4.0}/docs/assets/search.js +0 -0
  80. {justhtml-3.2.0 → justhtml-3.4.0}/docs/building.md +0 -0
  81. {justhtml-3.2.0 → justhtml-3.4.0}/docs/comparison.md +0 -0
  82. {justhtml-3.2.0 → justhtml-3.4.0}/docs/encoding.md +0 -0
  83. {justhtml-3.2.0 → justhtml-3.4.0}/docs/index.md +0 -0
  84. {justhtml-3.2.0 → justhtml-3.4.0}/docs/migration-examples.md +0 -0
  85. {justhtml-3.2.0 → justhtml-3.4.0}/docs/playground/app.js +0 -0
  86. {justhtml-3.2.0 → justhtml-3.4.0}/docs/playground/index.html +0 -0
  87. {justhtml-3.2.0 → justhtml-3.4.0}/docs/playground/py/__init__.py +0 -0
  88. {justhtml-3.2.0 → justhtml-3.4.0}/docs/playground/py/install_latest_justhtml.py +0 -0
  89. {justhtml-3.2.0 → justhtml-3.4.0}/docs/playground/py/render.py +0 -0
  90. {justhtml-3.2.0 → justhtml-3.4.0}/docs/playground/py/use_local_repo.py +0 -0
  91. {justhtml-3.2.0 → justhtml-3.4.0}/docs/selectors.md +0 -0
  92. {justhtml-3.2.0 → justhtml-3.4.0}/llms.txt +0 -0
  93. {justhtml-3.2.0 → justhtml-3.4.0}/run_tests.py +0 -0
  94. {justhtml-3.2.0 → justhtml-3.4.0}/scripts/release.py +0 -0
  95. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/__init__.py +0 -0
  96. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/__main__.py +0 -0
  97. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/__init__.py +0 -0
  98. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/constants.py +0 -0
  99. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/doctype.py +0 -0
  100. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/errors.py +0 -0
  101. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/rawtext.py +0 -0
  102. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/text.py +0 -0
  103. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/core/types.py +0 -0
  104. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/dom/builder.py +0 -0
  105. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/parser/context.py +0 -0
  106. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/parser/encoding.py +0 -0
  107. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/parser/options.py +0 -0
  108. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/parser/scanner.py +0 -0
  109. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/py.typed +0 -0
  110. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/__init__.py +0 -0
  111. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/css.py +0 -0
  112. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
  113. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/rawtext.py +0 -0
  114. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
  115. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/policy.py +0 -0
  116. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/sanitizer/url/spec.py +0 -0
  117. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/selector/__init__.py +0 -0
  118. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/selector/core.py +0 -0
  119. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/serializer/__init__.py +0 -0
  120. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/serializer/markdown.py +0 -0
  121. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/transforms/linkify.py +0 -0
  122. {justhtml-3.2.0 → justhtml-3.4.0}/src/justhtml/transforms/spec.py +0 -0
  123. {justhtml-3.2.0 → justhtml-3.4.0}/tests/README.md +0 -0
  124. {justhtml-3.2.0 → justhtml-3.4.0}/tests/__init__.py +0 -0
  125. {justhtml-3.2.0 → justhtml-3.4.0}/tests/data/wikipedia.html +0 -0
  126. {justhtml-3.2.0 → justhtml-3.4.0}/tests/harness/__init__.py +0 -0
  127. {justhtml-3.2.0 → justhtml-3.4.0}/tests/harness/encoding.py +0 -0
  128. {justhtml-3.2.0 → justhtml-3.4.0}/tests/harness/regressions.py +0 -0
  129. {justhtml-3.2.0 → justhtml-3.4.0}/tests/harness/reporter.py +0 -0
  130. {justhtml-3.2.0 → justhtml-3.4.0}/tests/harness/serializer.py +0 -0
  131. {justhtml-3.2.0 → justhtml-3.4.0}/tests/harness/tree.py +0 -0
  132. {justhtml-3.2.0 → justhtml-3.4.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
  133. {justhtml-3.2.0 → justhtml-3.4.0}/tests/justhtml-tests/entities.test +0 -0
  134. /justhtml-3.2.0/tests/justhtml-tests/empty_stack_edge_cases.dat → /justhtml-3.4.0/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
  135. {justhtml-3.2.0 → justhtml-3.4.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
  136. /justhtml-3.2.0/tests/justhtml-tests/coverage_gaps.test → /justhtml-3.4.0/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
  137. {justhtml-3.2.0 → justhtml-3.4.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
  138. /justhtml-3.2.0/tests/justhtml-tests/xml_coercion_coverage.test → /justhtml-3.4.0/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
  139. {justhtml-3.2.0 → justhtml-3.4.0}/tests/linkify-it/LICENSE.txt +0 -0
  140. {justhtml-3.2.0 → justhtml-3.4.0}/tests/linkify-it/README.md +0 -0
  141. {justhtml-3.2.0 → justhtml-3.4.0}/tests/linkify-it/fixtures/links.txt +0 -0
  142. {justhtml-3.2.0 → justhtml-3.4.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
  143. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_builder.py +0 -0
  144. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_cli.py +0 -0
  145. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_encoding.py +0 -0
  146. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_linkify_it.py +0 -0
  147. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_linkify_transform.py +0 -0
  148. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_parser_regressions.py +0 -0
  149. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_parser_tree.py +0 -0
  150. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_playground_local_repo_file_list.py +0 -0
  151. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_sanitize_integration.py +0 -0
  152. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_sanitize_transform.py +0 -0
  153. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_selector.py +0 -0
  154. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_transforms_compiler.py +0 -0
  155. {justhtml-3.2.0 → justhtml-3.4.0}/tests/test_wikipedia.py +0 -0
@@ -41,15 +41,25 @@ jobs:
41
41
  git -C html5lib-tests checkout 9fb614afaa42ce8787840f057b32084308e76549
42
42
  git -C html5lib-tests rev-parse HEAD
43
43
 
44
+ - name: Checkout WPT tree-construction tests
45
+ run: |
46
+ cd ..
47
+ git clone --filter=blob:none --sparse https://github.com/web-platform-tests/wpt.git
48
+ git -C wpt fetch origin 75b089d70448e1da425ab8cf38971b6af41f3cea
49
+ git -C wpt checkout 75b089d70448e1da425ab8cf38971b6af41f3cea
50
+ git -C wpt sparse-checkout set html/syntax/parsing/resources
51
+ git -C wpt rev-parse HEAD
52
+
44
53
  - name: Setup test fixtures
45
54
  run: |
46
55
  cp -a ../html5lib-tests/tokenizer tests/html5lib-tests-tokenizer
47
- cp -a ../html5lib-tests/tree-construction tests/html5lib-tests-tree
56
+ cp -a ../wpt/html/syntax/parsing/resources tests/html5lib-tests-tree
48
57
  cp -a ../html5lib-tests/serializer tests/html5lib-tests-serializer
49
58
  cp -a ../html5lib-tests/encoding tests/html5lib-tests-encoding
50
59
 
51
60
  - name: Run pre-commit
52
61
  id: precommit
62
+ if: matrix.python-version == '3.14'
53
63
  run: pre-commit run --show-diff-on-failure --color=always --all-files
54
64
  env:
55
65
  SKIP: mypy
@@ -61,14 +71,14 @@ jobs:
61
71
  - name: Diagnose test failures (verbose)
62
72
  if: always()
63
73
  run: |
64
- if [ "${{ steps.precommit.outcome }}" = "success" ] && [ "${{ steps.run_tests.outcome }}" = "success" ]; then
74
+ if [ "${{ steps.precommit.outcome }}" != "failure" ] && [ "${{ steps.run_tests.outcome }}" = "success" ]; then
65
75
  echo "No failures detected; skipping verbose rerun."
66
76
  exit 0
67
77
  fi
68
78
  python run_tests.py -v --no-write-summary
69
79
 
70
80
  - name: Run mypy
71
- if: matrix.python-version == '3.12'
81
+ if: matrix.python-version == '3.14'
72
82
  run: mypy
73
83
 
74
84
  test-pyodide:
@@ -33,7 +33,7 @@ repos:
33
33
  hooks:
34
34
  - id: tests-coverage
35
35
  name: Tests & Coverage
36
- entry: bash -c 'coverage run run_tests.py --no-write-summary && coverage report'
36
+ entry: bash -c 'coverage run run_tests.py && coverage report'
37
37
  language: system
38
38
  pass_filenames: false
39
39
  always_run: true
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.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
+
20
+ ## [3.3.0] - 2026-07-04
21
+
22
+ ### Changed
23
+ - Document that `Linkify(...)` must run before an explicit `Sanitize(...)` in a transform list, not after, and correct the `clean_url_value()` example to escape the returned value with `escape_attr_value()` before embedding it in HTML. Previously, the docs suggested orderings that silently skip URL-policy validation on Linkify-generated links, and separately claimed `clean_url_value()` output needed no further escaping for HTML attribute embedding.
24
+
25
+ ### Fixed
26
+ - Copy `override_attrs` when cloning a node instead of storing the caller's dict by reference. Previously, calling `clone_node(override_attrs=...)` more than once with the same dict (or holding onto that dict after cloning) produced clones that aliased the same attribute dictionary, so mutating one clone's attributes silently mutated another clone or the caller's own dict.
27
+
28
+ ### Performance
29
+ - Avoid rescanning the entire open-elements stack for every unmatched end tag inside foreign (SVG/MathML) content in the streaming parser (`stream()`), by tracking open-element name counts the same way the main parser engine does. Previously, a run of unmatched end tags deep inside foreign content was quadratic in the depth of nesting.
30
+ - Bound `Linkify(...)`'s email-candidate pattern to RFC 5321's 64-octet local-part limit instead of an unbounded match. Previously, the greedy local-part match followed by a required (and often absent) `@` forced a full-length backtrack at every candidate start position, making long "@"-free runs of otherwise email-local-part-legal text (such as repeated dotted-numeric tokens) quadratic to scan.
31
+ - Share cached sibling/ancestor lookups across nodes when matching structural selectors (`:nth-child`, sibling combinators) in custom `EditAttrs(...)`/`SetAttrs(...)`-style transform pipelines, the same way `query()` already does. Previously, each node rebuilt this data from scratch, making structural-selector transforms quadratic over documents with many siblings. Sharing is only enabled when every transform in the pipeline is provably attribute-only; a pipeline that also uses `Edit(...)`, `Decide(...)`, `Drop(...)`, `Linkify(...)`, or similar structure-changing transforms is unaffected and keeps its previous (already correct) behavior.
32
+ - Access the parser's open-elements stack through a plain attribute again instead of a property, removing a Python-level function call from one of the hottest paths in tree construction.
33
+
34
+ ### Security
35
+ - (Severity: Low) HTML-escape a literal `&` left over after percent-encoding a URL for the `HTMLContext.URL` serialization context (used for `href`/`src`-style attributes). Previously, a URL containing a legacy (no-semicolon) named HTML entity immediately after a decoded `&` could be interpreted differently once embedded in an HTML attribute and re-parsed by a browser than what was validated.
36
+
10
37
  ## [3.2.0] - 2026-07-04
11
38
 
12
39
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: justhtml
3
- Version: 3.2.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`.
@@ -602,7 +602,7 @@ To override the encoding for byte input, pass `encoding=...`.
602
602
  | `"end"` | `tag_name` | Closing tag |
603
603
  | `"text"` | `text_content` | Text content |
604
604
  | `"comment"` | `comment_text` | HTML comment |
605
- | `"doctype"` | `doctype_name` | DOCTYPE declaration |
605
+ | `"doctype"` | `(name, public_id, system_id)` | DOCTYPE declaration fields |
606
606
 
607
607
  ---
608
608
 
@@ -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