Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/pytest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Installs the wheel built into dist/ into a fresh virtualenv for each Python
# version given (or the default Python, with none) and runs the tests.
set -euo pipefail

# Only uv's own Pythons: otherwise a free-threaded 3.15t that the runner has
# for building would do for "3.15".
export UV_PYTHON_PREFERENCE=only-managed

for py in "${@:-}"; do
rm -rf .venv-test
uv venv -q ${py:+-p "$py"} .venv-test
if [ -d .venv-test/Scripts ]; then python=.venv-test/Scripts/python.exe; else python=.venv-test/bin/python; fi
uv pip install -q -p "$python" pyhtml2text --no-index --no-deps --find-links dist --reinstall
uv pip install -q -p "$python" pyhtml2text pytest markdown-it-py html5lib
"$python" -c 'import sys; print(sys.version)'
"$python" -m pytest
done
68 changes: 31 additions & 37 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i python3.14t
args: --release --out dist -i python3.14t python3.15t
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: auto
rust-toolchain: ${{ env.PGO_RUST_TOOLCHAIN }}
Expand All @@ -68,18 +68,11 @@ jobs:
with:
name: wheels-linux-${{ matrix.platform.target }}
path: dist
- uses: astral-sh/setup-uv@v10.3.2
- uses: astral-sh/setup-uv@v10.3.3
if: ${{ matrix.platform.target == 'x86_64' || matrix.platform.target == 'aarch64' }}
- name: pytest
if: ${{ matrix.platform.target == 'x86_64' || matrix.platform.target == 'aarch64' }}
shell: bash
run: |
set -e
uv venv .venv
source .venv/bin/activate
uv pip install pyhtml2text --no-index --no-deps --find-links dist --reinstall
uv pip install pyhtml2text pytest markdown-it-py html5lib
pytest
run: .github/pytest.sh 3.10 3.14t 3.15 3.15t
- name: pytest
if: ${{ contains(fromJSON('["armv7", "s390x", "ppc64le"]'), matrix.platform.target) }}
uses: uraimo/run-on-arch-action@v3
Expand Down Expand Up @@ -129,7 +122,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i python3.14t
args: --release --out dist -i python3.14t python3.15t
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: musllinux_1_2
- name: Upload wheels
Expand Down Expand Up @@ -176,12 +169,18 @@ jobs:
target: x64
python_arch: x64
pgo: true
pythons: 3.10 3.14t 3.15 3.15t
- runner: windows-latest
target: x86
python_arch: x86
pythons: >-
cpython-3.10-windows-x86 cpython-3.14t-windows-x86
cpython-3.15-windows-x86 cpython-3.15t-windows-x86
- runner: windows-11-arm
target: aarch64
python_arch: arm64
# There is no uv-managed Python 3.10 for arm64 Windows.
pythons: 3.11 3.14t 3.15 3.15t
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
Expand All @@ -198,34 +197,34 @@ jobs:
with:
target: ${{ matrix.platform.target }}
args: --release --out dist
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
# sccache before 0.19 can't parse the `-Tp <path>` that cc 1.9 and
# later pass cl.exe for mimalloc's C++ source (rust-lang/cc-rs#2063).
sccache: false
rust-toolchain: ${{ env.PGO_RUST_TOOLCHAIN }}
- uses: actions/setup-python@v7
with:
python-version: "3.14t"
python-version: |
3.14t
3.15t
architecture: ${{ matrix.platform.python_arch }}
- name: Build free-threaded wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i python3.14t
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
args: --release --out dist -i python3.14t python3.15t
# sccache before 0.19 can't parse the `-Tp <path>` that cc 1.9 and
# later pass cl.exe for mimalloc's C++ source (rust-lang/cc-rs#2063).
sccache: false
rust-toolchain: ${{ env.PGO_RUST_TOOLCHAIN }}
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-windows-${{ matrix.platform.target }}
path: dist
- uses: astral-sh/setup-uv@v10.3.2
- uses: astral-sh/setup-uv@v10.3.3
- name: pytest
shell: bash
run: |
set -e
uv venv .venv
source .venv/Scripts/activate
uv pip install pyhtml2text --no-index --no-deps --find-links dist --reinstall
uv pip install pyhtml2text pytest markdown-it-py html5lib
pytest
run: .github/pytest.sh ${{ matrix.platform.pythons }}

macos:
runs-on: ${{ matrix.platform.runner }}
Expand Down Expand Up @@ -258,28 +257,24 @@ jobs:
rust-toolchain: ${{ env.PGO_RUST_TOOLCHAIN }}
- uses: actions/setup-python@v7
with:
python-version: "3.14t"
python-version: |
3.14t
3.15t
- name: Build free-threaded wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist -i python3.14t
args: --release --out dist -i python3.14t python3.15t
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
rust-toolchain: ${{ env.PGO_RUST_TOOLCHAIN }}
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-macos-${{ matrix.platform.target }}
path: dist
- uses: astral-sh/setup-uv@v10.3.2
- uses: astral-sh/setup-uv@v10.3.3
- name: pytest
run: |
set -e
uv venv .venv
source .venv/bin/activate
uv pip install pyhtml2text --no-index --no-deps --find-links dist --reinstall
uv pip install pyhtml2text pytest markdown-it-py html5lib
pytest
run: .github/pytest.sh 3.10 3.14t 3.15 3.15t

sdist:
runs-on: ubuntu-latest
Expand All @@ -305,10 +300,9 @@ jobs:
name: pypi
url: https://pypi.org/p/pyhtml2text
permissions:
# Use to sign the release artifacts
# Trusted publishing to PyPI, and signing the attestations
id-token: write
# Used to upload release artifacts
contents: write
contents: read
# Used to generate artifact attestation
attestations: write
steps:
Expand All @@ -319,7 +313,7 @@ jobs:
subject-path: 'wheels-*/*'
- name: Install uv
if: ${{ startsWith(github.ref, 'refs/tags/') }}
uses: astral-sh/setup-uv@v10.3.2
uses: astral-sh/setup-uv@v10.3.3
- name: Publish to PyPI
if: ${{ startsWith(github.ref, 'refs/tags/') }}
# Trusted publishing (OIDC) - no token stored in the repo.
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
target/
dist/
.venv/
.venv*/
__pycache__/
*.so
.DS_Store
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## Unreleased

- Deeply nested lists, definitions, paragraphs and emphasis are rendered in
linear time again. In 0.4.0 each level cost time for every level around
it: 400,000 nested lists took 49 s, and now take 0.5 s. Output is
unchanged.
- Wheels for free-threaded Python 3.15 (3.15t), alongside 3.14t. The abi3
wheels already cover Python 3.15. CI now tests on 3.10, 3.14t, 3.15 and
3.15t.
- Character widths from Unicode 18 (unicode-width 0.2.3). The halfwidth
katakana sound marks ゙ and ゚ count one column, as terminals show them,
rather than none, and characters new in Unicode 18 (emoji, kana,
ideographic characters and combining marks) get their own widths. Other
text wraps as before.
- html5ever 0.40.1, still with
[servo/html5ever#800](https://github.com/servo/html5ever/pull/800).
Output is unchanged. Building from source needs Rust 1.85 or later.

## 0.4.0 (2026-10-22)

- `html2text()` is rendered by this package's own code. The library it used
Expand Down
47 changes: 24 additions & 23 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.30", features = ["abi3-py310"] }
# The HTML parser; patched below.
html5ever = "0.39"
# Display width of characters, for wrapping.
unicode-width = "=0.2.2"
html5ever = "0.40"
# Display width of characters, for wrapping. Pinned, since newer Unicode data
# can change the output.
unicode-width = "=0.2.3"
# Reads the Markdown back to check that emphasis parses as intended.
pulldown-cmark = { version = "0.13", default-features = false }

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ html = """<h2>Before you start</h2>
print(html2markdown(html, base_url="https://example.com/docs/intro.html"))
```

Output:

```markdown
## Before you start

Expand Down Expand Up @@ -100,12 +102,13 @@ To plain text:
| **pyhtml2text** | **7.0 ms** | **1.3 ms** |

- The GIL is released while converting, so threads scale across cores. On
free-threaded Python (3.14t) the module runs without re-enabling the GIL.
free-threaded Python (3.14t and 3.15t) the module runs without re-enabling
the GIL.
- Wheels for Linux (x86_64, aarch64), macOS and Windows x64 are built with
profile-guided optimisation, which makes them about 25% faster. Other
platforms get the same code without it.
- Deeply nested HTML is parsed in linear time, so hostile input can't stall a
worker: 40,000 nested lists take 0.14 s, against 6 s with stock html5ever,
worker: 40,000 nested lists take 0.02 s, against 18 s with stock html5ever,
and 100,000 levels of nesting followed by unmatched end tags take under
0.1 s.

Expand Down
Loading
Loading