Repository navigation
Expand file tree
/
Copy pathtest_readable_html.py
More file actions
218 lines (178 loc) · 8.83 KB
/
Copy pathtest_readable_html.py
File metadata and controls
218 lines (178 loc) · 8.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import pathlib
import pytest
from dank.html_utils import html_base_url, html_text, remove_page_noise
from dank.web.post_html import sanitize_html, summarize_html
HTML = (
'<html><head><title>Metadata title</title><style>.tracking{}</style>'
'<script>headTracker()</script></head><body>'
'<p>Real & readable <strong>content</strong>.</p>'
'<script>bodyTracker()</script><style>.inlineTracking{}</style>'
'<pre><script>example()</script></pre>'
'</body></html>'
)
def test_noise_removal_preserves_article_markup_and_escaped_code() -> None:
cleaned = remove_page_noise(HTML)
assert '<strong>content</strong>' in cleaned
assert '<script>example()</script>' in cleaned
assert 'Tracker' not in cleaned and 'Tracking' not in cleaned
assert 'Metadata title' not in cleaned
def test_plain_text_keeps_word_and_paragraph_boundaries() -> None:
text = html_text('<p>per<strong>form</strong>ance</p><p>Next line.</p>')
assert text.split() == ['performance', 'Next', 'line.']
def test_preview_and_detail_do_not_display_styles_or_tracking_code() -> None:
summary = summarize_html(HTML)
assert summary == 'Real & readable content. <script>example()</script>'
cleaned = sanitize_html(HTML)
assert 'Tracker' not in cleaned and 'tracking' not in cleaned
assert '<script>' in cleaned
def test_implicit_end_of_head_does_not_hide_the_body() -> None:
assert html_text('<head><title>Hidden</title><body>Visible</body>') == (
'Visible'
)
@pytest.mark.parametrize("base, expected", [
('<base href="https://cdn.test/assets/">', 'https://cdn.test/assets/'),
('<base href="../assets/" />', 'https://blog.test/assets/'),
('<base href="//cdn.test/assets/">', 'https://cdn.test/assets/'),
('<base href="">', 'https://blog.test/posts/article'),
('<base href="javascript:bad">', 'https://blog.test/posts/article'),
('<base href="http://[">', 'https://blog.test/posts/article'),
('<base href="https://bad.test:no/path">', 'https://blog.test/posts/article'),
('', 'https://blog.test/posts/article'),
])
def test_html_base_uses_valid_first_href(base: str, expected: str) -> None:
document = f'<head>{base}</head><body>Article</body>'
actual = html_base_url(document, 'https://blog.test/posts/article')
assert actual == expected
def test_only_first_base_href_controls_document_links() -> None:
document = ('<head><base target="_blank"><base href="/first/">'
'<base href="https://other.test/"></head>')
assert html_base_url(document, 'https://blog.test/post') == (
'https://blog.test/first/'
)
def test_resolved_links_preserve_entities_code_and_non_http_links() -> None:
document = (
'<p><a href="../more?q=1&b=2" title="A & B">Next</a>'
'<img src="/cover.png" alt="A "quote"" />'
'<video controls src="clip.mp4" poster="poster.jpg"></video>'
'<a href="mailto:person@example.test">Mail</a>'
'<pre><img src="example.png"></pre></p>'
)
result = remove_page_noise(
document, base_url='https://blog.test/posts/article/',
)
assert 'href="https://blog.test/posts/more?q=1&b=2"' in result
assert 'title="A & B"' in result
assert 'src="https://blog.test/cover.png"' in result
assert 'alt="A "quote""' in result
assert ('<video controls src="https://blog.test/posts/article/clip.mp4"'
in result)
assert 'poster="https://blog.test/posts/article/poster.jpg"' in result
assert 'href="mailto:person@example.test"' in result
assert '<img src="example.png">' in result
@pytest.mark.parametrize("tag", ["pre", "code"])
def test_code_language_metadata_survives_sanitizing(tag: str) -> None:
document = (
f'<{tag} class="language-sql" data-lang="sql" data-language="sql"'
' style="color:red" onclick="alert(1)" data-unrelated="discard">'
'SELECT <example> & 1;'
f'</{tag}>'
)
assert sanitize_html(document) == (
f'<{tag} class="language-sql" data-lang="sql" data-language="sql">'
'SELECT <example> & 1;'
f'</{tag}>'
)
def test_language_metadata_is_not_allowed_on_other_elements() -> None:
document = '<p data-lang="sql" data-language="sql">Article body</p>'
assert sanitize_html(document) == '<p>Article body</p>'
def test_standard_table_structure_survives_sanitizing() -> None:
document = (pathlib.Path(__file__).parent / "fixtures"
/ "reader-table.html").read_text()
cleaned = sanitize_html(document)
for markup in (
'<caption>Benchmark comparison</caption>',
'<colgroup span="2">', '<col span="4">', '<thead>', '<tbody>',
'<tfoot>', '<th colspan="2" scope="colgroup">',
'<th rowspan="2" scope="rowgroup">',
'<th scope="col" abbr="Alpha">', '<td colspan="6">',
'<em>Tools benchmark</em>',
):
assert markup in cleaned
assert html_text(cleaned).split() == html_text(document).split()
assert "onclick" not in cleaned and "style=" not in cleaned
@pytest.mark.parametrize("tag", ["td", "th"])
@pytest.mark.parametrize("rowspan", ["0", "2"])
def test_table_cell_spans_are_preserved(tag: str, rowspan: str) -> None:
document = (
f'<table><tbody><tr><{tag} colspan="2" rowspan="{rowspan}"'
f' onclick="attack()">Cell</{tag}></tr></tbody></table>'
)
result = sanitize_html(document)
assert f'<{tag} colspan="2" rowspan="{rowspan}">' in result
assert "onclick" not in result
def test_table_attributes_are_not_globally_allowed() -> None:
document = '<p colspan="2" rowspan="2" scope="row">Text</p>'
assert sanitize_html(document) == '<p>Text</p>'
@pytest.mark.parametrize("wrapper", ["div", "figure"])
@pytest.mark.parametrize("base_url", [None, "https://example.test/story"])
def test_wordpress_captions_become_semantic_figures(
wrapper: str, base_url: str | None,
) -> None:
document = (
f'<{wrapper} class="aligncenter wp-caption" id="attachment_1">'
'<div><a href="/image.png"><picture><source srcset="/large.png 2x">'
'<img src="/image.png" alt="Diagram" /></picture></a></div>'
'<p class="wp-caption-text" id="caption-attachment-1">'
'Credit: <a href="/credit">A & B</a><br>More detail.</p>'
f'</{wrapper}><p>Following article paragraph.</p>'
)
cleaned = remove_page_noise(document, base_url=base_url)
assert cleaned.startswith('<figure class="aligncenter wp-caption"')
assert '<figcaption class="wp-caption-text"' in cleaned
assert 'More detail.</figcaption></figure><p>Following' in cleaned
prefix = "https://example.test" if base_url else ""
assert f'href="{prefix}/credit">A & B</a>' in cleaned
assert f'src="{prefix}/image.png"' in cleaned
assert html_text(cleaned).split() == html_text(document).split()
assert remove_page_noise(cleaned, base_url=base_url) == cleaned
@pytest.mark.parametrize("document", [
'<div><img src="/image.png"><p>Normal article text.</p></div>',
'<div class="wp-caption-other"><img src="/image.png">'
'<p class="wp-caption-text">Not a caption container.</p></div>',
'<p class="wp-caption-text">An orphan paragraph.</p>',
'<div class="wp-caption"><img src="/image.png">'
'<p class="wp-caption-text-other">Normal article text.</p></div>',
'<div class="wp-caption"><div><p class="wp-caption-text">'
'Unrelated nested text.</p></div></div>',
])
def test_caption_normalization_does_not_guess_from_image_adjacency(
document: str,
) -> None:
cleaned = remove_page_noise(document)
assert '<figcaption' not in cleaned
assert html_text(cleaned).split() == html_text(document).split()
def test_native_figures_and_escaped_markup_stay_intact() -> None:
document = (
'<figure><img src="/image.png"><figcaption>Existing credit.'
'</figcaption></figure><pre><div class="wp-caption">'
'<p class="wp-caption-text">Code example</p>'
'</div></pre>'
)
assert remove_page_noise(document) == document
def test_figure_text_keeps_boundaries_without_source_whitespace() -> None:
assert html_text(
'Before<figure><img src="/image.png"><figcaption>Credit'
'</figcaption></figure>After',
).split() == ['Before', 'Credit', 'After']
def test_wordpress_captions_preserve_sanitizer_boundary() -> None:
document = (
'<div class="wp-caption" style="width:600px" onclick="attack()">'
'<img src="/image.png" onerror="attack()">'
'<p class="wp-caption-text" id="credit" style="position:fixed">'
'Credit: <a href="javascript:attack()">unsafe</a> '
'<em>A & B</em><script>attack()</script></p></div>'
)
assert sanitize_html(document) == (
'<figure><img src="/image.png"><figcaption>Credit: '
'<a>unsafe</a> <em>A & B</em></figcaption></figure>'
)