Skip to content

Commit 726cf28

Browse files
authored
fix(#45): add math support (#70)
1 parent 6b49216 commit 726cf28

17 files changed

Lines changed: 1465 additions & 14 deletions

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ Supports:
2424
- [mkdocstrings Cross-References](https://mkdocstrings.github.io/usage/#cross-references)
2525
- [Python Markdown "Abbreviations"\*](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations)
2626
- \*Note: the markup (HTML) rendered for abbreviations is not useful for rendering. If important, I'm open to contributions because the implementation could be challenging
27+
- [Python Markdown "Attribute Lists"](https://python-markdown.github.io/extensions/attr_list)
28+
- Preserves attribute list syntax when using `--wrap` mode
29+
- [PyMdown Extensions "Arithmatex" (Math/LaTeX Support)](https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex) ([Material for MkDocs Math](https://squidfunk.github.io/mkdocs-material/reference/math))
30+
- This plugin combines three math rendering plugins from mdit-py-plugins:
31+
1. **dollarmath**: Handles `$...$` (inline) and `$$...$$` (block) with smart dollar mode that prevents false positives (e.g., `$3.00` is not treated as math)
32+
1. **texmath**: Handles `\(...\)` (inline) and `\[...\]` (block) LaTeX bracket notation
33+
1. **amsmath**: Handles LaTeX environments like `\begin{align}...\end{align}`, `\begin{cases}...\end{cases}`, `\begin{matrix}...\end{matrix}`, etc.
34+
- Can be deactivated entirely with the `--no-mkdocs-math` flag
2735
- [Python Markdown "Snippets"\*](https://facelessuser.github.io/pymdown-extensions/extensions/snippets)
2836
- \*Note: the markup (HTML) renders the plain text without implementing the snippet logic. I'm open to contributions if anyone needs full support for snippets
2937

@@ -123,6 +131,8 @@ md.render(text)
123131
124132
- `--ignore-missing-references` if set, do not escape link references when no definition is found. This is required when references are dynamic, such as with python mkdocstrings
125133
134+
- `--no-mkdocs-math` if set, deactivate math/LaTeX rendering (Arithmatex). By default, math is enabled. This can be useful if you want to format markdown without processing math syntax.
135+
126136
You can also use the toml configuration (https://mdformat.readthedocs.io/en/stable/users/configuration_file.html):
127137
128138
```toml
@@ -131,6 +141,7 @@ You can also use the toml configuration (https://mdformat.readthedocs.io/en/stab
131141
[plugin.mkdocs]
132142
align_semantic_breaks_in_lists = true
133143
ignore_missing_references = true
144+
no_mkdocs_math = true
134145
```
135146

136147
## Contributing

mdformat_mkdocs/_normalize_list.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from itertools import starmap
1010
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeVar
1111

12-
from more_itertools import unzip, zip_equal
12+
from more_itertools import unzip
1313

1414
from ._helpers import (
1515
EOL,
@@ -380,7 +380,7 @@ def _insert_newlines(
380380
"""Extend zipped_lines with newlines if necessary."""
381381
newline = ("", "")
382382
new_lines: list[tuple[str, str]] = []
383-
for line, zip_line in zip_equal(parsed_lines, zipped_lines):
383+
for line, zip_line in zip(parsed_lines, zipped_lines, strict=True):
384384
new_lines.append(zip_line)
385385
if (
386386
line.parsed.syntax == Syntax.EDGE_CODE
@@ -421,12 +421,14 @@ def parse_text(
421421
for indent in map_lookback(_parse_html_line, lines, None)
422422
]
423423
# When both, code_indents take precedence
424-
block_indents = [_c or _h for _c, _h in zip_equal(code_indents, html_indents)]
425-
new_indents = [*starmap(_format_new_indent, zip_equal(lines, block_indents))]
424+
block_indents = [
425+
_c or _h for _c, _h in zip(code_indents, html_indents, strict=True)
426+
]
427+
new_indents = [*starmap(_format_new_indent, zip(lines, block_indents, strict=True))]
426428

427429
new_contents = [
428430
_format_new_content(line, inc_numbers, ci is not None)
429-
for line, ci in zip_equal(lines, code_indents)
431+
for line, ci in zip(lines, code_indents, strict=True)
430432
]
431433

432434
if use_sem_break:
@@ -437,10 +439,10 @@ def parse_text(
437439
)
438440
new_indents = [
439441
_trim_semantic_indent(indent, s_i, in_defbody)
440-
for indent, s_i in zip_equal(new_indents, semantic_indents)
442+
for indent, s_i in zip(new_indents, semantic_indents, strict=True)
441443
]
442444

443-
new_lines = _insert_newlines(lines, [*zip_equal(new_indents, new_contents)])
445+
new_lines = _insert_newlines(lines, [*zip(new_indents, new_contents, strict=True)])
444446
return ParsedText(
445447
new_lines=new_lines,
446448
debug_original_lines=lines,
@@ -468,7 +470,9 @@ def _join(*, new_lines: list[tuple[str, str]]) -> str:
468470

469471
return "".join(
470472
f"{new_indent}{new_content}{EOL}"
471-
for new_indent, new_content in zip_equal(new_indents_iter, new_contents_iter)
473+
for new_indent, new_content in zip(
474+
new_indents_iter, new_contents_iter, strict=True
475+
)
472476
)
473477

474478

mdformat_mkdocs/mdit_plugins/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
)
2424
from ._pymd_abbreviations import PYMD_ABBREVIATIONS_PREFIX, pymd_abbreviations_plugin
2525
from ._pymd_admon import pymd_admon_plugin
26+
from ._pymd_arithmatex import (
27+
AMSMATH_BLOCK,
28+
DOLLARMATH_BLOCK,
29+
DOLLARMATH_BLOCK_LABEL,
30+
DOLLARMATH_INLINE,
31+
TEXMATH_BLOCK_EQNO,
32+
pymd_arithmatex_plugin,
33+
)
2634
from ._pymd_captions import PYMD_CAPTIONS_PREFIX, pymd_captions_plugin
2735
from ._pymd_snippet import PYMD_SNIPPET_PREFIX, pymd_snippet_plugin
2836
from ._python_markdown_attr_list import (
@@ -31,6 +39,10 @@
3139
)
3240

3341
__all__ = (
42+
"AMSMATH_BLOCK",
43+
"DOLLARMATH_BLOCK",
44+
"DOLLARMATH_BLOCK_LABEL",
45+
"DOLLARMATH_INLINE",
3446
"MATERIAL_ADMON_MARKERS",
3547
"MATERIAL_CONTENT_TAB_MARKERS",
3648
"MKDOCSTRINGS_AUTOREFS_PREFIX",
@@ -40,6 +52,7 @@
4052
"PYMD_CAPTIONS_PREFIX",
4153
"PYMD_SNIPPET_PREFIX",
4254
"PYTHON_MARKDOWN_ATTR_LIST_PREFIX",
55+
"TEXMATH_BLOCK_EQNO",
4356
"escape_deflist",
4457
"material_admon_plugin",
4558
"material_content_tabs_plugin",
@@ -48,6 +61,7 @@
4861
"mkdocstrings_crossreference_plugin",
4962
"pymd_abbreviations_plugin",
5063
"pymd_admon_plugin",
64+
"pymd_arithmatex_plugin",
5165
"pymd_captions_plugin",
5266
"pymd_snippet_plugin",
5367
"python_markdown_attr_list_plugin",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
r"""Python-Markdown Extensions: Arithmatex (Math Support).
2+
3+
Uses existing mdit-py-plugins for LaTeX/MathJax mathematical expressions.
4+
5+
Inline math delimiters:
6+
- $...$ (with smart_dollar rules: no whitespace adjacent to $)
7+
- \\(...\\)
8+
9+
Block math delimiters:
10+
- $$...$$
11+
- \\[...\\]
12+
- \\begin{env}...\\end{env}
13+
14+
Docs: <https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex>
15+
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from typing import TYPE_CHECKING
21+
22+
from mdit_py_plugins.amsmath import amsmath_plugin
23+
from mdit_py_plugins.dollarmath import dollarmath_plugin
24+
from mdit_py_plugins.texmath import texmath_plugin
25+
26+
if TYPE_CHECKING:
27+
from markdown_it import MarkdownIt
28+
29+
# Token types from the plugins
30+
# Note: dollarmath and texmath share the same token types for inline/block math:
31+
# - "math_inline" is used for both $...$ and \(...\)
32+
# - "math_block" is used for both $$...$$ and \[...\]
33+
DOLLARMATH_INLINE = "math_inline"
34+
DOLLARMATH_BLOCK = "math_block"
35+
DOLLARMATH_BLOCK_LABEL = "math_block_label" # For $$...$$ (label) syntax
36+
TEXMATH_BLOCK_EQNO = "math_block_eqno" # For \[...\] (label) syntax
37+
AMSMATH_BLOCK = "amsmath"
38+
39+
40+
def pymd_arithmatex_plugin(md: MarkdownIt) -> None:
41+
r"""Register Arithmatex support using existing mdit-py-plugins.
42+
43+
This is a convenience wrapper that configures three existing plugins:
44+
- dollarmath_plugin: for $...$ and $$...$$
45+
- texmath_plugin: for \\(...\\) and \\[...\\]
46+
- amsmath_plugin: for \\begin{env}...\\end{env}
47+
"""
48+
# Dollar syntax: $...$ and $$...$$
49+
# Defaults provide smart dollar mode (no digits/space adjacent to $)
50+
md.use(dollarmath_plugin)
51+
52+
# Bracket syntax: \(...\) and \[...\]
53+
md.use(texmath_plugin, delimiters="brackets")
54+
55+
# LaTeX environments: \begin{env}...\end{env}
56+
md.use(amsmath_plugin)

mdformat_mkdocs/plugin.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
from ._normalize_list import normalize_list as unbounded_normalize_list
1313
from ._postprocess_inline import postprocess_list_wrap
1414
from .mdit_plugins import (
15+
AMSMATH_BLOCK,
16+
DOLLARMATH_BLOCK,
17+
DOLLARMATH_BLOCK_LABEL,
18+
DOLLARMATH_INLINE,
1519
MKDOCSTRINGS_AUTOREFS_PREFIX,
1620
MKDOCSTRINGS_CROSSREFERENCE_PREFIX,
1721
MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX,
1822
PYMD_ABBREVIATIONS_PREFIX,
1923
PYMD_CAPTIONS_PREFIX,
2024
PYMD_SNIPPET_PREFIX,
2125
PYTHON_MARKDOWN_ATTR_LIST_PREFIX,
26+
TEXMATH_BLOCK_EQNO,
2227
escape_deflist,
2328
material_admon_plugin,
2429
material_content_tabs_plugin,
@@ -27,6 +32,7 @@
2732
mkdocstrings_crossreference_plugin,
2833
pymd_abbreviations_plugin,
2934
pymd_admon_plugin,
35+
pymd_arithmatex_plugin,
3036
pymd_captions_plugin,
3137
pymd_snippet_plugin,
3238
python_markdown_attr_list_plugin,
@@ -62,6 +68,11 @@ def cli_is_align_semantic_breaks_in_lists(options: ContextOptions) -> bool:
6268
return bool(get_conf(options, "align_semantic_breaks_in_lists")) or False
6369

6470

71+
def cli_is_no_mkdocs_math(options: ContextOptions) -> bool:
72+
"""user-specified flag to disable math/LaTeX rendering."""
73+
return bool(get_conf(options, "no_mkdocs_math")) or False
74+
75+
6576
def add_cli_argument_group(group: argparse._ArgumentGroup) -> None:
6677
"""Add options to the mdformat CLI.
6778
@@ -80,11 +91,19 @@ def add_cli_argument_group(group: argparse._ArgumentGroup) -> None:
8091
const=True,
8192
help="If set, do not escape link references when no definition is found. This is required when references are dynamic, such as with python mkdocstrings",
8293
)
94+
group.add_argument(
95+
"--no-mkdocs-math",
96+
action="store_const",
97+
const=True,
98+
help="If set, disable math/LaTeX rendering (Arithmatex). By default, math is enabled.",
99+
)
83100

84101

85102
def update_mdit(mdit: MarkdownIt) -> None:
86103
"""Update the parser."""
87104
mdit.use(material_admon_plugin)
105+
if not cli_is_no_mkdocs_math(mdit.options):
106+
mdit.use(pymd_arithmatex_plugin)
88107
mdit.use(pymd_captions_plugin)
89108
mdit.use(material_content_tabs_plugin)
90109
mdit.use(material_deflist_plugin)
@@ -103,6 +122,49 @@ def _render_node_content(node: RenderTreeNode, context: RenderContext) -> str:
103122
return node.content
104123

105124

125+
def _render_math_inline(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
126+
"""Render inline math with original delimiters."""
127+
markup = node.markup
128+
content = node.content
129+
if markup == "$":
130+
return f"${content}$"
131+
if markup == "\\(":
132+
return f"\\({content}\\)"
133+
# Fallback
134+
return f"${content}$"
135+
136+
137+
def _render_math_block(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
138+
"""Render block math with original delimiters."""
139+
markup = node.markup
140+
content = node.content
141+
if markup == "$$":
142+
return f"$$\n{content.strip()}\n$$"
143+
if markup == "\\[":
144+
return f"\\[\n{content.strip()}\n\\]"
145+
# Fallback
146+
return f"$$\n{content.strip()}\n$$"
147+
148+
149+
def _render_math_block_eqno(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
150+
"""Render block math with equation label."""
151+
markup = node.markup
152+
content = node.content
153+
label = node.info # Label is stored in info field
154+
if markup == "$$":
155+
return f"$$\n{content.strip()}\n$$ ({label})"
156+
if markup == "\\[":
157+
return f"\\[\n{content.strip()}\n\\] ({label})"
158+
# Fallback
159+
return f"$$\n{content.strip()}\n$$ ({label})"
160+
161+
162+
def _render_amsmath(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
163+
"""Render amsmath environment."""
164+
# Content already includes \begin{} and \end{}
165+
return node.content
166+
167+
106168
def _render_meta_content(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
107169
"""Return node content without additional processing."""
108170
return node.meta.get("content", "")
@@ -214,6 +276,13 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str:
214276
"dl": render_material_definition_list,
215277
"dt": render_material_definition_term,
216278
"dd": render_material_definition_body,
279+
# Math support (from mdit-py-plugins)
280+
DOLLARMATH_INLINE: _render_math_inline,
281+
DOLLARMATH_BLOCK: _render_math_block,
282+
DOLLARMATH_BLOCK_LABEL: _render_math_block_eqno,
283+
TEXMATH_BLOCK_EQNO: _render_math_block_eqno,
284+
AMSMATH_BLOCK: _render_amsmath,
285+
# Other plugins
217286
PYMD_CAPTIONS_PREFIX: render_pymd_caption,
218287
MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content,
219288
MKDOCSTRINGS_CROSSREFERENCE_PREFIX: _render_cross_reference,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
"mdit-py-plugins >= 0.4.1",
1919
"more-itertools >= 10.5.0",
2020
]
21-
description = "A mdformat plugin for mkdocs and mkdocs-material"
21+
description = "An mdformat plugin for mkdocs and Material for MkDocs"
2222
keywords = ["markdown", "markdown-it", "mdformat", "mdformat_plugin_template"]
2323
license = "MIT"
2424
license-files = ["LICENSE"]

0 commit comments

Comments
 (0)