htomd 0.1.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.
- htomd-0.1.0/CHANGELOG.md +17 -0
- htomd-0.1.0/CONTRIBUTING.md +33 -0
- htomd-0.1.0/LICENSE +21 -0
- htomd-0.1.0/MANIFEST.in +6 -0
- htomd-0.1.0/PKG-INFO +109 -0
- htomd-0.1.0/README.md +90 -0
- htomd-0.1.0/RELEASING.md +97 -0
- htomd-0.1.0/pyproject.toml +65 -0
- htomd-0.1.0/setup.cfg +4 -0
- htomd-0.1.0/src/htomd/__init__.py +37 -0
- htomd-0.1.0/src/htomd/__main__.py +6 -0
- htomd-0.1.0/src/htomd/_cli.py +92 -0
- htomd-0.1.0/src/htomd/_metadata.py +137 -0
- htomd-0.1.0/src/htomd/_models.py +28 -0
- htomd-0.1.0/src/htomd/_parser.py +133 -0
- htomd-0.1.0/src/htomd/_render.py +270 -0
- htomd-0.1.0/src/htomd/_selection.py +339 -0
- htomd-0.1.0/src/htomd/_tree.py +53 -0
- htomd-0.1.0/src/htomd/_urls.py +43 -0
- htomd-0.1.0/src/htomd/py.typed +0 -0
- htomd-0.1.0/src/htomd.egg-info/PKG-INFO +109 -0
- htomd-0.1.0/src/htomd.egg-info/SOURCES.txt +23 -0
- htomd-0.1.0/src/htomd.egg-info/dependency_links.txt +1 -0
- htomd-0.1.0/src/htomd.egg-info/entry_points.txt +2 -0
- htomd-0.1.0/src/htomd.egg-info/top_level.txt +1 -0
htomd-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-09-09
|
|
4
|
+
|
|
5
|
+
Initial alpha release of htomd, a pure Python HTML-to-Markdown and metadata
|
|
6
|
+
extractor for Python 3.12 and newer, with no runtime dependencies.
|
|
7
|
+
|
|
8
|
+
- `convert()` extracts Markdown from decoded HTML.
|
|
9
|
+
- `extract()` returns immutable Markdown, metadata, and diagnostic results.
|
|
10
|
+
- Source URLs resolve relative references without fetching network content.
|
|
11
|
+
- The `htomd` command and `python -m htomd` read UTF-8 HTML from stdin and write
|
|
12
|
+
Markdown or JSON, with help and installed-version commands.
|
|
13
|
+
- The package includes typing information and an MIT license.
|
|
14
|
+
|
|
15
|
+
Extraction is best suited to articles and documentation and may miss content or
|
|
16
|
+
retain clutter. JavaScript, browser layout, math, and SVG are unsupported. Simple
|
|
17
|
+
tables use GFM; complex tables become row/cell text.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Follow the [README setup](README.md#development), then install the hooks:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
mise exec -- uv run --locked pre-commit install
|
|
7
|
+
mise run hooks
|
|
8
|
+
mise run check
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Hooks apply Ruff safe fixes and formatting to staged Python files, excluding
|
|
12
|
+
fixtures. Review hook changes before staging. Checks include lint, formatting,
|
|
13
|
+
strict mypy, offline tests, and workflow validation.
|
|
14
|
+
|
|
15
|
+
Keep runtime dependencies empty and add focused regressions for behavior changes.
|
|
16
|
+
See the [fixture guide](tests/fixtures/real/README.md) when updating snapshots.
|
|
17
|
+
|
|
18
|
+
## Releases
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
mise run check
|
|
22
|
+
mise run build
|
|
23
|
+
mise exec -- uv run --locked twine check dist/*
|
|
24
|
+
mise exec -- env RELEASE_TAG=v0.1.0 uv run --locked python tools/check_release.py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
See [RELEASING.md](RELEASING.md) for the manual version, changelog, tag, and
|
|
28
|
+
GitHub Release steps.
|
|
29
|
+
|
|
30
|
+
The [release workflow](.github/workflows/release.yml) publishes to PyPI when a
|
|
31
|
+
GitHub Release is published. It runs CI, verifies the tag matches the package
|
|
32
|
+
version, validates both distributions, and uploads them to PyPI and the GitHub
|
|
33
|
+
Release. Pushing a branch or tag alone does not publish a package.
|
htomd-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JXD Ltd
|
|
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.
|
htomd-0.1.0/MANIFEST.in
ADDED
htomd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: htomd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Focused Markdown and metadata from messy HTML, in pure Python
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/jamiedavenport/htomd
|
|
7
|
+
Project-URL: Issues, https://github.com/jamiedavenport/htomd/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/jamiedavenport/htomd/blob/main/CHANGELOG.md
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# htomd
|
|
21
|
+
|
|
22
|
+
Extract Markdown and metadata from HTML. Pure Python 3.12+, with no runtime
|
|
23
|
+
dependencies or network access.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
python -m pip install htomd
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For the command line, install in an isolated tool environment:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
uv tool install htomd
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Python API
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import htomd
|
|
41
|
+
|
|
42
|
+
html = "<article><h1>Hello</h1><p>Readable text.</p></article>"
|
|
43
|
+
markdown = htomd.convert(html)
|
|
44
|
+
document = htomd.extract(html, url="https://example.com/article")
|
|
45
|
+
print(document.markdown, document.metadata.title, document.diagnostics)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Pass decoded HTML strings. `url` resolves relative references. `extract()` returns
|
|
49
|
+
immutable results; missing metadata is `None`. Empty content yields empty Markdown;
|
|
50
|
+
invalid argument types raise `TypeError`.
|
|
51
|
+
|
|
52
|
+
Best suited to articles and documentation. Extraction can miss content or retain
|
|
53
|
+
clutter. JavaScript, browser layout, math, and SVG are unsupported. Simple tables
|
|
54
|
+
use GFM; complex tables become row/cell text.
|
|
55
|
+
|
|
56
|
+
## Command line
|
|
57
|
+
|
|
58
|
+
Installing the package also installs the `htomd` command. To install a locally
|
|
59
|
+
built wheel in a virtual environment:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
python -m venv .venv
|
|
63
|
+
source .venv/bin/activate
|
|
64
|
+
python -m pip install dist/htomd-0.1.0-py3-none-any.whl
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In Windows PowerShell, activate with `.venv\Scripts\Activate.ps1` instead.
|
|
68
|
+
See [Contributing](https://github.com/jamiedavenport/htomd/blob/main/CONTRIBUTING.md#releases)
|
|
69
|
+
for the build commands.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
curl -s https://example.com/article | htomd convert --url https://example.com/article
|
|
73
|
+
cat page.html | htomd convert > page.md
|
|
74
|
+
cat page.html | htomd extract > page.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Both commands read stdin to EOF and accept no file arguments. Input must be UTF-8
|
|
78
|
+
(an optional UTF-8 BOM is accepted); output is UTF-8. `--url` supplies source
|
|
79
|
+
context for relative references and metadata; it never fetches the URL.
|
|
80
|
+
|
|
81
|
+
`convert` writes Markdown unchanged. `extract` writes indented JSON containing
|
|
82
|
+
`markdown`, `metadata`, and `diagnostics`, matching the Python result structure.
|
|
83
|
+
Missing metadata is `null`; diagnostic notes are an array. Empty or malformed
|
|
84
|
+
HTML receives the same best-effort handling as the library.
|
|
85
|
+
|
|
86
|
+
Run `htomd`, `htomd help`, or `htomd --help` for usage and pipeline examples.
|
|
87
|
+
Use `htomd help convert` or `htomd convert --help` for command-specific help
|
|
88
|
+
(likewise for `extract`). `htomd version` or `htomd --version` prints the installed
|
|
89
|
+
package version. Help and version commands exit successfully without reading stdin.
|
|
90
|
+
|
|
91
|
+
`python -m htomd` supports the same commands. Successful conversion exits with code 0, I/O and
|
|
92
|
+
decoding failures with code 1, and usage errors with code 2. Errors go to stderr.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
mise trust
|
|
98
|
+
mise install
|
|
99
|
+
mise run setup
|
|
100
|
+
mise exec -- uv run --locked pytest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`mise run check` runs all checks. See
|
|
104
|
+
[CONTRIBUTING.md](https://github.com/jamiedavenport/htomd/blob/main/CONTRIBUTING.md)
|
|
105
|
+
for hooks and releases.
|
|
106
|
+
|
|
107
|
+
MIT license, copyright 2026 JXD Ltd.
|
|
108
|
+
[Fixtures](https://github.com/jamiedavenport/htomd/blob/main/tests/fixtures/real/README.md)
|
|
109
|
+
have separate licenses.
|
htomd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# htomd
|
|
2
|
+
|
|
3
|
+
Extract Markdown and metadata from HTML. Pure Python 3.12+, with no runtime
|
|
4
|
+
dependencies or network access.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
python -m pip install htomd
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For the command line, install in an isolated tool environment:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
uv tool install htomd
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Python API
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import htomd
|
|
22
|
+
|
|
23
|
+
html = "<article><h1>Hello</h1><p>Readable text.</p></article>"
|
|
24
|
+
markdown = htomd.convert(html)
|
|
25
|
+
document = htomd.extract(html, url="https://example.com/article")
|
|
26
|
+
print(document.markdown, document.metadata.title, document.diagnostics)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Pass decoded HTML strings. `url` resolves relative references. `extract()` returns
|
|
30
|
+
immutable results; missing metadata is `None`. Empty content yields empty Markdown;
|
|
31
|
+
invalid argument types raise `TypeError`.
|
|
32
|
+
|
|
33
|
+
Best suited to articles and documentation. Extraction can miss content or retain
|
|
34
|
+
clutter. JavaScript, browser layout, math, and SVG are unsupported. Simple tables
|
|
35
|
+
use GFM; complex tables become row/cell text.
|
|
36
|
+
|
|
37
|
+
## Command line
|
|
38
|
+
|
|
39
|
+
Installing the package also installs the `htomd` command. To install a locally
|
|
40
|
+
built wheel in a virtual environment:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
python -m venv .venv
|
|
44
|
+
source .venv/bin/activate
|
|
45
|
+
python -m pip install dist/htomd-0.1.0-py3-none-any.whl
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In Windows PowerShell, activate with `.venv\Scripts\Activate.ps1` instead.
|
|
49
|
+
See [Contributing](https://github.com/jamiedavenport/htomd/blob/main/CONTRIBUTING.md#releases)
|
|
50
|
+
for the build commands.
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
curl -s https://example.com/article | htomd convert --url https://example.com/article
|
|
54
|
+
cat page.html | htomd convert > page.md
|
|
55
|
+
cat page.html | htomd extract > page.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Both commands read stdin to EOF and accept no file arguments. Input must be UTF-8
|
|
59
|
+
(an optional UTF-8 BOM is accepted); output is UTF-8. `--url` supplies source
|
|
60
|
+
context for relative references and metadata; it never fetches the URL.
|
|
61
|
+
|
|
62
|
+
`convert` writes Markdown unchanged. `extract` writes indented JSON containing
|
|
63
|
+
`markdown`, `metadata`, and `diagnostics`, matching the Python result structure.
|
|
64
|
+
Missing metadata is `null`; diagnostic notes are an array. Empty or malformed
|
|
65
|
+
HTML receives the same best-effort handling as the library.
|
|
66
|
+
|
|
67
|
+
Run `htomd`, `htomd help`, or `htomd --help` for usage and pipeline examples.
|
|
68
|
+
Use `htomd help convert` or `htomd convert --help` for command-specific help
|
|
69
|
+
(likewise for `extract`). `htomd version` or `htomd --version` prints the installed
|
|
70
|
+
package version. Help and version commands exit successfully without reading stdin.
|
|
71
|
+
|
|
72
|
+
`python -m htomd` supports the same commands. Successful conversion exits with code 0, I/O and
|
|
73
|
+
decoding failures with code 1, and usage errors with code 2. Errors go to stderr.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
mise trust
|
|
79
|
+
mise install
|
|
80
|
+
mise run setup
|
|
81
|
+
mise exec -- uv run --locked pytest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`mise run check` runs all checks. See
|
|
85
|
+
[CONTRIBUTING.md](https://github.com/jamiedavenport/htomd/blob/main/CONTRIBUTING.md)
|
|
86
|
+
for hooks and releases.
|
|
87
|
+
|
|
88
|
+
MIT license, copyright 2026 JXD Ltd.
|
|
89
|
+
[Fixtures](https://github.com/jamiedavenport/htomd/blob/main/tests/fixtures/real/README.md)
|
|
90
|
+
have separate licenses.
|
htomd-0.1.0/RELEASING.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Releasing htomd
|
|
2
|
+
|
|
3
|
+
Releases are prepared manually. Publishing a GitHub Release starts the
|
|
4
|
+
[release workflow](.github/workflows/release.yml), which runs CI, validates the
|
|
5
|
+
version and distributions, publishes to PyPI, and attaches the wheel and source
|
|
6
|
+
archive to the GitHub Release.
|
|
7
|
+
|
|
8
|
+
## Publishing identity
|
|
9
|
+
|
|
10
|
+
PyPI Trusted Publishing uses these settings:
|
|
11
|
+
|
|
12
|
+
| Field | Value |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Project | `htomd` |
|
|
15
|
+
| Repository owner | `jamiedavenport` |
|
|
16
|
+
| Repository | `htomd` |
|
|
17
|
+
| Workflow filename | `release.yml` |
|
|
18
|
+
| GitHub environment | `pypi` |
|
|
19
|
+
|
|
20
|
+
Configure the publisher in the project's PyPI settings, or use an account-level
|
|
21
|
+
pending publisher for the first upload. No stored PyPI API token is needed.
|
|
22
|
+
See [PyPI's setup instructions](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/).
|
|
23
|
+
|
|
24
|
+
## 1. Prepare the version and release notes
|
|
25
|
+
|
|
26
|
+
Set the version in `pyproject.toml` and refresh `uv.lock` if the version changed.
|
|
27
|
+
Update `CHANGELOG.md` with the release date and user-facing changes. Version
|
|
28
|
+
`0.1.0` is used in the examples below; substitute the new version for later
|
|
29
|
+
releases.
|
|
30
|
+
|
|
31
|
+
## 2. Validate, commit, and push
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
mise run check
|
|
35
|
+
mise run build
|
|
36
|
+
mise exec -- uv run --locked twine check dist/*
|
|
37
|
+
mise exec -- env RELEASE_TAG=v0.1.0 uv run --locked python tools/check_release.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The build inspects package contents and installs both artifacts in separate
|
|
41
|
+
virtual environments to exercise the API and CLI. Keep exactly one wheel and
|
|
42
|
+
one source archive in `dist/`; move aside old-version artifacts before building.
|
|
43
|
+
|
|
44
|
+
Commit and push the release changes to `main`. Wait for all CI jobs to pass,
|
|
45
|
+
including the Windows package build. The release workflow must be included in
|
|
46
|
+
the commit you tag.
|
|
47
|
+
|
|
48
|
+
## 3. Create the tag and GitHub Release
|
|
49
|
+
|
|
50
|
+
From the clean, validated `main` checkout:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
git tag -a v0.1.0 -m "htomd 0.1.0"
|
|
54
|
+
git push origin v0.1.0
|
|
55
|
+
gh release create v0.1.0 --verify-tag --title "htomd 0.1.0" --notes-file CHANGELOG.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For later releases, pass a file containing only that version's changelog entry
|
|
59
|
+
as `--notes-file`. Pushing the tag alone does not publish. Publishing the GitHub
|
|
60
|
+
Release triggers the PyPI upload; saving a draft does not.
|
|
61
|
+
|
|
62
|
+
## 4. Monitor publishing and verify installation
|
|
63
|
+
|
|
64
|
+
Find the new Release run, then watch it using its numeric ID:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
gh run list --workflow release.yml --limit 5
|
|
68
|
+
gh run watch RUN_ID --exit-status
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Verify PyPI installation in an isolated environment:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
mise exec -- uv run --isolated --no-project --refresh-package htomd \
|
|
75
|
+
--default-index https://pypi.org/simple/ --with htomd==0.1.0 \
|
|
76
|
+
python -I -c "import htomd; assert htomd.convert('<h1>Hello</h1>') == '# Hello\n'"
|
|
77
|
+
mise exec -- uv run --isolated --no-project --refresh-package htomd \
|
|
78
|
+
--default-index https://pypi.org/simple/ --with htomd==0.1.0 \
|
|
79
|
+
htomd --version
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Check the package page on [PyPI](https://pypi.org/project/htomd/) and both
|
|
83
|
+
attachments on [GitHub Releases](https://github.com/jamiedavenport/htomd/releases).
|
|
84
|
+
|
|
85
|
+
If publisher configuration fails, correct it and rerun the failed jobs on the
|
|
86
|
+
same release. If only the release-asset job fails, PyPI publication has already
|
|
87
|
+
succeeded; rerun that job to attach the existing artifacts. Published artifacts
|
|
88
|
+
cannot be replaced with changed files; corrections need a new version.
|
|
89
|
+
|
|
90
|
+
## Documentation references
|
|
91
|
+
|
|
92
|
+
Commands were checked against uv `0.12.11` using Context7 library ID
|
|
93
|
+
`/astral-sh/uv` (requested version `0.12.11`; current unversioned documentation)
|
|
94
|
+
and the [uv publishing guide](https://docs.astral.sh/uv/guides/package/).
|
|
95
|
+
The GitHub CLI release command was checked against installed `gh` `2.100.0` and
|
|
96
|
+
Context7 library ID `/websites/cli_github_manual` (unversioned), alongside the
|
|
97
|
+
[GitHub CLI manual](https://cli.github.com/manual/gh_release_create).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==82.0.1"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "htomd"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Focused Markdown and metadata from messy HTML, in pure Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
htomd = "htomd._cli:main"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Repository = "https://github.com/jamiedavenport/htomd"
|
|
28
|
+
Issues = "https://github.com/jamiedavenport/htomd/issues"
|
|
29
|
+
Changelog = "https://github.com/jamiedavenport/htomd/blob/main/CHANGELOG.md"
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"mypy==1.20.2",
|
|
34
|
+
"pre-commit==4.5.1",
|
|
35
|
+
"pytest==9.0.2",
|
|
36
|
+
"ruff==0.15.6",
|
|
37
|
+
"setuptools==82.0.1",
|
|
38
|
+
"twine==6.2.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
htomd = ["py.typed"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
addopts = "-ra"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
target-version = "py312"
|
|
53
|
+
line-length = 100
|
|
54
|
+
extend-exclude = ["tests/fixtures"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
58
|
+
# Explicit branches are clearer for parser and renderer state transitions.
|
|
59
|
+
ignore = ["SIM108"]
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = "3.12"
|
|
63
|
+
strict = true
|
|
64
|
+
files = ["src", "tests", "tools"]
|
|
65
|
+
exclude = ["tests/fixtures"]
|
htomd-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Focused Markdown and metadata from decoded HTML, with no network access."""
|
|
2
|
+
|
|
3
|
+
from ._metadata import read_metadata, refine_metadata
|
|
4
|
+
from ._models import Diagnostics, Document, Metadata
|
|
5
|
+
from ._parser import parse
|
|
6
|
+
from ._render import render
|
|
7
|
+
from ._selection import select
|
|
8
|
+
from ._urls import document_base
|
|
9
|
+
|
|
10
|
+
__all__ = ["Diagnostics", "Document", "Metadata", "convert", "extract"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract(html: str, *, url: str | None = None) -> Document:
|
|
14
|
+
"""Extract relevant Markdown and explicit metadata from an HTML string.
|
|
15
|
+
|
|
16
|
+
``url`` provides source context and resolves references; no fetching occurs.
|
|
17
|
+
Malformed HTML receives best-effort recovery. Result dataclasses are immutable.
|
|
18
|
+
"""
|
|
19
|
+
if not isinstance(html, str):
|
|
20
|
+
raise TypeError("html must be a decoded str")
|
|
21
|
+
if url is not None and not isinstance(url, str):
|
|
22
|
+
raise TypeError("url must be a str or None")
|
|
23
|
+
root, parsing_notes = parse(html)
|
|
24
|
+
base = document_base(root, url)
|
|
25
|
+
metadata = read_metadata(root, url, base)
|
|
26
|
+
selected, diagnostics = select(root)
|
|
27
|
+
metadata = refine_metadata(metadata, selected)
|
|
28
|
+
markdown = render(selected, base)
|
|
29
|
+
diagnostics = Diagnostics(
|
|
30
|
+
diagnostics.strategy if markdown else "none", parsing_notes + diagnostics.notes
|
|
31
|
+
)
|
|
32
|
+
return Document(markdown, metadata, diagnostics)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def convert(html: str, *, url: str | None = None) -> str:
|
|
36
|
+
"""Return the Markdown produced by :func:`extract`."""
|
|
37
|
+
return extract(html, url=url).markdown
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Command-line conversion of HTML supplied on stdin."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from importlib.metadata import version
|
|
9
|
+
|
|
10
|
+
from . import convert, extract
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> int:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="htomd",
|
|
16
|
+
description=(
|
|
17
|
+
"Extract Markdown and metadata from UTF-8 HTML on stdin. "
|
|
18
|
+
"Pipe input from cat or curl; output goes to stdout. No file arguments or fetching."
|
|
19
|
+
),
|
|
20
|
+
epilog=(
|
|
21
|
+
"Examples:\n"
|
|
22
|
+
" cat page.html | htomd convert > page.md\n"
|
|
23
|
+
" cat page.html | htomd extract > page.json\n"
|
|
24
|
+
" curl -fsSL https://example.com | htomd convert --url https://example.com\n"
|
|
25
|
+
"\nUse 'htomd help COMMAND' for command-specific help."
|
|
26
|
+
),
|
|
27
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
28
|
+
)
|
|
29
|
+
version_text = f"htomd {version('htomd')}"
|
|
30
|
+
parser.add_argument("--version", action="version", version=version_text)
|
|
31
|
+
commands = parser.add_subparsers(dest="command")
|
|
32
|
+
command_parsers: dict[str, argparse.ArgumentParser] = {}
|
|
33
|
+
for name, description in (
|
|
34
|
+
("convert", "Write Markdown to stdout."),
|
|
35
|
+
("extract", "Write Markdown, metadata, and diagnostics as JSON to stdout."),
|
|
36
|
+
):
|
|
37
|
+
command = commands.add_parser(
|
|
38
|
+
name,
|
|
39
|
+
help=description,
|
|
40
|
+
description=f"Read UTF-8 HTML from stdin to EOF. {description}",
|
|
41
|
+
epilog=f"Example: cat page.html | htomd {name}",
|
|
42
|
+
)
|
|
43
|
+
command.add_argument(
|
|
44
|
+
"--url", help="Source URL for resolving references; no fetching occurs."
|
|
45
|
+
)
|
|
46
|
+
command_parsers[name] = command
|
|
47
|
+
command_parsers["version"] = commands.add_parser(
|
|
48
|
+
"version",
|
|
49
|
+
help="Show the installed package version.",
|
|
50
|
+
description="Show the installed version.",
|
|
51
|
+
)
|
|
52
|
+
help_parser = commands.add_parser(
|
|
53
|
+
"help",
|
|
54
|
+
help="Show general or command-specific help.",
|
|
55
|
+
description="Show help for a command.",
|
|
56
|
+
)
|
|
57
|
+
command_parsers["help"] = help_parser
|
|
58
|
+
help_parser.add_argument(
|
|
59
|
+
"topic", nargs="?", choices=command_parsers, help="Command to describe."
|
|
60
|
+
)
|
|
61
|
+
args = parser.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
if args.command is None or args.command == "help":
|
|
64
|
+
topic = args.topic if args.command == "help" else None
|
|
65
|
+
(command_parsers[topic] if topic else parser).print_help()
|
|
66
|
+
return 0
|
|
67
|
+
if args.command == "version":
|
|
68
|
+
print(version_text)
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
html = sys.stdin.buffer.read().decode("utf-8-sig")
|
|
73
|
+
if args.command == "convert":
|
|
74
|
+
output = convert(html, url=args.url)
|
|
75
|
+
else:
|
|
76
|
+
output = json.dumps(asdict(extract(html, url=args.url)), ensure_ascii=False, indent=2)
|
|
77
|
+
output += "\n"
|
|
78
|
+
except (OSError, UnicodeError) as error:
|
|
79
|
+
print(f"htomd: {error}", file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
84
|
+
sys.stdout.buffer.flush()
|
|
85
|
+
except OSError as error:
|
|
86
|
+
# Prevent a second output failure when Python flushes stdout during shutdown.
|
|
87
|
+
with open(os.devnull, "wb") as sink:
|
|
88
|
+
os.dup2(sink.fileno(), sys.stdout.fileno())
|
|
89
|
+
if not isinstance(error, BrokenPipeError):
|
|
90
|
+
print(f"htomd: {error}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
return 0
|