justhtml 3.11.2__tar.gz → 3.12.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.
- {justhtml-3.11.2 → justhtml-3.12.0}/CHANGELOG.md +14 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/PKG-INFO +1 -1
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/linkify.md +3 -2
- {justhtml-3.11.2 → justhtml-3.12.0}/pyproject.toml +1 -1
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/parser/engine.py +28 -8
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/transforms/runtime.py +0 -3
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_parser_engine.py +30 -1
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_sanitize_integration.py +11 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_transforms.py +23 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/.github/workflows/ci.yml +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/.github/workflows/publish.yml +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/.gitignore +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/.pre-commit-config.yaml +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/AGENTS.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/ARCHITECTURE.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/CODE_OF_CONDUCT.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/CONTRIBUTING.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/LICENSE +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/Makefile +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/PERFORMANCE.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/README.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/SECURITY.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/assets/justhtml-readme-explainer.png +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/benchmarks/chromium_parser_diff.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/benchmarks/correctness.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/benchmarks/fuzz.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/benchmarks/html5lib_engine_diff.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/benchmarks/parser_engine_gate.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/benchmarks/performance.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/benchmarks/profile.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/_config.yml +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/_layouts/default.html +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/api.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/assets/search.js +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/bleach-migration.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/building.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/cli.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/comparison.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/correctness.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/encoding.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/errors.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/fragments.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/html-cleaning.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/index.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/migration-examples.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/playground/app.js +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/playground/index.html +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/playground/py/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/playground/py/install_latest_justhtml.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/playground/py/render.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/playground/py/use_local_repo.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/quickstart.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/sanitization.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/selectors.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/streaming.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/text.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/transforms.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/unsafe-handling.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/docs/url-cleaning.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/llms.txt +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/project_ideas.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/run_tests.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/scripts/release.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/__main__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/constants.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/doctype.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/entities.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/errors.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/rawtext.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/text.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/core/types.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/dom/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/dom/builder.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/parser/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/parser/context.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/parser/encoding.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/parser/options.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/parser/scanner.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/parser/stream.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/py.typed +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/css.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/dom.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/policy.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/policy_defaults.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/rawtext.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/url/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/url/policy.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/url/runtime.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/sanitizer/url/spec.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/selector/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/selector/core.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/serializer/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/serializer/html.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/serializer/markdown.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/transforms/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/transforms/compile.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/transforms/linkify.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/transforms/linkify_core.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/src/justhtml/transforms/spec.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/test-summary.txt +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/README.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/data/wikipedia.html +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/harness/__init__.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/harness/encoding.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/harness/regressions.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/harness/reporter.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/harness/scaling.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/harness/serializer.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/harness/tree.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-sanitize-tests/cases.json +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/character_preprocessing.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/document_shell_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/entities.test +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/foreign_content.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/foreign_stack_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/frameset_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/head_noscript_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/iframe_srcdoc.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/in_body_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/raw_text_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/select_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/selectedcontent.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/table_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/template_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/tokenizer_recovery.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/tokenizer_state_transitions.test +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/tokenizer_states.test +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/xml_coercion.dat +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/justhtml-tests/xml_coercion_invalid_characters.test +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/linkify-it/LICENSE.txt +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/linkify-it/README.md +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/linkify-it/fixtures/links.txt +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/linkify-it/fixtures/not_links.txt +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_builder.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_cli.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_docs_examples.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_encoding.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_entities.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_errors.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_linkify_internals.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_linkify_it.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_linkify_transform.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_node.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_parser_regressions.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_parser_tree.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_playground_local_repo_file_list.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_sanitize.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_sanitize_transform.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_selector.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_serialize.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_stream.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_transform_runtime.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_transforms_compiler.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/tests/test_transforms_sanitize_integration.py +0 -0
- {justhtml-3.11.2 → justhtml-3.12.0}/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.12.0] - 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Match HTML5 form recovery in template fragments, including nested and misnested forms.
|
|
15
|
+
- Preserve frameset eligibility when sanitized parsing omits template wrappers from body content.
|
|
16
|
+
|
|
17
|
+
## [3.11.3] - 2026-09-27
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Apply earlier transforms to allowed children hoisted when sanitization removes a parent (#85).
|
|
22
|
+
- Drop foreign subtrees inside disallowed HTML parents in the default parsing path.
|
|
23
|
+
|
|
10
24
|
## [3.11.2] - 2026-08-27
|
|
11
25
|
|
|
12
26
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: justhtml
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.12.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
|
|
@@ -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
|
-
#
|
|
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()`.
|
|
@@ -1923,6 +1923,11 @@ class ParseEngine:
|
|
|
1923
1923
|
self._after_head = False
|
|
1924
1924
|
self._explicit_head = True
|
|
1925
1925
|
else:
|
|
1926
|
+
if self._find_open_html_index("body") is not None:
|
|
1927
|
+
# The compiled safe path omits parser-only template wrappers
|
|
1928
|
+
# from the projected tree. Keep their frameset eligibility
|
|
1929
|
+
# effect when they occur in body content.
|
|
1930
|
+
self._frameset_blocked = True
|
|
1926
1931
|
self._repair_stack_for_start("template")
|
|
1927
1932
|
self._append_text_boundary(self._current_parent())
|
|
1928
1933
|
self._stack.append(Element("template", {}, _PARSER_ONLY_NAMESPACE))
|
|
@@ -2932,7 +2937,11 @@ class ParseEngine:
|
|
|
2932
2937
|
self._close_open_template(tag_start, tag_end)
|
|
2933
2938
|
self._finish_head_reentry()
|
|
2934
2939
|
return pos
|
|
2935
|
-
|
|
2940
|
+
template_fragment = self._fragment_context_name == "template" and self._fragment_context_namespace in {
|
|
2941
|
+
None,
|
|
2942
|
+
"html",
|
|
2943
|
+
}
|
|
2944
|
+
if name == "form" and not self._template_modes and not template_fragment:
|
|
2936
2945
|
form_node = self._form_element
|
|
2937
2946
|
self._form_element = None
|
|
2938
2947
|
if form_node is None:
|
|
@@ -3522,6 +3531,15 @@ class ParseEngine:
|
|
|
3522
3531
|
elif parent_name != "tr":
|
|
3523
3532
|
return pos
|
|
3524
3533
|
|
|
3534
|
+
template_fragment = self._fragment_context_name == "template" and self._fragment_context_namespace in {
|
|
3535
|
+
None,
|
|
3536
|
+
"html",
|
|
3537
|
+
}
|
|
3538
|
+
if template_fragment and name == "form":
|
|
3539
|
+
node = self._insert_compiled_safe_element(name, attrs, self_closing, parent)
|
|
3540
|
+
self._nodes_to_unwrap.append(node)
|
|
3541
|
+
return pos
|
|
3542
|
+
|
|
3525
3543
|
if not action.allowed:
|
|
3526
3544
|
if action.pre_linefeed: # pragma: no branch - opposite edge requires invalid parser state
|
|
3527
3545
|
self._ignore_lf = True # pragma: no cover - unreachable after parser-state guards
|
|
@@ -3862,8 +3880,12 @@ class ParseEngine:
|
|
|
3862
3880
|
)
|
|
3863
3881
|
return pos
|
|
3864
3882
|
|
|
3883
|
+
template_fragment = self._fragment_context_name == "template" and self._fragment_context_namespace in {
|
|
3884
|
+
None,
|
|
3885
|
+
"html",
|
|
3886
|
+
}
|
|
3865
3887
|
if html_text_parsing and name == "form" and not self._template_modes:
|
|
3866
|
-
if self._form_element is not None:
|
|
3888
|
+
if self._form_element is not None and not template_fragment:
|
|
3867
3889
|
return pos
|
|
3868
3890
|
self._repair_stack_for_start(name)
|
|
3869
3891
|
node = self._insert_sanitized_element(
|
|
@@ -3874,7 +3896,8 @@ class ParseEngine:
|
|
|
3874
3896
|
tag_start=tag_start,
|
|
3875
3897
|
tag_end=tag_end,
|
|
3876
3898
|
)
|
|
3877
|
-
|
|
3899
|
+
if not template_fragment:
|
|
3900
|
+
self._form_element = node
|
|
3878
3901
|
table_idx = self._find_open_index("table")
|
|
3879
3902
|
in_table_mode = (
|
|
3880
3903
|
table_idx is not None
|
|
@@ -4212,11 +4235,8 @@ class ParseEngine:
|
|
|
4212
4235
|
self._foreign_context_seen = True
|
|
4213
4236
|
self._nodes_to_unwrap.append(node)
|
|
4214
4237
|
parent_namespace = getattr(parent, "namespace", None)
|
|
4215
|
-
if parent is self._fragment_context_node or parent_namespace in {None, "html"}:
|
|
4216
|
-
|
|
4217
|
-
parent is self._fragment_context_node or parent.name in self._allowed_tags
|
|
4218
|
-
):
|
|
4219
|
-
self._nodes_to_drop.append(node)
|
|
4238
|
+
if parent is self._fragment_context_node or parent_namespace in {None, "html", _PARSER_ONLY_NAMESPACE}:
|
|
4239
|
+
self._nodes_to_drop.append(node)
|
|
4220
4240
|
elif name not in self._allowed_tags:
|
|
4221
4241
|
self._nodes_to_unwrap.append(node)
|
|
4222
4242
|
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:
|
|
@@ -1050,6 +1050,35 @@ class TestParserFragmentsAndTextModes(_ParserEngineTestCase):
|
|
|
1050
1050
|
with self.subTest(html=html, context=context):
|
|
1051
1051
|
self.assert_parses_to(html, expected, fragment_context=context)
|
|
1052
1052
|
|
|
1053
|
+
def test_template_fragment_form_recovery(self) -> None:
|
|
1054
|
+
cases = [
|
|
1055
|
+
("<form><form>", "<form><form></form></form>", ""),
|
|
1056
|
+
(
|
|
1057
|
+
"<br>BC<form>D<div>E</form>F</div>G",
|
|
1058
|
+
"<br>BC<form>D<div>E</div></form>FG",
|
|
1059
|
+
"<br>BCD<div>E</div>FG",
|
|
1060
|
+
),
|
|
1061
|
+
]
|
|
1062
|
+
for html, expected, safe_expected in cases:
|
|
1063
|
+
with self.subTest(html=html):
|
|
1064
|
+
self.assert_parses_to(
|
|
1065
|
+
html,
|
|
1066
|
+
expected,
|
|
1067
|
+
fragment_context=FragmentContext("template"),
|
|
1068
|
+
sanitize=False,
|
|
1069
|
+
)
|
|
1070
|
+
self.assert_parses_to(
|
|
1071
|
+
html,
|
|
1072
|
+
safe_expected,
|
|
1073
|
+
fragment_context=FragmentContext("template"),
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
def test_disallowed_template_in_body_blocks_frameset_replacement(self) -> None:
|
|
1077
|
+
self.assert_parses_to(
|
|
1078
|
+
"<p><template></template><frameset>",
|
|
1079
|
+
"<html><head></head><body><p></p></body></html>",
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1053
1082
|
def test_fragment_unwraps_disallowed_template_contents(self) -> None:
|
|
1054
1083
|
self.assert_parses_to("<template>x</template>", "x", fragment=True)
|
|
1055
1084
|
|
|
@@ -1745,7 +1774,7 @@ class TestParserAttributeProjection(_ParserEngineTestCase):
|
|
|
1745
1774
|
)
|
|
1746
1775
|
self.assert_parses_to(
|
|
1747
1776
|
"<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
|
|
1777
|
+
"<tbody><tr> </tr></tbody><table></table>\n<blockquote></blockquote>",
|
|
1749
1778
|
fragment_context=FragmentContext("table"),
|
|
1750
1779
|
)
|
|
1751
1780
|
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
|
|
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.2 → justhtml-3.12.0}/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
|