Skip to main content

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 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

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 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

markfly-0.1.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

markfly-0.1.0-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

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

Hashes for markfly-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2f3464859673fba4ecc5b3a699c5c98b21b0eb4a5438186bd0847e2e04957cac
MD5 ecea6bff1496a4049ad5b5959a2368d3
BLAKE2b-256 2075b5c059e9229d9f44f9a76be6a47acdd71d785301be6faea96f3733dd120e

See more details on using hashes here.

Provenance

The following attestation bundles were made for markfly-0.1.0.tar.gz:

Publisher: publish.yml on dx-bear/markfly

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for markfly-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 57f831cc836fa5efeb8fb379dd89ed9d6751cbb05ae8661b6d2c8579d36fefba
MD5 68b0d67d48bb07bf2ea8e12e09d57509
BLAKE2b-256 4dfe194dd7ab620444af5023caab0ef4d519ee1a59923baf95d657c80a017d6c

See more details on using hashes here.

Provenance

The following attestation bundles were made for markfly-0.1.0-py3-none-any.whl:

Publisher: publish.yml on dx-bear/markfly

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page