Widgets
Widgets are how markast carries rich, structured components through Markdown into the AST.
Syntax
:::widget-name key="value" key2=value
Body content with full **markdown** support.
# slot-name
Content of an additional slot.
:::
…lands in the AST as:
{
"type": "widget",
"widget": "widget-name",
"props": { "key": "value", "key2": "value" },
"slots": {
"default": [ /* body */ ],
"slot-name": [ /* slot */ ]
}
}
Built-in widgets
| Widget | Purpose | Notable props |
|---|---|---|
tip | Friendly callout | title, icon |
note | Neutral aside | title, icon |
info | Information callout | title, icon |
warning | Caution callout | title, icon |
caution | Stronger warning | title, icon |
danger | Destructive-action warning | title, icon |
card | Container with header/body/footer | title, color, elevated |
video | Video embed | src*, poster, controls, … |
code-group | Tabbed code blocks | default_tab |
code-collapse | Collapsible block | summary, open |
tabs | Generic tabbed content | default, vertical |
steps | Numbered step list | start |
badge | Inline label/pill | label*, color |
Writing a custom widget
from markast import BaseWidget, Parser, WidgetParam
class CalloutWidget(BaseWidget):
"""A simple coloured callout."""
name = "callout"
params = {
"level": WidgetParam(str, default="info",
choices=["info", "warn", "error"],
description="Callout severity"),
"title": WidgetParam(str, default=None),
}
parser = Parser(widgets=[CalloutWidget])
doc = parser.parse(""":::callout level=warn title="Heads up"
Something **important**.
:::""")
print(doc.to_html())
That's the entire contract. The base class handles parsing the header into typed props, filling defaults, validating choices and emitting W004 diagnostics, required-prop checks (W005), Markdown roundtrip, and a default HTML render. You only override what you need.
Slots
A widget body can be split into named slots using bare # slot-name H1 headings at the root of the body:
:::card title="Profile"
Some default body content.
# header
*Custom* header content.
# footer
A footer.
:::
For a widget to acknowledge the extra slot names, declare them on the class:
class CardWidget(BaseWidget):
name = "card"
slots = ["header", "footer"]
params = { "title": WidgetParam(str, default=None) }
If you want to accept any slot names (e.g. for tabs or steps), leave slots = [].
Param types
type_ | Markdown value | Result |
|---|---|---|
str | key=hi | "hi" |
str | key="multi word" | "multi word" |
int | count=3 | 3 |
float | ratio=1.5 | 1.5 |
bool | autoplay=true | True (also 1, yes, on) |
list | tags=a,b,c | ["a", "b", "c"] |
list | tags=[1,2,3] | [1, 2, 3] (JSON form) |
dict | meta={"a":1} | {"a": 1} (JSON only) |
Enum | level=high | matching Enum member |
Custom validators:
WidgetParam(int, default=10,
validator=lambda x: None if 1 <= x <= 100 else "must be 1..100")
Custom rendering
class BadgeWidget(BaseWidget):
name = "badge"
params = {
"label": WidgetParam(str, required=True),
"color": WidgetParam(str, default="gray"),
}
def to_html(self, node, render_children):
p = node["props"]
return f'<span class="badge badge-{p["color"]}">{p["label"]}</span>'
render_children is a callable handed to your widget. Pass any list of child nodes and you get back the rendered string in the renderer's target format. Use it to render slot contents recursively:
def to_html(self, node, render_children):
body = render_children(node["slots"]["default"])
return f'<aside class="callout">{body}</aside>'
Registration patterns
# 1. Per-parser (recommended) — keeps state local.
from markast import Parser
parser = Parser(widgets=[MyWidget])
# 2. Globally on the default registry — then the top-level parse() works.
from markast.widgets import default_registry
default_registry.register(MyWidget)
from markast import parse
parse(":::my-widget\n...\n:::")
Schema introspection
CalloutWidget.schema()
# {
# "name": "callout",
# "params": {
# "level": {"type": "str", "required": False, "default": "info",
# "description": "Callout severity",
# "choices": ["info", "warn", "error"]},
# "title": {"type": "str", "required": False}
# },
# "slots": ["default"],
# "doc": "A simple coloured callout."
# }