Model or dataset
elliotgao2/tomd avatar
elliotgao2/tomd

tomd turns rendered HTML into Markdown with one function call

Convert HTML to Markdown.

530 stars70 forksPythonMIT

At a glance

What is it?
tomd is a MIT licensed Python library that converts an HTML string into Markdown by walking the parsed tree with lxml, covering nested lists, tables, fenced code with language hints, entities, hard breaks and definition lists. It ships as a single module, tomd.py, with two runtime dependencies and a version 1.1.0.
Who is it for?
Adopt tomd if you keep scraped or archived HTML and want Markdown without pulling in a browser-grade converter. Skip it if you need to preserve complex layout, since everything outside the supported tag list falls through to plain text.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A single-module converter built on lxml

tomd is described as a small, fast Python library that turns an HTML string into Markdown, and the README gives the use cases directly: archiving articles, normalising scraped content, piping rendered pages into LLM context, or working with plain text instead of a DOM. The repository layout matches that description. There is one source file, tomd.py, alongside a pyproject.toml, a uv.lock, a tests directory, a .pre-commit-config.yaml, a .github directory and a README.md, with the build configuration including only tomd.py in the wheel. The public surface is two entry points, a module-level convert function and a Tomd class whose markdown attribute holds the result. The project version is 1.1.0, the licence is MIT and the classifier marks it as production stable and OS independent, and the last push to the repository was on 2026-10-07. The two runtime dependencies are pyquery at 2.0 or later and lxml at 5.0 or later, with Python 3.10 as the floor.

The two ways to call it

The quickstart uses the function form.

python
import tomd

tomd.convert("<h1>Hello, world!</h1>")
# => '# Hello, world!\n'

And the class form gives you the same result through an attribute.

python
from tomd import Tomd

Tomd("<p><b>bold</b> and <i>italic</i></p>").markdown
# => '**bold** and *italic*\n'

The class form is the one to prefer when you want to inspect more than the final string, since the object can hold intermediate state, and the function form is a thin wrapper for the common case. Note that both return a trailing newline, which matters if you are concatenating output or diffing against files that do not have one. The README's framing is that the library is meant for real-world HTML rather than tidy input, and the class form is what the worked example uses for the longer sample.

What the converter handles and what it discards

The conversion table is the specification, and it is worth reading as a contract. Headings h1 to h6 become Markdown headings. Bold and italic come from b, strong, i and em. Strikethrough comes from del, s and strike. Inline code comes from code, kbd and samp. Links from a href, including relative links, mailto links and title attributes. Images from img with src, alt and title. Lists from ul, ol with a start attribute and li, with nesting and mixed types supported. Blockquotes from blockquote, including nested and multi-paragraph ones. Horizontal rules from hr. Fenced code blocks from pre containing code with a language-X class. Tables from table with or without thead and tbody. Hard line breaks from br become two trailing spaces and a newline. Definition lists from dl, dt and dd. What is dropped is listed separately: script, style, iframe, head and friends are stripped. HTML entities such as &amp;, &#8212; and &lt; are decoded. And unknown tags fall through to their text content, which is the behaviour that decides what happens to markup the table does not list. That last rule is the one to internalise, because it means the converter degrades to text extraction rather than dropping content, but it also means an unlisted structural element loses its structure.

A worked example with nested lists and a code block

The README's example puts several of those rules in one input, and the expected output is shown so you can see how nesting and fenced code survive the round trip.

python
from tomd import Tomd

html = """
<article>
  <h1>Heading</h1>
  <p><b>bold</b>, <i>italic</i>, and a <a href="https://example.com">link</a>.</p>
  <ul>
    <li>one</li>
    <li>two
      <ul><li>nested</li></ul>
    </li>
  </ul>
  <pre><code class="language-python">print("hi")</code></pre>
</article>
"""

print(Tomd(html).markdown)

The output keeps the heading, the bold and italic runs, the link with its href, the nested list with the child list indented under its parent item, and the fenced code block with its language hint preserved. The language hint survives because the converter reads the class attribute on the code element, which is the standard convention for marking a code block's language in HTML and the reason the class attribute rather than the element text is the thing to check when a block comes out unlabelled. The article wrapper element itself is not in the conversion table, so it falls through to its text content, which is exactly the case the fallback rule was written for.

Working on it: uv, pytest and ruff

The development workflow is the modern Python one, driven by uv, and the README gives it in full.

bash
git clone https://github.com/elliotgao2/tomd.git
cd tomd
uv sync                       # install deps into .venv
uv run pytest                 # run tests
uv run ruff check .           # lint
uv run ruff format --check .  # format check

Pre-commit hooks are installed with one more command.

bash
uv run pre-commit install

The configuration backs that up. The build backend is hatchling, and the dev dependency group is pytest at 8.0 or later, pytest-cov, pre-commit and ruff at 0.6 or later. Ruff is configured with a line length of 100, a target version of py310, and a lint selection of E, F, W, I, B, C4 and UP, with E501 ignored, which reads as a deliberate policy of letting long lines through while enforcing import sorting, bugbear checks, comprehensions and modern Python syntax. The pytest configuration points at the tests directory. The contributing guidance asks that an issue be opened first for non-trivial changes and that pytest, ruff check and ruff format check all pass before submitting.

Where tomd is the wrong tool

Three limits, and they follow directly from the design. The first is that a single-module implementation is a single point of failure for edge cases. The whole library is tomd.py, so behaviour on unusual markup is whatever that file happens to do, and the README does not document a plugin mechanism, an extension point or a way to register a custom rule. The fallback rule of unknown tags becoming their text content is doing real work here, and it means a page whose layout depends on elements outside the table loses that layout entirely. If your input is a real article archive, that is usually fine, since the article is prose and the prose survives. If your input is a rendered web application with nested layout tables, the result will be a wall of text. The second limit is scope: no CSS-aware extraction, no handling of lazy-loaded content that never made it into the HTML, and no notion of main content versus navigation, which is the job of a readability algorithm rather than a converter. The third is the alternative that a lot of teams reach for instead. If your HTML comes from a browser, the mainstream approach is to render it and hand the resulting DOM to a full converter such as html2text or markdownify, or to use a readability library and then convert. Those are heavier, and they handle the layout and boilerplate cases that tomd does not. tomd is faster and simpler because it skips all of that, which is the right trade for normalised scraped content and the wrong one for whole-page archival fidelity.

Editorial conclusion

Adopt tomd if you keep scraped or archived HTML and want Markdown without pulling in a browser-grade converter. Skip it if you need to preserve complex layout, since everything outside the supported tag list falls through to plain text. Before you rely on it, test the cases you care about, because the whole implementation is a single file and the README documents what is stripped as plainly as what is converted.

Frequently asked questions

What is tomd and what does it do?

It is a small, fast Python library that turns an HTML string into Markdown, walking the parsed tree with lxml. The README lists archiving articles, normalising scraped content, piping rendered pages into LLM context and working with plain text instead of a DOM as the intended uses.

How do I install tomd and convert a page?

With `pip install tomd` or `uv add tomd`, requiring Python 3.10 or later. Then call `tomd.convert("<h1>Hello, world!</h1>")` for the function form, or `Tomd(html).markdown` for the class form. The package is on PyPI as tomd.

Which HTML elements does tomd convert and which does it drop?

It converts headings, bold, italic, strikethrough, inline code, links, images, nested and mixed lists, blockquotes, horizontal rules, fenced code blocks with a language class, tables with or without thead and tbody, hard line breaks and definition lists. Script, style, iframe and head elements are stripped, HTML entities are decoded, and unknown tags fall through to their text content.

How do I run the tomd tests and lint checks?

The README uses uv throughout: `uv sync` to install deps into .venv, then `uv run pytest` for the tests, `uv run ruff check .` for linting and `uv run ruff format --check .` for the format check. Pre-commit hooks are installed with `uv run pre-commit install`, and all three checks should pass before a pull request.

Official sources

  1. elliotgao2/tomd on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elliotgao2-tomd.svg)](https://hysenlabs.com/projects/elliotgao2-tomd)

Was this useful?