Skip to content

Python API

Four functions and twelve node types. No classes to subclass, no registry, no configuration object, no parser instance to keep around.

Every function here is total: it never raises, on any input. A docstring cannot crash your help output.

Functions

parse(text) -> Document

Parse a docstring into a document tree.

python - <<'EOF'
import simplemark
print(simplemark.parse("Compute ``n`` factorial."))
EOF
Document(children=(Paragraph(children=(Text(text='Compute '), Code(text='n'), Text(text=' factorial.'))),))

Pass the raw docstring: obj.__doc__, not inspect.getdoc(obj). Simplemark does its own dedenting, and it does it better than cleandoc for docstrings whose only indented content is an example.

parse_inline(text) -> tuple[Inline, ...]

Parse a fragment as inline content only: no dedenting, no blocks, a flat tuple.

python -c "
import simplemark
print(simplemark.parse_inline('Set *strict* to fail early.'))
"
(Text(text='Set '), Emphasis(text='strict'), Text(text=' to fail early.'))

Use it for one-line argparse help strings, or anywhere you want to apply your own styling to the spans.

check(text, *, compat=False) -> list[Diagnostic]

Report problems, in source order. Never raises.

With compat=True, also report reStructuredText constructs and older docstring conventions: migration advice rather than problems. See Diagnostics.

python - <<'EOF'
import simplemark
for d in simplemark.check("The __init__ and __del__ methods."):
    print(repr(d.line), repr(d.col), d.code, d.severity.value)
    print(d.message)
EOF
1 1 CM001 commonmark
CommonMark reads some of these delimiters as emphasis

render_text(doc, *, width=None, indent=0, style="auto") -> str

Render a document as text.

argument meaning
width Columns to wrap to. None asks the terminal, falling back to 80.
indent Left margin for every line.
style "none", "ansi", or "auto".

"auto" resolves to "ansi" when stdout is a TTY, TERM is not dumb, and NO_COLOR is unset; otherwise "none".

Rendering

With no styling, code spans are quoted and emphasis loses its markers:

Node style="none" style="ansi"
Code 'text' bold
Strong text bold
Emphasis text underline
Link <url> underlined <url>

The rule behind that table: a distinction that carries meaning must survive on a terminal with no styling, and one that is decoration need not. Marking an identifier is meaning; making a word emphatic is decoration.

Width arithmetic counts visible characters only, so escape sequences never push a line past the limit. Verbatim blocks, code spans and autolinks are never broken across lines.

Node types

Inline content is flat: Code, Strong and Emphasis carry text, not children, because reStructuredText forbids nested inline markup.

Text(text)                      # literal text; may contain newlines
Code(text)                      # a code span, however it was spelled
Strong(text)
Emphasis(text)
Link(url, label)                # label == url for an autolink

Paragraph(children)             # tuple[Inline, ...]
Heading(level, children)        # level 1 to 6
Verbatim(text)                  # exact, common indentation removed
Item(children)                  # tuple[Block, ...]
BulletList(items)               # tuple[Item, ...]
OrderedList(items, start)
Document(children)              # tuple[Block, ...]

All are frozen, slotted dataclasses built from tuples, so they compare by value and hash:

python -c "
import simplemark
from simplemark import Document, Paragraph, Text
print(simplemark.parse('Hello.') == Document((Paragraph((Text('Hello.'),)),)))
"
True

That makes them convenient to pattern-match on:

match node:
    case Code(text):
        return f"<code>{escape(text)}</code>"
    case Link(url, label):
        return f'<a href="{escape(url)}">{escape(label)}</a>'

The AST is spelling-agnostic

``x`` and `x` both produce Code("x"); plain CommonMark, where a run of backticks is closed by an equal run. A fence and an indented block both produce Verbatim:

python - <<'EOF'
import simplemark
print(simplemark.parse("a ``x`` b") == simplemark.parse("a `x` b"))
EOF
True

Diagnostic

Diagnostic(line, col, code, message, severity)

line and col are 1-based and point into the original source, not the dedented text, so an editor or CI system can jump straight to it. severity is a Severity enum: ERROR, COMMONMARK or LEGACY. str(diagnostic) gives line:col: CODE message.

Writing your own renderer

There is no base class. Renderers dispatch on node type, and one that handles only some node types is perfectly valid; it just ignores the rest:

from functools import singledispatch

@singledispatch
def to_html(node):
    return ""                                    # anything unhandled

@to_html.register
def _(node: simplemark.Text):
    return html.escape(node.text)

@to_html.register
def _(node: simplemark.Code):
    return f"<code>{html.escape(node.text)}</code>"

An HTML renderer is roughly forty lines this way, because HTML escaping is three characters. tools/sm2rst.py is a worked example (a reStructuredText renderer over this AST, needing nothing from the parser), and at 129 lines it is a useful corrective to "each renderer is about forty lines": almost all of its bulk is escaping, and reStructuredText is far harder to escape into than HTML.