webreader 2.3.8__py3-none-any.whl
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.
- webreader/__init__.py +165 -0
- webreader/__main__.py +6 -0
- webreader/cli.py +166 -0
- webreader/core.py +501 -0
- webreader/default.config +0 -0
- webreader/extractors.py +447 -0
- webreader/formatter.py +89 -0
- webreader/markdown.py +237 -0
- webreader/sanitizer.py +117 -0
- webreader/selectors.py +462 -0
- webreader-2.3.8.dist-info/METADATA +188 -0
- webreader-2.3.8.dist-info/RECORD +15 -0
- webreader-2.3.8.dist-info/WHEEL +5 -0
- webreader-2.3.8.dist-info/licenses/LICENSE +21 -0
- webreader-2.3.8.dist-info/top_level.txt +1 -0
webreader/core.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""Tree-based HTML parsing engine built on top of the parser package."""
|
|
2
|
+
|
|
3
|
+
import codecs
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from html import escape as _escape
|
|
8
|
+
from html.parser import HTMLParser as _BaseHTMLParser
|
|
9
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
10
|
+
|
|
11
|
+
from pafer.cache import LRUCache
|
|
12
|
+
from pafer.config import (
|
|
13
|
+
Config,
|
|
14
|
+
PathLike,
|
|
15
|
+
load_config as _load_config,
|
|
16
|
+
save_config as _save_config,
|
|
17
|
+
)
|
|
18
|
+
from pafer.exceptions import ConfigLoadError, HTMLPaferError
|
|
19
|
+
from pafer.utils import extract_doctype, minify_html
|
|
20
|
+
|
|
21
|
+
VOID_ELEMENTS = {
|
|
22
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
23
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
BLOCK_TAGS = {
|
|
27
|
+
'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd',
|
|
28
|
+
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
|
|
29
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li',
|
|
30
|
+
'main', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'tbody', 'thead',
|
|
31
|
+
'tfoot', 'tr', 'ul',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ROOT_TAG = ':root'
|
|
35
|
+
COMMENT_TAG = ':comment'
|
|
36
|
+
|
|
37
|
+
PRESERVE_WHITESPACE_TAGS = {'pre', 'textarea'}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def detect_encoding(data: bytes, default: str = 'utf-8') -> str:
|
|
41
|
+
"""Detect the encoding of raw HTML: BOM sniff, then <meta charset>."""
|
|
42
|
+
if data.startswith(codecs.BOM_UTF8):
|
|
43
|
+
return 'utf-8-sig'
|
|
44
|
+
if data.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)):
|
|
45
|
+
return 'utf-32'
|
|
46
|
+
if data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
|
|
47
|
+
return 'utf-16'
|
|
48
|
+
head = data[:2048].decode('latin-1', errors='ignore')
|
|
49
|
+
match = re.search(
|
|
50
|
+
r'<meta[^>]+charset\s*=\s*["\']?\s*([\w.:+-]+)', head, re.IGNORECASE
|
|
51
|
+
)
|
|
52
|
+
if match:
|
|
53
|
+
try:
|
|
54
|
+
codecs.lookup(match.group(1))
|
|
55
|
+
return match.group(1)
|
|
56
|
+
except LookupError:
|
|
57
|
+
pass
|
|
58
|
+
return default
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Node:
|
|
62
|
+
"""A single element or comment node in the parsed HTML tree."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, tag: str, attrs: Optional[Dict[str, str]] = None,
|
|
65
|
+
parent: Optional['Node'] = None) -> None:
|
|
66
|
+
self.tag = tag
|
|
67
|
+
self.attrs: Dict[str, str] = dict(attrs or {})
|
|
68
|
+
self.children: List[Any] = []
|
|
69
|
+
self.parent: Optional[Node] = parent
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def is_comment(self) -> bool:
|
|
73
|
+
return self.tag == COMMENT_TAG
|
|
74
|
+
|
|
75
|
+
def get_attr(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
76
|
+
return self.attrs.get(name, default)
|
|
77
|
+
|
|
78
|
+
def has_class(self, name: str) -> bool:
|
|
79
|
+
return name in (self.get_attr('class') or '').split()
|
|
80
|
+
|
|
81
|
+
def set_attr(self, name: str, value: str) -> 'Node':
|
|
82
|
+
self.attrs[name] = value
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def remove_attr(self, name: str) -> 'Node':
|
|
86
|
+
self.attrs.pop(name, None)
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def append_child(self, node: 'Node') -> 'Node':
|
|
90
|
+
if node.parent is not None:
|
|
91
|
+
node.parent.children.remove(node)
|
|
92
|
+
self.children.append(node)
|
|
93
|
+
node.parent = self
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def remove(self) -> 'Node':
|
|
97
|
+
if self.parent is not None:
|
|
98
|
+
self.parent.children.remove(self)
|
|
99
|
+
self.parent = None
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def replace_with(self, *nodes: 'Node') -> 'Node':
|
|
103
|
+
if self.parent is None:
|
|
104
|
+
raise PaferError('cannot replace a detached node')
|
|
105
|
+
siblings = self.parent.children
|
|
106
|
+
index = siblings.index(self)
|
|
107
|
+
replacements = list(nodes)
|
|
108
|
+
for node in replacements:
|
|
109
|
+
if node.parent is not None:
|
|
110
|
+
node.parent.children.remove(node)
|
|
111
|
+
siblings[index:index + 1] = replacements
|
|
112
|
+
for node in replacements:
|
|
113
|
+
node.parent = self.parent
|
|
114
|
+
self.parent = None
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def unwrap(self) -> 'Node':
|
|
118
|
+
if self.parent is None:
|
|
119
|
+
raise PaferError('cannot unwrap a detached node')
|
|
120
|
+
siblings = self.parent.children
|
|
121
|
+
index = siblings.index(self)
|
|
122
|
+
children = list(self.children)
|
|
123
|
+
siblings[index:index + 1] = children
|
|
124
|
+
for child in children:
|
|
125
|
+
if isinstance(child, Node):
|
|
126
|
+
child.parent = self.parent
|
|
127
|
+
self.children = []
|
|
128
|
+
self.parent = None
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def child_elements(self) -> List['Node']:
|
|
132
|
+
return [
|
|
133
|
+
child
|
|
134
|
+
for child in self.children
|
|
135
|
+
if isinstance(child, Node) and not child.is_comment
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
def text_content(self) -> str:
|
|
139
|
+
parts: List[str] = []
|
|
140
|
+
for child in self.children:
|
|
141
|
+
if isinstance(child, str):
|
|
142
|
+
parts.append(child)
|
|
143
|
+
else:
|
|
144
|
+
parts.append(child.text_content())
|
|
145
|
+
return ''.join(parts)
|
|
146
|
+
|
|
147
|
+
def iter_elements(self) -> Iterator['Node']:
|
|
148
|
+
for child in self.children:
|
|
149
|
+
if isinstance(child, Node) and not child.is_comment:
|
|
150
|
+
yield child
|
|
151
|
+
yield from child.iter_elements()
|
|
152
|
+
|
|
153
|
+
def to_html(self) -> str:
|
|
154
|
+
if self.is_comment:
|
|
155
|
+
return f'<!--{self.text_content()}-->'
|
|
156
|
+
attrs = ''.join(
|
|
157
|
+
f' {name}' if value == '' else f' {name}="{_escape(value, quote=True)}"'
|
|
158
|
+
for name, value in self.attrs.items()
|
|
159
|
+
)
|
|
160
|
+
if self.tag in VOID_ELEMENTS:
|
|
161
|
+
return f'<{self.tag}{attrs}>'
|
|
162
|
+
inner = ''.join(
|
|
163
|
+
_escape(child, quote=False) if isinstance(child, str) else child.to_html()
|
|
164
|
+
for child in self.children
|
|
165
|
+
)
|
|
166
|
+
return f'<{self.tag}{attrs}>{inner}</{self.tag}>'
|
|
167
|
+
|
|
168
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
169
|
+
return {
|
|
170
|
+
'tag': self.tag,
|
|
171
|
+
'attrs': dict(self.attrs),
|
|
172
|
+
'text': self.text_content().strip(),
|
|
173
|
+
'html': self.to_html(),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
return f'<Node {self.tag}>'
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class _TreeBuilder(_BaseHTMLParser):
|
|
181
|
+
"""Builds a Node tree from HTML using the stdlib tokenizer."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, config: Config) -> None:
|
|
184
|
+
super().__init__(convert_charrefs=True)
|
|
185
|
+
self.config = config
|
|
186
|
+
self.root = Node(ROOT_TAG)
|
|
187
|
+
self._stack: List[Node] = [self.root]
|
|
188
|
+
self.comments: List[str] = []
|
|
189
|
+
self.errors: List[str] = []
|
|
190
|
+
self._max_depth = int(config.get('max_depth', 100) or 100)
|
|
191
|
+
self._preserve_comments = bool(config.get('preserve_comments', False))
|
|
192
|
+
self._strip_whitespace = bool(config.get('strip_whitespace', True))
|
|
193
|
+
self._strict = bool(config.get('strict_mode', False))
|
|
194
|
+
|
|
195
|
+
def _current(self) -> Node:
|
|
196
|
+
return self._stack[-1]
|
|
197
|
+
|
|
198
|
+
def _attach(self, node: Node) -> None:
|
|
199
|
+
parent = self._current()
|
|
200
|
+
if len(self._stack) <= self._max_depth:
|
|
201
|
+
parent.children.append(node)
|
|
202
|
+
node.parent = parent
|
|
203
|
+
elif self._strict:
|
|
204
|
+
self.errors.append(
|
|
205
|
+
f'<{node.tag}> exceeds max_depth={self._max_depth}'
|
|
206
|
+
)
|
|
207
|
+
self._stack.append(node)
|
|
208
|
+
|
|
209
|
+
def handle_starttag(self, tag, attrs) -> None:
|
|
210
|
+
node = Node(tag, {k: (v if v is not None else '') for k, v in attrs})
|
|
211
|
+
self._attach(node)
|
|
212
|
+
if tag in VOID_ELEMENTS:
|
|
213
|
+
self._stack.pop()
|
|
214
|
+
|
|
215
|
+
def handle_startendtag(self, tag, attrs) -> None:
|
|
216
|
+
node = Node(tag, {k: (v if v is not None else '') for k, v in attrs})
|
|
217
|
+
if len(self._stack) <= self._max_depth:
|
|
218
|
+
parent = self._current()
|
|
219
|
+
parent.children.append(node)
|
|
220
|
+
node.parent = parent
|
|
221
|
+
|
|
222
|
+
def handle_endtag(self, tag) -> None:
|
|
223
|
+
if tag in VOID_ELEMENTS:
|
|
224
|
+
return
|
|
225
|
+
for i in range(len(self._stack) - 1, 0, -1):
|
|
226
|
+
if self._stack[i].tag == tag:
|
|
227
|
+
if len(self._stack) - 1 > i:
|
|
228
|
+
unclosed = [n.tag for n in self._stack[i + 1:]]
|
|
229
|
+
self.errors.append(
|
|
230
|
+
f'unclosed tag(s) <{"> <".join(unclosed)}> before </{tag}>'
|
|
231
|
+
)
|
|
232
|
+
del self._stack[i:]
|
|
233
|
+
return
|
|
234
|
+
self.errors.append(f'closing tag </{tag}> has no matching open tag')
|
|
235
|
+
|
|
236
|
+
def handle_data(self, data) -> None:
|
|
237
|
+
if not data:
|
|
238
|
+
return
|
|
239
|
+
if self._strip_whitespace and not any(
|
|
240
|
+
node.tag in PRESERVE_WHITESPACE_TAGS for node in self._stack
|
|
241
|
+
):
|
|
242
|
+
data = re.sub(r'\s+', ' ', data)
|
|
243
|
+
self._current().children.append(data)
|
|
244
|
+
|
|
245
|
+
def handle_comment(self, data) -> None:
|
|
246
|
+
self.comments.append(data)
|
|
247
|
+
if self._preserve_comments:
|
|
248
|
+
comment = Node(COMMENT_TAG)
|
|
249
|
+
comment.children.append(data)
|
|
250
|
+
if len(self._stack) <= self._max_depth:
|
|
251
|
+
parent = self._current()
|
|
252
|
+
parent.children.append(comment)
|
|
253
|
+
comment.parent = parent
|
|
254
|
+
|
|
255
|
+
def close(self) -> None:
|
|
256
|
+
super().close()
|
|
257
|
+
if len(self._stack) > 1:
|
|
258
|
+
unclosed = [n.tag for n in self._stack[1:]]
|
|
259
|
+
self.errors.append(
|
|
260
|
+
f'unclosed tag(s) at end of input: <{"> <".join(unclosed)}>'
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ParsedDocument:
|
|
265
|
+
"""Structured, lazily-enriched result of parsing an HTML document."""
|
|
266
|
+
|
|
267
|
+
def __init__(self, doctype: Optional[str], root: Node,
|
|
268
|
+
comments: Optional[List[str]] = None,
|
|
269
|
+
config: Optional[Config] = None,
|
|
270
|
+
errors: Optional[List[str]] = None) -> None:
|
|
271
|
+
self.doctype = doctype
|
|
272
|
+
self.root = root
|
|
273
|
+
self.comments = comments or []
|
|
274
|
+
self.errors = errors or []
|
|
275
|
+
self.config = config or Config()
|
|
276
|
+
self._metadata: Optional[Dict[str, Any]] = None
|
|
277
|
+
self._links: Optional[List[Dict[str, Any]]] = None
|
|
278
|
+
self._tables: Optional[List[Dict[str, Any]]] = None
|
|
279
|
+
self._text: Optional[str] = None
|
|
280
|
+
self._images: Optional[List[Dict[str, Any]]] = None
|
|
281
|
+
self._forms: Optional[List[Dict[str, Any]]] = None
|
|
282
|
+
self._headings: Optional[Dict[str, Any]] = None
|
|
283
|
+
self._scripts: Optional[List[Dict[str, Any]]] = None
|
|
284
|
+
self._stylesheets: Optional[List[Dict[str, Any]]] = None
|
|
285
|
+
self._markdown: Optional[str] = None
|
|
286
|
+
|
|
287
|
+
def metadata(self) -> Dict[str, Any]:
|
|
288
|
+
from .extractors import extract_metadata
|
|
289
|
+
if self._metadata is None:
|
|
290
|
+
self._metadata = extract_metadata(self)
|
|
291
|
+
return self._metadata
|
|
292
|
+
|
|
293
|
+
def links(self, base_url: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
294
|
+
from .extractors import extract_links
|
|
295
|
+
if base_url is None and self._links is not None:
|
|
296
|
+
return self._links
|
|
297
|
+
result = extract_links(self, base_url=base_url)
|
|
298
|
+
if base_url is None:
|
|
299
|
+
self._links = result
|
|
300
|
+
return result
|
|
301
|
+
|
|
302
|
+
def tables(self) -> List[Dict[str, Any]]:
|
|
303
|
+
from .extractors import extract_tables
|
|
304
|
+
if self._tables is None:
|
|
305
|
+
self._tables = extract_tables(self)
|
|
306
|
+
return self._tables
|
|
307
|
+
|
|
308
|
+
def text(self) -> str:
|
|
309
|
+
from .extractors import extract_text
|
|
310
|
+
if self._text is None:
|
|
311
|
+
self._text = extract_text(self)
|
|
312
|
+
return self._text
|
|
313
|
+
|
|
314
|
+
def find_all(self, selector: str) -> List[Node]:
|
|
315
|
+
from .selectors import find_all
|
|
316
|
+
return find_all(self.root, selector)
|
|
317
|
+
|
|
318
|
+
def find(self, selector: str) -> Optional[Node]:
|
|
319
|
+
from .selectors import find
|
|
320
|
+
return find(self.root, selector)
|
|
321
|
+
|
|
322
|
+
def images(self) -> List[Dict[str, Any]]:
|
|
323
|
+
from .extractors import extract_images
|
|
324
|
+
if self._images is None:
|
|
325
|
+
self._images = extract_images(self)
|
|
326
|
+
return self._images
|
|
327
|
+
|
|
328
|
+
def forms(self) -> List[Dict[str, Any]]:
|
|
329
|
+
from .extractors import extract_forms
|
|
330
|
+
if self._forms is None:
|
|
331
|
+
self._forms = extract_forms(self)
|
|
332
|
+
return self._forms
|
|
333
|
+
|
|
334
|
+
def headings(self) -> Dict[str, Any]:
|
|
335
|
+
from .extractors import extract_headings
|
|
336
|
+
if self._headings is None:
|
|
337
|
+
self._headings = extract_headings(self)
|
|
338
|
+
return self._headings
|
|
339
|
+
|
|
340
|
+
def scripts(self) -> List[Dict[str, Any]]:
|
|
341
|
+
from .extractors import extract_scripts
|
|
342
|
+
if self._scripts is None:
|
|
343
|
+
self._scripts = extract_scripts(self)
|
|
344
|
+
return self._scripts
|
|
345
|
+
|
|
346
|
+
def stylesheets(self) -> List[Dict[str, Any]]:
|
|
347
|
+
from .extractors import extract_stylesheets
|
|
348
|
+
if self._stylesheets is None:
|
|
349
|
+
self._stylesheets = extract_stylesheets(self)
|
|
350
|
+
return self._stylesheets
|
|
351
|
+
|
|
352
|
+
def to_markdown(self) -> str:
|
|
353
|
+
from .markdown import document_to_markdown
|
|
354
|
+
if self._markdown is None:
|
|
355
|
+
self._markdown = document_to_markdown(self)
|
|
356
|
+
return self._markdown
|
|
357
|
+
|
|
358
|
+
def to_html(self) -> str:
|
|
359
|
+
return ''.join(
|
|
360
|
+
_escape(child, quote=False) if isinstance(child, str) else child.to_html()
|
|
361
|
+
for child in self.root.children
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def stats(self) -> Dict[str, int]:
|
|
365
|
+
element_count = 0
|
|
366
|
+
max_depth = 0
|
|
367
|
+
|
|
368
|
+
def walk(node: Node, depth: int) -> None:
|
|
369
|
+
nonlocal element_count, max_depth
|
|
370
|
+
for child in node.children:
|
|
371
|
+
if isinstance(child, Node) and not child.is_comment:
|
|
372
|
+
element_count += 1
|
|
373
|
+
if depth + 1 > max_depth:
|
|
374
|
+
max_depth = depth + 1
|
|
375
|
+
walk(child, depth + 1)
|
|
376
|
+
|
|
377
|
+
walk(self.root, 0)
|
|
378
|
+
return {
|
|
379
|
+
'elements': element_count,
|
|
380
|
+
'comments': len(self.comments),
|
|
381
|
+
'max_depth': max_depth,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
385
|
+
return {
|
|
386
|
+
'doctype': self.doctype,
|
|
387
|
+
'stats': self.stats(),
|
|
388
|
+
'metadata': self.metadata(),
|
|
389
|
+
'links': self.links(),
|
|
390
|
+
'tables': self.tables(),
|
|
391
|
+
'text': self.text(),
|
|
392
|
+
'comments': list(self.comments),
|
|
393
|
+
'errors': list(self.errors),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
def __repr__(self) -> str:
|
|
397
|
+
return f'<ParsedDocument {self.stats()["elements"]} elements>'
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class Engine:
|
|
401
|
+
"""High-level HTML processing engine configured by a pafer Config."""
|
|
402
|
+
|
|
403
|
+
def __init__(self, config: Optional[Config] = None, use_cache: bool = True) -> None:
|
|
404
|
+
self.config = config or Config()
|
|
405
|
+
self.cache: Optional[LRUCache] = None
|
|
406
|
+
if use_cache:
|
|
407
|
+
self.cache = LRUCache(max_size=int(self.config.get('cache_size', 50) or 50))
|
|
408
|
+
|
|
409
|
+
def parse(self, html: Union[str, bytes]) -> ParsedDocument:
|
|
410
|
+
"""Parse HTML (str or bytes) into a ParsedDocument."""
|
|
411
|
+
if isinstance(html, bytes):
|
|
412
|
+
encoding = detect_encoding(
|
|
413
|
+
html,
|
|
414
|
+
default=str(self.config.get('encoding', 'utf-8') or 'utf-8'),
|
|
415
|
+
)
|
|
416
|
+
html = html.decode(encoding, errors='replace')
|
|
417
|
+
key = hashlib.sha256(html.encode('utf-8', errors='replace')).hexdigest()
|
|
418
|
+
if self.cache is not None:
|
|
419
|
+
cached = self.cache.get(key)
|
|
420
|
+
if cached is not None:
|
|
421
|
+
return cached
|
|
422
|
+
self._check_size(html)
|
|
423
|
+
builder = _TreeBuilder(self.config)
|
|
424
|
+
try:
|
|
425
|
+
builder.feed(html)
|
|
426
|
+
builder.close()
|
|
427
|
+
except PaferError:
|
|
428
|
+
raise
|
|
429
|
+
except Exception as exc:
|
|
430
|
+
raise PaferError(f'failed to parse HTML: {exc}') from exc
|
|
431
|
+
if bool(self.config.get('strict_mode', False)) and builder.errors:
|
|
432
|
+
summary = '; '.join(builder.errors[:5])
|
|
433
|
+
raise PaferError(f'malformed HTML (strict mode): {summary}')
|
|
434
|
+
doc = ParsedDocument(
|
|
435
|
+
doctype=extract_doctype(html),
|
|
436
|
+
root=builder.root,
|
|
437
|
+
comments=builder.comments,
|
|
438
|
+
config=self.config,
|
|
439
|
+
errors=builder.errors,
|
|
440
|
+
)
|
|
441
|
+
if self.cache is not None:
|
|
442
|
+
self.cache.set(key, doc)
|
|
443
|
+
return doc
|
|
444
|
+
|
|
445
|
+
def _check_size(self, html: str) -> None:
|
|
446
|
+
cap = self.config.get('max_file_size_mb', 10)
|
|
447
|
+
if not cap:
|
|
448
|
+
return
|
|
449
|
+
size_mb = len(html.encode('utf-8', errors='replace')) / (1024 * 1024)
|
|
450
|
+
if size_mb > float(cap):
|
|
451
|
+
raise PaferError(
|
|
452
|
+
f'input of {size_mb:.2f} MB exceeds max_file_size_mb={cap}'
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def minify(self, html: str) -> str:
|
|
456
|
+
"""Minify HTML honoring remove_comments from the configuration."""
|
|
457
|
+
return minify_html(html, remove_comments=bool(self.config.get('remove_comments', True)))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def parse_raw(html: Union[str, bytes],
|
|
461
|
+
config: Optional[Config] = None) -> ParsedDocument:
|
|
462
|
+
"""Parse with whitespace preserved; used by pretty and markdown."""
|
|
463
|
+
cfg = (config or Config()).update({'strip_whitespace': False})
|
|
464
|
+
return Engine(cfg, use_cache=False).parse(html)
|
|
465
|
+
|
|
466
|
+
def get_config():
|
|
467
|
+
"""Return the active configuration object."""
|
|
468
|
+
return config
|
|
469
|
+
|
|
470
|
+
def get_engine() -> Engine:
|
|
471
|
+
"""Return the active parsing engine."""
|
|
472
|
+
return _engine
|
|
473
|
+
|
|
474
|
+
def load_config(path: Optional[PathLike] = None) -> Dict[str, Any]:
|
|
475
|
+
"""Load user config."""
|
|
476
|
+
global config, _engine
|
|
477
|
+
try:
|
|
478
|
+
cfg = _load_config(path)
|
|
479
|
+
except ConfigLoadError:
|
|
480
|
+
cfg = {}
|
|
481
|
+
try:
|
|
482
|
+
_save_config(cfg)
|
|
483
|
+
except (ConfigLoadError, OSError):
|
|
484
|
+
pass
|
|
485
|
+
config = cfg
|
|
486
|
+
_engine = Engine(cfg)
|
|
487
|
+
return cfg
|
|
488
|
+
|
|
489
|
+
def save_config(cfg) -> None:
|
|
490
|
+
try:
|
|
491
|
+
_save_config(cfg)
|
|
492
|
+
except OSError:
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
config = _load_config(None)
|
|
498
|
+
except Exception:
|
|
499
|
+
config = Config()
|
|
500
|
+
|
|
501
|
+
_engine = Engine(config)
|
webreader/default.config
ADDED
|
Binary file
|