justhtml 3.11.0__tar.gz → 3.11.2__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.
- {justhtml-3.11.0 → justhtml-3.11.2}/CHANGELOG.md +14 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/PKG-INFO +2 -2
- {justhtml-3.11.0 → justhtml-3.11.2}/benchmarks/correctness.py +27 -10
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/comparison.md +1 -1
- {justhtml-3.11.0 → justhtml-3.11.2}/pyproject.toml +1 -1
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/parser/engine.py +13 -6
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/serializer/html.py +36 -7
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_parser_engine.py +4 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_sanitize.py +35 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_serialize.py +10 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/.github/workflows/ci.yml +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/.github/workflows/publish.yml +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/.gitignore +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/.pre-commit-config.yaml +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/AGENTS.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/ARCHITECTURE.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/CONTRIBUTING.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/LICENSE +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/Makefile +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/PERFORMANCE.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/README.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/SECURITY.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/benchmarks/fuzz.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/benchmarks/performance.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/benchmarks/profile.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/_config.yml +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/_layouts/default.html +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/api.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/assets/search.js +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/bleach-migration.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/building.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/cli.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/correctness.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/encoding.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/errors.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/fragments.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/html-cleaning.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/index.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/linkify.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/migration-examples.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/playground/app.js +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/playground/index.html +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/playground/py/render.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/quickstart.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/sanitization.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/selectors.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/streaming.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/text.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/transforms.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/unsafe-handling.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/docs/url-cleaning.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/llms.txt +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/project_ideas.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/run_tests.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/scripts/release.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/__main__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/entities.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/text.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/core/types.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/parser/scanner.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/parser/stream.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/py.typed +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/transforms/runtime.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/test-summary.txt +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/README.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/data/wikipedia.html +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/harness/__init__.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/harness/encoding.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/harness/regressions.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/harness/reporter.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/harness/scaling.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/harness/serializer.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/harness/tree.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/character_preprocessing.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/document_shell_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/foreign_content.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/frameset_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/head_noscript_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/in_body_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/raw_text_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/select_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/selectedcontent.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/table_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/template_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/tokenizer_recovery.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/tokenizer_states.test +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/linkify-it/README.md +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_builder.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_cli.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_docs_examples.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_encoding.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_entities.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_errors.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_linkify_it.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_node.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_parser_regressions.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_parser_tree.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_sanitize_integration.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_selector.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_stream.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_transform_runtime.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_transforms.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.11.0 → justhtml-3.11.2}/tests/test_wikipedia.py +0 -0
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.11.2] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Preserve content following closed `script` or `style` elements in dropped SVG and MathML subtrees when parsing
|
|
15
|
+
fragments with a custom sanitization policy.
|
|
16
|
+
- WPT update: Foster invalid row content correctly when a disallowed template appears inside a table.
|
|
17
|
+
|
|
18
|
+
## [3.11.1] - 2026-08-16
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Preserve rendered word separation when pretty HTML serialization normalizes a newline next to an inline element.
|
|
23
|
+
|
|
10
24
|
## [3.11.0] - 2026-08-07
|
|
11
25
|
|
|
12
26
|
### Performance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: justhtml
|
|
3
|
-
Version: 3.11.
|
|
3
|
+
Version: 3.11.2
|
|
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
|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
from dataclasses import dataclass
|
|
17
17
|
from enum import Enum
|
|
18
18
|
from pathlib import Path
|
|
19
|
+
from typing import Final
|
|
19
20
|
|
|
20
21
|
from justhtml import JustHTML
|
|
21
22
|
from justhtml.parser.context import FragmentContext
|
|
@@ -577,6 +578,19 @@ NS_MATHML = "http://www.w3.org/1998/Math/MathML"
|
|
|
577
578
|
NS_XLINK = "http://www.w3.org/1999/xlink"
|
|
578
579
|
NS_XML = "http://www.w3.org/XML/1998/namespace"
|
|
579
580
|
NS_XMLNS = "http://www.w3.org/2000/xmlns/"
|
|
581
|
+
_TURBOHTML_FOREIGN_ATTRIBUTE_NAMES: Final[dict[str, str]] = {
|
|
582
|
+
"xlink:actuate": "xlink actuate",
|
|
583
|
+
"xlink:arcrole": "xlink arcrole",
|
|
584
|
+
"xlink:href": "xlink href",
|
|
585
|
+
"xlink:role": "xlink role",
|
|
586
|
+
"xlink:show": "xlink show",
|
|
587
|
+
"xlink:title": "xlink title",
|
|
588
|
+
"xlink:type": "xlink type",
|
|
589
|
+
"xml:lang": "xml lang",
|
|
590
|
+
"xml:space": "xml space",
|
|
591
|
+
"xmlns": "xmlns xmlns",
|
|
592
|
+
"xmlns:xlink": "xmlns xlink",
|
|
593
|
+
}
|
|
580
594
|
|
|
581
595
|
|
|
582
596
|
def _serialize_lxml_document_siblings(root, etree):
|
|
@@ -914,14 +928,15 @@ def _turbohtml_to_test_format(nodes, indent):
|
|
|
914
928
|
return f"math {name}"
|
|
915
929
|
return name
|
|
916
930
|
|
|
917
|
-
def attribute_name(name):
|
|
918
|
-
if
|
|
919
|
-
return
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
931
|
+
def attribute_name(namespace, name):
|
|
932
|
+
if namespace not in {Namespace.SVG, Namespace.MATHML}:
|
|
933
|
+
return name
|
|
934
|
+
return _TURBOHTML_FOREIGN_ATTRIBUTE_NAMES.get(name, name)
|
|
935
|
+
|
|
936
|
+
def attribute_value(value):
|
|
937
|
+
if isinstance(value, list):
|
|
938
|
+
return " ".join(value)
|
|
939
|
+
return value or ""
|
|
925
940
|
|
|
926
941
|
def process(node, node_indent):
|
|
927
942
|
prefix = " " * node_indent
|
|
@@ -939,8 +954,10 @@ def _turbohtml_to_test_format(nodes, indent):
|
|
|
939
954
|
return [line for child in node.children for line in process(child, node_indent)]
|
|
940
955
|
|
|
941
956
|
lines = [f"| {prefix}<{qualified_name(node.namespace, node.tag)}>"]
|
|
942
|
-
for name, value in sorted(
|
|
943
|
-
|
|
957
|
+
for name, value in sorted(
|
|
958
|
+
(attribute_name(node.namespace, name), attribute_value(value)) for name, value in node.attrs.items()
|
|
959
|
+
):
|
|
960
|
+
lines.append(f'| {prefix} {name}="{value}"')
|
|
944
961
|
|
|
945
962
|
if node.tag == "template" and node.namespace is Namespace.HTML:
|
|
946
963
|
lines.append(f"| {prefix} content")
|
|
@@ -13,7 +13,7 @@ Use a different tool when one narrow requirement matters more than the whole pip
|
|
|
13
13
|
| **JustHTML**<br>Pure Python | ✅ 100% | ⚡ Fast | ✅ CSS selectors | ✅ `element()` | ✅ Built-in | Correct, secure, easy to install, and fast enough. |
|
|
14
14
|
| **`selectolax`**<br>Python wrapper of C-based Lexbor | ✅ 100% | 🚀 Very Fast | ✅ CSS selectors | ✅ `create_node()` | ❌ Needs sanitization | Very fast. |
|
|
15
15
|
| **Chromium**<br>browser engine | 🟡 95.0% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
|
|
16
|
-
| **`turbohtml`**<br>Python wrapper of a C core | 🟡 94.
|
|
16
|
+
| **`turbohtml`**<br>Python wrapper of a C core | 🟡 94.8% | 🚀 Very Fast | ✅ CSS selectors, XPath | ✅ `E.*` builder | ✅ Built-in | Broad, compiled alternative with parsing, querying, and sanitization. |
|
|
17
17
|
| **WebKit**<br>browser engine | 🟡 93.9% [2] | 🚀 Very Fast | — | — | — | Current browser-harness result. |
|
|
18
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. |
|
|
@@ -1435,8 +1435,7 @@ class ParseEngine:
|
|
|
1435
1435
|
# rules, which may splice a form or run adoption, rather than
|
|
1436
1436
|
# popping the foreign elements sitting above it.
|
|
1437
1437
|
return False
|
|
1438
|
-
|
|
1439
|
-
self._set_end_span(node, name, tag_start, tag_end)
|
|
1438
|
+
self._set_end_span(node, name, tag_start, tag_end)
|
|
1440
1439
|
del stack[target_index:]
|
|
1441
1440
|
return True
|
|
1442
1441
|
|
|
@@ -5250,14 +5249,22 @@ class ParseEngine:
|
|
|
5250
5249
|
): # pragma: no branch - opposite edge requires invalid parser state
|
|
5251
5250
|
return None # pragma: no cover - unreachable after parser-state guards
|
|
5252
5251
|
table_idx = self._find_open_index("table")
|
|
5253
|
-
# Fostering runs once per node placed inside a table, and no template is
|
|
5254
|
-
# open for the overwhelming majority of them. Both counters below are
|
|
5255
|
-
# maintained anyway, so consulting them skips two stack lookups.
|
|
5256
5252
|
parser_only_template_idx = (
|
|
5257
5253
|
self._open_parser_only_template_index() if self._parser_only_template_depth else None
|
|
5258
5254
|
)
|
|
5259
|
-
|
|
5255
|
+
# A parser-only template projects direct children into its rendered
|
|
5256
|
+
# table parent. Descendants of those children still foster within the
|
|
5257
|
+
# projected template contents, for example a <div> inside a <tr>.
|
|
5258
|
+
if (
|
|
5259
|
+
table_idx is not None
|
|
5260
|
+
and parser_only_template_idx is not None
|
|
5261
|
+
and table_idx < parser_only_template_idx
|
|
5262
|
+
and self._stack[table_idx] is parent
|
|
5263
|
+
):
|
|
5260
5264
|
return None
|
|
5265
|
+
# Fostering runs once per node placed inside a table, and no template is
|
|
5266
|
+
# open for the overwhelming majority of them. Both counters below are
|
|
5267
|
+
# maintained anyway, so consulting them skips two stack lookups.
|
|
5261
5268
|
template_idx = self._open_template_index() if self._template_modes else None
|
|
5262
5269
|
if table_idx is not None and template_idx is not None and table_idx < template_idx:
|
|
5263
5270
|
table_idx = None
|
|
@@ -495,7 +495,12 @@ def _collapse_html_whitespace(text: str) -> str:
|
|
|
495
495
|
return " ".join(text.split())
|
|
496
496
|
|
|
497
497
|
|
|
498
|
-
def _normalize_formatting_whitespace(
|
|
498
|
+
def _normalize_formatting_whitespace(
|
|
499
|
+
text: str,
|
|
500
|
+
*,
|
|
501
|
+
preserve_leading: bool = False,
|
|
502
|
+
preserve_trailing: bool = False,
|
|
503
|
+
) -> str:
|
|
499
504
|
"""Normalize formatting whitespace within a text node.
|
|
500
505
|
|
|
501
506
|
Converts newlines/tabs/CR/FF to regular spaces and collapses runs that
|
|
@@ -543,9 +548,9 @@ def _normalize_formatting_whitespace(text: str) -> str:
|
|
|
543
548
|
out.append(ch)
|
|
544
549
|
|
|
545
550
|
normalized = "".join(out)
|
|
546
|
-
if starts_with_formatting and normalized.startswith(" "):
|
|
551
|
+
if starts_with_formatting and normalized.startswith(" ") and not preserve_leading:
|
|
547
552
|
normalized = normalized[1:]
|
|
548
|
-
if ends_with_formatting and normalized.endswith(" "):
|
|
553
|
+
if ends_with_formatting and normalized.endswith(" ") and not preserve_trailing:
|
|
549
554
|
normalized = normalized[:-1]
|
|
550
555
|
return normalized
|
|
551
556
|
|
|
@@ -1025,14 +1030,23 @@ def _node_to_html(node: Any, indent: int = 0, indent_size: int = 2, *, in_pre: b
|
|
|
1025
1030
|
continue
|
|
1026
1031
|
|
|
1027
1032
|
parts_template: list[tuple[str, Any]] = []
|
|
1028
|
-
for child in run:
|
|
1033
|
+
for run_index, child in enumerate(run):
|
|
1029
1034
|
if child.name == "#text":
|
|
1030
1035
|
data = child.data or ""
|
|
1031
1036
|
if not data.strip():
|
|
1032
1037
|
parts_template.append(("lit", data))
|
|
1033
1038
|
else:
|
|
1034
1039
|
parts_template.append(
|
|
1035
|
-
(
|
|
1040
|
+
(
|
|
1041
|
+
"lit",
|
|
1042
|
+
_escape_text(
|
|
1043
|
+
_normalize_formatting_whitespace(
|
|
1044
|
+
data,
|
|
1045
|
+
preserve_leading=run_index > 0,
|
|
1046
|
+
preserve_trailing=run_index < len(run) - 1,
|
|
1047
|
+
)
|
|
1048
|
+
),
|
|
1049
|
+
)
|
|
1036
1050
|
)
|
|
1037
1051
|
continue
|
|
1038
1052
|
|
|
@@ -1115,7 +1129,18 @@ def _node_to_html(node: Any, indent: int = 0, indent_size: int = 2, *, in_pre: b
|
|
|
1115
1129
|
inline_parts.append(("lit", data))
|
|
1116
1130
|
continue
|
|
1117
1131
|
|
|
1118
|
-
inline_parts.append(
|
|
1132
|
+
inline_parts.append(
|
|
1133
|
+
(
|
|
1134
|
+
"lit",
|
|
1135
|
+
_escape_text(
|
|
1136
|
+
_normalize_formatting_whitespace(
|
|
1137
|
+
data,
|
|
1138
|
+
preserve_leading=i != first_non_none_index,
|
|
1139
|
+
preserve_trailing=i != last_non_none_index,
|
|
1140
|
+
)
|
|
1141
|
+
),
|
|
1142
|
+
)
|
|
1143
|
+
)
|
|
1119
1144
|
continue
|
|
1120
1145
|
|
|
1121
1146
|
if _is_layout_blocky_element(child, memo):
|
|
@@ -1249,7 +1274,11 @@ def _node_to_html(node: Any, indent: int = 0, indent_size: int = 2, *, in_pre: b
|
|
|
1249
1274
|
if "\n" in data or "\r" in data or "\t" in data or len(data) > 2:
|
|
1250
1275
|
compact_parts_template.append(("lit", " "))
|
|
1251
1276
|
continue
|
|
1252
|
-
data = _normalize_formatting_whitespace(
|
|
1277
|
+
data = _normalize_formatting_whitespace(
|
|
1278
|
+
data,
|
|
1279
|
+
preserve_leading=i != first_non_none_index,
|
|
1280
|
+
preserve_trailing=i != last_non_none_index,
|
|
1281
|
+
)
|
|
1253
1282
|
compact_parts_template.append(("lit", _escape_text(data)))
|
|
1254
1283
|
continue
|
|
1255
1284
|
|
|
@@ -1451,6 +1451,10 @@ class TestParserRecoveryRegressions(_ParserEngineTestCase):
|
|
|
1451
1451
|
default_cases = [
|
|
1452
1452
|
("<script>x", "<html><head></head><body></body></html>"),
|
|
1453
1453
|
("<table><template>x</table></template>", "<html><head></head><body><table>x</table></body></html>"),
|
|
1454
|
+
(
|
|
1455
|
+
"<table><template><tr><div></div></tr></template>",
|
|
1456
|
+
"<html><head></head><body><table><tr></tr><div></div></table></body></html>",
|
|
1457
|
+
),
|
|
1454
1458
|
("<svg><noframes>x</svg></noframes>", "<html><head></head><body></body></html>"),
|
|
1455
1459
|
("<template><colgroup>x</colgroup></template>", "<html><head></head><body></body></html>"),
|
|
1456
1460
|
("<table><colgroup>x</table></colgroup>", "<html><head></head><body>x<table></table></body></html>"),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import unittest
|
|
4
|
+
from dataclasses import replace
|
|
4
5
|
|
|
5
6
|
import justhtml
|
|
6
7
|
from justhtml import JustHTML, Sanitize, SetAttrs
|
|
@@ -355,6 +356,40 @@ class TestSanitizePlumbing(unittest.TestCase):
|
|
|
355
356
|
|
|
356
357
|
|
|
357
358
|
class TestSanitizeDom(unittest.TestCase):
|
|
359
|
+
def test_custom_policy_preserves_content_after_closed_foreign_rawtext(self) -> None:
|
|
360
|
+
policies = (
|
|
361
|
+
replace(DEFAULT_POLICY),
|
|
362
|
+
SanitizationPolicy(
|
|
363
|
+
allowed_tags={"p"},
|
|
364
|
+
allowed_attributes={"p": set()},
|
|
365
|
+
url_policy=UrlPolicy(allow_rules={}),
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
for policy in policies:
|
|
370
|
+
for foreign_tag in ("math", "svg"):
|
|
371
|
+
for rawtext_tag in ("script", "style"):
|
|
372
|
+
html = f"<{foreign_tag}><{rawtext_tag}></{rawtext_tag}></{foreign_tag}>tail<p>end</p>"
|
|
373
|
+
with self.subTest(policy=policy, foreign_tag=foreign_tag, rawtext_tag=rawtext_tag):
|
|
374
|
+
assert JustHTML(html, fragment=True, policy=policy).to_html(pretty=False) == "tail<p>end</p>"
|
|
375
|
+
assert (
|
|
376
|
+
JustHTML(
|
|
377
|
+
html,
|
|
378
|
+
fragment=True,
|
|
379
|
+
transforms=[Sanitize(policy=policy)],
|
|
380
|
+
).to_html(pretty=False)
|
|
381
|
+
== "tail<p>end</p>"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def test_custom_policy_drops_content_after_unclosed_foreign_rawtext(self) -> None:
|
|
385
|
+
policy = replace(DEFAULT_POLICY)
|
|
386
|
+
|
|
387
|
+
for foreign_tag in ("math", "svg"):
|
|
388
|
+
for rawtext_tag in ("script", "style"):
|
|
389
|
+
html = f"<{foreign_tag}><{rawtext_tag}>unsafe</{foreign_tag}>tail<p>end</p>"
|
|
390
|
+
with self.subTest(foreign_tag=foreign_tag, rawtext_tag=rawtext_tag):
|
|
391
|
+
assert JustHTML(html, fragment=True, policy=policy).to_html(pretty=False) == ""
|
|
392
|
+
|
|
358
393
|
def test_sanitize_dom_document_fragment(self) -> None:
|
|
359
394
|
root = DocumentFragment()
|
|
360
395
|
root.append_child(Node("script"))
|
|
@@ -820,6 +820,16 @@ class TestSerialize(unittest.TestCase):
|
|
|
820
820
|
rendered_text = JustHTML(pretty_html).to_text(separator="", strip=False)
|
|
821
821
|
assert rendered_text == "BApplication(const char* signature);"
|
|
822
822
|
|
|
823
|
+
def test_pretty_print_preserves_formatting_whitespace_before_inline_element(self):
|
|
824
|
+
doc = JustHTML("<p>hello\n<code>world</code></p>", fragment=True)
|
|
825
|
+
|
|
826
|
+
assert doc.to_html() == "<p>hello <code>world</code></p>"
|
|
827
|
+
|
|
828
|
+
def test_pretty_print_preserves_formatting_whitespace_after_inline_element(self):
|
|
829
|
+
doc = JustHTML("<p><code>hello</code>\nworld</p>", fragment=True)
|
|
830
|
+
|
|
831
|
+
assert doc.to_html() == "<p><code>hello</code> world</p>"
|
|
832
|
+
|
|
823
833
|
def test_empty_attributes(self):
|
|
824
834
|
html = "<input disabled>"
|
|
825
835
|
doc = JustHTML(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
|
|
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
|
|
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
|
{justhtml-3.11.0 → justhtml-3.11.2}/tests/justhtml-tests/xml_coercion_invalid_characters.test
RENAMED
|
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
|