Markdown Authoring
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against Zensical 0.0.72 (the latest release on PyPI, published 2026-10-23, still an alpha) and
checked on 2026-10-26. The examples were built in a scratch project that documents Pocket Units, a tiny units-conversion
library written in Python and C#, with the bundled Python Markdown 3.11.2 and PyMdown Extensions 12.3. Zensical
converts Markdown to HTML with Python Markdown and the PyMdown Extensions, so most of the syntax is the one known from
Material for MkDocs. The settings live in zensical.toml; the project file layout is described in
Configuration, and the installation and the zensical commands
in Installation and CLI.
How Markdown extensions are enabled
Markdown extensions are listed in the [project.markdown_extensions] table of zensical.toml. The key is the
extension name and the value is a table of its options; an empty table {} enables an extension with its default
options, and true/false values and dotted keys set options:
[project.markdown_extensions]
admonition = {}
attr_list = {}
md_in_html = {}
toc.permalink = true
pymdownx.highlight.anchor_linenums = true
pymdownx.superfences = {}
pymdownx.tabbed.alternate_style = true
Source: Zensical docs: Admonitions
The same extensions can be written in mkdocs.yml as a YAML list (- admonition), because Zensical reads both files
(see Migrating from Material for MkDocs).
Everything below uses zensical.toml.
The mechanics that matter in practice were checked by building small projects:
-
Without a
markdown_extensionstable, Zensical enables a built-in list:abbr,admonition,attr_list,def_list,footnotes,md_in_html,toc(withpermalink), and the PyMdown extensionsarithmatex,betterem,caret,details,emoji,highlight,inlinehilite,keys,magiclink,mark,smartsymbols,superfences(with amermaidfence),tabbed(alternate style),tasklistandtilde. This list is theDEFAULT_MARKDOWN_EXTENSIONSconstant in the installedzensical/config.py; the documentation pages consulted do not state it. A project with no table rendered tabs, admonitions,==mark==andH~2~Owithout any configuration. -
As soon as the table exists, the defaults are replaced, not merged. A project that listed only
admonition = {}still rendered the admonition, but=== "A"stayed a plain paragraph,==mark==andH~2~Ostayed literal text and headings lost their permalink. Copy the full list thatzensical newwrites (below) and remove entries, rather than starting from one line. -
tocandtablesare always added by Zensical, whether they are listed or not. A table was rendered in the project that listed onlyadmonition. -
A name that cannot be imported stops the build. Listing
pymdownx.nonexistent = {}failed withRuntimeError: Python error: ModuleNotFoundError: No module named 'pymdownx.nonexistent'. -
Only extensions installed with Zensical are available. The package ships Python Markdown and PyMdown Extensions; a third-party Markdown extension must be installed into the same virtual environment.
zensical new writes the whole extension list into the generated zensical.toml (a few lines are shown here; the
file also holds abbr, attr_list, def_list, footnotes, md_in_html, pymdownx.betterem, caret, details,
inlinehilite, keys, magiclink, mark, smartsymbols, tilde and the emoji generators):
[project.markdown_extensions]
admonition = {}
toc.permalink = true
pymdownx.arithmatex.generic = true
pymdownx.highlight.anchor_linenums = true
pymdownx.highlight.line_spans = "__span"
pymdownx.highlight.pygments_lang_class = true
pymdownx.superfences.custom_fences = [
{ name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" },
]
pymdownx.tabbed.alternate_style = true
pymdownx.tabbed.combine_header_slug = true
pymdownx.tasklist.custom_checkbox = true
Source: scratch project created with zensical new, Zensical 0.0.72
The generated file also lists theme features (content.code.copy, content.code.select, content.code.annotate,
content.footnote.tooltips, content.tabs.link, content.tooltips, and others). Extensions turn Markdown syntax into
HTML; theme features add behaviour on top of that HTML, so many of the sections below need both. The Mermaid fence and
the arithmatex setting are described in
Diagrams, Math and Icons.
Figure: extensions decide which HTML is produced from Markdown; theme features decide how the theme styles and scripts that HTML.
The table below summarises which extension each feature needs. Several features need a combination.
| Feature | Extensions | Theme feature |
|---|---|---|
Admonitions |
|
none |
Collapsible blocks |
|
none |
Nested content in admonitions or tabs |
|
none |
Code block title, line numbers, highlights |
|
none |
Code copy, select and annotations |
|
|
Inline code highlighting |
|
none |
Content tabs |
|
|
Tables |
|
none |
Footnotes |
|
|
Grids and cards |
|
none |
Abbreviations and glossary |
|
|
Buttons |
|
none |
Keyboard keys |
|
none |
Task lists |
|
none |
Admonitions
An admonition is a highlighted block such as a note or a warning. It is written with !!!, a type and an optional
title in double quotes, and the content is indented by four spaces:
!!! note
Lengths use metres as the base unit.
!!! warning "Rounding"
Results are floating point values.
!!! tip ""
No title here.
??? example "Collapsed example"
Hidden until opened.
???+ info "Open by default"
Visible at load.
!!! info inline end "Side note"
Floats to the right.
Body text that sits beside the inline admonition.
!!! note "Outer"
Outer text.
!!! danger "Inner"
Inner text.
Source: Zensical docs: Admonitions
The build produced these elements:
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>Lengths use metres as the base unit.</p>
</div>
<div class="admonition tip">
<p>No title here.</p>
</div>
<details class="example">
<summary>Collapsed example</summary>
<p>Hidden until opened.</p>
</details>
<details class="info" open="open">
<summary>Open by default</summary>
<p>Visible at load.</p>
</details>
<div class="admonition info inline end">
Source: HTML of the scratch build, Zensical 0.0.72
The rules are:
-
!!! typewithout a title uses the capitalised type as the title. The type defaults tonote. An empty title (!!! tip "") removes both the title and the icon. -
The supported types are
note,abstract,info,tip,success,question,warning,failure,danger,bug,exampleandquote. Custom types need CSS; the documentation notes that CSS written for Material for MkDocs may need changes for themoderntheme variant. -
???starts a collapsible block rendered as adetailselement, and???+starts it expanded. It needspymdownx.details. -
inlinefloats the block to the left andinline endto the right. The inline block must be written before the content it sits beside; on narrow screens it takes the full width. -
To nest blocks, indent the inner block inside the outer block. Four spaces are required: a block whose body was indented with two spaces rendered an empty admonition followed by a plain paragraph in the scratch build.
-
GitHub-style callouts (
> [!NOTE], upper case) become admonitions whenpymdownx.quotesis enabled withcallouts = true. In the scratch build,pymdownx.quotes.callouts = truerendered a[!NOTE]blockquote asadmonition note. The same effect is available through thecalloutsorgh-admonitionsplugins, which Zensical maps to this extension.
The icon of each type is replaced in zensical.toml with bundled Octicons, Font Awesome or custom icons:
[project.theme.icon.admonition]
note = "octicons/tag-16"
tip = "octicons/squirrel-16"
warning = "octicons/alert-16"
Source: Zensical docs: Admonitions
Code blocks
Fenced code blocks are handled by pymdownx.superfences and highlighted by pymdownx.highlight (Pygments). The
options after the language shortcode are:
```py title="convert.py" linenums="1" hl_lines="2 3"
from pocket_units import LengthUnit, convert
miles = convert(10, LengthUnit.KILOMETRE, LengthUnit.MILE)
print(round(miles, 3))
```
```toml
[project]
site_name = "Pocket Units" # (1)!
```
1. The name shown in the header.
``` { .python .no-copy }
print("no copy button")
```
Use `#!python range()` inline.
Source: Zensical docs: Code blocks
-
title="..."adds a file name above the block. The first scratch build rendered it as a table row with classfilename. -
linenums="1"shows line numbers starting at the given number.hl_linestakes line numbers and ranges ("2 3","3-5") counted from 1 even whenlinenumsstarts elsewhere. Both options turned the block into ahighlighttablewith a line-number column in the scratch build. -
A
(1)marker in a comment of the block’s language becomes an annotation when the theme featurecontent.code.annotateis on; the numbered list after the block holds its text and may use Markdown. A!after the closing parenthesis ((1)!) removes the comment characters around the marker. The marker is replaced by browser JavaScript: the built HTML still contains# (1)!. Annotations inside JSON strings need[project.extra.annotate] json = [".s2"]. -
content.code.copyadds a copy button to every block,content.code.selecta select button, and the attribute forms{ .yaml .copy },{ .yaml .no-copy },.select,.no-selectand.annotateoverride them per block (the language comes first, with a dot). Theno-copyclass appeared on the rendereddivof the example above. -
#!python range()highlights inline code; it needspymdownx.inlinehilite. The scratch build gave<code class="language-python highlight">. -
pymdownx.snippetsembeds a file in a block with a line such as--8<-- ".browserslistrc". Itsbase_pathoption lists the directories searched; the snippets files are also watched by Zensical, so a change triggers a rebuild.
The settings used by the examples:
[project.theme]
features = [
"content.code.annotate",
"content.code.copy",
"content.code.select",
]
[project.markdown_extensions]
pymdownx.highlight.anchor_linenums = true
pymdownx.highlight.line_spans = "__span"
pymdownx.highlight.pygments_lang_class = true
pymdownx.inlinehilite = {}
pymdownx.snippets = {}
pymdownx.superfences = {}
Source: Zensical docs: Code blocks
Content tabs
Content tabs group alternatives, for example the same call in Python and C#. Each tab starts with === and a quoted
label, and its content is indented by four spaces. They need pymdownx.tabbed with alternate_style and
pymdownx.superfences:
=== "Python"
```python
convert(1, LengthUnit.MILE, LengthUnit.METRE)
```
=== "C#"
```csharp
Converter.Convert(1, LengthUnit.Mile, LengthUnit.Metre);
```
Source: Zensical docs: Content tabs
The build produced a tabbed-set tabbed-alternate element with one radio input per tab, a tabbed-labels element and a
tabbed-block per tab, so tabs work without JavaScript. Other points:
-
The theme feature
content.tabs.linkmakes all tabs with the same label switch together across the site; tabs are linked by label, not by position, and the choice persists across page loads and works with instant navigation. -
pymdownx.tabbed.combine_header_slug = true(written byzensical new) makes the anchor of a tab combine the heading and the label. Readable anchors can also be produced with the optionspymdownx.tabbed.slugify.object(set to the stringpymdownx.slugs.slugify) andpymdownx.tabbed.slugify.kwds.case(set tolower). -
With SuperFences, tabs can hold lists, code blocks and other tabs, and can sit inside admonitions and blockquotes. Lists need no special syntax.
Tables
Tables use the standard pipe syntax, and a colon in the divider row sets the alignment. Tables are always enabled
(tables = {} is only a safeguard):
| Unit | Metres | Symbol |
| :--- | -----: | :----: |
| Foot | 0.3048 | ft |
| Mile | 1609.344 | mi |
Source: Zensical docs: Data tables
The scratch build wrote style="text-align: left;", right and center on the header and body cells. Tables can be
made sortable with the third-party Tablesort script, which the documentation loads from a CDN and a small file in
docs/javascripts/:
document$.subscribe(function() {
var tables = document.querySelectorAll("article table:not([class])")
tables.forEach(function(table) {
new Tablesort(table)
})
})
Source: Zensical docs: Data tables
[project]
extra_javascript = [
"https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js",
"javascripts/tablesort.js",
]
Source: Zensical docs: Data tables
The sorting script was not executed here, because it needs a browser. The document$ observable fires on each page
load with instant navigation, which is why the script subscribes to it instead of running once.
Footnotes
Footnotes need the footnotes extension. A reference is [^id], and the definition can be anywhere in the file:
Footnote ref[^1] and another[^note].
[^1]: Short footnote.
[^note]:
Long footnote
over two lines.
Source: Zensical docs: Footnotes
The build numbered the references in order of appearance (the second one became 2 although its identifier is note),
kept the identifiers as anchors (#fn:1, #fn:note) and added a backlink to each definition. Footnote text is always
rendered at the bottom of the page. Paragraphs of a multi-line footnote must be indented by four spaces. The theme
feature content.footnote.tooltips shows the footnote as a tooltip when the reference is hovered or focused. A
reference with no definition stays literal text (Text[^9]) and the default build reported no issue; the
unresolved_footnotes and unused_footnotes checks of [project.validation] (see
Validation and Publishing) are deprecated in their
current form but still usable.
Grids and cards
Grids lay blocks out in columns. They need attr_list and md_in_html, because the markdown attribute on the HTML
div makes its content be parsed as Markdown:
<div class="grid cards" markdown>
- :material-ruler: __Lengths__ in four units
- :material-weight: __Masses__ are not supported
</div>
<div class="grid" markdown>
=== "One"
first
=== "Two"
second
</div>
Source: Zensical docs: Grids
-
A list inside
<div class="grid cards" markdown>becomes one card per item. In the scratch build the list kept itsuland the icons became inline SVG. -
The block syntax puts
{ .card }on its own line after a block inside adiv class="grid", so cards and other elements can be mixed. -
A
div class="grid"withoutcardsis a generic grid: any blocks (tabs, admonitions, code blocks) are placed side by side. The two tabs above rendered inside onediv class="grid". -
Icon shortcodes such as
:material-ruler:come frompymdownx.emojiwith the Zensical generators (zensical.extensions.emoji.to_svgandzensical.extensions.emoji.twemojiin the generatedzensical.toml). -
Without
md_in_html, the content of thedivis left as written: in a build with onlyadmonitionandattr_list, the list inside<div class="grid cards">stayed as literal text (- **A** one).
Tooltips, abbreviations and glossaries
Three mechanisms add hover text:
[Hover me](https://example.org "I am a tooltip")
The SI system is used. :material-information-outline:{ title="Important information" }
Source: Zensical docs: Tooltips
-
A link title becomes a tooltip. The build kept it as the
titleattribute of the link, and an attribute list (attr_list) can puttitle="..."on other elements such as icons. The theme featurecontent.tooltipsenables the improved tooltips of the theme instead of the plain browser ones. -
An abbreviation is defined with
*[HTML]: Hyper Text Markup Languageanywhere in the file (extensionabbr) and every occurrence in the page gets anabbrelement. -
A glossary is a file with abbreviation definitions that is appended to every page. The file is kept outside
docs_dir(the documentation usesincludes/abbreviations.md) and listed in the snippets optionauto_append.
[project.markdown_extensions]
abbr = {}
attr_list = {}
pymdownx.snippets.auto_append = ["includes/abbreviations.md"]
Source: Zensical docs: Tooltips
With includes/abbreviations.md containing *[SI]: International System of Units, the sentence above produced
<abbr title="International System of Units">SI</abbr>. The snippets base_path defaults to the current directory, so
the path in auto_append is relative to the project folder (the folder with zensical.toml); a scratch build with
base_path = ["."] set explicitly worked.
Buttons
A link becomes a button when the md-button class is added with an attribute list; adding md-button--primary marks
the primary button, and an icon shortcode can be part of the label:
[Get started](#){ .md-button }
[Download :material-download:](#){ .md-button .md-button--primary }
Source: Zensical docs: Buttons
Only attr_list is required. The build produced <a class="md-button" href="#"> and
<a class="md-button md-button--primary" href="#">.
Other extensions used in the generated project
The remaining extensions of the default list are small:
-
def_listwrites definition lists (a term line followed by: definition). -
pymdownx.keysrendersctrl+alt+delaskbdelements with key classes (key-control,key-alt,key-delete), as seen in the scratch build. -
pymdownx.tasklistwithcustom_checkboxrenders- [x] doneand- [ ] openas atask-listof disabled checkboxes. -
pymdownx.mark(==text==),pymdownx.caret(^^insert^^and^sup^),pymdownx.tilde(~~delete~~and~sub~),pymdownx.betterem(more predictable emphasis) andpymdownx.smartsymbols(typographic replacements) extend inline formatting.H~2~Orendered asH<sub>2</sub>Oand==mark==as amarkelement in a project with the defaults. -
pymdownx.magiclinkturns plain URLs into links. -
tocwithpermalink = trueadds the pilcrow link beside each heading; the table of contents itself is covered in Navigation.
Pitfalls and version notes
-
Indent block content by exactly four spaces (admonitions, tabs, footnotes). Two spaces produce an empty block and a plain paragraph, with no build error.
-
Restating
markdown_extensionsreplaces the defaults. Start from the list written byzensical new. -
Grids and cards need both
attr_listandmd_in_html, and themarkdownattribute on eachdiv. -
Annotations, copy and select buttons, tab linking and tooltips are theme features, not extensions: they need the matching entry in
[project.theme] features. -
Zensical is an alpha at 0.0.72. The announced 0.1.0 release line is planned for 2026-11-05 (see Roadmap and Upgrade Policy). The official pages describe a separate "Zensical Markdown" parser and Directives only as items for the paid Spark and early-access products, so this page covers the Python Markdown based behaviour that ships in the open source package.
-
The official pages list no extension compatibility matrix that could be fetched while writing; the list of extensions that work was taken from the installed package and the authoring pages. Check the compatibility section of the Zensical site for extensions not mentioned here.
-
The C# tab in the examples is only highlighted, not compiled; the Pocket Units code is shown for syntax only.