justhtml 2.4.0__tar.gz → 2.4.1__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 (142) hide show
  1. {justhtml-2.4.0 → justhtml-2.4.1}/CHANGELOG.md +7 -0
  2. {justhtml-2.4.0 → justhtml-2.4.1}/PKG-INFO +1 -1
  3. {justhtml-2.4.0 → justhtml-2.4.1}/pyproject.toml +1 -1
  4. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/treebuilder/core.py +5 -3
  5. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/treebuilder/modes.py +4 -0
  6. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/branch_coverage.dat +1 -1
  7. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/treebuilder_coverage.dat +3 -3
  8. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_treebuilder.py +57 -0
  9. {justhtml-2.4.0 → justhtml-2.4.1}/.github/copilot-instructions.md +0 -0
  10. {justhtml-2.4.0 → justhtml-2.4.1}/.github/workflows/ci.yml +0 -0
  11. {justhtml-2.4.0 → justhtml-2.4.1}/.github/workflows/publish.yml +0 -0
  12. {justhtml-2.4.0 → justhtml-2.4.1}/.gitignore +0 -0
  13. {justhtml-2.4.0 → justhtml-2.4.1}/.pre-commit-config.yaml +0 -0
  14. {justhtml-2.4.0 → justhtml-2.4.1}/CODE_OF_CONDUCT.md +0 -0
  15. {justhtml-2.4.0 → justhtml-2.4.1}/CONTRIBUTING.md +0 -0
  16. {justhtml-2.4.0 → justhtml-2.4.1}/LICENSE +0 -0
  17. {justhtml-2.4.0 → justhtml-2.4.1}/Makefile +0 -0
  18. {justhtml-2.4.0 → justhtml-2.4.1}/README.md +0 -0
  19. {justhtml-2.4.0 → justhtml-2.4.1}/SECURITY.md +0 -0
  20. {justhtml-2.4.0 → justhtml-2.4.1}/assets/justhtml-readme-explainer.png +0 -0
  21. {justhtml-2.4.0 → justhtml-2.4.1}/benchmarks/correctness.py +0 -0
  22. {justhtml-2.4.0 → justhtml-2.4.1}/benchmarks/fuzz.py +0 -0
  23. {justhtml-2.4.0 → justhtml-2.4.1}/benchmarks/performance.py +0 -0
  24. {justhtml-2.4.0 → justhtml-2.4.1}/benchmarks/profile.py +0 -0
  25. {justhtml-2.4.0 → justhtml-2.4.1}/docs/_config.yml +0 -0
  26. {justhtml-2.4.0 → justhtml-2.4.1}/docs/_layouts/default.html +0 -0
  27. {justhtml-2.4.0 → justhtml-2.4.1}/docs/api.md +0 -0
  28. {justhtml-2.4.0 → justhtml-2.4.1}/docs/assets/search.js +0 -0
  29. {justhtml-2.4.0 → justhtml-2.4.1}/docs/bleach-migration.md +0 -0
  30. {justhtml-2.4.0 → justhtml-2.4.1}/docs/building.md +0 -0
  31. {justhtml-2.4.0 → justhtml-2.4.1}/docs/cli.md +0 -0
  32. {justhtml-2.4.0 → justhtml-2.4.1}/docs/comparison.md +0 -0
  33. {justhtml-2.4.0 → justhtml-2.4.1}/docs/correctness.md +0 -0
  34. {justhtml-2.4.0 → justhtml-2.4.1}/docs/encoding.md +0 -0
  35. {justhtml-2.4.0 → justhtml-2.4.1}/docs/errors.md +0 -0
  36. {justhtml-2.4.0 → justhtml-2.4.1}/docs/fragments.md +0 -0
  37. {justhtml-2.4.0 → justhtml-2.4.1}/docs/html-cleaning.md +0 -0
  38. {justhtml-2.4.0 → justhtml-2.4.1}/docs/index.md +0 -0
  39. {justhtml-2.4.0 → justhtml-2.4.1}/docs/linkify.md +0 -0
  40. {justhtml-2.4.0 → justhtml-2.4.1}/docs/migration-examples.md +0 -0
  41. {justhtml-2.4.0 → justhtml-2.4.1}/docs/playground/app.js +0 -0
  42. {justhtml-2.4.0 → justhtml-2.4.1}/docs/playground/index.html +0 -0
  43. {justhtml-2.4.0 → justhtml-2.4.1}/docs/playground/py/__init__.py +0 -0
  44. {justhtml-2.4.0 → justhtml-2.4.1}/docs/playground/py/install_latest_justhtml.py +0 -0
  45. {justhtml-2.4.0 → justhtml-2.4.1}/docs/playground/py/render.py +0 -0
  46. {justhtml-2.4.0 → justhtml-2.4.1}/docs/playground/py/use_local_repo.py +0 -0
  47. {justhtml-2.4.0 → justhtml-2.4.1}/docs/quickstart.md +0 -0
  48. {justhtml-2.4.0 → justhtml-2.4.1}/docs/sanitization.md +0 -0
  49. {justhtml-2.4.0 → justhtml-2.4.1}/docs/selectors.md +0 -0
  50. {justhtml-2.4.0 → justhtml-2.4.1}/docs/streaming.md +0 -0
  51. {justhtml-2.4.0 → justhtml-2.4.1}/docs/text.md +0 -0
  52. {justhtml-2.4.0 → justhtml-2.4.1}/docs/transforms.md +0 -0
  53. {justhtml-2.4.0 → justhtml-2.4.1}/docs/unsafe-handling.md +0 -0
  54. {justhtml-2.4.0 → justhtml-2.4.1}/docs/url-cleaning.md +0 -0
  55. {justhtml-2.4.0 → justhtml-2.4.1}/llms.txt +0 -0
  56. {justhtml-2.4.0 → justhtml-2.4.1}/run_tests.py +0 -0
  57. {justhtml-2.4.0 → justhtml-2.4.1}/scripts/release.py +0 -0
  58. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/__init__.py +0 -0
  59. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/__main__.py +0 -0
  60. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/core/__init__.py +0 -0
  61. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/core/constants.py +0 -0
  62. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/core/entities.py +0 -0
  63. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/core/errors.py +0 -0
  64. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/core/rawtext.py +0 -0
  65. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/dom/__init__.py +0 -0
  66. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/dom/builder.py +0 -0
  67. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/parser/__init__.py +0 -0
  68. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/parser/context.py +0 -0
  69. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/parser/encoding.py +0 -0
  70. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/parser/stream.py +0 -0
  71. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/py.typed +0 -0
  72. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/__init__.py +0 -0
  73. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/css.py +0 -0
  74. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/dom.py +0 -0
  75. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/policy.py +0 -0
  76. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/policy_defaults.py +0 -0
  77. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/rawtext.py +0 -0
  78. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/url/__init__.py +0 -0
  79. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/url/policy.py +0 -0
  80. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/url/runtime.py +0 -0
  81. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/sanitizer/url/spec.py +0 -0
  82. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/selector/__init__.py +0 -0
  83. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/selector/core.py +0 -0
  84. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/serializer/__init__.py +0 -0
  85. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/serializer/html.py +0 -0
  86. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/serializer/markdown.py +0 -0
  87. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/tokenizer/__init__.py +0 -0
  88. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/tokenizer/html.py +0 -0
  89. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/tokenizer/tokens.py +0 -0
  90. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/transforms/__init__.py +0 -0
  91. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/transforms/compile.py +0 -0
  92. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/transforms/linkify.py +0 -0
  93. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/transforms/linkify_core.py +0 -0
  94. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/transforms/runtime.py +0 -0
  95. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/transforms/spec.py +0 -0
  96. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/treebuilder/__init__.py +0 -0
  97. {justhtml-2.4.0 → justhtml-2.4.1}/src/justhtml/treebuilder/utils.py +0 -0
  98. {justhtml-2.4.0 → justhtml-2.4.1}/test-summary.txt +0 -0
  99. {justhtml-2.4.0 → justhtml-2.4.1}/tests/README.md +0 -0
  100. {justhtml-2.4.0 → justhtml-2.4.1}/tests/__init__.py +0 -0
  101. {justhtml-2.4.0 → justhtml-2.4.1}/tests/data/wikipedia.html +0 -0
  102. {justhtml-2.4.0 → justhtml-2.4.1}/tests/harness/__init__.py +0 -0
  103. {justhtml-2.4.0 → justhtml-2.4.1}/tests/harness/encoding.py +0 -0
  104. {justhtml-2.4.0 → justhtml-2.4.1}/tests/harness/regressions.py +0 -0
  105. {justhtml-2.4.0 → justhtml-2.4.1}/tests/harness/reporter.py +0 -0
  106. {justhtml-2.4.0 → justhtml-2.4.1}/tests/harness/serializer.py +0 -0
  107. {justhtml-2.4.0 → justhtml-2.4.1}/tests/harness/tokenizer.py +0 -0
  108. {justhtml-2.4.0 → justhtml-2.4.1}/tests/harness/tree.py +0 -0
  109. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-sanitize-tests/cases.json +0 -0
  110. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/coverage_gaps.test +0 -0
  111. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/empty_stack_edge_cases.dat +0 -0
  112. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/entities.test +0 -0
  113. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
  114. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/tokenizer_edge_cases.test +0 -0
  115. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/xml_coercion.dat +0 -0
  116. {justhtml-2.4.0 → justhtml-2.4.1}/tests/justhtml-tests/xml_coercion_coverage.test +0 -0
  117. {justhtml-2.4.0 → justhtml-2.4.1}/tests/linkify-it/LICENSE.txt +0 -0
  118. {justhtml-2.4.0 → justhtml-2.4.1}/tests/linkify-it/README.md +0 -0
  119. {justhtml-2.4.0 → justhtml-2.4.1}/tests/linkify-it/fixtures/links.txt +0 -0
  120. {justhtml-2.4.0 → justhtml-2.4.1}/tests/linkify-it/fixtures/not_links.txt +0 -0
  121. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_builder.py +0 -0
  122. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_cli.py +0 -0
  123. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_docs_examples.py +0 -0
  124. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_encoding.py +0 -0
  125. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_errors.py +0 -0
  126. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_linkify_internals.py +0 -0
  127. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_linkify_it.py +0 -0
  128. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_linkify_transform.py +0 -0
  129. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_node.py +0 -0
  130. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_playground_local_repo_file_list.py +0 -0
  131. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_sanitize.py +0 -0
  132. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_sanitize_integration.py +0 -0
  133. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_sanitize_transform.py +0 -0
  134. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_selector.py +0 -0
  135. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_serialize.py +0 -0
  136. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_stream.py +0 -0
  137. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_tokenizer.py +0 -0
  138. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_transforms.py +0 -0
  139. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_transforms_compiler.py +0 -0
  140. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_transforms_edge_cases.py +0 -0
  141. {justhtml-2.4.0 → justhtml-2.4.1}/tests/test_transforms_sanitize_integration.py +0 -0
  142. {justhtml-2.4.0 → justhtml-2.4.1}/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
+ ## [2.4.1] - 2026-06-21
11
+
12
+ ### Fixed
13
+ - Prevent the leading-line-feed exception for `<pre>`, `<listing>`, and `<textarea>` from leaking past intervening non-character tokens and removing a later newline.
14
+ - Apply paragraph closing and leading-line-feed handling to `<pre>` and `<listing>` inside customizable `<select>` content.
15
+ - Preserve U+000C FORM FEED in HTML text like browsers do, while retaining its intentional conversion to a space in XML-coercion mode.
16
+
10
17
  ## [2.4.0] - 2026-06-20
11
18
 
12
19
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: justhtml
3
- Version: 2.4.0
3
+ Version: 2.4.1
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
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "justhtml"
3
3
  authors = [{ name = "Emil Stenström", email = "emil@emilstenstrom.se" }]
4
- version = "2.4.0"
4
+ version = "2.4.1"
5
5
  description = "A pure Python HTML5 parser that just works."
6
6
  readme = "README.md"
7
7
  license = { file = "LICENSE" }
@@ -411,6 +411,11 @@ class TreeBuilder(TreeBuilderModesMixin):
411
411
  def process_token(self, token: Any) -> Any:
412
412
  # Optimization: Use type() identity check instead of isinstance
413
413
  token_type = type(token)
414
+ # The leading-LF exception for <pre>, <listing>, and <textarea>
415
+ # applies only to the immediately following token. Character tokens
416
+ # consume it in _append_text; any other token cancels it.
417
+ if self.ignore_lf and token_type is not CharacterTokens:
418
+ self.ignore_lf = False
414
419
  if token_type is DoctypeToken:
415
420
  # Check for foreign content first - DOCTYPE in SVG/MathML is a parse error
416
421
  if self.open_elements:
@@ -668,9 +673,6 @@ class TreeBuilder(TreeBuilderModesMixin):
668
673
  if not text:
669
674
  return
670
675
 
671
- if "\f" in text:
672
- text = text.replace("\f", " ")
673
-
674
676
  # Guard against empty stack
675
677
  if not self.open_elements: # pragma: no cover
676
678
  return
@@ -1677,6 +1677,10 @@ class TreeBuilderModesMixin:
1677
1677
  self.ignore_lf = True
1678
1678
  self.frameset_ok = False
1679
1679
  return None
1680
+ if name in {"pre", "listing"}:
1681
+ self._parse_error("unexpected-start-tag-in-select", tag_name=name)
1682
+ self._handle_body_start_pre_listing(token)
1683
+ return None
1680
1684
  # Any other start tag is part of customizable select content.
1681
1685
  self._parse_error("unexpected-start-tag-in-select", tag_name=name)
1682
1686
  self._reconstruct_active_formatting_elements()
@@ -24,7 +24,7 @@
24
24
  | <body>
25
25
  | <math math>
26
26
  | <math mi>
27
- | " "
27
+ | " "
28
28
 
29
29
 
30
30
  #data
@@ -21,7 +21,7 @@
21
21
  | <body>
22
22
  | <math math>
23
23
  | <math mi>
24
- | " text"
24
+ | " text"
25
25
 
26
26
 
27
27
  #data
@@ -75,7 +75,7 @@
75
75
  | <head>
76
76
  | <body>
77
77
  | <select>
78
- | " text"
78
+ | " text"
79
79
 
80
80
 
81
81
  #data
@@ -175,7 +175,7 @@
175
175
  | <head>
176
176
  | <body>
177
177
  | <table>
178
- | " "
178
+ | " "
179
179
 
180
180
 
181
181
  #data
@@ -250,6 +250,63 @@ class TestTreeBuilder(unittest.TestCase):
250
250
  [error.code for error in doc.errors],
251
251
  )
252
252
 
253
+ def test_leading_lf_skip_does_not_leak_past_non_character_tokens(self) -> None:
254
+ cases = {
255
+ "<div><pre></pre>\nX</div>": "<div><pre></pre>\nX</div>",
256
+ "<div><pre><code></code></pre>\nX</div>": "<div><pre><code></code></pre>\nX</div>",
257
+ "<div><pre><!--x--></pre>\nX</div>": "<div><pre><!--x--></pre>\nX</div>",
258
+ "<div><listing></listing>\nX</div>": "<div><listing></listing>\nX</div>",
259
+ "<div><textarea></textarea>\nX</div>": "<div><textarea></textarea>\nX</div>",
260
+ "<div><pre>\nX</pre></div>": "<div><pre>X</pre></div>",
261
+ "<div><listing>\nX</listing></div>": "<div><listing>X</listing></div>",
262
+ "<div><textarea>\nX</textarea></div>": "<div><textarea>X</textarea></div>",
263
+ }
264
+
265
+ for html, expected in cases.items():
266
+ with self.subTest(html=html):
267
+ doc = JustHTML(html, fragment=True, sanitize=False)
268
+ self.assertEqual(doc.to_html(pretty=False), expected)
269
+
270
+ def test_pre_and_listing_in_select_use_their_in_body_start_rules(self) -> None:
271
+ cases = {
272
+ "<select><pre>\nX</pre></select>": "<select><pre>X</pre></select>",
273
+ "<select><listing>\nX</listing></select>": "<select><listing>X</listing></select>",
274
+ "<select><p>P<pre>\nX</pre></select>": "<select><p>P</p><pre>X</pre></select>",
275
+ "<select><p>P<listing>\nX</listing></select>": "<select><p>P</p><listing>X</listing></select>",
276
+ "<select><pre><code></code>\nX</pre></select>": ("<select><pre><code></code>\nX</pre></select>"),
277
+ }
278
+
279
+ for html, expected in cases.items():
280
+ with self.subTest(html=html):
281
+ doc = JustHTML(html, fragment=True, sanitize=False)
282
+ self.assertEqual(doc.to_html(pretty=False), expected)
283
+
284
+ def test_form_feed_is_preserved_in_html_text_like_chromium(self) -> None:
285
+ cases = {
286
+ "<div>A\fB</div>": "<div>A\fB</div>",
287
+ "<pre>A\fB</pre>": "<pre>A\fB</pre>",
288
+ "<textarea>A\fB</textarea>": "<textarea>A\fB</textarea>",
289
+ "<style>A\fB</style>": "<style>A\fB</style>",
290
+ "<script>A\fB</script>": "<script>A\fB</script>",
291
+ "<table>A\fB<tr><td>C\fD</td></tr></table>": ("A\fB<table><tbody><tr><td>C\fD</td></tr></tbody></table>"),
292
+ "<svg><text>A\fB</text></svg>": "<svg><text>A\fB</text></svg>",
293
+ }
294
+
295
+ for html, expected in cases.items():
296
+ with self.subTest(html=html):
297
+ doc = JustHTML(html, fragment=True, sanitize=False)
298
+ self.assertEqual(doc.to_html(pretty=False), expected)
299
+
300
+ def test_form_feed_is_still_coerced_for_xml_output(self) -> None:
301
+ doc = JustHTML(
302
+ "<div>A\fB</div>",
303
+ fragment=True,
304
+ sanitize=False,
305
+ _tokenizer_opts=TokenizerOpts(xml_coercion=True),
306
+ )
307
+
308
+ self.assertEqual(doc.to_html(pretty=False), "<div>A B</div>")
309
+
253
310
  def test_select_end_tags_create_p_and_close_custom_children_like_chromium(self) -> None:
254
311
  cases = {
255
312
  "<select>x</p>": "<select>x<p></p></select>",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes