📝 simplibs-markdown
Write Markdown as Python — escaped, checked and always well-formed.
A lightweight Python library that builds Markdown out of small, named Markdown objects
instead of hand-assembled strings. Nodes combine with the plain + operator, each syntax
family of Markdown (emphasis, links, headings, lists, references) collapses into one
parameterized mechanism instead of dozens of special cases, and the finished tree writes
ordinary Markdown text — to the screen, to a string or straight to a file.
from simplibs.markdown import Paragraph, MarkdownText
from simplibs.markdown.presets import H1, BOLD, NOTE, BULLET_LIST
doc = (
H1("simplibs")
+ Paragraph("Tiny libraries, " + BOLD("one job each") + ".")
+ NOTE("This text was written by Python.")
+ BULLET_LIST("regex", "markdown", "file")
)
print(MarkdownText(doc))
# simplibs
Tiny libraries, **one job each**.
> [!NOTE]
> This text was written by Python.
- regex
- markdown
- file
simplibs-markdownbuilds directly on the same architectural pattern assimplibs-regex(a base class + composable containers + presets) andsimplibs-rules— if either of those is familiar, this library's shape will be too.
🧭 The Core Philosophy
Markdown written by hand is a string, and a string cannot tell a heading from a sentence that
happens to start with #. A user name with an underscore turns half a paragraph into italics,
a | inside a table cell splits the cell, and a code sample that contains three backticks ends
its own block. simplibs-markdown replaces the string with a small tree of ordinary Python
objects: each node is independently named, typed and validated, and the tree composes with +
the way a sentence is built from words.
- A string is content, never Markdown. Everything you pass as a string is escaped, so
Paragraph("snake_case and *not emphasis*")writes exactly that text. To write formatting, pass a node (BOLD("this")); the one deliberate way out isText(..., escape=False). - One mechanism per family. Four spellings of emphasis become one
Emphasis(kind, marker). Twelve link forms become oneLink(kind, image). Three heading styles become oneHeading(level, style). The goal is never a new class for every piece of syntax, but the smallest set of genuinely different mechanisms, with every variant a parameter or a named preset. - Structure is checked when you build it. A heading inside bold text, a table inside a link, a setext heading of level 3, a list marker that does not fit the kind of list, a table row of the wrong width — each is rejected the moment you write it, with a message that names what is wrong, and not discovered later as a broken page.
- Delimiters that fit. A code span and a code fence are chosen to be one backtick longer than
anything inside them, a link destination with a space is wrapped in
<...>, a table cell's pipe is escaped — the text you give is the text you get. - Nesting is prefixing. A quote is its content with
>in front of every line; a list item is its content under a marker. Lists, code and quotes inside each other need no special case. - Layers that only look left.
_common←base_class←inline←blocks←containers←output←presets— every layer imports only from those before it.
Heading("Title", 1, HeadingStyle.SETEXT) # -> "Title\n====="
Link("docs", kind=LinkKind.REFERENCE, label="Foo Bar") # -> "[docs][Foo Bar]"
CodeBlock("x\n```\ny\n```", "md") # a fence of four backticks — the code cannot close it
📦 Installation
pip install simplibs-markdown
Requires Python 3.11+. Runtime dependencies (installed automatically): the simplibs exception
library (structured error messages) and the simplibs file library (the atomic write behind
to_file).
🚀 Quick Start in 60 Seconds
Level 1: Compose with + and print
from simplibs.markdown import Paragraph, MarkdownText
from simplibs.markdown.presets import H1, BOLD, NOTE, BULLET_LIST
doc = (
H1("simplibs")
+ Paragraph("Tiny libraries, " + BOLD("one job each") + ".")
+ NOTE("This text was written by Python.")
+ BULLET_LIST("regex", "markdown", "file")
)
print(MarkdownText(doc))
Inline nodes joined with + stay one line; anything joined with a block becomes a Document.
Level 2: Strings are safe, tables are checked
from simplibs.markdown import InlineCode, Link, MarkdownText, Paragraph, Table
print(MarkdownText(Paragraph("snake_case and *not emphasis* and # not a heading")))
# snake\_case and \*not emphasis\* and # not a heading
table = Table(
["Library", "Does"],
[
[InlineCode("regex"), "Patterns without regex syntax"],
[Link("markdown", "https://github.com/simplibs/simplibs-markdown"), "This one"],
],
)
print(MarkdownText(table))
# | Library | Does |
# | --------------------------------------------------------- | ----------------------------- |
# | `regex` | Patterns without regex syntax |
# | [markdown](https://github.com/simplibs/simplibs-markdown) | This one |
Level 3: A whole file — front matter, checklists, code — written to disk
from simplibs.markdown import CodeBlock, Document, Frontmatter, to_file
from simplibs.markdown.presets import H1, BULLET_LIST, DONE_ITEM, TASK_ITEM
report = Document(
Frontmatter({"title": "Release notes", "draft": False}),
H1("Release 0.1.0"),
BULLET_LIST(DONE_ITEM("write code"), TASK_ITEM("write tests")),
CodeBlock("pip install simplibs-markdown", "bash"),
)
to_file(report, "out/RELEASE.md") # the folder is created; the write is atomic
---
title: "Release notes"
draft: false
---
# Release 0.1.0
- [x] write code
- [ ] write tests
```bash
pip install simplibs-markdown
```
🛠️ Architecture & Package Structure
src/simplibs/markdown/
├── _common/ ◄── Helpers, enums and diagnostic raisers shared by more than one layer
├── base_class/ ◄── Abstract base classes Markdown & InlineMarkdown, the NodeLevel enum
├── inline/ ◄── Nodes inside a line (Text, Emphasis, InlineCode, Link, Entity, Math,
│ Reference, LineBreak, InlineJoin)
├── blocks/ ◄── Leaf blocks (Paragraph, Heading, CodeBlock, ThematicBreak, BlankLine,
│ LinkDefinition, Footnote, Frontmatter, Html)
├── containers/ ◄── Nodes that hold other blocks (Document, Quote, List, ListItem, Table)
├── output/ ◄── MarkdownText and to_file — from a node to text and to a file
└── presets/ ◄── Named shortcuts over nodes (alerts, code, emphasis, headings, html,
links, lists, references, spacing)
The root of the package exports the nodes, base classes, enums, the output layer, and all presets
so they can be used directly from the root or via simplibs.markdown.presets:
from simplibs.markdown import Heading, Table, Link, EmphasisKind, MarkdownText # nodes, enums, output
from simplibs.markdown import H1, BOLD, NOTE, BULLET_LIST, RULE # presets from root
from simplibs.markdown.presets import H1, BOLD, NOTE, BULLET_LIST, RULE
🧩 The Markdown Classes
Every node in this library inherits from Markdown. It defines the mandatory contract
(to_lines), the text forms built on it, the level tag and + composition out of the box.
InlineMarkdown is the shortcut for nodes that live inside a line and render as one string.
class Markdown(ABC):
node_level: NodeLevel # INLINE or BLOCK
@abstractmethod
def to_lines(self) -> tuple[str, ...]:
"""Return this node as lines, without line endings."""
raise NotImplementedError
def to_text(self) -> str: ... # the lines joined with "\n"
def __add__(self, other) -> "Markdown": ... # -> InlineJoin or Document
class InlineMarkdown(Markdown):
@abstractmethod
def to_inline(self) -> str:
"""Return this node as one string that can stand inside a line."""
➡️ Full method-by-method reference: README_MARKDOWN_CLASS
What + returns
| Left | Right | Result |
|---|---|---|
| inline node | inline node | InlineJoin — one line |
str |
inline node | InlineJoin; the string becomes escaped Text |
| block node | anything | Document |
str |
block node | Document; the string becomes a Paragraph |
| a node | 5, None… |
TypeError |
📖 Quick Reference
🔤 inline/ — Nodes inside a line
| Class | Description / Parameters |
|---|---|
Text |
Plain text, every formatting character escaped (value, escape). |
Emphasis |
Emphasis, strong, both and strike-through in one node (inner, kind, marker). |
InlineCode |
A code span; the delimiter and padding fit any content (text). |
Link |
Six forms × link or image: inline, reference, collapsed, shortcut, autolink, bare URL (text, dest, title, kind, image, label). |
Entity |
An HTML entity by name or by code point (name or codepoint). |
Math |
An inline $…$ or display $$…$$ formula (expr, display). |
Reference |
Mention, team, issue, repository issue, commit, emoji (kind, value). |
LineBreak |
A hard line break in three styles (style). |
InlineJoin |
Several inline nodes in a row — what + builds; always flat. |
🧱 blocks/ — Nodes that are lines of their own
| Class | Description / Parameters |
|---|---|
Paragraph |
Lines of inline content, optional hard-break style (*lines, break_style). |
Heading |
Levels 1–6 in three styles: ATX, closed ATX, setext (inner, level, style). |
CodeBlock |
Fenced (backtick or tilde) or indented; the fence fits the code (code, info, fence, indented). |
ThematicBreak |
A horizontal rule (char, length). |
BlankLine |
Explicit empty lines that replace the separator (count). |
LinkDefinition |
[label]: destination "title" (label, dest, title). |
Footnote |
The marker in the text or the note itself (label, *blocks, role). |
Frontmatter |
A YAML header between two --- lines (data). |
Html |
A block, an inline tag or a comment with validated names (tag, content, attrs, mode). |
📦 containers/ — Nodes that hold other blocks
| Class | Description / Parameters |
|---|---|
Document |
Blocks separated by blank lines; the root of composition (*blocks, gap, final_newline). |
Quote |
A block quote or a GitHub alert (*blocks, alert). |
List |
A bullet or an ordered list, tight or loose (*items, ordered, marker, start, tight). |
ListItem |
One item: any blocks and an optional checkbox (*blocks, checked). |
Table |
A GFM table with per-column alignment (head, rows, align). |
📤 output/ — From a node to text and to a file
MarkdownText(node)— the finished text of a node, rendered once:text,lines,str(), iteration,len()andto_file(path).to_file(source, path, *, overwrite=True)— one call from a string, a node or aMarkdownTextto a file; atomic, UTF-8,"\n"line endings.
🎁 presets/ — Names for the things you write often
alerts:NOTE,TIP,IMPORTANT,WARNING,CAUTIONcode:MERMAID,GEOJSON,TOPOJSON,STL,MATH_BLOCKemphasis:BOLD,ITALIC,BOLD_ITALIC,STRIKETHROUGHheadings:H1,H2,H3,H4,H5,H6html:DETAILS,SUP,SUB,MARK,KBD,COMMENT,ANCHORlinks:IMAGE,AUTOLINK,BARE_URLlists:BULLET_LIST,NUMBERED_LIST,TASK_ITEM,DONE_ITEMreferences:MENTION,ISSUE_REF,COMMIT_REF,EMOJI,COLOR_CHIPspacing:EMPTY_LINE,RULE,HARD_BREAK
A preset only calls a node, so every check and every error comes from the node itself.
⚠️ Known Limitations
To keep the library lightweight and robust, a few design trade-offs apply in version 0.1.0:
- Escaping is cautious: Characters like
_,&, or$are escaped broadly even when contextually safe (e.g.,snake_casebecomessnake\_case)[cite: 10]. - Node equality: Nodes do not implement
__eq__(==); to compare two nodes, compare theirto_text()orto_lines()[cite: 12]. - Frontmatter serialization:
Frontmatterdata is serialized once at construction time; subsequent modifications to the source dictionary are not reflected[cite: 13]. - List merging: Two consecutive lists with the exact same marker will merge in CommonMark renderers; use an explicit
COMMENTor a different marker between them[cite: 11].
⚠️ Exceptions
Nodes raise structured simplibs.exception cards
at construction time — a mistake never reaches the output. Every error name starts with
MARKDOWN_, and each is documented next to the node that raises it.
| Exception | Raised when |
|---|---|
ParamError |
An argument has the wrong type or an unusable value or combination (Heading level 7, a setext heading of level 3, a table row of the wrong width, a block inside bold text, @some user as a mention). |
ConversionError |
A Frontmatter value cannot be written as YAML (a set, NaN, a date, a key that is not a string). |
Writing a file (to_file) raises the errors of
simplibs-file — for example AlreadyExistsError when
overwrite=False and the file exists.
🔗 Related libraries
simplibs-exception— the structured exception cards behind every error here.simplibs-file— the atomic file write behindto_file.simplibs-regex— the sister library: the same architecture (base class, containers, presets) for regular expressions.
☯️ About simplibs
All libraries in the simplibs (Simple Libraries) ecosystem share a common engineering philosophy:
- Dyslexia-friendly: We actively minimize cognitive load. Code is atomized into small, self-contained units, files are named directly after the logical task they perform, and explanations describe why something is designed, not just what it is.
- Programmer's Zen: Nothing should be missing, and nothing should be superfluous. We value clean execution paths and robust, understandable code architectures over rushed, messy feature sets.
- Defensive Style: We actively anticipate edge cases and failure modes so that only safe operational paths remain. Our code is built to degrade gracefully rather than crash unexpectedly.
- Minimalism: Find the most direct path to the goal in as few operational steps as possible without taking shortcuts on safety, readability, or completeness.
- Code as Craft: Code should be pleasant to look at, readable at a glance, and evoke structural harmony. We treat software engineering as a precision trade.
🤝 Contributing & Community
This is an open-source project built with love and care. We strongly believe in community collaboration and welcome any feedback, bug reports, or feature ideas!
- Want to contribute? Feel free to open an Issue or submit a Pull Request.
- Want to get in touch? If you'd like to discuss the project further, collaborate, or just say hello, feel free to open a GitHub Issue or start a Discussion.
📝 License
This library is released under the MIT License. Build great things!
Metadata
Release files for simplibs-markdown 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| simplibs_markdown-0.1.0.tar.gz | 103.7 kB | Without provenance | Details for simplibs_markdown-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| simplibs_markdown-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for simplibs_markdown-0.1.0-py3-none-any.whl |
Total release size: 285.5 kB
Release files / simplibs_markdown-0.1.0.tar.gz
| Download URL | simplibs_markdown-0.1.0.tar.gz |
|---|---|
| Size | 103.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
294d835d068cdf525d0745152ef24d14ec36afb6cd4b1f4797224ec0a5f8dcc8
|
|
BLAKE2b-256 checksum How to use checksums |
023de7276ae503370bc178cb07adc2ea793743f768ade4446c96120ed29049b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / simplibs_markdown-0.1.0-py3-none-any.whl
| Download URL | simplibs_markdown-0.1.0-py3-none-any.whl |
|---|---|
| Size | 181.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9104c0466e31bb19c0572e7361aa4a52c49ad3177e891a57dca1faa240e1bfe7
|
|
BLAKE2b-256 checksum How to use checksums |
d7ba77a5c45f423ad43ce192e570d162bd849b15bcf739611c960c702d3053ba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Security for simplibs-markdown 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.