🦋 Markfly
HTML → Clean, Readable Markdown
Markfly is a lightweight Python library that converts HTML into clean, readable CommonMark / GitHub-Flavored Markdown (GFM).
It walks an lxml.html element tree and intelligently preserves the structure and content that matter, including:
- 🔗 Links
- 🖼️ Images
- 📝 Lists
- 📊 Tables
- 📚 Reference-style link definitions
- ✨ Formatting and text structure
🚀 Why Markfly?
Converting HTML to Markdown sounds simple — until you need to correctly handle nested elements, links, images, lists, tables, and references.
Markfly takes care of that for you.
🌐 HTML
↓
🦋 Markfly
↓
📝 Clean Markdown
The result is Markdown that is easy to read, store, process, and pass to downstream tools such as documentation generators, search pipelines, and LLM applications.
🧬 Inspired by Jina Reader
Markfly is a Python port of jina-ai/reader's MarkifyService, originally written in TypeScript.
It has been rebuilt from the ground up for Python using lxml, providing a familiar and efficient way to convert HTML element trees into Markdown.
💡 TypeScript → Python
MarkifyService→ MarkflyHTML→ Markdown
✨ Highlights
- 🐍 Built for Python
- 🌳 Works directly with
lxml.htmlelement trees - 📖 Produces CommonMark / GFM-compatible Markdown
- 🔗 Correctly tracks links and references
- 🖼️ Handles images
- 📋 Supports tables
- 📝 Preserves ordered and unordered lists
- 🧹 Focuses on producing clean, readable Markdown
🦋 Markfly — Turn HTML into Markdown, cleanly.
Features
- CommonMark + GFM output — headings, paragraphs, emphasis, lists, links, images, code blocks, blockquotes, and (optionally) GFM tables, strikethrough, and checkboxes.
- Smart image resolution — falls back through
srcset,data-src,data-lazy-src,data-original, and sibling<picture>/<source>elements whensrcis missing, empty, or a placeholder. - Reference-style links — supports
inlined,referenced(full / collapsed / shortcut), anddiscardedlink styles. - Configurable formatting — heading style (ATX or Setext), bullet markers, code fence style, emphasis/strong delimiters, and more.
- Custom rules — register your own per-tag replacement rules, or mark specific tags to be kept as raw HTML.
- MathML support — converts MathML to LaTeX via an optional pluggable
math_convertercallable, with sensible fallback when one isn't provided. - Base URL resolution — automatically resolves relative links and image sources against a
baseUrl.
Installation
pip install markfly
Quick start
from markfly import html_to_markdown
html = """
<h1>Hello <em>World</em></h1>
<p>This is <strong>bold</strong> and a <a href="https://example.com">link</a>.</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
"""
print(html_to_markdown(html))
Output:
# Hello _World_
This is **bold** and a [link](https://example.com).
* one
* two
Usage
Simple conversion
The fastest path is the html_to_markdown() convenience function, which parses the HTML string and returns Markdown in one call:
from markfly import html_to_markdown
markdown = html_to_markdown("<p>Hello <b>world</b></p>")
Pass options as keyword arguments:
markdown = html_to_markdown(
html,
gfm=True,
headingStyle="atx",
baseUrl="https://example.com",
)
Using MarkflyService directly
For more control — reusing an instance across many conversions, registering custom rules, or converting an already-parsed lxml element — use MarkflyService directly:
from lxml.html import fromstring
from markfly import MarkflyService, MarkflyOptions
options = MarkflyOptions(gfm=True, codeBlockStyle="fenced")
service = MarkflyService(options)
root = fromstring("<h2>Title</h2><p>Body text.</p>")
markdown = service.markfly(root)
Note: each call to
service.markfly(root)resets internal state (link/image tracking, list and table stacks), so a singleMarkflyServiceinstance can safely be reused for multiple, independent conversions.
Options
All options are set via MarkflyOptions, passed either as an options= object or as keyword arguments to html_to_markdown().
| Option | Type | Default | Description |
|---|---|---|---|
headingStyle |
"atx" | "setext" |
"atx" |
atx uses # headings; setext uses underlines for h1/h2. |
hr |
str |
"* * *" |
Markdown emitted for <hr>. |
bulletListMarker |
"-" | "+" | "*" |
"*" |
Marker used for unordered list items. |
codeBlockStyle |
"indented" | "fenced" |
"indented" |
How multi-line code blocks are rendered. |
fence |
"```" | "~~~" | None |
"```" |
Fence characters when codeBlockStyle="fenced". |
emDelimiter |
"_" | "*" |
"_" |
Delimiter for emphasis (<em>/<i>). |
strongDelimiter |
"__" | "**" |
"**" |
Delimiter for strong text (<strong>/<b>). |
linkStyle |
"inlined" | "referenced" | "discarded" |
"inlined" |
How <a> tags are rendered. |
linkReferenceStyle |
"full" | "collapsed" | "shortcut" |
"full" |
Reference format when linkStyle="referenced". |
preformattedCode |
bool |
False |
Reserved for preformatted code handling. |
footnoteStyle |
"inline" | "document" |
"inline" |
Reserved for footnote handling. |
baseUrl |
str | None |
None |
Base URL used to resolve relative links/images. blob:/data: URLs are ignored automatically. |
gfm |
bool |
False |
Enables GFM extensions: tables, strikethrough, checkboxes, and MathML. |
GFM mode
Pass gfm=True to enable GitHub-Flavored Markdown extensions:
html = """
<table>
<tr><th>Name</th><th>Role</th></tr>
<tr><td>Ada</td><td>Engineer</td></tr>
</table>
<p>Status: <s>Pending</s> Done</p>
<input type="checkbox" checked> Ship it
"""
print(html_to_markdown(html, gfm=True))
Output:
| Name | Role |
| --- | --- |
| Ada | Engineer |
Status: ~~Pending~~ Done
- [x] Ship it
GFM mode also enables MathML → LaTeX conversion for <math> elements (see Math support below).
Link styles
html = '<a href="https://example.com">Example</a>'
# Inlined (default)
html_to_markdown(html)
# -> [Example](https://example.com)
# Referenced, full style
html_to_markdown(html, linkStyle="referenced", linkReferenceStyle="full")
# -> [Example][1]
# ->
# -> [1]: https://example.com
# Discarded — keeps the text, drops the link
html_to_markdown(html, linkStyle="discarded")
# -> Example
Resolving relative URLs
Set baseUrl to resolve relative href and src values against a real origin:
html = '<a href="/docs">Docs</a> <img src="/logo.png" alt="logo">'
html_to_markdown(html, baseUrl="https://example.com")
[Docs](https://example.com/docs) 
Image fallback resolution
Markfly doesn't just read src. When src is missing, empty, or a known placeholder (e.g. a base64 GIF/PNG spacer), it tries, in order:
srcset/data-srcset— picks the highest-resolution candidatedata-src,data-lazy-src,data-original- Sibling
<source>elements inside a<picture>wrapper
This makes it resilient against lazy-loaded images from real-world scraped pages.
Custom rules
Register your own conversion logic for specific tags with addRule:
from lxml.html import fromstring
from markfly import MarkflyService, MarkflyRule
service = MarkflyService()
def render_mark(text, element, options, service):
return f"=={text}=="
service.addRule("highlight", MarkflyRule(filter="mark", replacement=render_mark))
root = fromstring("<p>This is <mark>important</mark>.</p>")
print(service.markfly(root))
Or preserve specific tags as raw HTML instead of converting them:
service.keep("iframe")
Math support
MathML → LaTeX conversion requires a converter callable, since there's no drop-in PyPI equivalent to @nomagick/mathml-to-latex. Without one, Markfly falls back to the element's alttext attribute or its plain text content.
from markfly import MarkflyService, MarkflyOptions
def my_math_converter(mathml_string: str) -> str:
# plug in your own MathML -> LaTeX conversion here
...
service = MarkflyService(MarkflyOptions(gfm=True), math_converter=my_math_converter)
API reference
html_to_markdown(html, options=None, **kwargs) -> str
Convenience entry point. Parses an HTML string and returns Markdown.
MarkflyService(options=None, math_converter=None)
The main converter class.
.markfly(element) -> str— convert anlxml.htmlelement tree to Markdown..addRule(name, rule)— register a custom per-tag replacement rule..keep(tag)— preserve a tag as raw HTML instead of converting it..use(rule_fns)— apply a list of rule-registration functions.
MarkflyOptions
Dataclass holding all converter options (see Options above).
MarkflyRule(filter, replacement)
Dataclass describing a custom rule: filter is a tag name or list of tag names; replacement is a callable (text, element, options, service) -> str.
Notes & known limitations
- MathML conversion has no built-in LaTeX backend — supply your own
math_converter. blob:anddata:URLs are never used as abaseUrl, since resolving relative links against them doesn't make sense.preformattedCodeandfootnoteStyleare present inMarkflyOptionsfor API parity with the original TypeScript implementation but aren't fully wired up yet.
Credits
Markfly is a Python port of the Markdown conversion logic from jina-ai/reader.
License
Add your license of choice here before publishing to PyPI.
Metadata
Release files for markfly 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| markfly-0.1.2.tar.gz | 13.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| markfly-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 27.8 kB
Release files / markfly-0.1.2.tar.gz
| Download URL | markfly-0.1.2.tar.gz |
|---|---|
| Size | 13.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ade23314d22efcb87d907212d7d148e308d197080cacb4250bc6ae923bd5e9d2
|
|
BLAKE2b-256 checksum How to use checksums |
ee555eb34ffb14942f180748190c59e5c02cb295296d74d7f338f8b541955519
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.
Transparency logRelease files / markfly-0.1.2-py3-none-any.whl
| Download URL | markfly-0.1.2-py3-none-any.whl |
|---|---|
| Size | 14.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
949f09249a54e1ebee0c79fb4c2259a58ee7cf9ef565e77e9be0c041032b0e5b
|
|
BLAKE2b-256 checksum How to use checksums |
b16cc338cff61564519d48d128b4746f1721eb18a1f12e848d3704c0b39be7eb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.
Transparency log