From markdownify¶
markdownify converts HTML to Markdown by walking a
BeautifulSoup tree. Its distinctive design is the MarkdownConverter class with a per-tag convert_<tag> override
system: subclass the converter, define a convert_a or convert_img method, and it replaces the built-in rendering
for that element. On top of that it exposes a flat set of keyword options (heading style, bullets, the shared strong/em
symbol, escaping toggles, autolinks, table header inference, line-break style, document trimming, and tag allow/deny
filters) and a command-line converter. It is a common choice for turning scraped or user-submitted HTML into Markdown in
content pipelines, documentation tooling, and chat/mail ingestion.
turbohtml covers the same conversion through to_markdown(), a single fully typed method on the
parsed WHATWG tree. It walks the tree in C off the WHATWG parse instead of running a second Python walk over a
BeautifulSoup tree, and groups markdownify’s flat options into named config dataclasses so each concept has one name.
turbohtml vs markdownify¶
Dimension |
turbohtml |
markdownify |
|---|---|---|
Scope |
Full WHATWG HTML parser and tree with Markdown, plain-text, and HTML serialization |
HTML-to-Markdown conversion over a BeautifulSoup tree |
Feature breadth |
Same Markdown option surface (headings, lists, inline marks, links, images, tables, wrapping, code, escaping) plus per-tag converters, text extraction, selectors, and sanitization on the shared tree |
Mature Markdown-conversion options, |
Performance |
C tree walk off the WHATWG parse, roughly two orders of magnitude faster on the benchmark below |
Pure-Python walk over a BeautifulSoup tree the parser must build first |
Typing |
Fully type annotated, config via frozen dataclasses |
Untyped public API |
Dependencies |
Compiled C extension (prebuilt wheels) |
Pure Python, but requires |
Maintenance |
Actively developed |
Actively developed |
Feature overlap¶
Port these markdownify calls 1:1 onto to_markdown() with a Markdown config:
markdownify(text)->turbohtml.parse(text).to_markdown().Heading style: ATX, closed ATX, or setext/underlined (
heading_style).List bullets, cycled by nesting depth (
bullets).Sub/superscript wrappers (
sub_symbol,sup_symbol).Escaping of asterisks, underscores, and miscellaneous Markdown characters (
escape_asterisks,escape_underscores,escape_misc).Autolinks and href-as-title (
autolinks,default_title).Table header inference (
table_infer_header).Line-break style and document trimming (
newline_style,strip_document).Default code-fence language (
code_language).Tag allow/deny filters (
strip,convert) and per-tag output overrides (convert_<tag>).
What turbohtml adds¶
One parsed tree serves Markdown, layout-aware plain text (
to_text()), and HTML (serialize()); markdownify is Markdown-only.Convert any subtree by calling
to_markdown()on a selected element (doc.find("article").to_markdown()) instead of pre-slicing the HTML string.Full spec-compliant WHATWG parsing feeds the conversion, so malformed markup is repaired the same way a browser does before it is rendered to Markdown, with no separate BeautifulSoup parser to pick or install.
Independent strong and emphasis markers (
Markdown.Inline(strong=..., emphasis=...)), a strict superset of markdownify’s singlestrong_em_symbolthat derives both from one character.Grouped, frozen-dataclass config (
Markdown.Links,Markdown.Images,Markdown.Tables…) is fully typed and discoverable, versus markdownify’s flat set of untyped keyword options.Reference-style links, image-render modes, transliteration, and a Google Docs inline-CSS mode (
Markdown.google_doc()) that markdownify does not expose.A shell entry point:
python -m turbohtml to-markdown(installed as theturbohtmlconsole script) reads a file or stdin, covering markdownify’s command-line converter.
What markdownify has that turbohtml does not¶
Subclass-based extension: markdownify lets a
MarkdownConvertersubclass override anyconvert_<tag>method and call the parent implementation withsuper(). turbohtml’sMarkdown(converters=...)maps a tag name to a callable receiving the element and its already-rendered child Markdown, which covers per-tag replacement but notsuper()chaining onto the built-in renderer.keep_inline_images_inrestricts which parent tags keep an inline image; turbohtml’sMarkdown.Images(mode=...)is document-wide, with no per-parent-tag equivalent.bs4_optionsfor parser selection. turbohtml always runs the WHATWG algorithm, so there is no parser to configure.
Performance¶
to Markdown |
turbohtml |
|
|---|---|---|
article (2 KiB) |
2.84 µs |
1.28 ms (451x) |
list (4 KiB) |
4.47 µs |
2.6 ms (581x) |
table (4 KiB) |
7.25 µs |
3.07 ms (424x) |
configured (4 KiB) |
15.3 µs |
2.77 ms (181x) |
How to migrate¶
Swap the import and the call:
# markdownify
from markdownify import markdownify
markdownify(text)
# turbohtml
import turbohtml
turbohtml.parse(text).to_markdown()
print(parse("<h1>Title</h1><p>Some <b>bold</b> text.</p>").to_markdown())
# Title
Some **bold** text.
The defaults emit opinionated GitHub-Flavored Markdown, and the markdownify options
map onto the grouped Markdown config fields with one name per concept:
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Subclassing MarkdownConverter to override convert_<tag> becomes a mapping of tag name to a callable(element,
content) -> str. Each callable receives the Element and its already-rendered child Markdown and
returns the replacement text:
from turbohtml import Markdown
def convert_a(element, content):
return f"[{content}]({element.attrs.get('href', '')})"
turbohtml.parse(text).to_markdown(Markdown(converters={"a": convert_a}))
Gotchas and pitfalls¶
The bold and italic markers are independent (
Markdown.Inline(strong=...)andMarkdown.Inline(emphasis=...)), where markdownify derives both from onestrong_em_symbol; set both to reproduce its behavior (strong_em_symbol"_"becomesMarkdown.Inline(strong="__", emphasis="_")).to_markdown()is a method on any node, so convert a subtree by calling it on the element you selected (doc.find("article").to_markdown()) instead of slicing the HTML string first.markdownify’s parser-selection options (
bs4_options) are dropped, since turbohtml always runs the WHATWG algorithm; malformed markup is repaired to the same tree a browser would build.Markdown.Links(base_url=...)does simple prefixing rather than full RFC-3986 URL resolution.markdownify’s
strip_documentdefaults toSTRIP; turbohtml’sMarkdown.Document(trim=...)also defaults to"strip", so document-edge trimming matches by default.