justhtml 3.6.1__tar.gz → 3.7.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 (158) hide show
  1. {justhtml-3.6.1 → justhtml-3.7.0}/CHANGELOG.md +10 -1
  2. {justhtml-3.6.1 → justhtml-3.7.0}/PKG-INFO +2 -2
  3. {justhtml-3.6.1 → justhtml-3.7.0}/benchmarks/correctness.py +1 -1
  4. {justhtml-3.6.1 → justhtml-3.7.0}/docs/comparison.md +9 -9
  5. {justhtml-3.6.1 → justhtml-3.7.0}/pyproject.toml +2 -2
  6. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/parser/engine.py +0 -24
  7. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_parser_engine.py +7 -0
  8. {justhtml-3.6.1 → justhtml-3.7.0}/.github/copilot-instructions.md +0 -0
  9. {justhtml-3.6.1 → justhtml-3.7.0}/.github/workflows/ci.yml +0 -0
  10. {justhtml-3.6.1 → justhtml-3.7.0}/.github/workflows/publish.yml +0 -0
  11. {justhtml-3.6.1 → justhtml-3.7.0}/.gitignore +0 -0
  12. {justhtml-3.6.1 → justhtml-3.7.0}/.pre-commit-config.yaml +0 -0
  13. {justhtml-3.6.1 → justhtml-3.7.0}/AGENTS.md +0 -0
  14. {justhtml-3.6.1 → justhtml-3.7.0}/CODE_OF_CONDUCT.md +0 -0
  15. {justhtml-3.6.1 → justhtml-3.7.0}/CONTRIBUTING.md +0 -0
  16. {justhtml-3.6.1 → justhtml-3.7.0}/LICENSE +0 -0
  17. {justhtml-3.6.1 → justhtml-3.7.0}/Makefile +0 -0
  18. {justhtml-3.6.1 → justhtml-3.7.0}/PERFORMANCE.md +0 -0
  19. {justhtml-3.6.1 → justhtml-3.7.0}/README.md +0 -0
  20. {justhtml-3.6.1 → justhtml-3.7.0}/SECURITY.md +0 -0
  21. {justhtml-3.6.1 → justhtml-3.7.0}/assets/justhtml-readme-explainer.png +0 -0
  22. {justhtml-3.6.1 → justhtml-3.7.0}/benchmarks/chromium_parser_diff.py +0 -0
  23. {justhtml-3.6.1 → justhtml-3.7.0}/benchmarks/fuzz.py +0 -0
  24. {justhtml-3.6.1 → justhtml-3.7.0}/benchmarks/html5lib_engine_diff.py +0 -0
  25. {justhtml-3.6.1 → justhtml-3.7.0}/benchmarks/parser_engine_gate.py +0 -0
  26. {justhtml-3.6.1 → justhtml-3.7.0}/benchmarks/performance.py +0 -0
  27. {justhtml-3.6.1 → justhtml-3.7.0}/benchmarks/profile.py +0 -0
  28. {justhtml-3.6.1 → justhtml-3.7.0}/docs/_config.yml +0 -0
  29. {justhtml-3.6.1 → justhtml-3.7.0}/docs/_layouts/default.html +0 -0
  30. {justhtml-3.6.1 → justhtml-3.7.0}/docs/api.md +0 -0
  31. {justhtml-3.6.1 → justhtml-3.7.0}/docs/assets/search.js +0 -0
  32. {justhtml-3.6.1 → justhtml-3.7.0}/docs/bleach-migration.md +0 -0
  33. {justhtml-3.6.1 → justhtml-3.7.0}/docs/building.md +0 -0
  34. {justhtml-3.6.1 → justhtml-3.7.0}/docs/cli.md +0 -0
  35. {justhtml-3.6.1 → justhtml-3.7.0}/docs/correctness.md +0 -0
  36. {justhtml-3.6.1 → justhtml-3.7.0}/docs/encoding.md +0 -0
  37. {justhtml-3.6.1 → justhtml-3.7.0}/docs/errors.md +0 -0
  38. {justhtml-3.6.1 → justhtml-3.7.0}/docs/fragments.md +0 -0
  39. {justhtml-3.6.1 → justhtml-3.7.0}/docs/html-cleaning.md +0 -0
  40. {justhtml-3.6.1 → justhtml-3.7.0}/docs/index.md +0 -0
  41. {justhtml-3.6.1 → justhtml-3.7.0}/docs/linkify.md +0 -0
  42. {justhtml-3.6.1 → justhtml-3.7.0}/docs/migration-examples.md +0 -0
  43. {justhtml-3.6.1 → justhtml-3.7.0}/docs/playground/app.js +0 -0
  44. {justhtml-3.6.1 → justhtml-3.7.0}/docs/playground/index.html +0 -0
  45. {justhtml-3.6.1 → justhtml-3.7.0}/docs/playground/py/__init__.py +0 -0
  46. {justhtml-3.6.1 → justhtml-3.7.0}/docs/playground/py/install_latest_justhtml.py +0 -0
  47. {justhtml-3.6.1 → justhtml-3.7.0}/docs/playground/py/render.py +0 -0
  48. {justhtml-3.6.1 → justhtml-3.7.0}/docs/playground/py/use_local_repo.py +0 -0
  49. {justhtml-3.6.1 → justhtml-3.7.0}/docs/quickstart.md +0 -0
  50. {justhtml-3.6.1 → justhtml-3.7.0}/docs/sanitization.md +0 -0
  51. {justhtml-3.6.1 → justhtml-3.7.0}/docs/selectors.md +0 -0
  52. {justhtml-3.6.1 → justhtml-3.7.0}/docs/streaming.md +0 -0
  53. {justhtml-3.6.1 → justhtml-3.7.0}/docs/text.md +0 -0
  54. {justhtml-3.6.1 → justhtml-3.7.0}/docs/transforms.md +0 -0
  55. {justhtml-3.6.1 → justhtml-3.7.0}/docs/unsafe-handling.md +0 -0
  56. {justhtml-3.6.1 → justhtml-3.7.0}/docs/url-cleaning.md +0 -0
  57. {justhtml-3.6.1 → justhtml-3.7.0}/llms.txt +0 -0
  58. {justhtml-3.6.1 → justhtml-3.7.0}/project_ideas.md +0 -0
  59. {justhtml-3.6.1 → justhtml-3.7.0}/run_tests.py +0 -0
  60. {justhtml-3.6.1 → justhtml-3.7.0}/scripts/release.py +0 -0
  61. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/__init__.py +0 -0
  62. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/__main__.py +0 -0
  63. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/__init__.py +0 -0
  64. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/constants.py +0 -0
  65. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/doctype.py +0 -0
  66. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/entities.py +0 -0
  67. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/errors.py +0 -0
  68. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/rawtext.py +0 -0
  69. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/text.py +0 -0
  70. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/core/types.py +0 -0
  71. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/dom/__init__.py +0 -0
  72. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/dom/builder.py +0 -0
  73. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/parser/__init__.py +0 -0
  74. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/parser/context.py +0 -0
  75. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/parser/encoding.py +0 -0
  76. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/parser/options.py +0 -0
  77. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/parser/scanner.py +0 -0
  78. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/parser/stream.py +0 -0
  79. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/py.typed +0 -0
  80. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/__init__.py +0 -0
  81. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/css.py +0 -0
  82. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/dom.py +0 -0
  83. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/policy.py +0 -0
  84. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
  85. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/rawtext.py +0 -0
  86. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
  87. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/url/policy.py +0 -0
  88. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
  89. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/sanitizer/url/spec.py +0 -0
  90. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/selector/__init__.py +0 -0
  91. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/selector/core.py +0 -0
  92. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/serializer/__init__.py +0 -0
  93. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/serializer/html.py +0 -0
  94. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/serializer/markdown.py +0 -0
  95. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/transforms/__init__.py +0 -0
  96. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/transforms/compile.py +0 -0
  97. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/transforms/linkify.py +0 -0
  98. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/transforms/linkify_core.py +0 -0
  99. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/transforms/runtime.py +0 -0
  100. {justhtml-3.6.1 → justhtml-3.7.0}/src/justhtml/transforms/spec.py +0 -0
  101. {justhtml-3.6.1 → justhtml-3.7.0}/test-summary.txt +0 -0
  102. {justhtml-3.6.1 → justhtml-3.7.0}/tests/README.md +0 -0
  103. {justhtml-3.6.1 → justhtml-3.7.0}/tests/__init__.py +0 -0
  104. {justhtml-3.6.1 → justhtml-3.7.0}/tests/data/wikipedia.html +0 -0
  105. {justhtml-3.6.1 → justhtml-3.7.0}/tests/harness/__init__.py +0 -0
  106. {justhtml-3.6.1 → justhtml-3.7.0}/tests/harness/encoding.py +0 -0
  107. {justhtml-3.6.1 → justhtml-3.7.0}/tests/harness/regressions.py +0 -0
  108. {justhtml-3.6.1 → justhtml-3.7.0}/tests/harness/reporter.py +0 -0
  109. {justhtml-3.6.1 → justhtml-3.7.0}/tests/harness/serializer.py +0 -0
  110. {justhtml-3.6.1 → justhtml-3.7.0}/tests/harness/tree.py +0 -0
  111. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
  112. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/character_preprocessing.dat +0 -0
  113. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/document_shell_recovery.dat +0 -0
  114. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/entities.test +0 -0
  115. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/foreign_content.dat +0 -0
  116. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
  117. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/frameset_recovery.dat +0 -0
  118. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/head_noscript_recovery.dat +0 -0
  119. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
  120. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/in_body_recovery.dat +0 -0
  121. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/raw_text_recovery.dat +0 -0
  122. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/select_recovery.dat +0 -0
  123. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/selectedcontent.dat +0 -0
  124. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/table_recovery.dat +0 -0
  125. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/template_recovery.dat +0 -0
  126. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/tokenizer_recovery.dat +0 -0
  127. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
  128. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/tokenizer_states.test +0 -0
  129. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
  130. {justhtml-3.6.1 → justhtml-3.7.0}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
  131. {justhtml-3.6.1 → justhtml-3.7.0}/tests/linkify-it/LICENSE.txt +0 -0
  132. {justhtml-3.6.1 → justhtml-3.7.0}/tests/linkify-it/README.md +0 -0
  133. {justhtml-3.6.1 → justhtml-3.7.0}/tests/linkify-it/fixtures/links.txt +0 -0
  134. {justhtml-3.6.1 → justhtml-3.7.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
  135. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_builder.py +0 -0
  136. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_cli.py +0 -0
  137. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_docs_examples.py +0 -0
  138. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_encoding.py +0 -0
  139. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_entities.py +0 -0
  140. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_errors.py +0 -0
  141. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_linkify_internals.py +0 -0
  142. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_linkify_it.py +0 -0
  143. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_linkify_transform.py +0 -0
  144. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_node.py +0 -0
  145. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_parser_regressions.py +0 -0
  146. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_parser_tree.py +0 -0
  147. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_playground_local_repo_file_list.py +0 -0
  148. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_sanitize.py +0 -0
  149. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_sanitize_integration.py +0 -0
  150. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_sanitize_transform.py +0 -0
  151. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_selector.py +0 -0
  152. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_serialize.py +0 -0
  153. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_stream.py +0 -0
  154. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_transform_runtime.py +0 -0
  155. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_transforms.py +0 -0
  156. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_transforms_compiler.py +0 -0
  157. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_transforms_sanitize_integration.py +0 -0
  158. {justhtml-3.6.1 → justhtml-3.7.0}/tests/test_wikipedia.py +0 -0
@@ -5,7 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [3.7.0] - 2026-07-15
9
+
10
+ ### Fixed
11
+
12
+ - Keep templates open in the head when they contain a processing instruction, matching the latest web-platform-tests tree-construction fixture.
13
+
14
+ ### Changed
15
+
16
+ - Refresh the web-platform-tests tree-construction fixtures and comparison scores.
17
+ - Make the correctness benchmark skip an optional `html5-parser` installation when its libxml2 ABI conflicts with `lxml`.
9
18
 
10
19
  ## [3.6.1] - 2026-07-13
11
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: justhtml
3
- Version: 3.6.1
3
+ Version: 3.7.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
@@ -40,7 +40,7 @@ Requires-Dist: html5-parser; extra == 'benchmark'
40
40
  Requires-Dist: html5lib; extra == 'benchmark'
41
41
  Requires-Dist: lxml; extra == 'benchmark'
42
42
  Requires-Dist: markupever; extra == 'benchmark'
43
- Requires-Dist: selectolax>=0.4.8; extra == 'benchmark'
43
+ Requires-Dist: selectolax>=0.4.11; extra == 'benchmark'
44
44
  Requires-Dist: turbohtml>=1.3.1; extra == 'benchmark'
45
45
  Requires-Dist: zstandard; extra == 'benchmark'
46
46
  Provides-Extra: dev
@@ -148,7 +148,7 @@ def check_parser_available(parser_name):
148
148
  import html5_parser # noqa: F401
149
149
 
150
150
  return True
151
- except ImportError:
151
+ except (ImportError, RuntimeError):
152
152
  return False
153
153
  if parser_name == "markupever":
154
154
  try:
@@ -11,22 +11,22 @@ Use a different tool when one narrow requirement matters more than the whole pip
11
11
  | Tool | HTML5 parsing [1] | Speed | Query | Build | Sanitize | Notes |
12
12
  |------|------------------------------------------|-------|----------|-------|------------------|-------|
13
13
  | **JustHTML**<br>Pure Python | ✅ 100% | ⚡ Fast | ✅ CSS selectors | ✅ `element()` | ✅ Built-in | Correct, secure, easy to install, and fast enough. |
14
- | **`selectolax`**<br>Python wrapper of C-based Lexbor | 🟡 95.4% | 🚀 Very Fast | ✅ CSS selectors | ✅ `create_node()` | ❌ Needs sanitization | Very fast. |
15
- | **Chromium**<br>browser engine | 🟡 95.1% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
16
- | **`turbohtml`**<br>Python wrapper of a C core | 🟡 94.3% | 🚀 Very Fast | ✅ CSS selectors, XPath | ✅ `E.*` builder | ✅ Built-in | Broad, compiled alternative with parsing, querying, and sanitization. |
17
- | **WebKit**<br>browser engine | 🟡 94.0% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
18
- | **Firefox**<br>browser engine | 🟡 93.2% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
14
+ | **`selectolax`**<br>Python wrapper of C-based Lexbor | ✅ 100% | 🚀 Very Fast | ✅ CSS selectors | ✅ `create_node()` | ❌ Needs sanitization | Very fast. |
15
+ | **Chromium**<br>browser engine | 🟡 95.0% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
16
+ | **`turbohtml`**<br>Python wrapper of a C core | 🟡 94.2% | 🚀 Very Fast | ✅ CSS selectors, XPath | ✅ `E.*` builder | ✅ Built-in | Broad, compiled alternative with parsing, querying, and sanitization. |
17
+ | **WebKit**<br>browser engine | 🟡 93.9% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
18
+ | **Firefox**<br>browser engine | 🟡 93.1% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
19
19
  | **`html5lib`**<br>Pure Python | 🟡 82.3% | 🐢 Slow | 🟡 XPath (lxml) | 🟡 Tree API | 🔴 [Deprecated](https://github.com/html5lib/html5lib-python/issues/443) | Unmaintained reference implementation; incomplete coverage of the tree-construction fixtures. |
20
- | **`markupever`**<br>Python wrapper of Rust-based html5ever | 🟡 79.4% | 🚀 Very Fast | ✅ CSS selectors | ✅ `TreeDom .create_*()` | ❌ Needs sanitization | Fast, but 105 fixture cases abort its current parser process. |
20
+ | **`markupever`**<br>Python wrapper of Rust-based html5ever | 🟡 79.4% | 🚀 Very Fast | ✅ CSS selectors | ✅ `TreeDom .create_*()` | ❌ Needs sanitization | Fast, but many fixtures cases abort its current parser process. |
21
21
  | **`html5_parser`**<br>Python wrapper of C-based Gumbo | 🔴 47.8% | 🚀 Very Fast | 🟡 XPath (lxml) | 🟡 `etree` (lxml) | ❌ Needs sanitization | Fast, but its public tree API loses information needed by many fixtures. |
22
22
  | **`BeautifulSoup`**<br>Pure Python | 🔴 0.3% (default) | 🐢 Slow | 🟡 Custom API | ✅ `new_tag()` API | ❌ Needs sanitization | Wraps `html.parser` (default). Can use lxml or html5lib. |
23
23
  | **`html.parser`**<br>Python stdlib | 🔴 0.3% | ⚡ Fast | ❌ None | ❌ None | ❌ Needs sanitization | Standard library. Chokes on malformed HTML. |
24
- | **`lxml`**<br>Python wrapper of C-based libxml2 | 🔴 0.3% | 🚀 Very Fast | 🟡 XPath | ✅ `etree` / E-factory | ❌ Needs sanitization | Fast but not HTML5 compliant. Context-fragment cases are skipped; supported cases still perform poorly. Don't use the old lxml.html.clean module! |
24
+ | **`lxml`**<br>Python wrapper of C-based libxml2 | 🔴 0.9% | 🚀 Very Fast | 🟡 XPath | ✅ `etree` / E-factory | ❌ Needs sanitization | Fast but not HTML5 compliant. Context-fragment cases are skipped; supported cases still perform poorly. Don't use the old lxml.html.clean module! |
25
25
 
26
- [1]: Correctness scores are counted against the [web-platform-tests tree-construction tests](https://github.com/web-platform-tests/wpt/tree/master/html/syntax/parsing/resources). They were run locally at [2026-07-11](https://github.com/web-platform-tests/wpt/commit/ba458f52c8a848bbd9c525ac98fb1f7b30e41dc2).
26
+ [1]: Library correctness scores are counted against the [web-platform-tests tree-construction tests](https://github.com/web-platform-tests/wpt/tree/master/html/syntax/parsing/resources). They were run locally at [2026-07-15](https://github.com/web-platform-tests/wpt/commit/4830edb033cb486fd0cd6f85b5e937cfc718704d) and cover 1,880 cases after excluding scripting-directed fixtures. JustHTML and Selectolax 0.4.11 passed all 1,880 cases; MarkupEver's score counts its 103 process-aborting cases as errors.
27
27
 
28
28
 
29
- [2]: Current local rerun with [`justhtml-html5lib-tests-bench`](https://github.com/EmilStenstrom/justhtml-html5lib-tests-bench) against WPT commit [`ba458f52`](https://github.com/web-platform-tests/wpt/commit/ba458f52c8a848bbd9c525ac98fb1f7b30e41dc2). The browser harness skips 12 `#script-on` cases but includes `#script-off` cases, so these scores are not directly comparable to the 1,879-case Python-parser scores above.
29
+ [2]: Current local rerun with [`justhtml-html5lib-tests-bench`](https://github.com/EmilStenstrom/justhtml-html5lib-tests-bench) against WPT commit [`4830edb`](https://github.com/web-platform-tests/wpt/commit/4830edb033cb486fd0cd6f85b5e937cfc718704d). Chromium 143.0.7499.4, WebKit 26.0, and Firefox 144.0.2 were compared against 1,908 cases; the harness skips 12 `#script-on` cases but includes `#script-off` cases, so these scores are not directly comparable to the 1,880-case Python-parser scores above.
30
30
 
31
31
  ## Why JustHTML
32
32
 
@@ -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.6.1"
4
+ version = "3.7.0"
5
5
  description = "A pure Python HTML5 parser that just works."
6
6
  readme = "README.md"
7
7
  license = { file = "LICENSE" }
@@ -18,7 +18,7 @@ benchmark = [
18
18
  "html5lib",
19
19
  "lxml",
20
20
  "beautifulsoup4",
21
- "selectolax>=0.4.8",
21
+ "selectolax>=0.4.11",
22
22
  "html5-parser",
23
23
  "markupever",
24
24
  "turbohtml>=1.3.1",
@@ -1253,32 +1253,8 @@ class ParseEngine:
1253
1253
  if self._has_null and "\0" in data:
1254
1254
  data = data.replace("\0", "\ufffd")
1255
1255
  node = ProcessingInstruction(data=data)
1256
- self._move_head_template_to_body_for_processing_instruction()
1257
1256
  self._append_misc_node(node, source_pos)
1258
1257
 
1259
- def _move_head_template_to_body_for_processing_instruction(self) -> None:
1260
- if self._fragment or self._head is None or self._html is None:
1261
- return
1262
- stack = self._stack
1263
- template_idx = self._open_template_index()
1264
- if template_idx is None:
1265
- return
1266
- outer_template_idx = template_idx
1267
- for idx in range(template_idx, 0, -1):
1268
- candidate = stack[idx]
1269
- if type(candidate) is Template and candidate.parent is self._head:
1270
- outer_template_idx = idx
1271
- template = stack[outer_template_idx]
1272
- if type(template) is not Template or template.parent is not self._head:
1273
- return
1274
- head_children = self._head.children
1275
- if head_children is not None: # pragma: no branch - Document head always owns a children list
1276
- head_children.remove(template)
1277
- self._append(self._body, template)
1278
- self._stack = _CountingStack([self._doc, self._html, self._body, *stack[outer_template_idx:]]) # type: ignore[list-item]
1279
- self._body_mode_seen = True
1280
- self._after_head = False
1281
-
1282
1258
  def _processing_instruction_data(self, content_start: int, content_end: int) -> str | None:
1283
1259
  html = self._html_input
1284
1260
  if content_start >= content_end or not _is_processing_instruction_name_start(html[content_start]):
@@ -14,6 +14,7 @@ from justhtml.parser.engine import (
14
14
  )
15
15
  from justhtml.parser.options import ParserOptions
16
16
  from justhtml.sanitizer import DEFAULT_DOCUMENT_POLICY, DEFAULT_POLICY, SanitizationPolicy, UrlPolicy, UrlRule
17
+ from justhtml.serializer import to_test_format
17
18
  from tests.harness.tree import TestRunner
18
19
 
19
20
 
@@ -48,6 +49,12 @@ class TestParserSyntaxAndRecovery(_ParserEngineTestCase):
48
49
  document = JustHTML("<?\ud83d\ude80\ud83dX\ud83d\ude80>", sanitize=False)
49
50
  assert document.root.children[0].data == "?🚀\ud83dX🚀"
50
51
 
52
+ def test_processing_instruction_keeps_open_head_template_in_head(self) -> None:
53
+ document = JustHTML("<template><?pi>", sanitize=False)
54
+ assert to_test_format(document.root) == (
55
+ "| <html>\n| <head>\n| <template>\n| content\n| <?pi ?>\n| <body>"
56
+ )
57
+
51
58
  def test_rawtext_processing_instruction_branch(self) -> None:
52
59
  script = Element("script", {}, "html")
53
60
  engine = ParseEngine("<?x>", fragment=True, plan=compile_raw_engine_plan(fragment=True))
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
File without changes
File without changes