pyhtml2text 0.1.0__tar.gz → 0.2.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.
- pyhtml2text-0.2.0/CHANGELOG.md +21 -0
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/Cargo.lock +43 -4
- pyhtml2text-0.2.0/Cargo.toml +32 -0
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/PKG-INFO +28 -5
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/README.md +26 -3
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/pyproject.toml +15 -2
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/python/pyhtml2text/__init__.pyi +3 -5
- pyhtml2text-0.2.0/src/lib.rs +99 -0
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/tests/test_basic.py +31 -0
- pyhtml2text-0.2.0/vendor/README.md +13 -0
- pyhtml2text-0.2.0/vendor/html5ever/Cargo.toml +61 -0
- pyhtml2text-0.2.0/vendor/html5ever/LICENSE-APACHE +201 -0
- pyhtml2text-0.2.0/vendor/html5ever/LICENSE-MIT +25 -0
- pyhtml2text-0.2.0/vendor/html5ever/README.md +60 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/driver.rs +162 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/encoding.rs +173 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/lib.rs +31 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/macros.rs +34 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/serialize/mod.rs +255 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tokenizer/char_ref/mod.rs +438 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tokenizer/interface.rs +141 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tokenizer/mod.rs +2324 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tokenizer/states.rs +131 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tree_builder/data.rs +170 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tree_builder/mod.rs +1917 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tree_builder/open_elems.rs +134 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tree_builder/rules.rs +1690 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tree_builder/tag_sets.rs +109 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/tree_builder/types.rs +111 -0
- pyhtml2text-0.2.0/vendor/html5ever/src/util/str.rs +52 -0
- pyhtml2text-0.1.0/.github/workflows/CI.yml +0 -298
- pyhtml2text-0.1.0/Cargo.toml +0 -21
- pyhtml2text-0.1.0/src/lib.rs +0 -62
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/.gitignore +0 -0
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/LICENSE +0 -0
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/python/pyhtml2text/__init__.py +0 -0
- {pyhtml2text-0.1.0 → pyhtml2text-0.2.0}/python/pyhtml2text/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
- 1.5–1.7× faster than 0.1.0 on real web pages, and 2× faster on short
|
|
6
|
+
snippets. Output is unchanged.
|
|
7
|
+
- Wheels for Linux x86_64/aarch64, macOS and Windows x64 are built with
|
|
8
|
+
profile-guided optimisation.
|
|
9
|
+
- Uses mimalloc as the allocator (except on glibc armv7, ppc64le and s390x).
|
|
10
|
+
- Style rules are shared rather than rebuilt and copied for every call.
|
|
11
|
+
- Deeply nested HTML no longer takes quadratic time to parse: 40,000 nested
|
|
12
|
+
`<ul>` lists went from 6s to 0.14s (patched html5ever, see `vendor/`).
|
|
13
|
+
- `html` can be UTF-8 `bytes` as well as `str`.
|
|
14
|
+
- An internal error now raises `ValueError` rather than `PanicException`
|
|
15
|
+
(which `except Exception` doesn't catch).
|
|
16
|
+
- Supports free-threaded Python without re-enabling the GIL.
|
|
17
|
+
- Requires Python 3.10 or later (3.9 is end-of-life).
|
|
18
|
+
|
|
19
|
+
## 0.1.0 (2026-10-15)
|
|
20
|
+
|
|
21
|
+
- First release: `html2text()` with `base_url` support.
|
|
@@ -14,6 +14,16 @@ version = "2.13.2"
|
|
|
14
14
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
15
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
|
16
16
|
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "cc"
|
|
19
|
+
version = "1.8.0"
|
|
20
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
+
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"find-msvc-tools",
|
|
24
|
+
"shlex",
|
|
25
|
+
]
|
|
26
|
+
|
|
17
27
|
[[package]]
|
|
18
28
|
name = "cfg-if"
|
|
19
29
|
version = "1.0.5"
|
|
@@ -26,6 +36,12 @@ version = "2.5.0"
|
|
|
26
36
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
37
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
|
28
38
|
|
|
39
|
+
[[package]]
|
|
40
|
+
name = "find-msvc-tools"
|
|
41
|
+
version = "0.1.14"
|
|
42
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
43
|
+
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
|
44
|
+
|
|
29
45
|
[[package]]
|
|
30
46
|
name = "heck"
|
|
31
47
|
version = "0.5.0"
|
|
@@ -35,7 +51,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
|
35
51
|
[[package]]
|
|
36
52
|
name = "html2text"
|
|
37
53
|
version = "0.17.1"
|
|
38
|
-
source = "git+https://github.com/jaredlynskey/rust-html2text?rev=
|
|
54
|
+
source = "git+https://github.com/jaredlynskey/rust-html2text?rev=9c109fac38728040cf3c32b411305cdd840bd511#9c109fac38728040cf3c32b411305cdd840bd511"
|
|
39
55
|
dependencies = [
|
|
40
56
|
"html5ever",
|
|
41
57
|
"tendril",
|
|
@@ -46,8 +62,6 @@ dependencies = [
|
|
|
46
62
|
[[package]]
|
|
47
63
|
name = "html5ever"
|
|
48
64
|
version = "0.39.0"
|
|
49
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
50
|
-
checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
|
|
51
65
|
dependencies = [
|
|
52
66
|
"log",
|
|
53
67
|
"markup5ever",
|
|
@@ -68,6 +82,15 @@ version = "0.2.190"
|
|
|
68
82
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
69
83
|
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
|
70
84
|
|
|
85
|
+
[[package]]
|
|
86
|
+
name = "libmimalloc-sys"
|
|
87
|
+
version = "0.1.49"
|
|
88
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
89
|
+
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
|
|
90
|
+
dependencies = [
|
|
91
|
+
"cc",
|
|
92
|
+
]
|
|
93
|
+
|
|
71
94
|
[[package]]
|
|
72
95
|
name = "lock_api"
|
|
73
96
|
version = "0.4.14"
|
|
@@ -103,6 +126,15 @@ dependencies = [
|
|
|
103
126
|
"autocfg",
|
|
104
127
|
]
|
|
105
128
|
|
|
129
|
+
[[package]]
|
|
130
|
+
name = "mimalloc"
|
|
131
|
+
version = "0.1.52"
|
|
132
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
133
|
+
checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
|
|
134
|
+
dependencies = [
|
|
135
|
+
"libmimalloc-sys",
|
|
136
|
+
]
|
|
137
|
+
|
|
106
138
|
[[package]]
|
|
107
139
|
name = "new_debug_unreachable"
|
|
108
140
|
version = "1.0.6"
|
|
@@ -200,9 +232,10 @@ dependencies = [
|
|
|
200
232
|
|
|
201
233
|
[[package]]
|
|
202
234
|
name = "pyhtml2text"
|
|
203
|
-
version = "0.
|
|
235
|
+
version = "0.2.0"
|
|
204
236
|
dependencies = [
|
|
205
237
|
"html2text",
|
|
238
|
+
"mimalloc",
|
|
206
239
|
"pyo3",
|
|
207
240
|
]
|
|
208
241
|
|
|
@@ -326,6 +359,12 @@ dependencies = [
|
|
|
326
359
|
"syn 3.0.9",
|
|
327
360
|
]
|
|
328
361
|
|
|
362
|
+
[[package]]
|
|
363
|
+
name = "shlex"
|
|
364
|
+
version = "2.0.1"
|
|
365
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
366
|
+
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
367
|
+
|
|
329
368
|
[[package]]
|
|
330
369
|
name = "siphasher"
|
|
331
370
|
version = "1.0.4"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "pyhtml2text"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
publish = false
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "_pyhtml2text"
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
pyo3 = { version = "0.26", features = ["extension-module", "abi3-py310"] }
|
|
15
|
+
# Fork of jugglerchris/rust-html2text with Config::base_url and some
|
|
16
|
+
# performance changes (branch pyhtml2text-perf).
|
|
17
|
+
html2text = { git = "https://github.com/jaredlynskey/rust-html2text", rev = "9c109fac38728040cf3c32b411305cdd840bd511" }
|
|
18
|
+
|
|
19
|
+
# Much faster than the system allocators for html2text's many small allocations.
|
|
20
|
+
# Not on glibc armv7/ppc64le/s390x: their manylinux2014 cross compilers are too
|
|
21
|
+
# old to build it (keep in sync with the cfg in src/lib.rs).
|
|
22
|
+
[target.'cfg(not(all(target_env = "gnu", any(target_arch = "arm", target_arch = "powerpc64", target_arch = "s390x"))))'.dependencies]
|
|
23
|
+
mimalloc = { version = "0.1", default-features = false, features = ["local_dynamic_tls"] }
|
|
24
|
+
|
|
25
|
+
[patch.crates-io]
|
|
26
|
+
# Fixes quadratic parsing of deeply nested HTML, see vendor/README.md.
|
|
27
|
+
html5ever = { path = "vendor/html5ever" }
|
|
28
|
+
|
|
29
|
+
[profile.release]
|
|
30
|
+
lto = true
|
|
31
|
+
codegen-units = 1
|
|
32
|
+
strip = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyhtml2text
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
@@ -9,7 +9,7 @@ Summary: Fast HTML to plain text, with relative link resolution (Python bindings
|
|
|
9
9
|
Keywords: html,text,markdown,html2text
|
|
10
10
|
Author-email: Jared Lynskey <jared@lynskey.co.nz>
|
|
11
11
|
License-Expression: MIT
|
|
12
|
-
Requires-Python: >=3.
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
13
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
14
14
|
Project-URL: Source, https://github.com/jaredlynskey/pyhtml2text
|
|
15
15
|
Project-URL: Upstream, https://github.com/jugglerchris/rust-html2text
|
|
@@ -33,6 +33,9 @@ print(html2text(html, base_url="https://example.com/docs/page.html"))
|
|
|
33
33
|
# [1]: https://example.com/login
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
`html` can be a `str` or UTF-8 `bytes` (e.g. `response.content`). Requires
|
|
37
|
+
Python 3.10 or later.
|
|
38
|
+
|
|
36
39
|
## Options
|
|
37
40
|
|
|
38
41
|
`html2text(html, width=80, *, ...)`
|
|
@@ -47,10 +50,30 @@ print(html2text(html, base_url="https://example.com/docs/page.html"))
|
|
|
47
50
|
| `max_wrap_width` | `None` | wrap paragraphs narrower than `width` |
|
|
48
51
|
| `raw` | `False` | flatten tables so cell text stays together |
|
|
49
52
|
|
|
53
|
+
## Performance
|
|
54
|
+
|
|
55
|
+
Median time to render a page, on an Apple Silicon Mac:
|
|
56
|
+
|
|
57
|
+
| | Wikipedia article (780 KB) | BBC News front page (370 KB) |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| [html2text](https://pypi.org/project/html2text/) (pure Python) | 71.5 ms | 14.9 ms |
|
|
60
|
+
| [html2text-rs](https://pypi.org/project/html2text-rs/) | 15.2 ms | 2.5 ms |
|
|
61
|
+
| **pyhtml2text** | **10.9 ms** | **1.6 ms** |
|
|
62
|
+
|
|
63
|
+
- The GIL is released while rendering, so threads scale across cores. On
|
|
64
|
+
free-threaded Python (3.14t) the module runs without re-enabling the GIL.
|
|
65
|
+
- Wheels for Linux (x86_64, aarch64), macOS and Windows x64 are built with
|
|
66
|
+
profile-guided optimisation, which makes them about 25% faster. Other
|
|
67
|
+
platforms get the same code without it.
|
|
68
|
+
- Deeply nested HTML is parsed in linear time, so hostile input can't stall a
|
|
69
|
+
worker: 40,000 nested lists take 0.14 s, against 6 s with stock html5ever.
|
|
70
|
+
|
|
50
71
|
## Credits
|
|
51
72
|
|
|
52
73
|
The rendering is done by rust-html2text by Chris Emerson (MIT). This package
|
|
53
|
-
uses [a fork](https://github.com/jaredlynskey/rust-html2text/tree/
|
|
54
|
-
that adds `base_url
|
|
55
|
-
|
|
74
|
+
uses [a fork](https://github.com/jaredlynskey/rust-html2text/tree/pyhtml2text-perf)
|
|
75
|
+
that adds `base_url` and some performance changes, and a patched copy of
|
|
76
|
+
[html5ever](https://github.com/servo/html5ever) (MIT/Apache-2.0, see
|
|
77
|
+
`vendor/`). It isn't affiliated with or endorsed by either upstream project, so
|
|
78
|
+
please report issues here rather than upstream.
|
|
56
79
|
|
|
@@ -17,6 +17,9 @@ print(html2text(html, base_url="https://example.com/docs/page.html"))
|
|
|
17
17
|
# [1]: https://example.com/login
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
`html` can be a `str` or UTF-8 `bytes` (e.g. `response.content`). Requires
|
|
21
|
+
Python 3.10 or later.
|
|
22
|
+
|
|
20
23
|
## Options
|
|
21
24
|
|
|
22
25
|
`html2text(html, width=80, *, ...)`
|
|
@@ -31,9 +34,29 @@ print(html2text(html, base_url="https://example.com/docs/page.html"))
|
|
|
31
34
|
| `max_wrap_width` | `None` | wrap paragraphs narrower than `width` |
|
|
32
35
|
| `raw` | `False` | flatten tables so cell text stays together |
|
|
33
36
|
|
|
37
|
+
## Performance
|
|
38
|
+
|
|
39
|
+
Median time to render a page, on an Apple Silicon Mac:
|
|
40
|
+
|
|
41
|
+
| | Wikipedia article (780 KB) | BBC News front page (370 KB) |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| [html2text](https://pypi.org/project/html2text/) (pure Python) | 71.5 ms | 14.9 ms |
|
|
44
|
+
| [html2text-rs](https://pypi.org/project/html2text-rs/) | 15.2 ms | 2.5 ms |
|
|
45
|
+
| **pyhtml2text** | **10.9 ms** | **1.6 ms** |
|
|
46
|
+
|
|
47
|
+
- The GIL is released while rendering, so threads scale across cores. On
|
|
48
|
+
free-threaded Python (3.14t) the module runs without re-enabling the GIL.
|
|
49
|
+
- Wheels for Linux (x86_64, aarch64), macOS and Windows x64 are built with
|
|
50
|
+
profile-guided optimisation, which makes them about 25% faster. Other
|
|
51
|
+
platforms get the same code without it.
|
|
52
|
+
- Deeply nested HTML is parsed in linear time, so hostile input can't stall a
|
|
53
|
+
worker: 40,000 nested lists take 0.14 s, against 6 s with stock html5ever.
|
|
54
|
+
|
|
34
55
|
## Credits
|
|
35
56
|
|
|
36
57
|
The rendering is done by rust-html2text by Chris Emerson (MIT). This package
|
|
37
|
-
uses [a fork](https://github.com/jaredlynskey/rust-html2text/tree/
|
|
38
|
-
that adds `base_url
|
|
39
|
-
|
|
58
|
+
uses [a fork](https://github.com/jaredlynskey/rust-html2text/tree/pyhtml2text-perf)
|
|
59
|
+
that adds `base_url` and some performance changes, and a patched copy of
|
|
60
|
+
[html5ever](https://github.com/servo/html5ever) (MIT/Apache-2.0, see
|
|
61
|
+
`vendor/`). It isn't affiliated with or endorsed by either upstream project, so
|
|
62
|
+
please report issues here rather than upstream.
|
|
@@ -4,12 +4,12 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyhtml2text"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Fast HTML to plain text, with relative link resolution (Python bindings to rust-html2text)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
13
|
authors = [{ name = "Jared Lynskey", email = "jared@lynskey.co.nz" }]
|
|
14
14
|
keywords = ["html", "text", "markdown", "html2text"]
|
|
15
15
|
classifiers = [
|
|
@@ -25,6 +25,19 @@ Upstream = "https://github.com/jugglerchris/rust-html2text"
|
|
|
25
25
|
[tool.maturin]
|
|
26
26
|
python-source = "python"
|
|
27
27
|
module-name = "pyhtml2text._pyhtml2text"
|
|
28
|
+
# cargo leaves out vendor/html5ever (it has its own Cargo.toml), but the
|
|
29
|
+
# [patch] in Cargo.toml needs it; the PGO data and CI files aren't needed.
|
|
30
|
+
include = [
|
|
31
|
+
{ path = "vendor/html5ever/Cargo.toml", format = "sdist" },
|
|
32
|
+
{ path = "vendor/html5ever/LICENSE-*", format = "sdist" },
|
|
33
|
+
{ path = "vendor/html5ever/README.md", format = "sdist" },
|
|
34
|
+
{ path = "vendor/html5ever/src/**/*.rs", format = "sdist" },
|
|
35
|
+
]
|
|
36
|
+
exclude = [
|
|
37
|
+
{ path = "pgo/**/*", format = "sdist" },
|
|
38
|
+
{ path = "bench/**/*", format = "sdist" },
|
|
39
|
+
{ path = ".github/**/*", format = "sdist" },
|
|
40
|
+
]
|
|
28
41
|
|
|
29
42
|
[tool.maturin.generate-ci.github]
|
|
30
43
|
pytest = true
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
def html2text(
|
|
4
|
-
html: str,
|
|
2
|
+
html: str | bytes,
|
|
5
3
|
width: int = 80,
|
|
6
4
|
*,
|
|
7
|
-
base_url:
|
|
5
|
+
base_url: str | None = None,
|
|
8
6
|
decorate: bool = True,
|
|
9
7
|
link_footnotes: bool = True,
|
|
10
8
|
table_borders: bool = True,
|
|
11
9
|
wrap_links: bool = True,
|
|
12
|
-
max_wrap_width:
|
|
10
|
+
max_wrap_width: int | None = None,
|
|
13
11
|
raw: bool = False,
|
|
14
12
|
) -> str: ...
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
2
|
+
|
|
3
|
+
use html2text::config;
|
|
4
|
+
use pyo3::exceptions::{PyTypeError, PyValueError};
|
|
5
|
+
use pyo3::prelude::*;
|
|
6
|
+
use pyo3::types::{PyBytes, PyString};
|
|
7
|
+
|
|
8
|
+
#[cfg(not(all(
|
|
9
|
+
target_env = "gnu",
|
|
10
|
+
any(target_arch = "arm", target_arch = "powerpc64", target_arch = "s390x")
|
|
11
|
+
)))]
|
|
12
|
+
#[global_allocator]
|
|
13
|
+
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
14
|
+
|
|
15
|
+
/// Render HTML as plain text.
|
|
16
|
+
#[pyfunction]
|
|
17
|
+
#[pyo3(name = "html2text", signature = (
|
|
18
|
+
html,
|
|
19
|
+
width = 80,
|
|
20
|
+
*,
|
|
21
|
+
base_url = None,
|
|
22
|
+
decorate = true,
|
|
23
|
+
link_footnotes = true,
|
|
24
|
+
table_borders = true,
|
|
25
|
+
wrap_links = true,
|
|
26
|
+
max_wrap_width = None,
|
|
27
|
+
raw = false,
|
|
28
|
+
))]
|
|
29
|
+
#[allow(clippy::too_many_arguments)]
|
|
30
|
+
fn render(
|
|
31
|
+
py: Python<'_>,
|
|
32
|
+
html: &Bound<'_, PyAny>,
|
|
33
|
+
width: usize,
|
|
34
|
+
base_url: Option<String>,
|
|
35
|
+
decorate: bool,
|
|
36
|
+
link_footnotes: bool,
|
|
37
|
+
table_borders: bool,
|
|
38
|
+
wrap_links: bool,
|
|
39
|
+
max_wrap_width: Option<usize>,
|
|
40
|
+
raw: bool,
|
|
41
|
+
) -> PyResult<String> {
|
|
42
|
+
// Borrow the input without copying it; bytes are taken to be UTF-8.
|
|
43
|
+
let input: &[u8] = if let Ok(s) = html.downcast::<PyString>() {
|
|
44
|
+
s.to_str()?.as_bytes()
|
|
45
|
+
} else if let Ok(b) = html.downcast::<PyBytes>() {
|
|
46
|
+
b.as_bytes()
|
|
47
|
+
} else {
|
|
48
|
+
return Err(PyTypeError::new_err(format!(
|
|
49
|
+
"html must be str or bytes, not {}",
|
|
50
|
+
html.get_type().name()?
|
|
51
|
+
)));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let mut conf = if decorate {
|
|
55
|
+
config::plain()
|
|
56
|
+
} else {
|
|
57
|
+
config::plain_no_decorate()
|
|
58
|
+
};
|
|
59
|
+
conf = conf.link_footnotes(link_footnotes).allow_width_overflow();
|
|
60
|
+
if let Some(base) = base_url {
|
|
61
|
+
conf = conf.base_url(base);
|
|
62
|
+
}
|
|
63
|
+
if !table_borders {
|
|
64
|
+
conf = conf.no_table_borders();
|
|
65
|
+
}
|
|
66
|
+
if !wrap_links {
|
|
67
|
+
conf = conf.no_link_wrapping();
|
|
68
|
+
}
|
|
69
|
+
if let Some(w) = max_wrap_width {
|
|
70
|
+
conf = conf.max_wrap_width(w);
|
|
71
|
+
}
|
|
72
|
+
if raw {
|
|
73
|
+
conf = conf.raw_mode(true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Rendering is pure Rust, so let other Python threads run meanwhile. A
|
|
77
|
+
// panic would otherwise surface as PanicException, which `except
|
|
78
|
+
// Exception` doesn't catch, so report it as a ValueError instead.
|
|
79
|
+
match py.detach(|| catch_unwind(AssertUnwindSafe(|| conf.string_from_read(input, width)))) {
|
|
80
|
+
Ok(Ok(text)) => Ok(text),
|
|
81
|
+
Ok(Err(e)) => Err(PyValueError::new_err(e.to_string())),
|
|
82
|
+
Err(panic) => {
|
|
83
|
+
let msg = panic
|
|
84
|
+
.downcast_ref::<&str>()
|
|
85
|
+
.copied()
|
|
86
|
+
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
|
|
87
|
+
.unwrap_or("unknown error");
|
|
88
|
+
Err(PyValueError::new_err(format!(
|
|
89
|
+
"html2text failed on this document: {msg}"
|
|
90
|
+
)))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[pymodule(gil_used = false)]
|
|
96
|
+
fn _pyhtml2text(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
97
|
+
m.add_function(wrap_pyfunction!(render, m)?)?;
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|
|
@@ -56,3 +56,34 @@ def test_tables():
|
|
|
56
56
|
def test_unicode_and_empty():
|
|
57
57
|
assert html2text("<p>한국어 텍스트 é</p>") == "한국어 텍스트 é\n"
|
|
58
58
|
assert html2text("") == ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_bytes_input():
|
|
62
|
+
assert html2text("<p>café</p>".encode()) == "café\n"
|
|
63
|
+
assert html2text(b'<a href="/x">x</a>', base_url=PAGE) == html2text(
|
|
64
|
+
'<a href="/x">x</a>', base_url=PAGE
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_rejects_other_types():
|
|
69
|
+
import pytest
|
|
70
|
+
|
|
71
|
+
with pytest.raises(TypeError, match="str or bytes"):
|
|
72
|
+
html2text(123)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_deep_nesting_is_linear():
|
|
76
|
+
# Parsing used to be quadratic in nesting depth (40,000 nested lists took
|
|
77
|
+
# 6s); with the patched html5ever this takes well under a second.
|
|
78
|
+
import platform
|
|
79
|
+
import sys
|
|
80
|
+
import time
|
|
81
|
+
|
|
82
|
+
html = "<ul><li>" * 100_000 + "x"
|
|
83
|
+
start = time.perf_counter()
|
|
84
|
+
out = html2text(html)
|
|
85
|
+
elapsed = time.perf_counter() - start
|
|
86
|
+
assert out.rstrip().endswith("x")
|
|
87
|
+
# Only time it where tests run natively; emulated CI runners are slow.
|
|
88
|
+
if platform.machine().lower() in ("x86_64", "amd64") or sys.platform == "darwin":
|
|
89
|
+
assert elapsed < 10, f"took {elapsed:.1f}s"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Vendored crates
|
|
2
|
+
|
|
3
|
+
## html5ever 0.39.0
|
|
4
|
+
|
|
5
|
+
Copied from the crates.io release (`src/`, licences, README and manifest;
|
|
6
|
+
examples, tests and benchmarks removed). Dual-licensed MIT / Apache-2.0,
|
|
7
|
+
see `html5ever/LICENSE-*`.
|
|
8
|
+
|
|
9
|
+
Patched (see git history of this directory) so that checking for an open `<p>`
|
|
10
|
+
element "in button scope" is O(1) instead of walking the whole stack of open
|
|
11
|
+
elements. That check runs for most block-level start tags, so deeply nested
|
|
12
|
+
input (e.g. tens of thousands of nested `<ul>`/`<div>`/`<blockquote>`) took
|
|
13
|
+
quadratic time to parse. Parse results are unchanged.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
|
2
|
+
#
|
|
3
|
+
# When uploading crates to the registry Cargo will automatically
|
|
4
|
+
# "normalize" Cargo.toml files for maximal compatibility
|
|
5
|
+
# with all versions of Cargo and also rewrite `path` dependencies
|
|
6
|
+
# to registry (e.g., crates.io) dependencies.
|
|
7
|
+
#
|
|
8
|
+
# If you are reading this file be aware that the original Cargo.toml
|
|
9
|
+
# will likely look very different (and much more reasonable).
|
|
10
|
+
# See Cargo.toml.orig for the original contents.
|
|
11
|
+
|
|
12
|
+
[package]
|
|
13
|
+
edition = "2021"
|
|
14
|
+
rust-version = "1.71.0"
|
|
15
|
+
name = "html5ever"
|
|
16
|
+
version = "0.39.0"
|
|
17
|
+
authors = ["The html5ever Project Developers"]
|
|
18
|
+
build = false
|
|
19
|
+
autolib = false
|
|
20
|
+
autobins = false
|
|
21
|
+
autoexamples = false
|
|
22
|
+
autotests = false
|
|
23
|
+
autobenches = false
|
|
24
|
+
description = "High-performance browser-grade HTML5 parser"
|
|
25
|
+
documentation = "https://docs.rs/html5ever"
|
|
26
|
+
readme = "README.md"
|
|
27
|
+
keywords = [
|
|
28
|
+
"html",
|
|
29
|
+
"html5",
|
|
30
|
+
"parser",
|
|
31
|
+
"parsing",
|
|
32
|
+
]
|
|
33
|
+
categories = [
|
|
34
|
+
"parser-implementations",
|
|
35
|
+
"web-programming",
|
|
36
|
+
]
|
|
37
|
+
license = "MIT OR Apache-2.0"
|
|
38
|
+
repository = "https://github.com/servo/html5ever"
|
|
39
|
+
|
|
40
|
+
[features]
|
|
41
|
+
serde = ["markup5ever/serde"]
|
|
42
|
+
trace_tokenizer = []
|
|
43
|
+
|
|
44
|
+
[lib]
|
|
45
|
+
name = "html5ever"
|
|
46
|
+
path = "src/lib.rs"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
[dependencies.log]
|
|
56
|
+
version = "0.4"
|
|
57
|
+
|
|
58
|
+
[dependencies.markup5ever]
|
|
59
|
+
version = "0.39"
|
|
60
|
+
|
|
61
|
+
|