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

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_extensions table, Zensical enables a built-in list: abbr, admonition, attr_list, def_list, footnotes, md_in_html, toc (with permalink), and the PyMdown extensions arithmatex, betterem, caret, details, emoji, highlight, inlinehilite, keys, magiclink, mark, smartsymbols, superfences (with a mermaid fence), tabbed (alternate style), tasklist and tilde. This list is the DEFAULT_MARKDOWN_EXTENSIONS constant in the installed zensical/config.py; the documentation pages consulted do not state it. A project with no table rendered tabs, admonitions, ==mark== and H~2~O without 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== and H~2~O stayed literal text and headings lost their permalink. Copy the full list that zensical new writes (below) and remove entries, rather than starting from one line.

  • toc and tables are always added by Zensical, whether they are listed or not. A table was rendered in the project that listed only admonition.

  • A name that cannot be imported stops the build. Listing pymdownx.nonexistent = {} failed with RuntimeError: 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.

flowchart LR A["docs/*.md and front matter"] --> B["Python Markdown + PyMdown Extensions"] C["project.markdown_extensions in zensical.toml"] --> B B --> D["HTML with CSS classes"] E["project.theme.features"] --> F["Theme CSS and JavaScript"] D --> F F --> G["Rendered page"]

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

admonition

none

Collapsible blocks

admonition, pymdownx.details

none

Nested content in admonitions or tabs

pymdownx.superfences

none

Code block title, line numbers, highlights

pymdownx.highlight, pymdownx.superfences

none

Code copy, select and annotations

pymdownx.highlight, pymdownx.superfences

content.code.copy, content.code.select, content.code.annotate

Inline code highlighting

pymdownx.inlinehilite

none

Content tabs

pymdownx.tabbed (alternate_style), pymdownx.superfences

content.tabs.link (optional, links tabs with the same label)

Tables

tables (always on)

none

Footnotes

footnotes

content.footnote.tooltips (optional)

Grids and cards

attr_list, md_in_html

none

Abbreviations and glossary

abbr, attr_list, pymdownx.snippets

content.tooltips (improved tooltips)

Buttons

attr_list

none

Keyboard keys

pymdownx.keys

none

Task lists

pymdownx.tasklist

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.

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:

  • !!! type without a title uses the capitalised type as the title. The type defaults to note. 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, example and quote. Custom types need CSS; the documentation notes that CSS written for Material for MkDocs may need changes for the modern theme variant.

  • ??? starts a collapsible block rendered as a details element, and ???+ starts it expanded. It needs pymdownx.details.

  • inline floats the block to the left and inline end to 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 when pymdownx.quotes is enabled with callouts = true. In the scratch build, pymdownx.quotes.callouts = true rendered a [!NOTE] blockquote as admonition note. The same effect is available through the callouts or gh-admonitions plugins, 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"

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.
  • title="..." adds a file name above the block. The first scratch build rendered it as a table row with class filename.

  • linenums="1" shows line numbers starting at the given number. hl_lines takes line numbers and ranges ("2 3", "3-5") counted from 1 even when linenums starts elsewhere. Both options turned the block into a highlighttable with 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 feature content.code.annotate is 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.copy adds a copy button to every block, content.code.select a select button, and the attribute forms { .yaml .copy }, { .yaml .no-copy }, .select, .no-select and .annotate override them per block (the language comes first, with a dot). The no-copy class appeared on the rendered div of the example above.

  • #!python range() highlights inline code; it needs pymdownx.inlinehilite. The scratch build gave <code class="language-python highlight">.

  • pymdownx.snippets embeds a file in a block with a line such as --8<-- ".browserslistrc". Its base_path option 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 = {}

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);
    ```

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.link makes 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 by zensical new) makes the anchor of a tab combine the heading and the label. Readable anchors can also be produced with the options pymdownx.tabbed.slugify.object (set to the string pymdownx.slugs.slugify) and pymdownx.tabbed.slugify.kwds.case (set to lower).

  • 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 |

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)
  })
})
[project]
extra_javascript = [
  "https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js",
  "javascripts/tablesort.js",
]

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.

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>
  • A list inside <div class="grid cards" markdown> becomes one card per item. In the scratch build the list kept its ul and the icons became inline SVG.

  • The block syntax puts { .card } on its own line after a block inside a div class="grid", so cards and other elements can be mixed.

  • A div class="grid" without cards is a generic grid: any blocks (tabs, admonitions, code blocks) are placed side by side. The two tabs above rendered inside one div class="grid".

  • Icon shortcodes such as :material-ruler: come from pymdownx.emoji with the Zensical generators (zensical.extensions.emoji.to_svg and zensical.extensions.emoji.twemoji in the generated zensical.toml).

  • Without md_in_html, the content of the div is left as written: in a build with only admonition and attr_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" }
  • A link title becomes a tooltip. The build kept it as the title attribute of the link, and an attribute list (attr_list) can put title="..." on other elements such as icons. The theme feature content.tooltips enables the improved tooltips of the theme instead of the plain browser ones.

  • An abbreviation is defined with *[HTML]: Hyper Text Markup Language anywhere in the file (extension abbr) and every occurrence in the page gets an abbr element.

  • A glossary is a file with abbreviation definitions that is appended to every page. The file is kept outside docs_dir (the documentation uses includes/abbreviations.md) and listed in the snippets option auto_append.

[project.markdown_extensions]
abbr = {}
attr_list = {}
pymdownx.snippets.auto_append = ["includes/abbreviations.md"]

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 }

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_list writes definition lists (a term line followed by : definition).

  • pymdownx.keys renders ctrl+alt+del as kbd elements with key classes (key-control, key-alt, key-delete), as seen in the scratch build.

  • pymdownx.tasklist with custom_checkbox renders - [x] done and - [ ] open as a task-list of disabled checkboxes.

  • pymdownx.mark (==text==), pymdownx.caret (^^insert^^ and ^sup^), pymdownx.tilde (~~delete~~ and ~sub~), pymdownx.betterem (more predictable emphasis) and pymdownx.smartsymbols (typographic replacements) extend inline formatting. H~2~O rendered as H<sub>2</sub>O and ==mark== as a mark element in a project with the defaults.

  • pymdownx.magiclink turns plain URLs into links.

  • toc with permalink = true adds 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_extensions replaces the defaults. Start from the list written by zensical new.

  • Grids and cards need both attr_list and md_in_html, and the markdown attribute on each div.

  • 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.