Skip to content

♻️ REFACTOR: docs example directive via sphinx-syntax-example - #1181

Merged
chrisjsewell merged 1 commit into
masterfrom
myst-example-via-sphinx-syntax-example
Jul 27, 2026
Merged

♻️ REFACTOR: docs example directive via sphinx-syntax-example#1181
chrisjsewell merged 1 commit into
masterfrom
myst-example-via-sphinx-syntax-example

Conversation

@chrisjsewell

Copy link
Copy Markdown
Member

What

Replaces the bespoke myst-example docs directive (myst_parser/_docs.py) with a
thin subclass of the published
sphinx-syntax-example 0.2.0,
and renames the directive — and its CSS classes — to the canonical
syntax-example.

This is a docs-tooling change only. Nothing in the shipped parser changes: the
only touched runtime module is myst_parser/_docs.py, which is imported solely
by docs/conf.py (it is excluded from the autodoc2 API docs and omitted from
coverage).

Why

Reuse over bespoke. The "show this markup as source and as rendered output"
directive is generic; it is now maintained as a standalone package rather than as
~35 lines of string templating inside this repo.

It removes a re-serialisation hack. The old implementation did not build
nodes — it built a synthetic MyST document that wrapped the content in
sphinx-design {div} fences and re-parsed it, growing its own backtick fence
(``````` → …) until it was longer than any fence in the content, just
so nested code fences would survive the round trip:

backticks = "```"
while backticks in content_str:
    backticks += "`"

The base class builds the container / literal-block / container subtree directly,
so that whole class of quoting problem disappears. As a side effect source-line
attribution improves
: content is now nested-parsed at its real offset instead of
inside a re-generated string, so warnings inside an example point at the line the
author actually wrote.

One canonical directive name across the ecosystem. syntax-example is the
name used by the published extension, by sphinx-needs, and by the ubCode docs; the
ubCode Rust engine recognises syntax-example natively. Aligning here means the
same markup works in all of them, and a reader who learns the directive in one
project already knows it in the next.

Exact parity

The subclass pins every behaviour the old directive had:

Behaviour How it is preserved
Never renders a title / rubric default_title = "" (the base suppresses the rubric element entirely, rather than emitting an empty one)
Takes no argument optional_arguments = 0 — the base allows an optional title argument, which would otherwise let a stray argument silently create a rubric. With it set to 0, MyST folds any opening-line text into the content instead
:highlight: option Same option name, inherited from the base's option_spec
:alt-output: option Added to option_spec; render_into() hands that value to the base class' nested_parse_text() helper, which parses it with the host document's parser — MyST here (used once, docs/syntax/typography.md)
Source pane highlights as myst Inferred, with no override. MystLexer is registered via app.add_lexer(), and the base class' lexer probe consults Sphinx's add_lexer registries as well as Pygments', so a Markdown document resolves to myst on its own
HTML divs carry no container class build_wrapper_node() and build_render_node() return nodes.container(is_div=True, design_component="div"). is_div is the flag sphinx-design's overridden container visitor keys on; without it the divs would render as class="docutils container" and pick up the theme's Bootstrap .container layout rules. design_component is inert here — read only by sphinx-design's is_component() for tab-set/grid/card children — and is set to match what {div} produced

MystLexer, MystToHTMLDirective and every other _docs.py member are otherwise
untouched.

One deliberate behaviour change: a bodyless syntax-example now hard-errors
(the base class calls assert_has_content()), where the old directive rendered an
empty frame. No current usage is bodyless.

The subclass is only four members: two one-line node factories, render_into()
for :alt-output:, and the class attributes. It re-implements nothing — per the
package's contract the node factories return bare nodes and run() applies
wrapper_classes / render_classes to them, so the CSS classes cannot drift out
of sync with the stylesheet.

The rename

myst-examplesyntax-example, applied consistently in three places that must
move together, because MystToHTMLDirective shares the frame styling via the CSS
class:

  1. docs/conf.py registers the subclass as syntax-example.
  2. All 126 usages in docs/**/*.md (124 active + the 2 commented-out ones, so
    re-enabling them later still works).
  3. The CSS classes become the base class defaults (syntax-example,
    syntax-example-source, syntax-example-render), with the selectors in
    docs/_static/local.css renamed to match and the myst-to-html directive now
    emitting ::::syntax-example.

The packaged extension's own stylesheet is not loaded: sphinx_syntax_example
is deliberately kept out of the extensions list and only the class is imported,
so local.css remains the single source of styling and there is no
double-styling despite the shared class names.

local.css rule bodies are unchanged, with one necessary exception. The base
class puts the source class directly on the literal_block, so the source pane
is the highlight-<lang> div rather than a wrapper around it, and the old
child-combinator selector no longer matches:

/* before */
.myst-example-source > div[class*="highlight-"] { margin: 0; }
/* after */
.syntax-example > .syntax-example-source { margin: 0; }

Same declaration, re-pointed at the new DOM, keeping the source pane flush inside
the frame (local.css loads last).

Verification

Built from a clean worktree with pip install -e .[linkify,rtd]:

  • sphinx-build -nW --keep-going succeeds for html, text, man and latex (the
    four strict builders between CI and RTD), with zero warnings.
  • Full test suite: 1241 passed, 15 skipped.
  • ruff check, ruff format and mypy clean. sphinx-syntax-example~=0.2.0 is
    added to the rtd extra and to the mypy pre-commit hook's
    additional_dependencies (the package ships py.typed).

Output compared against a build of unmodified master, in an environment pinned
to the same docutils and Sphinx:

  • All 127 example blocks are byte-for-byte identical after accounting for the
    class rename and the one collapsed wrapper level — 124 from the directive plus
    3 from myst-to-html, with the same per-file distribution.
  • Source-pane languages unchanged: 113 highlight-myst, 6 highlight-latex,
    5 highlight-html.
  • No container class on any syntax-example div.
  • The :alt-output: example still shows * * * as its source and an
    <hr class="docutils"> as its rendered output.
  • intro.html's myst-to-html blocks are unchanged and keep their frame.
  • Text-builder output differs only by 7 removed blank lines, all of them the extra
    blank line the old redundant source-pane wrapper used to produce.

Note for reviewers

Because syntax-example is now registered directly by docs/conf.py, adding
sphinx_syntax_example to the extensions list later would register the
directive twice. If we ever want the packaged stylesheet, the local registration
should be dropped at the same time.

Replace the bespoke `myst-example` docs directive with a thin subclass of
the published `sphinx_syntax_example.SyntaxExampleDirective`, and adopt the
canonical `syntax-example` name used across the ecosystem.

The old implementation built a synthetic MyST string wrapping the content in
sphinx-design `{div}` fences and re-parsed it, growing its backtick fence to
survive nested code fences. The base class builds the nodes directly, so the
re-serialisation is gone and content is attributed to its real source lines.

The subclass keeps the previous behaviour exactly: no title/rubric, no
arguments, the same `highlight` and `alt-output` option names, and the
sphinx-design `is_div` flag on the containers so the emitted HTML keeps its
`<div class="... docutils">` form without the `container` class that would
attract the theme's Bootstrap layout rules.

The wrapper/source/render CSS classes are renamed to the base class defaults
(`syntax-example*`), with `docs/_static/local.css` and the `myst-to-html`
directive's shared frame updated to match. The packaged stylesheet is not
loaded — the extension is deliberately not in `extensions`, only the class is
imported — so `local.css` remains the single source of styling.
@chrisjsewell
chrisjsewell merged commit 723cffc into master Jul 27, 2026
23 checks passed
@chrisjsewell
chrisjsewell deleted the myst-example-via-sphinx-syntax-example branch July 27, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant