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.
@@ -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.
@@ -0,0 +1,6 @@
1
+ include LICENSE README.md CONTRIBUTING.md RELEASING.md CHANGELOG.md
2
+ recursive-include src/htomd *.py py.typed
3
+ prune tests
4
+ prune tools
5
+ prune .github
6
+ exclude .pre-commit-config.yaml uv.lock
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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,6 @@
1
+ """Support ``python -m htomd``."""
2
+
3
+ from ._cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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