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.
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.
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
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.'),)),)))
"
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
Diagnostic¶
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.