Markdown to a typed AST
your front-end can render.

markast parses Markdown into a typed, structured tree. Everything happens in the library — parsing, validation, traversal, rendering. Ship the JSON to any client and switch on type.

What you get

🌳

Typed AST

Every node has a type discriminator and known fields. Walk it, query it, mutate it — without writing a single regex.

🧱

Pluggable widgets

Author :::widget components in Markdown that survive into the AST as structured nodes with typed props and named slots.

🛡️

Never crashes

Bad input produces a diagnostic, not an exception. parse always returns a valid AST so your CMS keeps shipping.

🔀

Three output formats

Roundtrip back to Markdown, render to HTML server-side, or ship as JSON. Three single-method calls.

⚙️

Transform pipeline

Slugify headings, build a TOC, autolink URLs, normalise text spans — chain them, write your own.

🌐

Front-end agnostic

Native mobile, React/Vue/Svelte, terminal, plain HTML — anything that can switch on a string.

30-second tour

pip install markast
from markast import parse

doc = parse("""
# Welcome

A paragraph with **bold** text and a [link](https://example.com).

:::tip title="Pro tip"
Markdown still works inside widgets.
:::
""")

doc.to_json()       # str — ship to any client
doc.to_markdown()   # str — roundtrip
doc.to_html()       # str — server-side render

Why a tree, not HTML?

HTML is a one-way street. Once your content is rendered, the structure is gone — clients can't selectively style headings differently per platform, can't replace a :::video with a native player, can't extract a TOC without re-parsing. A typed AST keeps the meaning intact:

  • Mobile apps render headings with their own typography rules.
  • The web renders :::video as a custom player; the terminal renders it as a link.
  • Search indexes the same structured nodes that drive the UI.
  • The same content powers a docs site, a CMS preview, and a CLI — without re-authoring.