justhtml 3.11.2__tar.gz → 3.11.3__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 (159) hide show
  1. {justhtml-3.11.2 → justhtml-3.11.3}/CHANGELOG.md +7 -0
  2. {justhtml-3.11.2 → justhtml-3.11.3}/PKG-INFO +1 -1
  3. {justhtml-3.11.2 → justhtml-3.11.3}/docs/linkify.md +3 -2
  4. {justhtml-3.11.2 → justhtml-3.11.3}/pyproject.toml +1 -1
  5. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/parser/engine.py +2 -5
  6. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/transforms/runtime.py +0 -3
  7. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_parser_engine.py +1 -1
  8. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_sanitize_integration.py +11 -0
  9. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_transforms.py +23 -0
  10. {justhtml-3.11.2 → justhtml-3.11.3}/.github/workflows/ci.yml +0 -0
  11. {justhtml-3.11.2 → justhtml-3.11.3}/.github/workflows/publish.yml +0 -0
  12. {justhtml-3.11.2 → justhtml-3.11.3}/.gitignore +0 -0
  13. {justhtml-3.11.2 → justhtml-3.11.3}/.pre-commit-config.yaml +0 -0
  14. {justhtml-3.11.2 → justhtml-3.11.3}/AGENTS.md +0 -0
  15. {justhtml-3.11.2 → justhtml-3.11.3}/ARCHITECTURE.md +0 -0
  16. {justhtml-3.11.2 → justhtml-3.11.3}/CODE_OF_CONDUCT.md +0 -0
  17. {justhtml-3.11.2 → justhtml-3.11.3}/CONTRIBUTING.md +0 -0
  18. {justhtml-3.11.2 → justhtml-3.11.3}/LICENSE +0 -0
  19. {justhtml-3.11.2 → justhtml-3.11.3}/Makefile +0 -0
  20. {justhtml-3.11.2 → justhtml-3.11.3}/PERFORMANCE.md +0 -0
  21. {justhtml-3.11.2 → justhtml-3.11.3}/README.md +0 -0
  22. {justhtml-3.11.2 → justhtml-3.11.3}/SECURITY.md +0 -0
  23. {justhtml-3.11.2 → justhtml-3.11.3}/assets/justhtml-readme-explainer.png +0 -0
  24. {justhtml-3.11.2 → justhtml-3.11.3}/benchmarks/chromium_parser_diff.py +0 -0
  25. {justhtml-3.11.2 → justhtml-3.11.3}/benchmarks/correctness.py +0 -0
  26. {justhtml-3.11.2 → justhtml-3.11.3}/benchmarks/fuzz.py +0 -0
  27. {justhtml-3.11.2 → justhtml-3.11.3}/benchmarks/html5lib_engine_diff.py +0 -0
  28. {justhtml-3.11.2 → justhtml-3.11.3}/benchmarks/parser_engine_gate.py +0 -0
  29. {justhtml-3.11.2 → justhtml-3.11.3}/benchmarks/performance.py +0 -0
  30. {justhtml-3.11.2 → justhtml-3.11.3}/benchmarks/profile.py +0 -0
  31. {justhtml-3.11.2 → justhtml-3.11.3}/docs/_config.yml +0 -0
  32. {justhtml-3.11.2 → justhtml-3.11.3}/docs/_layouts/default.html +0 -0
  33. {justhtml-3.11.2 → justhtml-3.11.3}/docs/api.md +0 -0
  34. {justhtml-3.11.2 → justhtml-3.11.3}/docs/assets/search.js +0 -0
  35. {justhtml-3.11.2 → justhtml-3.11.3}/docs/bleach-migration.md +0 -0
  36. {justhtml-3.11.2 → justhtml-3.11.3}/docs/building.md +0 -0
  37. {justhtml-3.11.2 → justhtml-3.11.3}/docs/cli.md +0 -0
  38. {justhtml-3.11.2 → justhtml-3.11.3}/docs/comparison.md +0 -0
  39. {justhtml-3.11.2 → justhtml-3.11.3}/docs/correctness.md +0 -0
  40. {justhtml-3.11.2 → justhtml-3.11.3}/docs/encoding.md +0 -0
  41. {justhtml-3.11.2 → justhtml-3.11.3}/docs/errors.md +0 -0
  42. {justhtml-3.11.2 → justhtml-3.11.3}/docs/fragments.md +0 -0
  43. {justhtml-3.11.2 → justhtml-3.11.3}/docs/html-cleaning.md +0 -0
  44. {justhtml-3.11.2 → justhtml-3.11.3}/docs/index.md +0 -0
  45. {justhtml-3.11.2 → justhtml-3.11.3}/docs/migration-examples.md +0 -0
  46. {justhtml-3.11.2 → justhtml-3.11.3}/docs/playground/app.js +0 -0
  47. {justhtml-3.11.2 → justhtml-3.11.3}/docs/playground/index.html +0 -0
  48. {justhtml-3.11.2 → justhtml-3.11.3}/docs/playground/py/__init__.py +0 -0
  49. {justhtml-3.11.2 → justhtml-3.11.3}/docs/playground/py/install_latest_justhtml.py +0 -0
  50. {justhtml-3.11.2 → justhtml-3.11.3}/docs/playground/py/render.py +0 -0
  51. {justhtml-3.11.2 → justhtml-3.11.3}/docs/playground/py/use_local_repo.py +0 -0
  52. {justhtml-3.11.2 → justhtml-3.11.3}/docs/quickstart.md +0 -0
  53. {justhtml-3.11.2 → justhtml-3.11.3}/docs/sanitization.md +0 -0
  54. {justhtml-3.11.2 → justhtml-3.11.3}/docs/selectors.md +0 -0
  55. {justhtml-3.11.2 → justhtml-3.11.3}/docs/streaming.md +0 -0
  56. {justhtml-3.11.2 → justhtml-3.11.3}/docs/text.md +0 -0
  57. {justhtml-3.11.2 → justhtml-3.11.3}/docs/transforms.md +0 -0
  58. {justhtml-3.11.2 → justhtml-3.11.3}/docs/unsafe-handling.md +0 -0
  59. {justhtml-3.11.2 → justhtml-3.11.3}/docs/url-cleaning.md +0 -0
  60. {justhtml-3.11.2 → justhtml-3.11.3}/llms.txt +0 -0
  61. {justhtml-3.11.2 → justhtml-3.11.3}/project_ideas.md +0 -0
  62. {justhtml-3.11.2 → justhtml-3.11.3}/run_tests.py +0 -0
  63. {justhtml-3.11.2 → justhtml-3.11.3}/scripts/release.py +0 -0
  64. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/__init__.py +0 -0
  65. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/__main__.py +0 -0
  66. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/__init__.py +0 -0
  67. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/constants.py +0 -0
  68. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/doctype.py +0 -0
  69. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/entities.py +0 -0
  70. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/errors.py +0 -0
  71. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/rawtext.py +0 -0
  72. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/text.py +0 -0
  73. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/core/types.py +0 -0
  74. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/dom/__init__.py +0 -0
  75. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/dom/builder.py +0 -0
  76. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/parser/__init__.py +0 -0
  77. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/parser/context.py +0 -0
  78. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/parser/encoding.py +0 -0
  79. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/parser/options.py +0 -0
  80. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/parser/scanner.py +0 -0
  81. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/parser/stream.py +0 -0
  82. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/py.typed +0 -0
  83. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/__init__.py +0 -0
  84. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/css.py +0 -0
  85. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/dom.py +0 -0
  86. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/policy.py +0 -0
  87. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/policy_defaults.py +0 -0
  88. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/rawtext.py +0 -0
  89. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/url/__init__.py +0 -0
  90. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/url/policy.py +0 -0
  91. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/url/runtime.py +0 -0
  92. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/sanitizer/url/spec.py +0 -0
  93. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/selector/__init__.py +0 -0
  94. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/selector/core.py +0 -0
  95. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/serializer/__init__.py +0 -0
  96. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/serializer/html.py +0 -0
  97. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/serializer/markdown.py +0 -0
  98. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/transforms/__init__.py +0 -0
  99. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/transforms/compile.py +0 -0
  100. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/transforms/linkify.py +0 -0
  101. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/transforms/linkify_core.py +0 -0
  102. {justhtml-3.11.2 → justhtml-3.11.3}/src/justhtml/transforms/spec.py +0 -0
  103. {justhtml-3.11.2 → justhtml-3.11.3}/test-summary.txt +0 -0
  104. {justhtml-3.11.2 → justhtml-3.11.3}/tests/README.md +0 -0
  105. {justhtml-3.11.2 → justhtml-3.11.3}/tests/__init__.py +0 -0
  106. {justhtml-3.11.2 → justhtml-3.11.3}/tests/data/wikipedia.html +0 -0
  107. {justhtml-3.11.2 → justhtml-3.11.3}/tests/harness/__init__.py +0 -0
  108. {justhtml-3.11.2 → justhtml-3.11.3}/tests/harness/encoding.py +0 -0
  109. {justhtml-3.11.2 → justhtml-3.11.3}/tests/harness/regressions.py +0 -0
  110. {justhtml-3.11.2 → justhtml-3.11.3}/tests/harness/reporter.py +0 -0
  111. {justhtml-3.11.2 → justhtml-3.11.3}/tests/harness/scaling.py +0 -0
  112. {justhtml-3.11.2 → justhtml-3.11.3}/tests/harness/serializer.py +0 -0
  113. {justhtml-3.11.2 → justhtml-3.11.3}/tests/harness/tree.py +0 -0
  114. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-sanitize-tests/cases.json +0 -0
  115. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/character_preprocessing.dat +0 -0
  116. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/document_shell_recovery.dat +0 -0
  117. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/entities.test +0 -0
  118. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/foreign_content.dat +0 -0
  119. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
  120. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/frameset_recovery.dat +0 -0
  121. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/head_noscript_recovery.dat +0 -0
  122. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
  123. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/in_body_recovery.dat +0 -0
  124. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/raw_text_recovery.dat +0 -0
  125. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/select_recovery.dat +0 -0
  126. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/selectedcontent.dat +0 -0
  127. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/table_recovery.dat +0 -0
  128. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/template_recovery.dat +0 -0
  129. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/tokenizer_recovery.dat +0 -0
  130. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
  131. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/tokenizer_states.test +0 -0
  132. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/xml_coercion.dat +0 -0
  133. {justhtml-3.11.2 → justhtml-3.11.3}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
  134. {justhtml-3.11.2 → justhtml-3.11.3}/tests/linkify-it/LICENSE.txt +0 -0
  135. {justhtml-3.11.2 → justhtml-3.11.3}/tests/linkify-it/README.md +0 -0
  136. {justhtml-3.11.2 → justhtml-3.11.3}/tests/linkify-it/fixtures/links.txt +0 -0
  137. {justhtml-3.11.2 → justhtml-3.11.3}/tests/linkify-it/fixtures/not_links.txt +0 -0
  138. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_builder.py +0 -0
  139. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_cli.py +0 -0
  140. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_docs_examples.py +0 -0
  141. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_encoding.py +0 -0
  142. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_entities.py +0 -0
  143. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_errors.py +0 -0
  144. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_linkify_internals.py +0 -0
  145. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_linkify_it.py +0 -0
  146. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_linkify_transform.py +0 -0
  147. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_node.py +0 -0
  148. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_parser_regressions.py +0 -0
  149. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_parser_tree.py +0 -0
  150. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_playground_local_repo_file_list.py +0 -0
  151. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_sanitize.py +0 -0
  152. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_sanitize_transform.py +0 -0
  153. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_selector.py +0 -0
  154. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_serialize.py +0 -0
  155. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_stream.py +0 -0
  156. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_transform_runtime.py +0 -0
  157. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_transforms_compiler.py +0 -0
  158. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_transforms_sanitize_integration.py +0 -0
  159. {justhtml-3.11.2 → justhtml-3.11.3}/tests/test_wikipedia.py +0 -0
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.11.3] - 2026-09-27
11
+
12
+ ### Fixed
13
+
14
+ - Apply earlier transforms to allowed children hoisted when sanitization removes a parent (#85).
15
+ - Drop foreign subtrees inside disallowed HTML parents in the default parsing path.
16
+
10
17
  ## [3.11.2] - 2026-08-27
11
18
 
12
19
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: justhtml
3
- Version: 3.11.2
3
+ Version: 3.11.3
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
@@ -136,6 +136,7 @@ policy = SanitizationPolicy(
136
136
  allowed_attributes={"a": ["href"]},
137
137
  url_policy=UrlPolicy(allow_rules={("a", "href"): UrlRule(allowed_schemes={"https"})}),
138
138
  )
139
+ # An example that uses HTTP, so it should NOT be allowed
139
140
  text = "See http://example.com"
140
141
 
141
142
  # Wrong: Linkify's generated <a href> is created after the sanitize point,
@@ -144,10 +145,10 @@ doc = JustHTML(text, transforms=[Sanitize(policy), Linkify()])
144
145
  print(doc.to_html(pretty=False))
145
146
  # => See <a href="http://example.com">http://example.com</a>
146
147
 
147
- # Right: Linkify runs first, so its output is covered by the same sanitize pass.
148
+ # Correct: Linkify runs first, so its output is covered by the same sanitize pass.
148
149
  doc = JustHTML(text, transforms=[Linkify(), Sanitize(policy)])
149
150
  print(doc.to_html(pretty=False))
150
- # => See http://example.com
151
+ # => See <a>http://example.com</a>
151
152
  ```
152
153
 
153
154
  If you don't need other transforms between them, the simplest safe option is to omit `Sanitize()` entirely and let `sanitize=True` (the default) append it automatically after `Linkify()`.
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "justhtml"
3
3
  authors = [{ name = "Emil Stenström", email = "emil@emilstenstrom.se" }]
4
- version = "3.11.2"
4
+ version = "3.11.3"
5
5
  description = "A pure Python HTML5 parser that just works."
6
6
  readme = "README.md"
7
7
  license = { file = "LICENSE" }
@@ -4212,11 +4212,8 @@ class ParseEngine:
4212
4212
  self._foreign_context_seen = True
4213
4213
  self._nodes_to_unwrap.append(node)
4214
4214
  parent_namespace = getattr(parent, "namespace", None)
4215
- if parent is self._fragment_context_node or parent_namespace in {None, "html"}:
4216
- if not self._parser_only_template_depth and (
4217
- parent is self._fragment_context_node or parent.name in self._allowed_tags
4218
- ):
4219
- self._nodes_to_drop.append(node)
4215
+ if parent is self._fragment_context_node or parent_namespace in {None, "html", _PARSER_ONLY_NAMESPACE}:
4216
+ self._nodes_to_drop.append(node)
4220
4217
  elif name not in self._allowed_tags:
4221
4218
  self._nodes_to_unwrap.append(node)
4222
4219
  if not is_void:
@@ -231,7 +231,6 @@ def apply_compiled_transforms(
231
231
 
232
232
  if moved:
233
233
  for child in moved:
234
- _mark_start(child, mark_new_start_index)
235
234
  child.parent = parent
236
235
  replacement.extend(moved)
237
236
 
@@ -294,7 +293,6 @@ def apply_compiled_transforms(
294
293
  moved_nodes = _detach_children_for_hoist(node, name)
295
294
  if moved_nodes:
296
295
  for child in moved_nodes:
297
- _mark_start(child, transform_index)
298
296
  child.parent = parent
299
297
  children[child_index : child_index + 1] = moved_nodes
300
298
  else:
@@ -865,7 +863,6 @@ def apply_compiled_transforms(
865
863
 
866
864
  if moved_nodes_unwrap:
867
865
  for child in moved_nodes_unwrap:
868
- _mark_start(child, idx + 1)
869
866
  child.parent = parent
870
867
  children[i : i + 1] = moved_nodes_unwrap
871
868
  else:
@@ -1745,7 +1745,7 @@ class TestParserAttributeProjection(_ParserEngineTestCase):
1745
1745
  )
1746
1746
  self.assert_parses_to(
1747
1747
  "<tr selected> <svg href=x><table href=x></table><math a=1> </svg>\n</tr></math>\n<blockquote a=1>",
1748
- "<tbody><tr> </tr></tbody><table></table> \n\n<blockquote></blockquote>",
1748
+ "<tbody><tr> </tr></tbody><table></table>\n<blockquote></blockquote>",
1749
1749
  fragment_context=FragmentContext("table"),
1750
1750
  )
1751
1751
  self.assert_parses_to(
@@ -8,6 +8,7 @@ from typing import Any
8
8
  from justhtml import DEFAULT_POLICY, JustHTML
9
9
  from justhtml.parser.context import FragmentContext
10
10
  from justhtml.sanitizer import SanitizationPolicy, UrlPolicy, UrlProxy, UrlRule
11
+ from justhtml.transforms import Sanitize
11
12
 
12
13
  _CASES_DIR = Path(__file__).with_name("justhtml-sanitize-tests")
13
14
 
@@ -89,6 +90,16 @@ def _build_policy(spec: Any) -> SanitizationPolicy:
89
90
 
90
91
 
91
92
  class TestSanitizeIntegration(unittest.TestCase):
93
+ def test_foreign_subtree_under_disallowed_parent_is_dropped(self) -> None:
94
+ for html in (
95
+ "<table><tr><td><select><math><mi>unsafe</mi><p>safe</table>",
96
+ "<template><svg><foo><template><foreignObject><div></template><div>",
97
+ ):
98
+ fused = JustHTML(html).to_html(pretty=False)
99
+ explicit = JustHTML(html, transforms=[Sanitize()]).to_html(pretty=False)
100
+ assert fused == explicit
101
+ assert "unsafe" not in fused
102
+
92
103
  def test_sanitize_cases(self) -> None:
93
104
  cases_path = _CASES_DIR / "cases.json"
94
105
  cases = json.loads(cases_path.read_text(encoding="utf-8"))
@@ -208,6 +208,29 @@ class TestTransforms(unittest.TestCase):
208
208
  doc = JustHTML("<p>Hello <span>world</span></p>", transforms=[Unwrap("span")])
209
209
  assert doc.to_html(pretty=False) == "<html><head></head><body><p>Hello world</p></body></html>"
210
210
 
211
+ def test_hoisted_children_receive_earlier_transforms(self) -> None:
212
+ for structural_transform in (Unwrap("p, span"), Escape("p, span")):
213
+ doc = JustHTML(
214
+ "<p><span><a href='https://eff.org'>EFF</a></span></p>",
215
+ fragment=True,
216
+ transforms=[SetAttrs("a", rel="noopener"), structural_transform],
217
+ )
218
+ assert '<a href="https://eff.org" rel="noopener">EFF</a>' in doc.to_html(pretty=False)
219
+
220
+ def test_sanitize_hoisted_children_receive_earlier_transforms(self) -> None:
221
+ policy = SanitizationPolicy(
222
+ allowed_tags={"a"},
223
+ allowed_attributes={"a": {"href", "rel"}},
224
+ url_policy=UrlPolicy(allow_rules={("a", "href"): UrlRule(allowed_schemes={"https"})}),
225
+ )
226
+ doc = _JustHTML(
227
+ '<p><span><a href="https://eff.org">EFF</a></span></p>',
228
+ fragment=True,
229
+ transforms=[SetAttrs("a", rel="noopener")],
230
+ policy=policy,
231
+ )
232
+ assert doc.to_html(pretty=False) == '<a href="https://eff.org" rel="noopener">EFF</a>'
233
+
211
234
  def test_unwrap_handles_empty_elements(self) -> None:
212
235
  doc = JustHTML("<div><span></span>ok</div>", transforms=[Unwrap("span")])
213
236
  assert doc.to_html(pretty=False) == "<html><head></head><body><div>ok</div></body></html>"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes