Metadata-Version: 2.3
Name: markfly
Version: 0.1.0
Summary: Add your description here
Keywords: markfly
Author: dx-bear
Author-email: dx-bear <dxbear@protonmail.com>
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Requires-Dist: lxml>=6.1.1
Requires-Python: >=3.14
Project-URL: Homepage, https://github.com/dx-bear/markfly
Project-URL: Repository, https://github.com/dx-bear/markfly.git
Project-URL: Bug Tracker, https://github.com/dx-bear/markfly/issues
Description-Content-Type: text/markdown

# 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](https://github.com/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:

```bash
pip install lxml
```

Once published:

```bash
pip install markfly
```

---

## Quick start

```python
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:

```markdown
# 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:

```python
from markfly import html_to_markdown

markdown = html_to_markdown("<p>Hello <b>world</b></p>")
```

Pass options as keyword arguments:

```python
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:

```python
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:

```python
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:

```markdown
| Name | Role |
| --- | --- |
| Ada | Engineer |

Status: ~~Pending~~ Done

- [x] Ship it
```

GFM mode also enables MathML → LaTeX conversion for `<math>` elements (see [Math support](#math-support) below).

---

## Link styles

```python
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:

```python
html = '<a href="/docs">Docs</a> <img src="/logo.png" alt="logo">'
html_to_markdown(html, baseUrl="https://example.com")
```

```markdown
[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`:

```python
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:

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

```python
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](#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](https://github.com/jina-ai/reader).

## License

Add your license of choice here before publishing to PyPI.