diff --git a/.github/workflows/sync.yaml b/.github/workflows/sync.yaml index 3812b14..0d82f14 100644 --- a/.github/workflows/sync.yaml +++ b/.github/workflows/sync.yaml @@ -8,6 +8,7 @@ on: paths: - src/scverse_doc/theme/scverse/static/styles/_tokens.css - src/scverse_doc/theme/scverse/static/scverse-fa.svg + - src/scverse_doc/static/mathmlfixes.css - scripts/** permissions: @@ -26,5 +27,10 @@ jobs: repository: scverse/scverse.github.io path: .upstream/website persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: tmke8/math-core + path: .upstream/math-core + persist-credentials: false - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 - - run: uv run scripts/sync_brand_tokens.py --website .upstream/website --check + - run: uv run scripts/sync_upstream.py --website .upstream/website --math-core .upstream/math-core --check diff --git a/CHANGELOG.md b/CHANGELOG.md index 5823aa2..2420188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ and this project adheres to [Semantic Versioning][]. ## [Unreleased] +### Changed + +- Sphinx 9.1 or newer is required + +### Added + +- `scverse_doc.math_core` renders math to MathML at build time via [math-core](https://github.com/tmke8/math-core) instead of MathJax (#75) + - `math_core_options` configures the converter, e.g. with macros + - math-core’s `mathmlfixes.css` is included, fixing e.g. `\boxed` and `\cancel` in Chromium + ### Fixed - rapids-singlecell’s accents are synced from the website (#78) diff --git a/biome.jsonc b/biome.jsonc index d7adb9e..595b5b7 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -7,6 +7,7 @@ "**", "!src/scverse_doc/registry.json", "!src/scverse_doc/_accents.json", + "!src/scverse_doc/static/mathmlfixes.css", "!src/scverse_doc/theme/scverse/static/styles/_tokens.css", ], }, diff --git a/docs/api.md b/docs/api.md index 2936d5c..7698d8b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -9,15 +9,16 @@ ```{toctree} :hidden: -api/config.md +api/config.rst api/theme.md api/source.md -api/registry.md -api/autodoc.md -api/napoleon.md -api/release_notes.md -api/typed_fields.md -api/xref_overrides.md +api/registry.rst +api/autodoc.rst +api/napoleon.rst +api/math_core.rst +api/release_notes.rst +api/typed_fields.rst +api/xref_overrides.rst ``` ## {doc}`api/config` @@ -61,6 +62,10 @@ and private bases left out of `:show-inheritance:`. Prose `Returns` sections in NumPy-style docstrings. +## {doc}`api/math_core` + +Math rendered to MathML at build time, via [math-core](https://github.com/tmke8/math-core) – no JavaScript needed. + ## {doc}`api/release_notes` A `release-notes` directive, rendering a directory of per-version files grouped by minor version. diff --git a/docs/api/autodoc.md b/docs/api/autodoc.md deleted file mode 100644 index 510a822..0000000 --- a/docs/api/autodoc.md +++ /dev/null @@ -1,5 +0,0 @@ -# `scverse_doc.autodoc` - -```{eval-rst} -.. automodule:: scverse_doc.autodoc -``` diff --git a/docs/api/autodoc.rst b/docs/api/autodoc.rst new file mode 100644 index 0000000..de87a06 --- /dev/null +++ b/docs/api/autodoc.rst @@ -0,0 +1,4 @@ +``scverse_doc.autodoc`` +======================= + +.. automodule:: scverse_doc.autodoc diff --git a/docs/api/config.md b/docs/api/config.rst similarity index 72% rename from docs/api/config.md rename to docs/api/config.rst index cc6ed3d..b93e716 100644 --- a/docs/api/config.md +++ b/docs/api/config.rst @@ -1,6 +1,6 @@ -# `scverse_doc.config` +``scverse_doc.config`` +====================== -```{eval-rst} .. automodule:: scverse_doc.config .. autodata:: EXTENSIONS @@ -8,4 +8,3 @@ .. autodata:: MYST_ENABLE_EXTENSIONS .. autodata:: DEFAULTS -``` diff --git a/docs/api/math_core.rst b/docs/api/math_core.rst new file mode 100644 index 0000000..0228c0f --- /dev/null +++ b/docs/api/math_core.rst @@ -0,0 +1,23 @@ +``scverse_doc.math_core`` +========================= + +.. automodule:: scverse_doc.math_core + +Configuration +------------- + +.. confval:: math_core_options + :type: dict + :default: ``{}`` + + Keyword arguments for math-core’s |LatexToMathML|_, e.g. macros + (named without the backslash, arguments as ``#1`` etc.): + + .. code-block:: python + + math_core_options = dict( + macros={"R": r"\mathbb{R}", "vec": r"\mathbf{#1}"}, + ) + +.. |LatexToMathML| replace:: ``LatexToMathML`` +.. _LatexToMathML: https://github.com/tmke8/math-core/tree/main/crates/math-core-python#latextomathml diff --git a/docs/api/napoleon.md b/docs/api/napoleon.md deleted file mode 100644 index 1116885..0000000 --- a/docs/api/napoleon.md +++ /dev/null @@ -1,5 +0,0 @@ -# `scverse_doc.napoleon` - -```{eval-rst} -.. automodule:: scverse_doc.napoleon -``` diff --git a/docs/api/napoleon.rst b/docs/api/napoleon.rst new file mode 100644 index 0000000..5b24ef7 --- /dev/null +++ b/docs/api/napoleon.rst @@ -0,0 +1,4 @@ +``scverse_doc.napoleon`` +======================== + +.. automodule:: scverse_doc.napoleon diff --git a/docs/api/registry.md b/docs/api/registry.rst similarity index 76% rename from docs/api/registry.md rename to docs/api/registry.rst index 230bbb0..0ff97e6 100644 --- a/docs/api/registry.md +++ b/docs/api/registry.rst @@ -1,6 +1,6 @@ -# `scverse_doc.registry` +``scverse_doc.registry`` +======================== -```{eval-rst} .. automodule:: scverse_doc.registry .. autodata:: packages @@ -11,4 +11,3 @@ .. autoclass:: Package :members: -``` diff --git a/docs/api/release_notes.md b/docs/api/release_notes.rst similarity index 54% rename from docs/api/release_notes.md rename to docs/api/release_notes.rst index 2e4daf6..eeca292 100644 --- a/docs/api/release_notes.md +++ b/docs/api/release_notes.rst @@ -1,6 +1,5 @@ -# `scverse_doc.release_notes` +``scverse_doc.release_notes`` +============================= -```{eval-rst} .. automodule:: scverse_doc.release_notes :members: FULL_VERSION_RE -``` diff --git a/docs/api/typed_fields.md b/docs/api/typed_fields.md deleted file mode 100644 index 7ccc3ad..0000000 --- a/docs/api/typed_fields.md +++ /dev/null @@ -1,5 +0,0 @@ -# `scverse_doc.typed_fields` - -```{eval-rst} -.. automodule:: scverse_doc.typed_fields -``` diff --git a/docs/api/typed_fields.rst b/docs/api/typed_fields.rst new file mode 100644 index 0000000..a4bb0c4 --- /dev/null +++ b/docs/api/typed_fields.rst @@ -0,0 +1,4 @@ +``scverse_doc.typed_fields`` +============================ + +.. automodule:: scverse_doc.typed_fields diff --git a/docs/api/xref_overrides.md b/docs/api/xref_overrides.rst similarity index 54% rename from docs/api/xref_overrides.md rename to docs/api/xref_overrides.rst index 57ac1a3..501220b 100644 --- a/docs/api/xref_overrides.md +++ b/docs/api/xref_overrides.rst @@ -1,6 +1,5 @@ -# `scverse_doc.xref_overrides` +``scverse_doc.xref_overrides`` +============================== -```{eval-rst} .. automodule:: scverse_doc.xref_overrides :members: DEFAULT_OVERRIDES -``` diff --git a/docs/conf.py b/docs/conf.py index 01448d1..3048c1c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,7 +2,12 @@ """Sphinx docs configuration.""" project = "scverse-doc" -extensions = ["scverse_doc", "sphinxcontrib.bibtex", "sphinx.ext.linkcode"] +extensions = [ + "scverse_doc", + "sphinxcontrib.bibtex", + "sphinx.ext.linkcode", + "sphinx_toolbox.rest_example", +] html_logo = "_static/scverse.svg" html_theme_options = {"announcement": ""} source_repository = "https://github.com/scverse/scverse-doc" diff --git a/pyproject.toml b/pyproject.toml index f7fe185..41aaa3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Typing :: Typed", ] dynamic = [ "readme", "version" ] -dependencies = [ "packaging", "sphinx>=8.1" ] +dependencies = [ "packaging", "sphinx>=9.1" ] # The extension stack of `scverse_doc.config`. optional-dependencies.config = [ "myst-nb>=1.1", @@ -34,7 +34,8 @@ optional-dependencies.config = [ "sphinxext-opengraph", ] # Everything, as the `scverse_doc` extension needs. -optional-dependencies.full = [ "scverse-doc[config,theme]" ] +optional-dependencies.full = [ "scverse-doc[config,math-core,theme]" ] +optional-dependencies.math-core = [ "math-core" ] # `html_theme = "scverse"`, i.e. `scverse_doc.theme`. optional-dependencies.theme = [ "pydata-sphinx-theme>=0.23" ] # https://docs.pypi.org/project_metadata/#project-urls @@ -64,6 +65,7 @@ doc = [ "lightningcss-cli", # to rebuild the theme’s stylesheet bundles "scverse-misc[sphinx]>=0.1.2", "sphinx-autodoc-typehints", + "sphinx-toolbox>=4.3.1", # `rest-example` directive "sphinxcontrib-bibtex>=1", ] typecheck = [ "mypy", "types-docutils", "wenmode" ] # wenmode for scripts/check_changelog.py diff --git a/scripts/sync_brand_tokens.py b/scripts/sync_upstream.py similarity index 88% rename from scripts/sync_brand_tokens.py rename to scripts/sync_upstream.py index 6f80cad..c64d17f 100755 --- a/scripts/sync_brand_tokens.py +++ b/scripts/sync_upstream.py @@ -4,7 +4,7 @@ # requires-python = ">=3.12" # dependencies = [] # /// -"""Refresh the generated brand assets from the scverse website. +"""Refresh the files generated or copied from upstream: the scverse website and math-core. The website is the brand’s source of truth, so transcribing its colours into this repository by hand @@ -20,9 +20,11 @@ The scverse logo used for the navbar link back to the website is copied verbatim for the same reason, as is ``accents.json``, which the registry reads the package accents from. +math-core’s stylesheet fixing browsers’ MathML rendering is copied verbatim from its repository. + Usage ----- - uv run scripts/sync_brand_tokens.py --website ../scverse.github.io + uv run scripts/sync_upstream.py --website ../scverse.github.io --math-core ../math-core """ from __future__ import annotations @@ -43,6 +45,11 @@ Path("assets/accents.json"): HERE.parent / "src" / "scverse_doc" / "_accents.json", } +#: math-core file -> package file, copied verbatim. +MATH_CORE_ASSETS = { + Path("css/mathmlfixes.css"): HERE.parent / "src" / "scverse_doc" / "static" / "mathmlfixes.css", +} + #: CSS custom property emitted here -> website custom property it is resolved from. #: The pairing mirrors the one scverse/scverse.github.io#329 used when replacing its SCSS variables. TOKEN_MAP = { @@ -66,7 +73,7 @@ "--scverse-color-gradient-end": ("scverse", "sky-blue"), } -BEGIN = "/* BEGIN GENERATED BRAND TOKENS – DO NOT EDIT; regenerate with scripts/sync_brand_tokens.py */" +BEGIN = "/* BEGIN GENERATED BRAND TOKENS – DO NOT EDIT; regenerate with scripts/sync_upstream.py */" END = "/* END GENERATED BRAND TOKENS */" #: The fenced region, captured together with the indentation of its opening marker. @@ -114,6 +121,7 @@ def main() -> int: """Write the refreshed token stylesheet to disk.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--website", type=Path, required=True, help="checkout of scverse/scverse.github.io") + parser.add_argument("--math-core", type=Path, required=True, help="checkout of tmke8/math-core") parser.add_argument("--check", action="store_true", help="fail instead of writing if the output would change") args = parser.parse_args() @@ -122,10 +130,11 @@ def main() -> int: updates = { TARGET: render(props, accents, TARGET.read_text()).encode(), **{dst: (args.website / src).read_bytes() for src, dst in ASSETS.items()}, + **{dst: (args.math_core / src).read_bytes() for src, dst in MATH_CORE_ASSETS.items()}, } if args.check: if outdated := [str(dst) for dst, new in updates.items() if not dst.is_file() or dst.read_bytes() != new]: - print(f"{', '.join(outdated)} out of date; rerun scripts/sync_brand_tokens.py", file=sys.stderr) + print(f"{', '.join(outdated)} out of date; rerun scripts/sync_upstream.py", file=sys.stderr) return 1 return 0 for dst, new in updates.items(): diff --git a/src/scverse_doc/__init__.py b/src/scverse_doc/__init__.py index 7bc1110..fad10a1 100644 --- a/src/scverse_doc/__init__.py +++ b/src/scverse_doc/__init__.py @@ -16,6 +16,8 @@ The layout of the API pages. :mod:`scverse_doc.config` The extension stack and the shared defaults. +:mod:`scverse_doc.math_core` + Math rendered to MathML at build time, via math-core. :mod:`scverse_doc.napoleon` Prose return sections in NumPy-style docstrings. :mod:`scverse_doc.registry` @@ -40,7 +42,7 @@ from sphinx.util.typing import ExtensionMetadata -from . import autodoc, config, napoleon, registry, release_notes, source, theme, typed_fields, xref_overrides +from . import autodoc, config, math_core, napoleon, registry, release_notes, source, theme, typed_fields, xref_overrides from .config import _is_set_by_user if TYPE_CHECKING: @@ -50,6 +52,7 @@ __all__ = [ "autodoc", "config", + "math_core", "napoleon", "registry", "release_notes", diff --git a/src/scverse_doc/math_core.py b/src/scverse_doc/math_core.py new file mode 100644 index 0000000..78db5fe --- /dev/null +++ b/src/scverse_doc/math_core.py @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: MPL-2.0 +r"""Math rendering via ``math-core``. + +Browsers support MathML today, +so math-core_ is the most lightweight way to render math on the web. + +Examples +-------- +Inline math, and a block equation referenced via its ``:label:``: + +.. rest-example:: + + :math:`e^{i\pi} = -1`, see :eq:`math-example`. + + .. math:: + :label: math-example + + \operatorname{AA}(c_\text{fg}, c_\text{bg}) \geq 4.5 + +Equations separated by blank lines, or rows separated by ``\\``, are aligned at ``&``: + +.. rest-example:: + + .. math:: + + a &= b + c + + d &= e + +``:no-wrap:`` passes the LaTeX as is, e.g. for your own environment: + +.. rest-example:: + + .. math:: + :no-wrap: + + \begin{cases} 1 & x > 0 \\ 0 & \text{otherwise} \end{cases} + +Configure the converter, e.g. with your own macros, via :confval:`math_core_options`. +math-core’s ``mathmlfixes.css`` is included, mainly to fix Chromium’s rendering. + +.. _math-core: https://github.com/tmke8/math-core +""" + +from pathlib import Path + +from docutils import nodes +from sphinx.application import Sphinx +from sphinx.config import Config +from sphinx.locale import _ +from sphinx.util import logging +from sphinx.util.math import get_node_equation_number +from sphinx.util.typing import ExtensionMetadata +from sphinx.writers.html5 import HTML5Translator + +logger = logging.getLogger(__name__) + + +class _MathVisitor: + def configure(self, app: Sphinx, config: Config) -> None: + """Create the converters from :confval:`math_core_options`.""" + from math_core import LatexToMathML + + self.converter = LatexToMathML(**config.math_core_options) + self.fallback = LatexToMathML(**{**config.math_core_options, "continue_on_error": True}) + + def visit(self, renderer: HTML5Translator, node: nodes.math | nodes.math_block): + """Handles inline math `` :math:`…` `` and block math ``.. math:: …``.""" + from math_core import LatexError + + tex = node.astext() + if display_style := isinstance(node, nodes.math_block): + # `starttag` sets expected `id` for `:eq:` references; markup mirrors `sphinx.ext.mathjax` + renderer.body.append(renderer.starttag(node, "div", CLASS="math")) + if node["number"]: + renderer.body.append(f'({get_node_equation_number(renderer, node)})') + renderer.add_permalink_ref(node, _("Link to this equation")) + renderer.body.append("") + if not node.get("no-wrap", node.get("nowrap", False)): + tex = _wrap(tex) + try: + out = self.converter.convert_all([(tex, display_style)]) + except LatexError as e: + logger.warning("LaTeX error:%s", e, location=node) + out = self.fallback.convert_all([(tex, display_style)]) + renderer.body.extend(out) + if display_style: + renderer.body.append("\n") + raise nodes.SkipNode + + +def _wrap(tex: str) -> str: + """Align multiple equations or rows, like `sphinx.ext.mathjax`.""" + # uses ``aligned``, as math-core lacks ``split``: + # https://github.com/tmke8/math-core/issues/154 + # TODO: once split exists, we should use Sphinx’ `wrap_displaymath` instead of this + parts = [part.strip() for part in tex.split("\n\n") if part.strip()] + if len(parts) == 1 and r"\\" not in parts[0]: + return parts[0] + return r"\begin{aligned}" + r"\\".join(parts) + r"\end{aligned}" + + +# `mathmlfixes.css` is synced from math-core by `scripts/sync_upstream.py` +_STATIC = Path(__file__).with_name("static") + + +def setup(app: Sphinx): + """Register the custom math-core HTML renderer.""" + visitor = _MathVisitor() + # we raise SkipNode, so no need for depart (the `None`s) + app.add_html_math_renderer("mathcore", (visitor.visit, None), (visitor.visit, None)) + app.add_config_value("math_core_options", {}, "env", types=frozenset({dict})) + app.connect("config-inited", visitor.configure) + app.add_static_dir(_STATIC) + app.add_css_file("mathmlfixes.css") + app.add_css_file("math_core.css") + return ExtensionMetadata(parallel_read_safe=True) diff --git a/src/scverse_doc/registry.py b/src/scverse_doc/registry.py index 2df8109..d19bd83 100644 --- a/src/scverse_doc/registry.py +++ b/src/scverse_doc/registry.py @@ -55,7 +55,7 @@ def _build_cache(app: Sphinx) -> Path: return _cache_dir -#: Brand accents from the website, copied by ``scripts/sync_brand_tokens.py``: +#: Brand accents from the website, copied by ``scripts/sync_upstream.py``: #: each key lists ``[name, [L, C, H]]`` pairs, primary first. _ACCENTS: dict[str, list[str]] = { pkg: ["oklch({} {} {})".format(*lch) for _, lch in colors] diff --git a/src/scverse_doc/static/math_core.css b/src/scverse_doc/static/math_core.css new file mode 100644 index 0000000..0580ac5 --- /dev/null +++ b/src/scverse_doc/static/math_core.css @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: MPL-2.0 */ +/* LaTeX that math-core couldn’t convert: the source, with the error as tooltip */ +.math-core-error { + color: var(--pst-color-danger, #d32f2f); + text-decoration: underline wavy; + cursor: help; +} + +/* pydata-sphinx-theme lays out `div.math` as a reversed flex row and only grows MathJax’s container */ +div.math > :not(.eqno) { + flex-grow: 1; + overflow: auto hidden; +} diff --git a/src/scverse_doc/static/mathmlfixes.css b/src/scverse_doc/static/mathmlfixes.css new file mode 100644 index 0000000..1837369 --- /dev/null +++ b/src/scverse_doc/static/mathmlfixes.css @@ -0,0 +1,93 @@ +/* Style for unknown command; feel free to customize it */ + +merror.math-core-unknown-cmd { + border: unset; /* `merror` default is `1px solid red` */ + background-color: unset; /* `merror` default is `lightYellow` */ + color: #c00; /* `merror` default is `unset` */ +} + +/* Styles for all browsers */ + +mtd { + /* This is the default padding for mtd elements as defined in the MathML Core specification. */ + padding: 0.5ex 0.4em; +} + +mtr > mtd:first-child { + padding-left: 0; +} + +mtr > mtd:last-child { + padding-right: 0; +} + +/* Styles for Chromium only */ +@supports (not (-webkit-backdrop-filter: blur(1px))) and + (not (-moz-appearance: none)) { + menclose { + position: relative; + padding: 0.5ex 0; + } + menclose[notation~="box" i] { + border: 0.04em solid; + padding: 0.3rem; + } + menclose[notation~="actuarial" i] { + padding: 0.03889rem 0.03889rem 0 0.03889rem; + border-width: 0.08em 0.08em 0em 0em; + border-style: solid; + margin-right: 0.03889rem; + } + menclose[notation~="phasorangle" i] { + border-bottom: 0.067em solid; + padding: 0.2em 0.2em 0.1em 0.6em; + } + mrow.menclose-phasorangle { + display: inline-block; + position: absolute; + left: 0.5px; + bottom: -0.04em; + height: 100%; + aspect-ratio: 0.5; + background-color: currentcolor; + clip-path: polygon( + 0.05em 100%, + 0 calc(100% - 0.05em), + calc(100% - 0.05em) 0, + 100% 0.05em + ); + } + mrow.menclose-updiagonalstrike, + mrow.menclose-downdiagonalstrike { + display: inline-block; + position: absolute; + left: 0.5px; + bottom: 0; + width: 100%; + height: 100%; + background-color: currentcolor; + } + mrow.menclose-updiagonalstrike { + clip-path: polygon( + 0.05em 100%, + 0 calc(100% - 0.05em), + calc(100% - 0.05em) 0, + 100% 0.05em + ); + } + mrow.menclose-downdiagonalstrike { + clip-path: polygon( + 0em 0.05em, + 0.05em 0em, + 100% calc(100% - 0.05em), + calc(100% - 0.05em) 100% + ); + } +} + +/* Styles for Chromium and Safari */ +@supports (not (-moz-appearance: none)) { + mover[accent="true" i] > mo:nth-child(2) { + margin-bottom: 0.15ex; + } +} diff --git a/src/scverse_doc/theme/scverse/static/styles/_tokens.css b/src/scverse_doc/theme/scverse/static/styles/_tokens.css index dd6c0b0..f64de56 100644 --- a/src/scverse_doc/theme/scverse/static/styles/_tokens.css +++ b/src/scverse_doc/theme/scverse/static/styles/_tokens.css @@ -8,7 +8,7 @@ */ :root { - /* BEGIN GENERATED BRAND TOKENS – DO NOT EDIT; regenerate with scripts/sync_brand_tokens.py */ + /* BEGIN GENERATED BRAND TOKENS – DO NOT EDIT; regenerate with scripts/sync_upstream.py */ --scverse-color-gradient-start: oklch(0.405 0.205 270); --scverse-color-gradient-end: oklch(0.799 0.108 236); --scverse-color-background: light-dark(#fff,color-mix(in oklch, #000 76%, oklch(60% .02 245))); diff --git a/tests/test_math_core.py b/tests/test_math_core.py new file mode 100644 index 0000000..91bae6c --- /dev/null +++ b/tests/test_math_core.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: MPL-2.0 +"""Math rendered to MathML, with Sphinx’s equation numbers and error fallback.""" + +from __future__ import annotations + +import re + +import pytest +from sphinx.testing.util import SphinxTestApp + +pytest.importorskip("math_core") + + +@pytest.fixture(scope="module") +def build(tmp_path_factory: pytest.TempPathFactory) -> tuple[str, str]: + """Build a page with inline, labelled and broken math, and return its HTML and warnings.""" + srcdir = tmp_path_factory.mktemp("math_core") + (srcdir / "conf.py").write_text( + 'extensions = ["scverse_doc.math_core"]\nmath_core_options = {"macros": {"R": r"\\mathbb{R}"}}\n' + ) + (srcdir / "index.rst").write_text( + r"""Inline :math:`a^2`, see :eq:`foo`, in :math:`\R`. + +.. math:: e = mc^2 + :label: foo + +.. math:: x<\frac{a + +.. math:: + + p &= q + + r &= s + +.. math:: + :no-wrap: + + \begin{cases} 1 \\ 0 \end{cases} +""" + ) + app = SphinxTestApp("html", srcdir=srcdir) + try: + app.build() + finally: + app.cleanup() + assert (app.outdir / "_static" / "math_core.css").is_file() + assert (app.outdir / "_static" / "mathmlfixes.css").is_file() + return (app.outdir / "index.html").read_text(), app.warning.getvalue() + + +def test_inline(build: tuple[str, str]) -> None: + html, _ = build + assert "" in html + + +def test_label(build: tuple[str, str]) -> None: + html, _ = build + assert '(1)' in html + assert '