Parsing

The [parse] section extends ubCode’s built-in reStructuredText parser with custom directives and roles from your Sphinx extensions. Use this when ubCode warns about unknown directives or roles that are provided by third-party packages.

It also configures which parser handles which file — for example adding Markdown (MyST) alongside reStructuredText within a single project — via the [parse.parsers.*] tables (see Parsers and file routing).

Minimal example

[parse]
ignore_directives = ["my-custom-directive"]

[parse.extend_directives.my-admonition]
argument = true
content = true
parse_content = true
description = "A custom admonition"
extension = "my-extension"

[parse.extend_roles.my-role]
description = "A custom inline role"
extension = "my-extension"

Core options

ignore_directives

Type: array (default: [])

List of directive names that won’t trigger warnings when encountered but not recognised by the parser. Useful for custom directives from external extensions.

ignore_directives = [
    "my-custom-directive",
    "experimental-feature",
    "legacy-directive"
]
extensions

Type: array or table (default: each port’s own default)

Switch built-in Sphinx-extension ports on or off (see Built-in extension ports below), named exactly as in a Sphinx conf.py extensions list. A port is a Sphinx extension ubCode reimplements natively, so its markup renders for real rather than being passed through.

Most ports are already on — you only need this key to switch one off, or to switch on one of the few that are off by default. The per-port defaults are in the table under Built-in extension ports.

Two forms are accepted, and a list entry means exactly the same as name = true:

[parse]
extensions = ["sphinx.ext.autosectionlabel"]

# or, to switch ports off as well as on:
[parse.extensions]
"sphinx.ext.autosectionlabel" = true
"sphinxcontrib.video" = false
"sphinx.ext.todo" = { active = false }

The entry-table form ({ active = false }) means the same as the plain boolean; active is its only field, an entry table that omits it switches the port on, and any other key is reported (config.unknown_extension_option) and ignored — a misspelt activ = false would otherwise silently leave the port on.

Switching a port off does not make its markup unknown: it still parses, its options are still validated and completed, and each use site renders as a labelled placeholder showing its source and warns directive.extension_disabled, naming the extension and this key. See What “off” looks like.

Unknown names are ignored, so you can mirror a conf.py extensions list wholesale — entries ubCode ships no port for (myst_parser, sphinx.ext.napoleon, …) are simply inert. Three cases get an answer instead of silence:

  • a name close to a known port’s name (for example sphinxcontrib.vide, or sphinx.ext.video for sphinxcontrib.video) is reported as a likely typo (config.unknown_extension), since it would otherwise switch nothing silently;

  • sphinx_needs is built into ubCode and always active, so an entry switching it off is reported (config.extension_always_on) and ignored;

  • a name ubCode knows but ships no port for — the sphinx.ext.autodoc family, sphinx.ext.graphviz — is reported when switched on (config.extension_unsupported), because listing it cannot make that markup render: only sphinx-build can run those extensions.

Like every other code these can be suppressed, via [lint] ignore = ["config.unknown_extension"] (see Code matching and .* wildcards).

Tip

If you also build with sphinx-build, list your extensions explicitly. The defaults here are a ubCode-side courtesy; Sphinx needs each extension named in its own extensions list, so a project that relies on ubCode’s defaults will find the same markup failing in the Sphinx build.

default_role

Type: string (default: unset)

The role applied to bare interpreted text (`` text ) in every document, i.e. the seed for the ``.. default-role:: state. When unset (or set to an empty string, which is equivalent), bare interpreted text uses the docutils default, title-reference.

[parse]
default_role = "emphasis"

An in-document .. default-role:: still overrides this from its point, and a bare .. default-role:: reset returns to title-reference (not to this configured value). If the value names a role ubCode does not recognise, a configuration diagnostic is reported and the seed is ignored.

This corresponds to Sphinx’s default_role confval; a conf.py projection loader that maps ubCode’s native keys onto Sphinx’s confval names is planned.

highlight_language

Type: string (default: unset)

The highlight language applied to language-less literal/code blocks in every document, i.e. the seed for the .. highlight:: state. When unset (or set to an empty string, which is equivalent), language-less blocks stay language-less (no language class), preserving ubCode’s default behaviour.

[parse]
highlight_language = "python"

An in-document .. highlight:: <lang> still overrides this from its point.

This corresponds to Sphinx’s highlight_language confval (whose Sphinx default is default; ubCode leaves it unset instead, so existing projects gain no language-class change). A conf.py projection loader that maps ubCode’s native keys onto Sphinx’s confval names is planned.

rst_prolog

Type: string (default: unset)

reStructuredText source prepended to every RST document in the rendered preview. The content is parsed at the very start of each document, so a .. default-role:: or .. role:: defined here applies to the whole document — its state flows out of the prologue into the body. When unset (or set to an empty string, which is equivalent), nothing is prepended.

[parse]
rst_prolog = """
.. |sub| replace:: substitution text
"""

This corresponds to Sphinx’s rst_prolog confval; a conf.py projection loader that maps ubCode’s native keys onto Sphinx’s confval names is planned.

rst_epilog

Type: string (default: unset)

reStructuredText source appended to every RST document in the rendered preview, parsed at the very end of each document. When unset (or set to an empty string, which is equivalent), nothing is appended.

[parse]
rst_epilog = """
.. |trademark| replace:: ™
"""

This corresponds to Sphinx’s rst_epilog confval; a conf.py projection loader that maps ubCode’s native keys onto Sphinx’s confval names is planned.

syntax_example_numbering

Type: boolean (default: false)

Number the syntax-example blocks of each document. When enabled, a block with no title argument is titled Example N and a block with one is titled Example N: <title>, where N counts 1, 2, through the document and restarts at each document. When disabled (the default) every block keeps its plain title — Example, or the title argument on its own.

[parse]
syntax_example_numbering = true

The option only has an effect while the sphinx_syntax_example port is active, which it is by default (see Available ports below) — the port is what renders the directive. It corresponds to the syntax_example_numbering configuration value of the sphinx-syntax-example Sphinx extension, so a project sharing one source tree between sphinx-build and ubCode gets the same numbering from both — requires sphinx-syntax-example 0.2.0 or later, since earlier versions have no such confval and ignore it, which would leave the Sphinx build unnumbered while ubCode numbers.

raw_enabled

Type: boolean (default: true)

Whether the raw directive passes its content through to the output. On by default, matching docutils and Sphinx: a raw block’s content reaches the built HTML verbatim, unescaped.

[parse]
raw_enabled = false

With the switch off, every .. raw:: (and MyST {raw}) passes nothing through: it raises a directive.raw_disabled warning — docutils’ "raw" directive disabled. message — and renders a placeholder showing its own source, escaped, instead. The :file: form is not read at all, so a disabled raw records no dependency on the file it names. This applies wherever a document is processed: ubc build, ubc check, and the editor preview.

Set it to false for pipelines that build contributions you do not control — an auto-deployed preview of an external pull request, say — where the author of a page is not the party you want writing raw HTML into it. It corresponds to the docutils setting of the same name, which Sphinx never changes from its default.

The switch gates the raw directive only, exactly as docutils’ does. HTML written directly in a Markdown document is not a directive and is unaffected by it; see Raw content for the separate, preview-only escaping that covers both.

Built-in extension ports

Added in version 0.31.0.

Some Sphinx extensions are ported into ubCode — reimplemented natively so their markup renders for real, both in the live preview and in ubc build. This is different from Extending directives below, which only declares a third-party directive so it stops warning (its content is passed through, not rendered).

You do not have to list a port to use it. Every port whose behaviour is triggered by a piece of markup is on by default: write .. video:: and it renders. [parse].extensions is the switchboard for changing that — switching a port off, or switching on one of the few that start off.

Built-in ports and their defaults

Extension

Default

What it provides

sphinxcontrib.video

on

the video directive

sphinx_syntax_example

on

the syntax-example directive

sphinx.ext.todo

on

the todo and todolist directives

sphinx_design

on

the grid/card/dropdown/tab-set directive family

sphinxcontrib.mermaid

on

the mermaid directive

sphinxcontrib.plantuml

on

the plantuml and uml directives

sphinx.ext.extlinks

data

link-shorthand roles — a populated [parse.extlinks] table switches the port on by itself

sphinx.ext.autosectionlabel

off

:ref: targets minted from section titles

sphinx_needs

always

the needs directives and roles; this one is ubCode and cannot be switched off

Everything else a conf.py may list is inert. Two families are worth naming because ubCode knows them by name and still cannot render them: the sphinx.ext.autodoc family and sphinx.ext.graphviz have no ubCode implementation at all (their directives parse, and render as labelled placeholders), so switching them on is reported (config.extension_unsupported) — only sphinx-build can run them.

Two more built-ins are gated by their own configuration section rather than by this key, and must not be listed here: [intersphinx] (inventory-backed cross-project references) and [codelinks] (source-code traceability). Both are active as soon as their section has content — the same data-implied rule [parse.extlinks] follows.

Switching a port off takes an entry, since the defaults are on:

[parse.extensions]
"sphinxcontrib.video" = false

# the entry-table form, for the same thing
"sphinx.ext.todo" = { active = false }

What “off” looks like

A switched-off port’s directives stay known. They parse, their options are validated, they are offered in completion and hover, and each use site:

  • renders as a labelled placeholder — a framed block showing the directive’s name, argument, options and body, so the source stays readable in the output;

  • warns directive.extension_disabled at that site in the editor, naming the extension and this [parse] extensions key;

  • is counted on ubc build html’s coverage radar (build.directive_unhandled), de-duplicated per directive name, with the same sentence.

“Off” therefore never means ubCode plays dumb about markup it can render; it means “shown as a placeholder, and told why”. Roles are the one exception, and it is a recorded gap: an extlinks role whose port is switched off is shown as a placeholder chip carrying the role name and its content, with no warning, and sphinx_design’s :octicon: role keeps rendering even with sphinx_design = false.

Available ports

sphinxcontrib.video

A port of sphinxcontrib-video, adding the video directive for embedding an HTML5 <video> player.

.. video:: media/demo.mp4
   :width: 640
   :muted:
   :loop:

In Markdown (MyST) the same directive is written as a fence:

```{video} media/demo.mp4
:width: 640
:muted:
:loop:
```

Local video files (and a :poster: image) are copied into the build output alongside your other assets; remote URLs are referenced as-is and never downloaded.

A source must name its file exactly — a * wildcard extension (media/demo.*) is not resolved for a video source. ubc build html reports build.video_glob_unsupported and emits the reference as written (minus any ?query/#fragment, which every asset reference is stripped of); nothing is copied for it. Wildcard extensions are an image feature: candidates are ranked by image type, which cannot choose a video file. A :poster: is an image, so :poster: media/thumb.* works normally.

That warning is emitted while the page is rendered, and it has no ubc check counterpart (the background index does not resolve video sources) — but ubc build html reports it on every build, warm or fresh: a page’s render warnings are stored with its rendered body, so a build that reuses the page re-reports them. Note also that only the <stem>.* form is a wildcard extension: media/*.mp4 is treated as a literal filename and fails to copy (build.image_read_failed), as it did before. The full upstream option set is supported — :alt:, :autoplay:, :nocontrols:, :loop:, :muted:, :playsinline:, :controlslist:, :poster:, :preload:, :width:, :height:, :class:, :align:, :caption: and :figwidth: — plus an optional second source argument for format fallback.

sphinx_syntax_example

A port of sphinx-syntax-example, adding the syntax-example directive, which shows a block of markup twice — once as its raw, highlighted source and once as the rendered result — for documentation that teaches a markup syntax. The optional argument is the title shown above the block (Example when omitted), and :highlight: overrides the source language. See Directives for the directive itself.

[parse]
syntax_example_numbering = true

# the port is on by default; this is how you switch it off
[parse.extensions]
sphinx_syntax_example = false

This port has one option of its own, [parse].syntax_example_numbering (documented under Core options above): it numbers each document’s examples Example N / Example N: <title>, and is off by default. Note the port name is the extension’s module name (an underscore, not the hyphenated distribution name), exactly as a Sphinx conf.py spells it.

sphinx.ext.todo

A port of sphinx.ext.todo, adding the todo directive — an admonition for an outstanding task — and todolist, which lists every todo in the project.

.. todo:: Document the new ``--strict`` flag.

.. todo::
   :class: blocking

   Decide whether the cache bump can be avoided.

In Markdown (MyST) the same directives are written as fences:

```{todo}
Document the new `--strict` flag.
```

```{todolist}
```

Each todo gets an anchor — todo-1, todo-2, … in document order, or the :name: you give it — so a todolist links back to the original. A todolist shows every project todo followed by that backlink, in source-path then document order.

This port has two options of its own, both under [parse] and both named exactly as their Sphinx configuration values are:

[parse]
todo_include_todos = true
todo_link_only = false

# the port is on by default; this is how you switch it off
[parse.extensions]
"sphinx.ext.todo" = false
todo_include_todos

Type: boolean (default: true)

Whether todos render at all. With it off, every todo and every todolist renders nothing — exactly as if the markup were absent — which is how a build reads for an audience that should not see outstanding work.

This corresponds to Sphinx’s todo_include_todos, whose default is false. The defaults differ deliberately: in Sphinx an extension is often listed for reasons other than showing todos, whereas in ubCode a project that writes todo markup is taken to want it rendered — hiding it is this one key, rather than the absence of an entry. A project sharing one source tree between sphinx-build and ubCode should set both values explicitly rather than rely on either default, since the Sphinx build shows nothing until its own value is true.

todo_link_only

Type: boolean (default: false)

Render a todolist entry’s backlink on its own, without the surrounding “is located in <file>, line <n>” sentence. Corresponds to Sphinx’s todo_link_only, and shares its default.

Differences from a sphinx-build of the same sources worth knowing:

  • inside a todolist, each entry’s body is shown as plain text, one line per block — a bullet list’s items, and any nested blocks, each get their own line, but the bullets themselves and any inline markup are not reproduced, where Sphinx copies the block whole and shows a real list. The original block, one click away, renders in full;

  • the backlink names the project-relative source path, where Sphinx interpolates an absolute filesystem path — so the built HTML is the same on every machine;

  • :class: adds to a todo’s styling, where Sphinx replaces it: .. todo:: :class: urgent keeps the todo accent in ubCode and loses it entirely in a Sphinx build, because the extension sets its own default through that same option;

  • listing order is by source path then document order. A Sphinx build’s order depends on which files it last re-read, so the two agree on a clean build and can differ after an incremental one;

  • :name: on a todo crashes sphinx-build (an unhandled error inside the extension); ubCode treats it as an ordinary admonition name. Avoid it in a shared source tree.

todo_emit_warnings is not ported yet.

sphinx.ext.extlinks

A port of sphinx.ext.extlinks, which turns a repetitive URL pattern into a short role. Unlike the video port it defines no fixed directive: you declare one role per entry in a [parse.extlinks] table, mapping a role name to a URL template — and an optional caption template — in which %s is replaced by the role’s text.

[parse.extlinks.issue]
url = "https://github.com/useblocks/ubcode/issues/%s"
caption = "issue %s"

The table is all it takes — a populated [parse.extlinks] switches the port on by itself, so there is no second key to remember. To keep the table but switch the roles off, say so explicitly:

[parse.extensions]
"sphinx.ext.extlinks" = false

That combination — a populated table with the port switched off — is reported (config.extlinks_data_inert), because the roles then go unregistered and each use renders as a plain chip with nothing pointing at the entry responsible. It is worth knowing when the = false is inherited: entries merge by name, so a base config that switches the port off keeps doing so for a child that only writes the table.

With that configuration the issue role links to the substituted URL — the first form below renders as issue 42 linking to .../issues/42:

See :issue:`42` for the bug, or :issue:`the tracker <100>` for the epic.

The same role in Markdown (MyST):

See {issue}`42` for the bug, or {issue}`the tracker <100>` for the epic.
  • caption is optional; when it is omitted the link text is the fully expanded URL.

  • An explicit title — the the tracker <100> form above — is used as the link text, and the target (100) fills the %s in the URL.

  • Write a literal percent as %%; each template may contain at most one %s. A template with more (or a stray %) is reported as a configuration warning and that role is skipped.

  • A role name that would shadow a built-in role (ref, pep, …) is refused with a configuration warning, so the built-in keeps working.

sphinx.ext.autosectionlabel

A port of sphinx.ext.autosectionlabel, which lets you reference every section by its title with :ref:, without writing an explicit .. _label: above it. Unlike the other ports it adds no directive or role: enabling it makes the labels exist, project-wide.

[parse]
extensions = ["sphinx.ext.autosectionlabel"]
autosectionlabel_prefix_document = true
autosectionlabel_maxdepth = 2

With autosectionlabel_prefix_document on (as above), a section Overview in guide/intro.rst is referenced by document and title, and the reference links straight to that section:

See :ref:`guide/intro:Overview` for the details.

The same reference in Markdown (MyST):

See {ref}`guide/intro:Overview` for the details.

Titles are matched case- and whitespace-insensitively, exactly as :ref: already matches explicit labels.

This port has two options of its own, both under [parse] and both named exactly as their Sphinx configuration values are:

autosectionlabel_prefix_document

Type: boolean (default: false)

Prefix each label with the document’s name (doc/path:Title). Off by default — matching Sphinx — a section is referenced by its bare title alone (Overview), with no document prefix. Turning it on is recommended: bare titles easily collide across documents, and a colliding label resolves to only one of its sections. Corresponds to Sphinx’s autosectionlabel_prefix_document, and shares its default.

autosectionlabel_maxdepth

Type: integer (default: unset — no limit)

Label only the top N section levels of each document (1 = every top-level section). Unset or 0 labels every section. Corresponds to Sphinx’s autosectionlabel_maxdepth, and shares its default.

Duplicate labels. Bare mode is the default, and repeated section titles (“Overview”, “Usage”, “Configuration”) are the norm rather than the exception, so enabling the port on an existing project can surface duplicates that were not there before. ubCode treats the two cases differently:

  • within one document — two same-titled sections in bare mode, or a section title colliding with an explicit .. _label: in the same file — is resolved silently and deterministically. An explicit label written anywhere in that file wins over the automatic one, and among automatic labels the first section in the document wins. sphinx-build does warn here, and lets the last one it reads win instead;

  • across documents — the same title in two files, or a section title colliding with an explicit .. _label: written in another file — is reported as std.duplicate_label, naming every contributing site. The reference resolves deterministically to the first contributing file in path order (Sphinx instead lets the last document it reads win).

sphinx-build reports the cross-document cases too, as duplicate label <name>. If the port’s duplicates are noise for your project, silence that class alone:

[lint]
ignore = ["std.duplicate_label"]

This is the counterpart of Sphinx’s suppress_warnings = ['autosectionlabel']: it is scoped to the port, so duplicates between explicitly authored targets keep reporting under std.duplicate_target.

When both apply to one name — two explicit .. _label: targets that a section title happens to share — both codes are reported: the explicit duplicate exists whether or not the port is enabled, so silencing the port’s class leaves it visible, and silencing std.duplicate_target leaves the port’s class visible. sphinx-build likewise emits two warnings for such a name.

Other differences from a sphinx-build of the same sources worth knowing:

  • in Markdown (MyST), a title containing inline markup (e.g. # My **bold** title) is labelled by its plain text (My bold title), where Sphinx keeps the raw markup in the label name. Plain-text titles — the overwhelming majority — agree exactly.

Extending directives

The extend_directives section declares directives ubCode does not know, so they parse without warning and their options are validated and completed. Each directive is configured as a subsection:

Note

It declares NEW names; it cannot redefine a built-in one. A name ubCode already has a directive for — including a port’s name such as video or todo, which are ordinary built-ins whether or not their port is on — keeps its built-in parse specification, and the entry has no effect on it. Use a different name, and if you want a built-in’s rendering to stop, switch its port off through [parse].extensions rather than redeclaring it.

[parse.extend_directives.my-directive]
argument = true
options = true
content = true
content_required = false
parse_content = true
description = "My custom directive"
extension = "my-package"

# Define named options
[parse.extend_directives.my-directive.named_options]
title = { description = "The title of the element" }
class = { description = "CSS classes to apply" }

Directive configuration options:

argument

Type: boolean (default: false)

Whether the directive accepts an argument (text on the same line as the directive name).

.. my-directive:: This is the argument

   Content here.
options

Type: boolean (default: false)

Whether the directive accepts options (field list immediately after the directive line).

.. my-directive::
   :option1: value1
   :option2: value2

   Content here.
content

Type: boolean (default: false)

Whether the directive can have content (indented text block after options).

content_required

Type: boolean (default: false)

When true, emit a warning if the directive has no content when content is expected.

parse_content

Type: boolean (default: false)

When true, parse the directive content as reStructuredText instead of treating it as literal text.

description

Type: string (default: "")

Human-readable description of what the directive does.

extension

Type: string (default: "")

Name of the extension or package that provides this directive.

named_options

Type: object (default: {})

Map defining the specific options this directive accepts. Each option supports the following properties:

description

Type: string (default: "")

A short, human-readable description of the option.

choices

Type: array | null (default: null)

A list of valid string values for the option. When set, ubCode can validate option values and offer autocompletion.

flag

Type: boolean (default: false)

When true, the option is a flag that takes no value. It is either present or absent.

[parse.extend_directives.figure.named_options]
width = { description = "Width of the figure" }
height = { description = "Height of the figure" }
alt = { description = "Alternative text for accessibility" }
align = { description = "Alignment", choices = ["left", "center", "right"] }
figclass = { description = "CSS class for the figure" }
nofooter = { description = "Hide footer", flag = true }

Extending roles

The extend_roles section allows you to define custom inline roles:

[parse.extend_roles.api]
description = "Reference to an API endpoint"
extension = "my-api-docs"

[parse.extend_roles.issue]
description = "Reference to a GitHub issue"
extension = "github-integration"

Role configuration options:

description

Type: string (default: "")

Human-readable description of what the role does.

extension

Type: string | null (default: null)

Name of the extension or package that provides this role.

Example usage

Once defined in configuration, you can use custom directives and roles in your RST files:

This is a paragraph with a :api:`/users/create` endpoint reference
and an :issue:`123` issue reference.

.. my-admonition:: Important Notice
   :class: warning highlight
   :name: security-note

   This is custom admonition content that will be parsed as RST.

Parsers and file routing

Added in version 0.30.0.

By default ubCode parses every discovered file as reStructuredText. The [parse.parsers.<name>] tables let you instead route different files to different parsers — for example reStructuredText and Markdown (MyST) within a single project.

The shared ignore_directives, extend_directives, and extend_roles options documented above apply to all parsers (RST and MyST share the same directive and role definitions); each [parse.parsers.<name>] table then configures one parser and selects the files it owns.

Classic mode vs. parser mode

The behaviour is modal, derived solely from whether any [parse.parsers.*] table is declared:

Classic mode (no parsers declared)

The default, and unchanged from earlier versions. File discovery is governed by [source].include (default ["*.rst"]) and every discovered file is parsed as reStructuredText. Existing projects keep behaving exactly as before.

Parser mode (one or more parsers declared)

The declared parsers are the complete set. File discovery is derived from the parsers’ include globs (the [source].include default of ["*.rst"] no longer applies), and each discovered file is routed to the parser that owns it. A lone [parse.parsers.md] therefore parses Markdown only.

Note

Because the declared parsers are the complete set, a mixed reStructuredText + Markdown project must list both parsers explicitly — adding [parse.parsers.md] alone would drop *.rst discovery:

[parse.parsers.rst]
[parse.parsers.md]

Parser options

# A mixed RST + Markdown project
[parse.parsers.rst]

[parse.parsers.md]
flavour = "myst"
include = ["*.md", "docs/**/*.md"]

[parse.parsers.md.extensions]
dollarmath = { allow_space = true }
deflist = true

Each [parse.parsers.<name>] table accepts the following keys:

type

Type: string ("rst" or "md", optional)

The parser type. The canonical keys rst and md infer their type automatically, so type only needs to be set for a table with a different name (e.g. [parse.parsers.notes] with type = "rst"). A non-canonical key without an explicit type falls back to Markdown and emits a configuration diagnostic, so a custom-named parser never silently becomes Markdown.

include

Type: array (default: ["*.rst"] for RST, ["*.md"] for Markdown)

Glob patterns selecting which discovered files this parser handles. In parser mode the union of all parsers’ include globs also drives file discovery itself (replacing the [source].include default). See File routing for how patterns are matched.

priority

Type: integer (default: 0)

Tie-breaker used when a file matches several parsers equally specifically (see File routing). Higher wins. This is an escape hatch; routing normally resolves on glob specificity alone.

flavour

Type: string ("commonmark", "myst", or "gfm", default: "commonmark")

(Markdown parsers only.) The Markdown flavour, which selects a base set of enabled extensions:

  • commonmark — plain CommonMark, no extensions enabled by default.

  • mystMyST Markdown, enabling directive/role syntax, front matter, and tables (the always-on MyST core). This is the flavour to use for Sphinx-Needs-style directives in Markdown. Matching MyST-Parser, no optional extensions (such as deflist, dollarmath, colon_fence, or fieldlist) are enabled by default — enable the ones you need explicitly via extensions (see Markdown extensions below).

  • gfmGitHub Flavored Markdown, enabling the strikethrough, tasklist, and gfm_autolink extensions.

extensions

Type: array or object (default: {})

(Markdown parsers only.) Markdown extensions to enable on top of the flavour defaults. See Markdown extensions below.

Markdown extensions

The effective extension set for a Markdown parser is the flavour’s defaults unioned with any extensions you enable explicitly.

Available extensions

ubCode recognises the following extension names (matching the MyST-Parser / markdown-it-py extension keys). Those marked (GFM default) are enabled automatically by the gfm flavour; every other extension is off by default and must be enabled explicitly, whatever the flavour.

deflist

Definition lists — a term line followed by one or more : definition blocks.

dollarmath

Dollar math: $...$ inline and $$...$$ block (block math may carry a label). Tunable via dollarmath.allow_space and dollarmath.allow_digits (see Per-extension options).

fieldlist

reStructuredText-style field lists (:name: value).

colon_fence

Colon-delimited directives and admonitions (:::{note} ... :::), an alternative fence to backticks and tildes. Tunable via colon_fence.exact_match (see Per-extension options).

strikethrough

Strikethrough with ~~text~~ (and, optionally, single-tilde ~text~). (GFM default.) Tunable via strikethrough.single_tilde (see Per-extension options).

tasklist

Task-list checkboxes in list items (- [x] done, - [ ] todo). (GFM default.)

gfm_autolink

Autolinking of bare URLs and email addresses. (GFM default.)

alert

GitHub-style alerts in blockquotes — a [!TYPE] marker on the first line, where TYPE is one of NOTE, TIP, IMPORTANT, WARNING, or CAUTION (matched case-insensitively):

> [!NOTE]
> Highlights information that users should take into account.

This is the set ubCode understands and acts on today. It is not a closed whitelist, though — unrecognised names are passed through rather than rejected (see Specifying extensions below), so a name not listed here is simply enabled verbatim.

Specifying extensions

Extensions can be given as a simple list of names…

[parse.parsers.md]
extensions = ["deflist", "dollarmath"]

…or as a table, which additionally lets you disable a flavour default (name = false) or pass per-extension options (name = { option = value }):

[parse.parsers.md]
flavour = "gfm"

[parse.parsers.md.extensions]
deflist = true                               # enable on top of the flavour
tasklist = false                             # disable a GFM default
dollarmath = { allow_space = true, allow_digits = false }
strikethrough = { single_tilde = true }
colon_fence = { exact_match = true }

Unknown extension names are passed through rather than rejected, so newer extensions work without a ubCode update.

Per-extension options

The following per-extension options are recognised:

dollarmath.allow_space

Type: boolean (default: true)

Allow spaces adjacent to $ in inline dollar-math.

dollarmath.allow_digits

Type: boolean (default: true)

Allow digits adjacent to $ in inline dollar-math.

strikethrough.single_tilde

Type: boolean (default: false)

Treat single-tilde ~text~ as strikethrough (in addition to the standard ~~text~~).

colon_fence.exact_match

Type: boolean (default: false)

Require a closing colon fence to match the opening fence length exactly.

File routing

When a file matches the include globs of more than one parser, the most specific glob winsnot the order of the tables in the file. Specificity is measured by path-prefix depth (number of / separators), then by the count of literal (non-wildcard) characters. Remaining ties are broken by priority (higher wins), then deterministically by parser name.

Glob patterns follow the same two forms as [source]:

  • Separator-less globs (e.g. *.md) match against a file’s base name, so they apply at any directory depth.

  • Path-prefix globs (e.g. docs/*.md or docs/**/*.md) are matched relative to the directory containing ubproject.toml.

For example, given both a broad and a narrow Markdown parser:

[parse.parsers.rst]

[parse.parsers.md]
include = ["*.md"]

[parse.parsers.design]
type = "md"
flavour = "myst"
include = ["docs/design/**/*.md"]

a file at docs/design/spec.md routes to the design parser (the deeper, more specific glob), while readme.md routes to the broad md parser.

In parser mode, a discovered file that matches no parser is reported with a diagnostic rather than parsed — an unmatched file is almost always a misconfiguration.

Discovery and the [source] section

In parser mode the parsers own inclusion, so the [source] section keeps only its discovery / traversal responsibilities:

  • exclude / extend_exclude, respect_gitignore, and follow_links still apply — they configure the filesystem walk itself, before any routing happens.

  • include / extend_include are ignored (with a diagnostic). Move those globs onto the relevant parser’s include instead.

In classic mode (no parsers declared) [source] behaves exactly as before.

Tip

To see which parser each discovered file routes to, run:

$ ubc build list-documents --parser
index.rst (rst)
guide.md (md)

This is the quickest way to debug a routing or discovery problem — if a file is missing, or owned by the wrong parser, check its parser’s include globs and their relative specificity.

Common patterns

Sphinx extension integration: Define directives from Sphinx extensions you use:

[parse.extend_directives.automodule]
argument = true
options = true
description = "Automatically document a Python module"
extension = "sphinx.ext.autodoc"

[parse.extend_directives.automodule.named_options]
members = { description = "Include module members" }
undoc-members = { description = "Include undocumented members" }

Custom documentation patterns: Define project-specific directives:

[parse.extend_directives.api-endpoint]
argument = true
options = true
content = true
description = "Document an API endpoint"
extension = "project-docs"

[parse.extend_directives.api-endpoint.named_options]
method = { description = "HTTP method (GET, POST, etc.)" }
path = { description = "URL path pattern" }
deprecated = { description = "Mark as deprecated" }