Currently in Beta, feedback welcome!
A Python interface for markdown-it.rs (and plugins), using Rust for blazingly fast Markdown parsing ⚡️
The goal of this project is to provide a fast, safe, extensible, and easy-to-use Markdown parser for Python. It is complimentary to markdown-it-py, which is a pure Python implementation of markdown-it, and here we aim to follow as close as possible the API for that package.
If you care primarily about speed, this is the library for you. For example, benchmarking the two libraries when parsing the CommonMark Spec file, markdown-it-pyrs is 20x faster than markdown-it-py.
| Name (time, ms) | Min | Max | Mean | Rounds |
|---|---|---|---|---|
| markdown-it-pyrs | 5.217 | 7.969 | 5.968 | 85 |
| markdown-it-py | 122.696 | 143.246 | 131.431 | 7 |
The drawback is that the library vendors compiled Rust code, and so:
First install the package:
pip install markdown-it-pyrs
Then use it like you would markdown-it-py:
from markdown_it_pyrs import MarkdownIt
md = MarkdownIt("commonmark").enable("table")
md.render("# Hello, world!")
# '<h1>Hello, world!</h1>\n'
markdown-it.rs does not generate a token stream, but instead directly generates a Node tree.
This is similar to the markdown-it-py's SyntaxTreeNode class, although the API is not identical.
(source mapping is also provided by byte-offset, rather than line only)
md = (
MarkdownIt("commonmark")
.enable("table")
.enable_many(["linkify", "strikethrough"])
)
node = md.tree("# Hello, world!")
print(node.walk())
# [Node(root), Node(heading), Node(text)]
print(node.pretty(srcmap=True, meta=True))
# <root srcmap="0:15">
# <heading srcmap="0:15">
# level: 1
# <text srcmap="2:15">
# content: Hello, world!
Note: Attributes of the Node class, such as Node.attrs, return a copy of the underlying data, and so mutating it will not affect what is stored on the node, e.g.
from markdown_it_pyrs import Node
node = Node("name")
# don't do this!
node.attrs["key"] = "value"
print(node.attrs) # {}
# do this instead (Python 3.9+)
node.attrs = node.attrs | {"key": "value"}
print(node.attrs) # {"key": "value"}
# Node.children is only a shallow copy though, so this is fine
child = Node("child")
node.children = [child]
node.children[0].name = "other"
print(child.name) # "other"
A CLI is also provided, which can be used like this:
echo "# Hello, world!" | markdown-it-pyrs html -
# <h1>Hello, world!</h1>
echo "# Hello, world!" | markdown-it-pyrs ast -
# <root>
# <heading>
# <text>
Replace - with a filename to read from a file,
and see markdown-it-pyrs --help for more options,
including initial configuration and enabling plugins.
Initialising MarkdownIt("zero") will not enable any plugins, and so you can add only the ones you need.
Use MarkdownIt("commonmark") to enable all the CommonMark plugins.
Use MarkdownIt("gfm") to enable all the CommonMark plugins, plus the GitHub Flavoured Markdown plugins.
All syntax rules in markdown-it.rs are implemented as plugins.
Plugins can be added to the parser by calling enable or enable_many with the name of the plugin.
The following plugins are currently supported:
CommonMark Blocks:
blockquote: Block quotes with >code: Indented code blocksfence: Backtick code blocksheading: # ATX headingshr: --- horizontal ruleslheading: --- underline setext headingslist: * unordered lists and 1. ordered listsparagraph: Paragraphsreference: Link reference definitions [id]: src "title"CommonMark Inlines:
autolink: <http://example.com>backticks: `code`emphasis: _emphasis_, *emphasis*, **strong**, __strong__entity: &escape: backslash escaping \image: link: [text](src "title"), [text][id], [text]newline: hard line breakshtml_block: HTML blockshtml_inline: HTML inlineGitHub Flavoured Markdown (https://github.github.com/gfm):
table:markdown
| foo | bar |
| --- | --- |
| baz | bim |
- strikethrough: ~~strikethrough~~
- tasklist: - [x] tasklist item
- autolink_ext: Extended autolink detection with "bare URLs" like https://example.com and www.example.com
- tagfilter: HTML tag filtering, e.g. <script> tags are removed
Others:
sourcepos: Add source mapping to rendered HTML, looks like this: <stuff data-sourcepos="1:1-2:3">, i.e. line:col-line:colreplacements: Typographic replacements, like -- to —smartquotes: Smart quotes, like " to “linkify: Automatically linkify URLs with https://crates.io/crates/linkify (note currently this only matches URLs with a scheme, e.g. https://example.com)heading_anchors: Add heading anchors, with defaults like GitHubfront_matter: YAML front matterfootnote: Pandoc-style footnotes (see https://pandoc.org/MANUAL.html#footnotes)deflist: Definition lists (see https://pandoc.org/MANUAL.html#definition-lists)I'm quite new to Rust, so if you see something that could be improved, issues and PRs are welcome!
PyO3 and Maturin are used to build the Python package, by wrapping markdown-it.rs in a Python module.
pre-commit is used to run code formatting and linting checks, and tox is used to run tests.
Improvements:
quotes: Quote characters, for smart quotes
Add plugins: ...
Allow options for plugins:
Open issue upstream:
text_join rule (to join adjacent text and text_special tokens)heading_anchors plugin does not allow for e.g. GitHub format where non-uniqueness is resolved by appending -1, -2, etc, and also removal of image textexamples/ferris/block_rule.rs::FerrisBlockScanner::run,
which currently describes the JS API not the new rust oneroot.ext items in core rules; it seems at present you have to swap memory and reswap at the end of the rule, see e.g. the InlineParserRuletest_rules_at_line to parse what the calling rule is, so that other rules can decide whether to interrupt based on the calling rule (in the check function), I think this would then allow behaviour similar to what alt did (possibly needed for footnote definition parsing)InlineRule.run, e.g. ((node1, length1), (node2, length2), ...)Node walk methods, allow the function to return something to indicate whether to continue walking the children of the nodematch statement, to match a Node against a NodeValue implementation? (rather than if/else for Node.cast)before or after another rule or all rulesMaintenance:
$ claude mcp add markdown-it-pyrs \
-- python -m otcore.mcp_server <graph>