Markfly
Markfly is a Python library that converts HTML into clean, readable Markdown. It walks an lxml.html element tree and emits CommonMark / GitHub-Flavored Markdown (GFM), correctly tracking links, images, lists, tables, and reference-style link definitions along the way.
Markfly is a Python port of jina-ai/reader's MarkifyService (originally written in TypeScript), rebuilt on top of lxml for use in Python projects.
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
Markfly isn't published to PyPI yet. In the meantime, drop markfly.py into your project, or install it from your local checkout:
pip install lxml
Once published:
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file markfly-0.1.0.tar.gz.
File metadata
- Download URL: markfly-0.1.0.tar.gz
- Upload date:
- Size: 12.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2f3464859673fba4ecc5b3a699c5c98b21b0eb4a5438186bd0847e2e04957cac
|
|
| MD5 |
ecea6bff1496a4049ad5b5959a2368d3
|
|
| BLAKE2b-256 |
2075b5c059e9229d9f44f9a76be6a47acdd71d785301be6faea96f3733dd120e
|
Provenance
The following attestation bundles were made for markfly-0.1.0.tar.gz:
Publisher:
publish.yml on dx-bear/markfly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
markfly-0.1.0.tar.gz -
Subject digest:
2f3464859673fba4ecc5b3a699c5c98b21b0eb4a5438186bd0847e2e04957cac - Sigstore transparency entry: 2398664737
- Sigstore integration time:
-
Permalink:
dx-bear/markfly@8d0e72cff5f2beed2e617c7b347ffef6b9e18717 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/dx-bear
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d0e72cff5f2beed2e617c7b347ffef6b9e18717 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file markfly-0.1.0-py3-none-any.whl.
File metadata
- Download URL: markfly-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57f831cc836fa5efeb8fb379dd89ed9d6751cbb05ae8661b6d2c8579d36fefba
|
|
| MD5 |
68b0d67d48bb07bf2ea8e12e09d57509
|
|
| BLAKE2b-256 |
4dfe194dd7ab620444af5023caab0ef4d519ee1a59923baf95d657c80a017d6c
|
Provenance
The following attestation bundles were made for markfly-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on dx-bear/markfly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
markfly-0.1.0-py3-none-any.whl -
Subject digest:
57f831cc836fa5efeb8fb379dd89ed9d6751cbb05ae8661b6d2c8579d36fefba - Sigstore transparency entry: 2398664781
- Sigstore integration time:
-
Permalink:
dx-bear/markfly@8d0e72cff5f2beed2e617c7b347ffef6b9e18717 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/dx-bear
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d0e72cff5f2beed2e617c7b347ffef6b9e18717 -
Trigger Event:
workflow_dispatch
-
Statement type: