webreader 2.3.8__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 webreader
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: webreader
3
+ Version: 2.3.8
4
+ Summary: Tree-based HTML reader for Python.
5
+ Author-email: morichan <morichan@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: User Interfaces
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: pafer
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: pytest-cov; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
26
+ ![Dependencies](https://img.shields.io/badge/dependencies-none-success)
27
+ ![Tests](https://img.shields.io/badge/tests-pytest-informational)
28
+
29
+ ## Features
30
+
31
+ - Tree-based parsing into an editable document tree
32
+ - CSS selectors: combinators, attributes, pseudo-classes, selector lists
33
+ - Extractors: metadata, links, tables, text, images, forms, headings,
34
+ scripts, stylesheets
35
+ - Sanitization with a removal report
36
+ - Minify, pretty-print, Markdown conversion
37
+ - JSON / CSV / Markdown export
38
+ - CLI tool
39
+ - Standard library only at runtime
40
+
41
+ ## Installation
42
+
43
+ ```
44
+ pip install webreader
45
+ ```
46
+
47
+ ## Quickstart
48
+
49
+ ```python
50
+ import webreader
51
+
52
+ html = '<h1 class="title">Hello</h1><a href="/x">link</a>'
53
+ doc = webreader.parse(html)
54
+
55
+ webreader.select(doc, 'h1.title') # [{'tag': ..., 'attrs': ..., 'text': ..., 'html': ...}]
56
+ doc.find('a').get_attr('href') # '/x'
57
+
58
+ webreader.minify(html) # collapsed whitespace
59
+ webreader.pretty(html) # re-indented, readable markup
60
+ webreader.to_markdown(html) # convert to Markdown
61
+
62
+ clean, report = webreader.sanitize(html)
63
+ ```
64
+
65
+ Bytes input is decoded using detected encoding: BOM sniffing first,
66
+ then `<meta charset>` in the first 2 KB, then the configured
67
+ `encoding` fallback.
68
+
69
+ In non-strict mode, malformed HTML does not raise; the warnings are
70
+ available on the document:
71
+
72
+ ```python
73
+ doc = webreader.parse('<div><p>x</p>') # unclosed <div>
74
+ doc.errors # ['unclosed tag(s) at end of input: <div>']
75
+ ```
76
+
77
+ ## Extraction
78
+
79
+ ```python
80
+ webreader.extract_metadata(html) # title, OG/Twitter, canonical, charset, JSON-LD
81
+ webreader.extract_links(html) # links and asset references
82
+ webreader.extract_tables(html) # matrices with colspan/rowspan expanded
83
+ webreader.extract_text(html) # readable text without boilerplate
84
+ webreader.extract_images(html) # src, alt, dimensions, srcset candidates
85
+ webreader.extract_forms(html) # form fields with types and defaults
86
+ webreader.extract_headings(html) # h1-h6 outline + word count + reading time
87
+ webreader.extract_scripts(html) # inline and external script inventory
88
+ webreader.extract_stylesheets(html) # link[rel~=stylesheet] and inline styles
89
+ ```
90
+
91
+ ## CSS selectors
92
+
93
+ Supported syntax: type selectors, `*`, `#id`, `.class`, attribute
94
+ selectors (`[attr]`, `[attr=value]` and the `^=`, `$=`, `*=`, `~=`,
95
+ `|=` operators), the pseudo-classes `:first-child`, `:last-child`,
96
+ `:nth-child(an+b)`, `:not(compound)` and `:contains(text)`, compounds
97
+ such as `div.content#main[href="x"]`, the combinators whitespace
98
+ (descendant), `>` (child), `+` (adjacent sibling) and `~` (general
99
+ sibling), and comma-separated selector lists.
100
+
101
+ ```python
102
+ webreader.select(doc, 'a[href^="https://"]')
103
+ webreader.select(doc, 'ul > li:nth-child(2n+1):not(.skip)')
104
+ webreader.select(doc, 'h2 + p, blockquote p:contains("note")')
105
+ ```
106
+
107
+ Invalid or unsupported selectors raise `SelectorSyntaxError`.
108
+
109
+ ## Tree editing
110
+
111
+ Nodes support a mutation API, and the edited tree can be
112
+ re-serialized:
113
+
114
+ ```python
115
+ doc = webreader.parse('<div><a href="/x">link</a></div>')
116
+ link = doc.find('a')
117
+ link.set_attr('rel', 'noopener').remove_attr('class')
118
+
119
+ new = webreader.Node('p')
120
+ new.children.append('added text')
121
+ doc.find('div').append_child(new)
122
+
123
+ link.unwrap() # replace the <a> with its inner text
124
+ doc.to_html() # '<div>link<p>added text</p></div>'
125
+ ```
126
+
127
+ `Node` also provides `remove()` and `replace_with(*nodes)`.
128
+
129
+ ## Markdown conversion
130
+
131
+ `to_markdown` renders headings, paragraphs, links, images,
132
+ emphasis/strong/strikethrough, inline and fenced code (with language
133
+ detection from `class="language-..."`), nested lists, blockquotes,
134
+ horizontal rules and tables.
135
+
136
+ ```python
137
+ webreader.to_markdown('<h1>T</h1><p>Hi <b>bold</b></p>')
138
+ # '# T\n\nHi **bold**'
139
+ ```
140
+
141
+ ## Sanitization
142
+
143
+ `sanitize()` returns `(clean_html, report)`:
144
+
145
+ - tags outside the whitelist are unwrapped, inner content kept
146
+ - `script`, `style`, `noscript`, `template` are dropped entirely
147
+ - attributes filtered through a safe-list, `on*` handlers removed
148
+ - `javascript:`, `vbscript:` and non-image `data:` URLs blocked
149
+ - the report lists removed tags/attributes, blocked URLs and detected threats
150
+
151
+ ## CLI
152
+
153
+ ```
154
+ python -m webreader parse -f input.html
155
+ python -m webreader select -f input.html -s 'a[href]'
156
+ python -m webreader links -f input.html --base-url https://example.com
157
+ python -m webreader meta -f input.html
158
+ python -m webreader tables -f input.html --format csv
159
+ python -m webreader text -f input.html
160
+ python -m webreader images -f input.html
161
+ python -m webreader forms -f input.html
162
+ python -m webreader minify -f input.html --out min.html
163
+ python -m webreader pretty -f input.html --out pretty.html
164
+ python -m webreader md -f input.html --out page.md
165
+ python -m webreader sanitize -f input.html --out clean.html --allowed p,a,ul
166
+ ```
167
+
168
+ Use `-f -` to read from stdin. Every output command accepts `--out`
169
+ to write to a file instead of stdout.
170
+
171
+ ### Default settings
172
+
173
+ | Key | Default | Purpose |
174
+ |---|---|---|
175
+ | `strip_whitespace` | `True` | collapse whitespace in text nodes |
176
+ | `encoding` | `utf-8` | fallback when bytes input has no detectable encoding |
177
+ | `max_depth` | `100` | maximum element nesting |
178
+ | `preserve_comments` | `False` | keep comments in the tree |
179
+ | `strict_mode` | `False` | raise on malformed HTML |
180
+ | `remove_comments` | `True` | strip comments when minifying |
181
+ | `allowed_tags` | `p, br, b, i, a, ul, ol, li, div, span` | sanitize whitelist |
182
+ | `max_file_size_mb` | `10` | reject oversized input |
183
+ | `cache_size` | `50` | parse cache capacity |
184
+
185
+ ## Exceptions
186
+
187
+ `HTMLPaferError` (base), `ConfigLoadError`, `SanitizationError`,
188
+ `SelectorSyntaxError`.
@@ -0,0 +1,164 @@
1
+ ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
2
+ ![Dependencies](https://img.shields.io/badge/dependencies-none-success)
3
+ ![Tests](https://img.shields.io/badge/tests-pytest-informational)
4
+
5
+ ## Features
6
+
7
+ - Tree-based parsing into an editable document tree
8
+ - CSS selectors: combinators, attributes, pseudo-classes, selector lists
9
+ - Extractors: metadata, links, tables, text, images, forms, headings,
10
+ scripts, stylesheets
11
+ - Sanitization with a removal report
12
+ - Minify, pretty-print, Markdown conversion
13
+ - JSON / CSV / Markdown export
14
+ - CLI tool
15
+ - Standard library only at runtime
16
+
17
+ ## Installation
18
+
19
+ ```
20
+ pip install webreader
21
+ ```
22
+
23
+ ## Quickstart
24
+
25
+ ```python
26
+ import webreader
27
+
28
+ html = '<h1 class="title">Hello</h1><a href="/x">link</a>'
29
+ doc = webreader.parse(html)
30
+
31
+ webreader.select(doc, 'h1.title') # [{'tag': ..., 'attrs': ..., 'text': ..., 'html': ...}]
32
+ doc.find('a').get_attr('href') # '/x'
33
+
34
+ webreader.minify(html) # collapsed whitespace
35
+ webreader.pretty(html) # re-indented, readable markup
36
+ webreader.to_markdown(html) # convert to Markdown
37
+
38
+ clean, report = webreader.sanitize(html)
39
+ ```
40
+
41
+ Bytes input is decoded using detected encoding: BOM sniffing first,
42
+ then `<meta charset>` in the first 2 KB, then the configured
43
+ `encoding` fallback.
44
+
45
+ In non-strict mode, malformed HTML does not raise; the warnings are
46
+ available on the document:
47
+
48
+ ```python
49
+ doc = webreader.parse('<div><p>x</p>') # unclosed <div>
50
+ doc.errors # ['unclosed tag(s) at end of input: <div>']
51
+ ```
52
+
53
+ ## Extraction
54
+
55
+ ```python
56
+ webreader.extract_metadata(html) # title, OG/Twitter, canonical, charset, JSON-LD
57
+ webreader.extract_links(html) # links and asset references
58
+ webreader.extract_tables(html) # matrices with colspan/rowspan expanded
59
+ webreader.extract_text(html) # readable text without boilerplate
60
+ webreader.extract_images(html) # src, alt, dimensions, srcset candidates
61
+ webreader.extract_forms(html) # form fields with types and defaults
62
+ webreader.extract_headings(html) # h1-h6 outline + word count + reading time
63
+ webreader.extract_scripts(html) # inline and external script inventory
64
+ webreader.extract_stylesheets(html) # link[rel~=stylesheet] and inline styles
65
+ ```
66
+
67
+ ## CSS selectors
68
+
69
+ Supported syntax: type selectors, `*`, `#id`, `.class`, attribute
70
+ selectors (`[attr]`, `[attr=value]` and the `^=`, `$=`, `*=`, `~=`,
71
+ `|=` operators), the pseudo-classes `:first-child`, `:last-child`,
72
+ `:nth-child(an+b)`, `:not(compound)` and `:contains(text)`, compounds
73
+ such as `div.content#main[href="x"]`, the combinators whitespace
74
+ (descendant), `>` (child), `+` (adjacent sibling) and `~` (general
75
+ sibling), and comma-separated selector lists.
76
+
77
+ ```python
78
+ webreader.select(doc, 'a[href^="https://"]')
79
+ webreader.select(doc, 'ul > li:nth-child(2n+1):not(.skip)')
80
+ webreader.select(doc, 'h2 + p, blockquote p:contains("note")')
81
+ ```
82
+
83
+ Invalid or unsupported selectors raise `SelectorSyntaxError`.
84
+
85
+ ## Tree editing
86
+
87
+ Nodes support a mutation API, and the edited tree can be
88
+ re-serialized:
89
+
90
+ ```python
91
+ doc = webreader.parse('<div><a href="/x">link</a></div>')
92
+ link = doc.find('a')
93
+ link.set_attr('rel', 'noopener').remove_attr('class')
94
+
95
+ new = webreader.Node('p')
96
+ new.children.append('added text')
97
+ doc.find('div').append_child(new)
98
+
99
+ link.unwrap() # replace the <a> with its inner text
100
+ doc.to_html() # '<div>link<p>added text</p></div>'
101
+ ```
102
+
103
+ `Node` also provides `remove()` and `replace_with(*nodes)`.
104
+
105
+ ## Markdown conversion
106
+
107
+ `to_markdown` renders headings, paragraphs, links, images,
108
+ emphasis/strong/strikethrough, inline and fenced code (with language
109
+ detection from `class="language-..."`), nested lists, blockquotes,
110
+ horizontal rules and tables.
111
+
112
+ ```python
113
+ webreader.to_markdown('<h1>T</h1><p>Hi <b>bold</b></p>')
114
+ # '# T\n\nHi **bold**'
115
+ ```
116
+
117
+ ## Sanitization
118
+
119
+ `sanitize()` returns `(clean_html, report)`:
120
+
121
+ - tags outside the whitelist are unwrapped, inner content kept
122
+ - `script`, `style`, `noscript`, `template` are dropped entirely
123
+ - attributes filtered through a safe-list, `on*` handlers removed
124
+ - `javascript:`, `vbscript:` and non-image `data:` URLs blocked
125
+ - the report lists removed tags/attributes, blocked URLs and detected threats
126
+
127
+ ## CLI
128
+
129
+ ```
130
+ python -m webreader parse -f input.html
131
+ python -m webreader select -f input.html -s 'a[href]'
132
+ python -m webreader links -f input.html --base-url https://example.com
133
+ python -m webreader meta -f input.html
134
+ python -m webreader tables -f input.html --format csv
135
+ python -m webreader text -f input.html
136
+ python -m webreader images -f input.html
137
+ python -m webreader forms -f input.html
138
+ python -m webreader minify -f input.html --out min.html
139
+ python -m webreader pretty -f input.html --out pretty.html
140
+ python -m webreader md -f input.html --out page.md
141
+ python -m webreader sanitize -f input.html --out clean.html --allowed p,a,ul
142
+ ```
143
+
144
+ Use `-f -` to read from stdin. Every output command accepts `--out`
145
+ to write to a file instead of stdout.
146
+
147
+ ### Default settings
148
+
149
+ | Key | Default | Purpose |
150
+ |---|---|---|
151
+ | `strip_whitespace` | `True` | collapse whitespace in text nodes |
152
+ | `encoding` | `utf-8` | fallback when bytes input has no detectable encoding |
153
+ | `max_depth` | `100` | maximum element nesting |
154
+ | `preserve_comments` | `False` | keep comments in the tree |
155
+ | `strict_mode` | `False` | raise on malformed HTML |
156
+ | `remove_comments` | `True` | strip comments when minifying |
157
+ | `allowed_tags` | `p, br, b, i, a, ul, ol, li, div, span` | sanitize whitelist |
158
+ | `max_file_size_mb` | `10` | reject oversized input |
159
+ | `cache_size` | `50` | parse cache capacity |
160
+
161
+ ## Exceptions
162
+
163
+ `HTMLPaferError` (base), `ConfigLoadError`, `SanitizationError`,
164
+ `SelectorSyntaxError`.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=78.0.2", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "webreader"
7
+ version = "2.3.8"
8
+ description = "Tree-based HTML reader for Python."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ {name = "morichan", email = "morichan@gmail.com"}
13
+ ]
14
+ requires-python = ">=3.8"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: User Interfaces",
25
+ ]
26
+
27
+ dependencies = [
28
+ "pafer"
29
+ ]
30
+
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest",
35
+ "pytest-cov",
36
+ ]
37
+
38
+ [tool.setuptools.package-data]
39
+ webreader = [
40
+ "*.config"
41
+ ]
42
+
43
+ [tool.setuptools]
44
+ packages = ["webreader"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ packages=find_packages(),
5
+ include_package_data=True,
6
+ )
@@ -0,0 +1,165 @@
1
+ """Tree-based HTML parsing, extraction and sanitization."""
2
+
3
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
4
+
5
+ import pafer
6
+ from pafer.exceptions import (
7
+ ConfigLoadError,
8
+ HTMLPaferError,
9
+ SanitizationError,
10
+ SelectorSyntaxError,
11
+ )
12
+
13
+ from . import core as _core
14
+ from .core import Engine, Node, ParsedDocument, get_config, get_engine, load_config
15
+
16
+ def _as_doc(target: Union[str, bytes, ParsedDocument]) -> ParsedDocument:
17
+ if isinstance(target, ParsedDocument):
18
+ return target
19
+ return _core.get_engine().parse(target)
20
+
21
+
22
+ def parse(html: Union[str, bytes]) -> ParsedDocument:
23
+ """Parse HTML into a ParsedDocument using the active configuration."""
24
+ return _core.get_engine().parse(html)
25
+
26
+
27
+ def select(target: Union[str, bytes, ParsedDocument],
28
+ selector: str) -> List[Dict[str, Any]]:
29
+ """Run a CSS selector; returns dicts with tag/attrs/text/html per match."""
30
+ return [node.to_dict() for node in _as_doc(target).find_all(selector)]
31
+
32
+
33
+ def extract_metadata(target: Union[str, bytes, ParsedDocument]) -> Dict[str, Any]:
34
+ """Extract title, OG/Twitter, canonical, lang, charset and JSON-LD."""
35
+ return _as_doc(target).metadata()
36
+
37
+
38
+ def extract_links(target: Union[str, bytes, ParsedDocument],
39
+ base_url: Optional[str] = None) -> List[Dict[str, Any]]:
40
+ """Extract links and asset references, absolutized against base_url."""
41
+ return _as_doc(target).links(base_url=base_url)
42
+
43
+
44
+ def extract_tables(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
45
+ """Extract tables as header/data matrices with span expansion."""
46
+ return _as_doc(target).tables()
47
+
48
+
49
+ def extract_text(target: Union[str, bytes, ParsedDocument]) -> str:
50
+ """Extract readable text with boilerplate removal."""
51
+ return _as_doc(target).text()
52
+
53
+
54
+ def sanitize(target: Union[str, bytes, ParsedDocument],
55
+ allowed_tags: Optional[Set[str]] = None) -> Tuple[str, Dict[str, Any]]:
56
+ """Return (clean_html, removal_report) using config allowed_tags by default."""
57
+ from .sanitizer import sanitize_document
58
+ return sanitize_document(_as_doc(target), allowed_tags=allowed_tags)
59
+
60
+
61
+ def minify(html: str) -> str:
62
+ """Minify HTML honoring remove_comments from the active configuration."""
63
+ return _core.get_engine().minify(html)
64
+
65
+
66
+ def pretty(html: Union[str, bytes], indent: str = ' ') -> str:
67
+ """Reformat HTML into indented, readable markup."""
68
+ from .formatter import pretty_html
69
+ return pretty_html(html, indent=indent, config=_core.config)
70
+
71
+
72
+ def to_markdown(target: Union[str, bytes, ParsedDocument]) -> str:
73
+ """Convert HTML or a parsed document to Markdown."""
74
+ from .markdown import to_markdown as _to_md
75
+ return _to_md(target, config=_core.config)
76
+
77
+ def extract_images(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
78
+ """Extract img elements with src, alt, dimensions and srcset candidates."""
79
+ return _as_doc(target).images()
80
+
81
+
82
+ def extract_forms(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
83
+ """Extract forms with their input fields and default values."""
84
+ return _as_doc(target).forms()
85
+
86
+
87
+ def extract_headings(target: Union[str, bytes, ParsedDocument]) -> Dict[str, Any]:
88
+ """Extract the h1-h6 outline plus word count and reading time."""
89
+ return _as_doc(target).headings()
90
+
91
+
92
+ def extract_scripts(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
93
+ """Inventory script elements, both inline and external."""
94
+ return _as_doc(target).scripts()
95
+
96
+
97
+ def extract_stylesheets(target: Union[str, bytes, ParsedDocument]) -> List[Dict[str, Any]]:
98
+ """Inventory stylesheets: link[rel~=stylesheet] and inline styles."""
99
+ return _as_doc(target).stylesheets()
100
+
101
+
102
+ def export(data: Any, fmt: str = 'json', indent: int = 2) -> str:
103
+ """Export data to json, csv or markdown using pafer.exporters."""
104
+ if fmt == 'json':
105
+ return pafer.exporters.DataExporter.to_json(data, indent=indent)
106
+ if fmt == 'csv':
107
+ if isinstance(data, list) and data and all(isinstance(row, dict) for row in data):
108
+ headers = list(data[0].keys())
109
+ rows = [[str(row.get(header, '')) for header in headers] for row in data]
110
+ return pafer.exporters.DataExporter.to_csv([headers] + rows)
111
+ return pafer.exporters.DataExporter.to_csv(data)
112
+ if fmt == 'md':
113
+ return _export_markdown(data)
114
+ raise ValueError(f'unsupported export format: {fmt!r}')
115
+
116
+
117
+ def _export_markdown(data: Any) -> str:
118
+ if isinstance(data, list) and data and all(isinstance(item, dict) and 'rows' in item
119
+ for item in data):
120
+ lines: List[str] = []
121
+ for table in data:
122
+ matrix = list(table.get('headers', [])) + list(table.get('rows', []))
123
+ width = max((len(row) for row in matrix), default=0)
124
+ if not width:
125
+ continue
126
+ padded = [list(row) + [''] * (width - len(row)) for row in matrix]
127
+ lines.append('| ' + ' | '.join(str(cell) for cell in padded[0]) + ' |')
128
+ lines.append('|' + '---|' * width)
129
+ lines.extend(
130
+ '| ' + ' | '.join(str(cell) for cell in row) + ' |'
131
+ for row in padded[1:]
132
+ )
133
+ lines.append('')
134
+ return '\n'.join(lines)
135
+ return '```json\n' + pafer.exporters.DataExporter.to_json(data) + '\n```'
136
+
137
+
138
+ __all__ = [
139
+ 'Engine',
140
+ 'Node',
141
+ 'ParsedDocument',
142
+ 'load_config',
143
+ 'get_config',
144
+ 'get_engine',
145
+ 'parse',
146
+ 'select',
147
+ 'extract_metadata',
148
+ 'extract_links',
149
+ 'extract_tables',
150
+ 'extract_text',
151
+ 'sanitize',
152
+ 'minify',
153
+ 'pretty',
154
+ 'to_markdown',
155
+ 'extract_images',
156
+ 'extract_forms',
157
+ 'extract_headings',
158
+ 'extract_scripts',
159
+ 'extract_stylesheets',
160
+ 'export',
161
+ 'HTMLPaferError',
162
+ 'ConfigLoadError',
163
+ 'SanitizationError',
164
+ 'SelectorSyntaxError',
165
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow running the package as `python -m webreader`."""
2
+
3
+ from .cli import run
4
+
5
+ if __name__ == '__main__':
6
+ run()