Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ¦‹ 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.