Skip to main content

🦋 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 → Markfly HTML → Markdown


✨ Highlights

  • 🐍 Built for Python
  • 🌳 Works directly with lxml.html element 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 when src is missing, empty, or a placeholder.
  • Reference-style links — supports inlined, referenced (full / collapsed / shortcut), and discarded link 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_converter callable, 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 single MarkflyService instance 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) ![logo](https://example.com/logo.png)

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:

  1. srcset / data-srcset — picks the highest-resolution candidate
  2. data-src, data-lazy-src, data-original
  3. 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 an lxml.html element 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: and data: URLs are never used as a baseUrl, since resolving relative links against them doesn't make sense.
  • preformattedCode and footnoteStyle are present in MarkflyOptions for 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)

Source distribution for markfly 0.1.2
File Size Uploaded
markfly-0.1.2.tar.gz 13.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for markfly 0.1.2
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page