From 8bda58f89d4879b1d918a6b2958f5776e8794cab Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Nov 2025 05:45:31 +0000 Subject: [PATCH 1/9] feat(#45): comprehensive resolution of issue #45 This commit implements complete solutions for all items in issue #45: 1. **Arithmatex Support (NEW)**: Implemented full PyMdown Extensions Arithmatex support for LaTeX/math expressions - Inline math: $...$ (smart dollar mode) and \(...\) - Block math: $$...$$, \[...\], and \begin{env}...\end{env} - Smart dollar mode prevents false positives (e.g., $3.00) - Comprehensive test coverage with 6 new test cases 2. **  Entity Replacement (DOCUMENTED)**: Added documentation explaining that   conversion to Unicode is core mdformat behavior - Added "Known Behaviors" section to README - Documented CSS alternatives for spacing (padding, margin, gap, etc.) - Updated test fixture comments to explain behavior 3. **Attribute Lists (COMPLETED)**: Already fixed in previous commits - Attribute lists preserved when using --wrap mode - Updated outdated FIXME comments to accurate descriptions - All tests passing (165/165) Changes: - Added mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py - Added tests/format/fixtures/pymd_arithmatex.md - Updated README.md with Arithmatex support and Known Behaviors section - Updated plugin.py to register Arithmatex plugin - Updated test fixtures to remove outdated FIXME comments - All tests passing (165 passed, +6 new Arithmatex tests) Resolves #45 --- README.md | 33 +++ mdformat_mkdocs/mdit_plugins/__init__.py | 8 + .../mdit_plugins/_pymd_arithmatex.py | 214 ++++++++++++++++++ mdformat_mkdocs/plugin.py | 6 + tests/format/fixtures/pymd_arithmatex.md | 127 +++++++++++ .../fixtures/python_markdown_attr_list.md | 4 +- tests/format/test_format.py | 1 + .../fixtures/python_markdown_attr_list.md | 6 +- 8 files changed, 394 insertions(+), 5 deletions(-) create mode 100644 mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py create mode 100644 tests/format/fixtures/pymd_arithmatex.md diff --git a/README.md b/README.md index 76bcebc..b4a1e67 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,12 @@ Supports: - [mkdocstrings Cross-References](https://mkdocstrings.github.io/usage/#cross-references) - [Python Markdown "Abbreviations"\*](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations) - \*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 +- [Python Markdown "Attribute Lists"](https://python-markdown.github.io/extensions/attr_list/) + - Preserves attribute list syntax when using `--wrap` mode +- [PyMdown Extensions "Arithmatex" (Math/LaTeX Support)](https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex/) + - Inline math: `$E = mc^2$` or `\(x + y\)` + - Block math: `$$...$$`, `\[...\]`, or `\begin{env}...\end{env}` + - Supports smart dollar mode (prevents false positives like `$3.00`) - [Python Markdown "Snippets"\*](https://facelessuser.github.io/pymdown-extensions/extensions/snippets) - \*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 @@ -133,6 +139,33 @@ align_semantic_breaks_in_lists = true ignore_missing_references = true ``` +## Known Behaviors + +### HTML Entity Replacement + +`mdformat` (via `markdown-it-py`) automatically converts HTML entities like ` ` to their Unicode equivalents (U+00A0 non-breaking space). This is core `mdformat` behavior and not specific to this plugin. + +**Recommended alternatives**: Use CSS for spacing instead of HTML entities: + +```css +/* Instead of   for button spacing */ +.md-button svg { + margin-right: 0.5em; +} + +.md-button { + padding: 0.625em 2em; +} + +/* For general spacing */ +.container { + display: flex; + gap: 1em; +} +``` + +This approach is more maintainable, responsive-friendly, and semantically correct. + ## Contributing See [CONTRIBUTING.md](https://github.com/kyleking/mdformat-mkdocs/blob/main/CONTRIBUTING.md) diff --git a/mdformat_mkdocs/mdit_plugins/__init__.py b/mdformat_mkdocs/mdit_plugins/__init__.py index ef33809..3711047 100644 --- a/mdformat_mkdocs/mdit_plugins/__init__.py +++ b/mdformat_mkdocs/mdit_plugins/__init__.py @@ -23,6 +23,11 @@ ) from ._pymd_abbreviations import PYMD_ABBREVIATIONS_PREFIX, pymd_abbreviations_plugin from ._pymd_admon import pymd_admon_plugin +from ._pymd_arithmatex import ( + PYMD_ARITHMATEX_BLOCK_PREFIX, + PYMD_ARITHMATEX_INLINE_PREFIX, + pymd_arithmatex_plugin, +) from ._pymd_captions import PYMD_CAPTIONS_PREFIX, pymd_captions_plugin from ._pymd_snippet import PYMD_SNIPPET_PREFIX, pymd_snippet_plugin from ._python_markdown_attr_list import ( @@ -37,6 +42,8 @@ "MKDOCSTRINGS_CROSSREFERENCE_PREFIX", "MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX", "PYMD_ABBREVIATIONS_PREFIX", + "PYMD_ARITHMATEX_BLOCK_PREFIX", + "PYMD_ARITHMATEX_INLINE_PREFIX", "PYMD_CAPTIONS_PREFIX", "PYMD_SNIPPET_PREFIX", "PYTHON_MARKDOWN_ATTR_LIST_PREFIX", @@ -48,6 +55,7 @@ "mkdocstrings_crossreference_plugin", "pymd_abbreviations_plugin", "pymd_admon_plugin", + "pymd_arithmatex_plugin", "pymd_captions_plugin", "pymd_snippet_plugin", "python_markdown_attr_list_plugin", diff --git a/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py new file mode 100644 index 0000000..401ce56 --- /dev/null +++ b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py @@ -0,0 +1,214 @@ +"""Python-Markdown Extensions: Arithmatex (Math Support). + +Supports LaTeX/MathJax mathematical expressions using PyMdown Extensions Arithmatex syntax. + +Inline math delimiters: +- $...$ (with smart_dollar rules: no whitespace adjacent to $) +- \\(...\\) + +Block math delimiters: +- $$...$$ +- \\[...\\] +- \\begin{env}...\\end{env} + +Docs: + +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from mdit_py_plugins.utils import is_code_block + +from mdformat_mkdocs._synced.admon_factories import new_token + +if TYPE_CHECKING: + from markdown_it import MarkdownIt + from markdown_it.rules_block import StateBlock + from markdown_it.rules_inline import StateInline + +# Inline math patterns (adapted from Arithmatex) +# Smart dollar: requires non-whitespace adjacent to $ +_INLINE_DOLLAR_SMART = re.compile( + r"(?:(?[a-z]+\*?)\}[ \t]*$") + +PYMD_ARITHMATEX_INLINE_PREFIX = "pymd_arithmatex_inline" +PYMD_ARITHMATEX_BLOCK_PREFIX = "pymd_arithmatex_block" + + +def _pymd_arithmatex_inline(state: StateInline, silent: bool) -> bool: + """Parse inline math expressions ($...$ or \\(...\\)).""" + # Try dollar sign first + match = _INLINE_DOLLAR_SMART.match(state.src[state.pos : state.posMax]) + if match: + # Check if it's an escaped sequence (even number of backslashes before $) + if match.group(1): # Even backslashes before $, not math + return False + + if silent: + return True + + math_content = match.group(3) # The content between $...$ + original_pos = state.pos + + with new_token(state, PYMD_ARITHMATEX_INLINE_PREFIX, "span") as token: + token.meta = {"delimiter": "$", "content": math_content} + token.content = f"${math_content}$" + + state.pos = original_pos + match.end() + return True + + # Try parenthesis notation + match = _INLINE_PAREN.match(state.src[state.pos : state.posMax]) + if match: + # Check if it's an escaped sequence + if match.group(1): # Even backslashes before \(, not math + return False + + if silent: + return True + + math_content = match.group(3) # The content between \(...\) + original_pos = state.pos + + with new_token(state, PYMD_ARITHMATEX_INLINE_PREFIX, "span") as token: + token.meta = {"delimiter": "paren", "content": math_content} + token.content = f"\\({math_content}\\)" + + state.pos = original_pos + match.end() + return True + + return False + + +def _get_line_content(state: StateBlock, line: int) -> str: + """Get the content of a line.""" + if line >= state.lineMax: + return "" + start = state.bMarks[line] + state.tShift[line] + maximum = state.eMarks[line] + return state.src[start:maximum] + + +def _pymd_arithmatex_block( + state: StateBlock, + start_line: int, + end_line: int, + silent: bool, +) -> bool: + """Parse block math expressions ($$...$$, \\[...\\], \\begin{}...\\end{}).""" + if is_code_block(state, start_line): + return False + + # Check if line is indented (would be code block) + if state.sCount[start_line] - state.blkIndent >= 4: + return False + + first_line = _get_line_content(state, start_line) + + # Try to match block delimiters + delimiter_type = None + env_name = None + + if _BLOCK_DOLLAR.match(first_line): + delimiter_type = "dollar" + end_pattern = _BLOCK_DOLLAR + elif _BLOCK_SQUARE.match(first_line): + delimiter_type = "square" + end_pattern = _BLOCK_SQUARE_END + else: + begin_match = _BLOCK_BEGIN.match(first_line) + if begin_match: + delimiter_type = "begin" + env_name = begin_match.group("env") + end_pattern = re.compile(rf"^\\end\{{{re.escape(env_name)}\}}[ \t]*$") + else: + return False + + if silent: + return True + + # Find the end of the math block + current_line = start_line + 1 + content_lines: list[str] = [] + found_end = False + + while current_line < end_line: + line_content = _get_line_content(state, current_line) + + # Check for end delimiter + if end_pattern.match(line_content): + found_end = True + break + + # Check for empty line (not allowed in math blocks) + if not line_content.strip(): + # Empty line found, not a valid math block + return False + + content_lines.append(line_content) + current_line += 1 + + # Must find closing delimiter + if not found_end: + return False + + # Construct the full math block content + if delimiter_type == "dollar": + full_content = "$$\n" + "\n".join(content_lines) + "\n$$" + elif delimiter_type == "square": + full_content = "\\[\n" + "\n".join(content_lines) + "\n\\]" + elif delimiter_type == "begin": + full_content = ( + f"\\begin{{{env_name}}}\n" + "\n".join(content_lines) + f"\n\\end{{{env_name}}}" + ) + else: + return False + + # Create the token + with new_token(state, PYMD_ARITHMATEX_BLOCK_PREFIX, "div"): + tkn_inline = state.push("inline", "", 0) + tkn_inline.content = full_content + tkn_inline.map = [start_line, current_line + 1] + tkn_inline.children = [] + + state.line = current_line + 1 + + return True + + +def pymd_arithmatex_plugin(md: MarkdownIt) -> None: + """Register the Arithmatex plugin with markdown-it.""" + # Register inline math parser + md.inline.ruler.before( + "escape", + PYMD_ARITHMATEX_INLINE_PREFIX, + _pymd_arithmatex_inline, + ) + + # Register block math parser + md.block.ruler.before( + "fence", + PYMD_ARITHMATEX_BLOCK_PREFIX, + _pymd_arithmatex_block, + {"alt": ["paragraph", "reference", "blockquote", "list"]}, + ) diff --git a/mdformat_mkdocs/plugin.py b/mdformat_mkdocs/plugin.py index ec4ccb2..f5fe2fd 100644 --- a/mdformat_mkdocs/plugin.py +++ b/mdformat_mkdocs/plugin.py @@ -16,6 +16,8 @@ MKDOCSTRINGS_CROSSREFERENCE_PREFIX, MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX, PYMD_ABBREVIATIONS_PREFIX, + PYMD_ARITHMATEX_BLOCK_PREFIX, + PYMD_ARITHMATEX_INLINE_PREFIX, PYMD_CAPTIONS_PREFIX, PYMD_SNIPPET_PREFIX, PYTHON_MARKDOWN_ATTR_LIST_PREFIX, @@ -27,6 +29,7 @@ mkdocstrings_crossreference_plugin, pymd_abbreviations_plugin, pymd_admon_plugin, + pymd_arithmatex_plugin, pymd_captions_plugin, pymd_snippet_plugin, python_markdown_attr_list_plugin, @@ -85,6 +88,7 @@ def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: def update_mdit(mdit: MarkdownIt) -> None: """Update the parser.""" mdit.use(material_admon_plugin) + mdit.use(pymd_arithmatex_plugin) mdit.use(pymd_captions_plugin) mdit.use(material_content_tabs_plugin) mdit.use(material_deflist_plugin) @@ -214,6 +218,8 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str: "dl": render_material_definition_list, "dt": render_material_definition_term, "dd": render_material_definition_body, + PYMD_ARITHMATEX_INLINE_PREFIX: _render_node_content, + PYMD_ARITHMATEX_BLOCK_PREFIX: _render_inline_content, PYMD_CAPTIONS_PREFIX: render_pymd_caption, MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content, MKDOCSTRINGS_CROSSREFERENCE_PREFIX: _render_cross_reference, diff --git a/tests/format/fixtures/pymd_arithmatex.md b/tests/format/fixtures/pymd_arithmatex.md new file mode 100644 index 0000000..513b6b9 --- /dev/null +++ b/tests/format/fixtures/pymd_arithmatex.md @@ -0,0 +1,127 @@ +PyMdown Extensions Arithmatex (Math Support) +. +# Inline Math + +The equation $E = mc^2$ represents energy-mass equivalence. + +Multiple inline: $x + y = z$ and $a^2 + b^2 = c^2$. + +Parenthesis notation: \(F = ma\) is Newton's second law. + +Not math (smart dollar): I have $3.00 and you have $5.00. + +Complex inline: $\frac{p(y|x)p(x)}{p(y)} = p(x|y)$. +. +# Inline Math + +The equation $E = mc^2$ represents energy-mass equivalence. + +Multiple inline: $x + y = z$ and $a^2 + b^2 = c^2$. + +Parenthesis notation: \(F = ma\) is Newton's second law. + +Not math (smart dollar): I have $3.00 and you have $5.00. + +Complex inline: $\frac{p(y|x)p(x)}{p(y)} = p(x|y)$. +. + +Block Math with Double Dollar +. +The Restricted Boltzmann Machine energy function: + +$$ +E(\mathbf{v}, \mathbf{h}) = -\sum_{i,j}w_{ij}v_i h_j - \sum_i b_i v_i - \sum_j c_j h_j +$$ + +This defines the joint probability distribution. +. +The Restricted Boltzmann Machine energy function: + +$$ +E(\mathbf{v}, \mathbf{h}) = -\sum_{i,j}w_{ij}v_i h_j - \sum_i b_i v_i - \sum_j c_j h_j +$$ + +This defines the joint probability distribution. +. + +Block Math with Square Brackets +. +The conditional probabilities are: + +\[ +p(v_i=1|\mathbf{h}) = \sigma\left(\sum_j w_{ij}h_j + b_i\right) +\] + +Where $\sigma$ is the sigmoid function. +. +The conditional probabilities are: + +\[ +p(v_i=1|\mathbf{h}) = \sigma\left(\sum_j w_{ij}h_j + b_i\right) +\] + +Where $\sigma$ is the sigmoid function. +. + +Block Math with LaTeX Environments - align +. +The forward and backward passes: + +\begin{align} +p(v_i=1|\mathbf{h}) & = \sigma\left(\sum_j w_{ij}h_j + b_i\right) \\ +p(h_j=1|\mathbf{v}) & = \sigma\left(\sum_i w_{ij}v_i + c_j\right) +\end{align} + +These equations describe the Gibbs sampling process. +. +The forward and backward passes: + +\begin{align} +p(v_i=1|\mathbf{h}) & = \sigma\left(\sum_j w_{ij}h_j + b_i\right) \\ +p(h_j=1|\mathbf{v}) & = \sigma\left(\sum_i w_{ij}v_i + c_j\right) +\end{align} + +These equations describe the Gibbs sampling process. +. + +Block Math with LaTeX Environments - equation +. +Einstein's field equations: + +\begin{equation} +R_{\mu\nu} - \frac{1}{2}Rg_{\mu\nu} + \Lambda g_{\mu\nu} = \frac{8\pi G}{c^4}T_{\mu\nu} +\end{equation} + +This is the foundation of general relativity. +. +Einstein's field equations: + +\begin{equation} +R_{\mu\nu} - \frac{1}{2}Rg_{\mu\nu} + \Lambda g_{\mu\nu} = \frac{8\pi G}{c^4}T_{\mu\nu} +\end{equation} + +This is the foundation of general relativity. +. + +Mixed Inline and Block Math +. +For the wave equation $\frac{\partial^2 u}{\partial t^2} = c^2 \nabla^2 u$, the solution in one dimension is: + +$$ +u(x,t) = f(x - ct) + g(x + ct) +$$ + +Where $f$ and $g$ are arbitrary functions determined by initial conditions. + +The dispersion relation \(\omega = ck\) relates frequency and wave number. +. +For the wave equation $\frac{\partial^2 u}{\partial t^2} = c^2 \nabla^2 u$, the solution in one dimension is: + +$$ +u(x,t) = f(x - ct) + g(x + ct) +$$ + +Where $f$ and $g$ are arbitrary functions determined by initial conditions. + +The dispersion relation \(\omega = ck\) relates frequency and wave number. +. diff --git a/tests/format/fixtures/python_markdown_attr_list.md b/tests/format/fixtures/python_markdown_attr_list.md index 9fe6d16..d6a5286 100644 --- a/tests/format/fixtures/python_markdown_attr_list.md +++ b/tests/format/fixtures/python_markdown_attr_list.md @@ -36,7 +36,7 @@ Example from https://github.com/KyleKing/mdformat-mkdocs/issues/45 and source ht .
- + [:material-account-box:+ .lg .middle +  **About**  ](about/index.md){ .md-button style="text-align: center; display: block;" } @@ -46,7 +46,7 @@ Example from https://github.com/KyleKing/mdformat-mkdocs/issues/45 and source ht .
- + [:material-account-box:+ .lg .middle +  **About**  ](about/index.md){ .md-button style="text-align: center; display: block;" } diff --git a/tests/format/test_format.py b/tests/format/test_format.py index 4bedd77..eef5934 100644 --- a/tests/format/test_format.py +++ b/tests/format/test_format.py @@ -25,6 +25,7 @@ def flatten(nested_list: list[list[T]]) -> list[T]: "material_deflist.md", "mkdocstrings_autorefs.md", "pymd_abbreviations.md", + "pymd_arithmatex.md", "pymd_snippet.md", "python_markdown_attr_list.md", "regression.md", diff --git a/tests/render/fixtures/python_markdown_attr_list.md b/tests/render/fixtures/python_markdown_attr_list.md index 014721b..dcd9673 100644 --- a/tests/render/fixtures/python_markdown_attr_list.md +++ b/tests/render/fixtures/python_markdown_attr_list.md @@ -1,5 +1,5 @@ Examples from https://python-markdown.github.io/extensions/attr_list - + . {: #someid .someclass somekey='some value' #id1 .class1 id=id2 class="class2 class3" .class4 } @@ -31,7 +31,7 @@ Example from https://github.com/KyleKing/mdformat-mkdocs/issues/45 and source ht .
- + [:material-account-box:+ .lg .middle +  **About**  ](about/index.md){ .md-button style="text-align: center; display: block;" } @@ -40,7 +40,7 @@ Example from https://github.com/KyleKing/mdformat-mkdocs/issues/45 and source ht
.
- +

:material-account-box:+ .lg .middle +  About   .md-button style="text-align: center; display: block;"

:fontawesome-brands-blogger-b:+ .lg .middle +  Blogs   .md-button style="text-align: center; display: block;"

From 7b831576b62c774b27615e7933b8bda1c4365c06 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 04:56:47 +0000 Subject: [PATCH 2/9] refactor: simplify Arithmatex using existing mdit-py-plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced custom 214-line implementation with a 59-line wrapper around well-tested mdit-py-plugins (dollarmath, texmath, amsmath). Benefits: - 72% code reduction (214 → 59 lines) - Uses battle-tested, maintained upstream plugins - Simpler maintenance burden - Same functionality and test coverage - All 165 tests passing Technical changes: - mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py: - Now wraps dollarmath_plugin, texmath_plugin, amsmath_plugin - Removed custom regex parsing and token generation - Token types now: math_inline, math_block, amsmath - mdformat_mkdocs/plugin.py: - Added _render_math_inline(), _render_math_block(), _render_amsmath() - Uses token.markup to preserve original delimiters ($, $$, \(, \[, etc.) - Updated RENDERERS mapping This addresses the complexity concern raised in code review. --- .../mdit_plugins/_pymd_arithmatex.py | 219 +++--------------- mdformat_mkdocs/plugin.py | 37 ++- 2 files changed, 67 insertions(+), 189 deletions(-) diff --git a/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py index 401ce56..697b573 100644 --- a/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py +++ b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py @@ -1,6 +1,6 @@ """Python-Markdown Extensions: Arithmatex (Math Support). -Supports LaTeX/MathJax mathematical expressions using PyMdown Extensions Arithmatex syntax. +Uses existing mdit-py-plugins for LaTeX/MathJax mathematical expressions. Inline math delimiters: - $...$ (with smart_dollar rules: no whitespace adjacent to $) @@ -17,198 +17,43 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING -from mdit_py_plugins.utils import is_code_block - -from mdformat_mkdocs._synced.admon_factories import new_token +from mdit_py_plugins.amsmath import amsmath_plugin +from mdit_py_plugins.dollarmath import dollarmath_plugin +from mdit_py_plugins.texmath import texmath_plugin if TYPE_CHECKING: from markdown_it import MarkdownIt - from markdown_it.rules_block import StateBlock - from markdown_it.rules_inline import StateInline -# Inline math patterns (adapted from Arithmatex) -# Smart dollar: requires non-whitespace adjacent to $ -_INLINE_DOLLAR_SMART = re.compile( - r"(?:(?[a-z]+\*?)\}[ \t]*$") - -PYMD_ARITHMATEX_INLINE_PREFIX = "pymd_arithmatex_inline" -PYMD_ARITHMATEX_BLOCK_PREFIX = "pymd_arithmatex_block" - - -def _pymd_arithmatex_inline(state: StateInline, silent: bool) -> bool: - """Parse inline math expressions ($...$ or \\(...\\)).""" - # Try dollar sign first - match = _INLINE_DOLLAR_SMART.match(state.src[state.pos : state.posMax]) - if match: - # Check if it's an escaped sequence (even number of backslashes before $) - if match.group(1): # Even backslashes before $, not math - return False - - if silent: - return True - - math_content = match.group(3) # The content between $...$ - original_pos = state.pos - - with new_token(state, PYMD_ARITHMATEX_INLINE_PREFIX, "span") as token: - token.meta = {"delimiter": "$", "content": math_content} - token.content = f"${math_content}$" - - state.pos = original_pos + match.end() - return True - - # Try parenthesis notation - match = _INLINE_PAREN.match(state.src[state.pos : state.posMax]) - if match: - # Check if it's an escaped sequence - if match.group(1): # Even backslashes before \(, not math - return False - - if silent: - return True - - math_content = match.group(3) # The content between \(...\) - original_pos = state.pos - - with new_token(state, PYMD_ARITHMATEX_INLINE_PREFIX, "span") as token: - token.meta = {"delimiter": "paren", "content": math_content} - token.content = f"\\({math_content}\\)" - - state.pos = original_pos + match.end() - return True - - return False - - -def _get_line_content(state: StateBlock, line: int) -> str: - """Get the content of a line.""" - if line >= state.lineMax: - return "" - start = state.bMarks[line] + state.tShift[line] - maximum = state.eMarks[line] - return state.src[start:maximum] - - -def _pymd_arithmatex_block( - state: StateBlock, - start_line: int, - end_line: int, - silent: bool, -) -> bool: - """Parse block math expressions ($$...$$, \\[...\\], \\begin{}...\\end{}).""" - if is_code_block(state, start_line): - return False - - # Check if line is indented (would be code block) - if state.sCount[start_line] - state.blkIndent >= 4: - return False - - first_line = _get_line_content(state, start_line) - - # Try to match block delimiters - delimiter_type = None - env_name = None - - if _BLOCK_DOLLAR.match(first_line): - delimiter_type = "dollar" - end_pattern = _BLOCK_DOLLAR - elif _BLOCK_SQUARE.match(first_line): - delimiter_type = "square" - end_pattern = _BLOCK_SQUARE_END - else: - begin_match = _BLOCK_BEGIN.match(first_line) - if begin_match: - delimiter_type = "begin" - env_name = begin_match.group("env") - end_pattern = re.compile(rf"^\\end\{{{re.escape(env_name)}\}}[ \t]*$") - else: - return False - - if silent: - return True - - # Find the end of the math block - current_line = start_line + 1 - content_lines: list[str] = [] - found_end = False - - while current_line < end_line: - line_content = _get_line_content(state, current_line) - - # Check for end delimiter - if end_pattern.match(line_content): - found_end = True - break - - # Check for empty line (not allowed in math blocks) - if not line_content.strip(): - # Empty line found, not a valid math block - return False - - content_lines.append(line_content) - current_line += 1 - - # Must find closing delimiter - if not found_end: - return False - - # Construct the full math block content - if delimiter_type == "dollar": - full_content = "$$\n" + "\n".join(content_lines) + "\n$$" - elif delimiter_type == "square": - full_content = "\\[\n" + "\n".join(content_lines) + "\n\\]" - elif delimiter_type == "begin": - full_content = ( - f"\\begin{{{env_name}}}\n" + "\n".join(content_lines) + f"\n\\end{{{env_name}}}" - ) - else: - return False - - # Create the token - with new_token(state, PYMD_ARITHMATEX_BLOCK_PREFIX, "div"): - tkn_inline = state.push("inline", "", 0) - tkn_inline.content = full_content - tkn_inline.map = [start_line, current_line + 1] - tkn_inline.children = [] - - state.line = current_line + 1 - - return True +# Token types from the plugins +DOLLARMATH_INLINE = "math_inline" +DOLLARMATH_BLOCK = "math_block" +TEXMATH_INLINE = "math_inline_double" +TEXMATH_BLOCK = "math_block_eqno" +AMSMATH_BLOCK = "amsmath" def pymd_arithmatex_plugin(md: MarkdownIt) -> None: - """Register the Arithmatex plugin with markdown-it.""" - # Register inline math parser - md.inline.ruler.before( - "escape", - PYMD_ARITHMATEX_INLINE_PREFIX, - _pymd_arithmatex_inline, - ) - - # Register block math parser - md.block.ruler.before( - "fence", - PYMD_ARITHMATEX_BLOCK_PREFIX, - _pymd_arithmatex_block, - {"alt": ["paragraph", "reference", "blockquote", "list"]}, - ) + """Register Arithmatex support using existing mdit-py-plugins. + + This is a convenience wrapper that configures three existing plugins: + - dollarmath_plugin: for $...$ and $$...$$ + - texmath_plugin: for \\(...\\) and \\[...\\] + - amsmath_plugin: for \\begin{env}...\\end{env} + """ + # Dollar syntax: $...$ and $$...$$ + # Defaults provide smart dollar mode (no digits/space adjacent to $) + md.use(dollarmath_plugin) + + # Bracket syntax: \(...\) and \[...\] + md.use(texmath_plugin, delimiters="brackets") + + # LaTeX environments: \begin{env}...\end{env} + md.use(amsmath_plugin) + + +# For backwards compatibility, export the same prefixes +# Map to the actual token types created by the plugins +PYMD_ARITHMATEX_INLINE_PREFIX = DOLLARMATH_INLINE # "math_inline" +PYMD_ARITHMATEX_BLOCK_PREFIX = DOLLARMATH_BLOCK # "math_block" diff --git a/mdformat_mkdocs/plugin.py b/mdformat_mkdocs/plugin.py index 19d1da3..e7c3c20 100644 --- a/mdformat_mkdocs/plugin.py +++ b/mdformat_mkdocs/plugin.py @@ -107,6 +107,36 @@ def _render_node_content(node: RenderTreeNode, context: RenderContext) -> str: return node.content +def _render_math_inline(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 + """Render inline math with original delimiters.""" + markup = node.markup + content = node.content + if markup == "$": + return f"${content}$" + if markup == "\\(": + return f"\\({content}\\)" + # Fallback + return f"${content}$" + + +def _render_math_block(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 + """Render block math with original delimiters.""" + markup = node.markup + content = node.content + if markup == "$$": + return f"$$\n{content.strip()}\n$$" + if markup == "\\[": + return f"\\[\n{content.strip()}\n\\]" + # Fallback + return f"$$\n{content.strip()}\n$$" + + +def _render_amsmath(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 + """Render amsmath environment.""" + # Content already includes \begin{} and \end{} + return node.content + + def _render_meta_content(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 """Return node content without additional processing.""" return node.meta.get("content", "") @@ -218,8 +248,11 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str: "dl": render_material_definition_list, "dt": render_material_definition_term, "dd": render_material_definition_body, - PYMD_ARITHMATEX_INLINE_PREFIX: _render_node_content, - PYMD_ARITHMATEX_BLOCK_PREFIX: _render_inline_content, + # Math support (from mdit-py-plugins) + "math_inline": _render_math_inline, + "math_block": _render_math_block, + "amsmath": _render_amsmath, + # Other plugins PYMD_CAPTIONS_PREFIX: render_pymd_caption, MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content, MKDOCSTRINGS_CROSSREFERENCE_PREFIX: _render_cross_reference, From da24bf93eec51cd9a2bf7764ef19b89d9a290a21 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 23 Nov 2025 19:31:47 -0600 Subject: [PATCH 3/9] docs: remove irrelevant CSS explanation --- README.md | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 299fd65..2aba838 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Supports: - [mkdocstrings Cross-References](https://mkdocstrings.github.io/usage/#cross-references) - [Python Markdown "Abbreviations"\*](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations) - \*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 -- [Python Markdown "Attribute Lists"](https://python-markdown.github.io/extensions/attr_list/) +- [Python Markdown "Attribute Lists"](https://python-markdown.github.io/extensions/attr_list) - Preserves attribute list syntax when using `--wrap` mode -- [PyMdown Extensions "Arithmatex" (Math/LaTeX Support)](https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex/) +- [PyMdown Extensions "Arithmatex" (Math/LaTeX Support)](https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex) - Inline math: `$E = mc^2$` or `\(x + y\)` - Block math: `$$...$$`, `\[...\]`, or `\begin{env}...\end{env}` - Supports smart dollar mode (prevents false positives like `$3.00`) @@ -139,33 +139,6 @@ align_semantic_breaks_in_lists = true ignore_missing_references = true ``` -## Known Behaviors - -### HTML Entity Replacement - -`mdformat` (via `markdown-it-py`) automatically converts HTML entities like ` ` to their Unicode equivalents (U+00A0 non-breaking space). This is core `mdformat` behavior and not specific to this plugin. - -**Recommended alternatives**: Use CSS for spacing instead of HTML entities: - -```css -/* Instead of   for button spacing */ -.md-button svg { - margin-right: 0.5em; -} - -.md-button { - padding: 0.625em 2em; -} - -/* For general spacing */ -.container { - display: flex; - gap: 1em; -} -``` - -This approach is more maintainable, responsive-friendly, and semantically correct. - ## Contributing See [CONTRIBUTING.md](https://github.com/kyleking/mdformat-mkdocs/blob/main/CONTRIBUTING.md) From dfb5fc7058693bd560ccdccacb36e6c54426beec Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 23 Nov 2025 20:06:34 -0600 Subject: [PATCH 4/9] fix: use zip(.., strict=True) --- mdformat_mkdocs/_normalize_list.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mdformat_mkdocs/_normalize_list.py b/mdformat_mkdocs/_normalize_list.py index b2163f5..aee37ec 100644 --- a/mdformat_mkdocs/_normalize_list.py +++ b/mdformat_mkdocs/_normalize_list.py @@ -9,7 +9,7 @@ from itertools import starmap from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeVar -from more_itertools import unzip, zip_equal +from more_itertools import unzip from ._helpers import ( EOL, @@ -380,7 +380,7 @@ def _insert_newlines( """Extend zipped_lines with newlines if necessary.""" newline = ("", "") new_lines: list[tuple[str, str]] = [] - for line, zip_line in zip_equal(parsed_lines, zipped_lines): + for line, zip_line in zip(parsed_lines, zipped_lines, strict=True): new_lines.append(zip_line) if ( line.parsed.syntax == Syntax.EDGE_CODE @@ -421,12 +421,14 @@ def parse_text( for indent in map_lookback(_parse_html_line, lines, None) ] # When both, code_indents take precedence - block_indents = [_c or _h for _c, _h in zip_equal(code_indents, html_indents)] - new_indents = [*starmap(_format_new_indent, zip_equal(lines, block_indents))] + block_indents = [ + _c or _h for _c, _h in zip(code_indents, html_indents, strict=True) + ] + new_indents = [*starmap(_format_new_indent, zip(lines, block_indents, strict=True))] new_contents = [ _format_new_content(line, inc_numbers, ci is not None) - for line, ci in zip_equal(lines, code_indents) + for line, ci in zip(lines, code_indents) ] if use_sem_break: @@ -437,10 +439,10 @@ def parse_text( ) new_indents = [ _trim_semantic_indent(indent, s_i, in_defbody) - for indent, s_i in zip_equal(new_indents, semantic_indents) + for indent, s_i in zip(new_indents, semantic_indents, strict=True) ] - new_lines = _insert_newlines(lines, [*zip_equal(new_indents, new_contents)]) + new_lines = _insert_newlines(lines, [*zip(new_indents, new_contents, strict=True)]) return ParsedText( new_lines=new_lines, debug_original_lines=lines, From 1268038689cf562b3c91953a26e751c85f3f9d05 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 23 Nov 2025 20:07:09 -0600 Subject: [PATCH 5/9] refactor: ensure material math support --- README.md | 2 +- mdformat_mkdocs/mdit_plugins/__init__.py | 10 ++- .../mdit_plugins/_pymd_arithmatex.py | 13 +--- mdformat_mkdocs/plugin.py | 11 +-- pyproject.toml | 2 +- tests/format/fixtures/material_math.md | 78 +++++++++++++++++++ tests/format/test_format.py | 1 + 7 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 tests/format/fixtures/material_math.md diff --git a/README.md b/README.md index 2aba838..e66d8f8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Supports: - \*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 - [Python Markdown "Attribute Lists"](https://python-markdown.github.io/extensions/attr_list) - Preserves attribute list syntax when using `--wrap` mode -- [PyMdown Extensions "Arithmatex" (Math/LaTeX Support)](https://facelessuser.github.io/pymdown-extensions/extensions/arithmatex) +- [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)) - Inline math: `$E = mc^2$` or `\(x + y\)` - Block math: `$$...$$`, `\[...\]`, or `\begin{env}...\end{env}` - Supports smart dollar mode (prevents false positives like `$3.00`) diff --git a/mdformat_mkdocs/mdit_plugins/__init__.py b/mdformat_mkdocs/mdit_plugins/__init__.py index 3711047..3115be2 100644 --- a/mdformat_mkdocs/mdit_plugins/__init__.py +++ b/mdformat_mkdocs/mdit_plugins/__init__.py @@ -24,8 +24,9 @@ from ._pymd_abbreviations import PYMD_ABBREVIATIONS_PREFIX, pymd_abbreviations_plugin from ._pymd_admon import pymd_admon_plugin from ._pymd_arithmatex import ( - PYMD_ARITHMATEX_BLOCK_PREFIX, - PYMD_ARITHMATEX_INLINE_PREFIX, + AMSMATH_BLOCK, + DOLLARMATH_BLOCK, + DOLLARMATH_INLINE, pymd_arithmatex_plugin, ) from ._pymd_captions import PYMD_CAPTIONS_PREFIX, pymd_captions_plugin @@ -36,14 +37,15 @@ ) __all__ = ( + "AMSMATH_BLOCK", + "DOLLARMATH_BLOCK", + "DOLLARMATH_INLINE", "MATERIAL_ADMON_MARKERS", "MATERIAL_CONTENT_TAB_MARKERS", "MKDOCSTRINGS_AUTOREFS_PREFIX", "MKDOCSTRINGS_CROSSREFERENCE_PREFIX", "MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX", "PYMD_ABBREVIATIONS_PREFIX", - "PYMD_ARITHMATEX_BLOCK_PREFIX", - "PYMD_ARITHMATEX_INLINE_PREFIX", "PYMD_CAPTIONS_PREFIX", "PYMD_SNIPPET_PREFIX", "PYTHON_MARKDOWN_ATTR_LIST_PREFIX", diff --git a/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py index 697b573..98ea5e1 100644 --- a/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py +++ b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py @@ -1,4 +1,4 @@ -"""Python-Markdown Extensions: Arithmatex (Math Support). +r"""Python-Markdown Extensions: Arithmatex (Math Support). Uses existing mdit-py-plugins for LaTeX/MathJax mathematical expressions. @@ -29,13 +29,14 @@ # Token types from the plugins DOLLARMATH_INLINE = "math_inline" DOLLARMATH_BLOCK = "math_block" +AMSMATH_BLOCK = "amsmath" +# FIXME: How should these be used? TEXMATH_INLINE = "math_inline_double" TEXMATH_BLOCK = "math_block_eqno" -AMSMATH_BLOCK = "amsmath" def pymd_arithmatex_plugin(md: MarkdownIt) -> None: - """Register Arithmatex support using existing mdit-py-plugins. + r"""Register Arithmatex support using existing mdit-py-plugins. This is a convenience wrapper that configures three existing plugins: - dollarmath_plugin: for $...$ and $$...$$ @@ -51,9 +52,3 @@ def pymd_arithmatex_plugin(md: MarkdownIt) -> None: # LaTeX environments: \begin{env}...\end{env} md.use(amsmath_plugin) - - -# For backwards compatibility, export the same prefixes -# Map to the actual token types created by the plugins -PYMD_ARITHMATEX_INLINE_PREFIX = DOLLARMATH_INLINE # "math_inline" -PYMD_ARITHMATEX_BLOCK_PREFIX = DOLLARMATH_BLOCK # "math_block" diff --git a/mdformat_mkdocs/plugin.py b/mdformat_mkdocs/plugin.py index e7c3c20..c67c2a7 100644 --- a/mdformat_mkdocs/plugin.py +++ b/mdformat_mkdocs/plugin.py @@ -12,12 +12,13 @@ from ._normalize_list import normalize_list as unbounded_normalize_list from ._postprocess_inline import postprocess_list_wrap from .mdit_plugins import ( + AMSMATH_BLOCK, + DOLLARMATH_BLOCK, + DOLLARMATH_INLINE, MKDOCSTRINGS_AUTOREFS_PREFIX, MKDOCSTRINGS_CROSSREFERENCE_PREFIX, MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX, PYMD_ABBREVIATIONS_PREFIX, - PYMD_ARITHMATEX_BLOCK_PREFIX, - PYMD_ARITHMATEX_INLINE_PREFIX, PYMD_CAPTIONS_PREFIX, PYMD_SNIPPET_PREFIX, PYTHON_MARKDOWN_ATTR_LIST_PREFIX, @@ -249,9 +250,9 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str: "dt": render_material_definition_term, "dd": render_material_definition_body, # Math support (from mdit-py-plugins) - "math_inline": _render_math_inline, - "math_block": _render_math_block, - "amsmath": _render_amsmath, + DOLLARMATH_INLINE: _render_math_inline, + DOLLARMATH_BLOCK: _render_math_block, + AMSMATH_BLOCK: _render_amsmath, # Other plugins PYMD_CAPTIONS_PREFIX: render_pymd_caption, MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content, diff --git a/pyproject.toml b/pyproject.toml index 76c9958..5ac4cd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "mdit-py-plugins >= 0.4.1", "more-itertools >= 10.5.0", ] -description = "A mdformat plugin for mkdocs and mkdocs-material" +description = "A mdformat plugin for mkdocs and Material for MkDocs" keywords = ["markdown", "markdown-it", "mdformat", "mdformat_plugin_template"] license = "MIT" license-files = ["LICENSE"] diff --git a/tests/format/fixtures/material_math.md b/tests/format/fixtures/material_math.md new file mode 100644 index 0000000..94a313c --- /dev/null +++ b/tests/format/fixtures/material_math.md @@ -0,0 +1,78 @@ +Block Math with Double Dollar +. +The cosine series expansion: + +$$ +\cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} +$$ + +This is a fundamental trigonometric identity. +. +The cosine series expansion: + +$$ +\cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} +$$ + +This is a fundamental trigonometric identity. +. + +Block Math with Square Brackets +. +The same formula using LaTeX bracket notation: + +\[ +\cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} +\] + +This demonstrates equivalent notation. +. +The same formula using LaTeX bracket notation: + +\[ +\cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} +\] + +This demonstrates equivalent notation. +. + +Inline Math with Dollar Signs +. +The homomorphism $f$ is injective if and only if its kernel is only the singleton set $e_G$, because otherwise $\exists a,b\in G$ with $a\neq b$ such that $f(a)=f(b)$. + +This shows how inline math integrates with text. +. +The homomorphism $f$ is injective if and only if its kernel is only the singleton set $e_G$, because otherwise $\exists a,b\in G$ with $a\neq b$ such that $f(a)=f(b)$. + +This shows how inline math integrates with text. +. + +Inline Math with Parentheses +. +Using LaTeX parenthesis notation: \(f\) is injective if and only if its kernel is only the singleton set \(e_G\), because otherwise \(\exists a,b\in G\) with \(a\neq b\) such that \(f(a)=f(b)\). + +This demonstrates alternative inline notation. +. +Using LaTeX parenthesis notation: \(f\) is injective if and only if its kernel is only the singleton set \(e_G\), because otherwise \(\exists a,b\in G\) with \(a\neq b\) such that \(f(a)=f(b)\). + +This demonstrates alternative inline notation. +. + +Mixed Math Syntax +. +Combining different notations: the function $f: G \to H$ satisfies \(f(a \cdot b) = f(a) \cdot f(b)\) for all $a,b \in G$. + +$$ +f(a \cdot b) = f(a) \cdot f(b) +$$ + +This shows how different delimiters can be mixed. +. +Combining different notations: the function $f: G \to H$ satisfies \(f(a \cdot b) = f(a) \cdot f(b)\) for all $a,b \in G$. + +$$ +f(a \cdot b) = f(a) \cdot f(b) +$$ + +This shows how different delimiters can be mixed. +. diff --git a/tests/format/test_format.py b/tests/format/test_format.py index eef5934..a0db47f 100644 --- a/tests/format/test_format.py +++ b/tests/format/test_format.py @@ -23,6 +23,7 @@ def flatten(nested_list: list[list[T]]) -> list[T]: for fixture_path in ( "material_content_tabs.md", "material_deflist.md", + "material_math.md", "mkdocstrings_autorefs.md", "pymd_abbreviations.md", "pymd_arithmatex.md", From 8c11bd8fac56bc496c754bc6daec5ec264759179 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 23 Nov 2025 21:15:26 -0600 Subject: [PATCH 6/9] ci: resolve failures --- mdformat_mkdocs/_normalize_list.py | 6 ++++-- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mdformat_mkdocs/_normalize_list.py b/mdformat_mkdocs/_normalize_list.py index aee37ec..6ea4cc8 100644 --- a/mdformat_mkdocs/_normalize_list.py +++ b/mdformat_mkdocs/_normalize_list.py @@ -428,7 +428,7 @@ def parse_text( new_contents = [ _format_new_content(line, inc_numbers, ci is not None) - for line, ci in zip(lines, code_indents) + for line, ci in zip(lines, code_indents, strict=True) ] if use_sem_break: @@ -470,7 +470,9 @@ def _join(*, new_lines: list[tuple[str, str]]) -> str: return "".join( f"{new_indent}{new_content}{EOL}" - for new_indent, new_content in zip_equal(new_indents_iter, new_contents_iter) + for new_indent, new_content in zip( + new_indents_iter, new_contents_iter, strict=True + ) ) diff --git a/pyproject.toml b/pyproject.toml index 5ac4cd2..409f224 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "mdit-py-plugins >= 0.4.1", "more-itertools >= 10.5.0", ] -description = "A mdformat plugin for mkdocs and Material for MkDocs" +description = "An mdformat plugin for mkdocs and Material for MkDocs" keywords = ["markdown", "markdown-it", "mdformat", "mdformat_plugin_template"] license = "MIT" license-files = ["LICENSE"] From 6bfb45a8c3bd0d0f2ab9494c777e05b6890ca8ac Mon Sep 17 00:00:00 2001 From: Kyle King Date: Thu, 27 Nov 2025 19:57:15 -0600 Subject: [PATCH 7/9] test: verify that attr list wrap is preserved --- tests/format/test_wrap.py | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/format/test_wrap.py b/tests/format/test_wrap.py index 08014d6..a6b666f 100644 --- a/tests/format/test_wrap.py +++ b/tests/format/test_wrap.py @@ -223,6 +223,49 @@ def gcd(a, b): /// """ +# Regression test for issue #45 - long links with titles should not break attribute lists +CASE_LONG_LINK_WITH_TITLE = """ +See [https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins](https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins "Code Formatter Plugin Link") for more information about developing code formatter plugins. +""" + +CASE_LONG_LINK_WITH_TITLE_WRAP_80 = """ +See +[https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins](https://mdformat.readthedocs.io/en/stable/contributors/contributing.html#developing-code-formatter-plugins "Code Formatter Plugin Link") +for more information about developing code formatter plugins. +""" + +# Regression test for issue #45 - attribute lists wrap inline with paragraph +CASE_PARAGRAPH_ATTR_LIST = """ +This is a very long paragraph that exceeds the wrapping limit and should be wrapped but the attribute list should stay with the paragraph. +{:.class1 .class2} +""" + +CASE_PARAGRAPH_ATTR_LIST_WRAP_80 = """ +This is a very long paragraph that exceeds the wrapping limit and should be +wrapped but the attribute list should stay with the paragraph. {:.class1 +.class2} +""" + +# Regression test for issue #45 - multiple classes in attribute list wrap inline +CASE_MULTICLASS_ATTR_LIST = """ +Short paragraph with many classes below. +{:.class1 .class2 .class3 .class4 .class5 .class6 .class7 .class8 .class9 .class10} +""" + +CASE_MULTICLASS_ATTR_LIST_WRAP_80 = """ +Short paragraph with many classes below. {:.class1 .class2 .class3 .class4 +.class5 .class6 .class7 .class8 .class9 .class10} +""" + +# Regression test for issue #45 - link with attribute list +CASE_LINK_ATTR_LIST = """ +[A very long link text that should be wrapped when it exceeds the line limit](http://example.com){: .button .primary .large } +""" + +CASE_LINK_ATTR_LIST_WRAP_80 = """ +[A very long link text that should be wrapped when it exceeds the line limit](http://example.com){: .button .primary .large } +""" + DEF_LIST_WITH_NESTED_WRAP = dedent( """\ term @@ -269,6 +312,11 @@ def gcd(a, b): (CASE_ATTR_LIST_WRAP, CASE_ATTR_LIST_WRAP_TRUE_80, True, 80), (CASE_CAPTION_WRAP, CASE_CAPTION_WRAP_TRUE_40, True, 40), (DEF_LIST_WITH_NESTED_WRAP, DEF_LIST_WITH_NESTED_WRAP_EXPECTED, True, 80), + # Regression tests for issue #45 - attribute lists should not be wrapped + (CASE_LONG_LINK_WITH_TITLE, CASE_LONG_LINK_WITH_TITLE_WRAP_80, True, 80), + (CASE_PARAGRAPH_ATTR_LIST, CASE_PARAGRAPH_ATTR_LIST_WRAP_80, True, 80), + (CASE_MULTICLASS_ATTR_LIST, CASE_MULTICLASS_ATTR_LIST_WRAP_80, True, 80), + (CASE_LINK_ATTR_LIST, CASE_LINK_ATTR_LIST_WRAP_80, True, 80), ], ids=[ "CASE_1_FALSE_40", @@ -281,6 +329,10 @@ def gcd(a, b): "CASE_ATTR_LIST_WRAP_TRUE_80", "CASE_CAPTION_WRAP_TRUE_40", "DEF_LIST_WITH_NESTED_WRAP", + "CASE_LONG_LINK_WITH_TITLE_WRAP_80", + "CASE_PARAGRAPH_ATTR_LIST_WRAP_80", + "CASE_MULTICLASS_ATTR_LIST_WRAP_80", + "CASE_LINK_ATTR_LIST_WRAP_80", ], ) def test_wrap(text: str, expected: str, align_lists: bool, wrap: int): From df6e05265299b8db03fce08a1577c33c88ff751c Mon Sep 17 00:00:00 2001 From: Kyle King Date: Fri, 28 Nov 2025 06:15:53 -0600 Subject: [PATCH 8/9] feat: expand math support and make opt-out --- mdformat_mkdocs/mdit_plugins/__init__.py | 4 + .../mdit_plugins/_pymd_arithmatex.py | 8 +- mdformat_mkdocs/plugin.py | 31 +- .../fixtures/math_with_mkdocs_features.md | 244 +++++++++++ .../pymd_arithmatex_ams_environments.md | 403 ++++++++++++++++++ .../fixtures/pymd_arithmatex_edge_cases.md | 243 +++++++++++ tests/format/test_format.py | 3 + tests/render/fixtures/pymd_arithmatex.md | 114 +++++ tests/render/test_render.py | 2 + 9 files changed, 1048 insertions(+), 4 deletions(-) create mode 100644 tests/format/fixtures/math_with_mkdocs_features.md create mode 100644 tests/format/fixtures/pymd_arithmatex_ams_environments.md create mode 100644 tests/format/fixtures/pymd_arithmatex_edge_cases.md create mode 100644 tests/render/fixtures/pymd_arithmatex.md diff --git a/mdformat_mkdocs/mdit_plugins/__init__.py b/mdformat_mkdocs/mdit_plugins/__init__.py index 3115be2..d278c6b 100644 --- a/mdformat_mkdocs/mdit_plugins/__init__.py +++ b/mdformat_mkdocs/mdit_plugins/__init__.py @@ -26,7 +26,9 @@ from ._pymd_arithmatex import ( AMSMATH_BLOCK, DOLLARMATH_BLOCK, + DOLLARMATH_BLOCK_LABEL, DOLLARMATH_INLINE, + TEXMATH_BLOCK_EQNO, pymd_arithmatex_plugin, ) from ._pymd_captions import PYMD_CAPTIONS_PREFIX, pymd_captions_plugin @@ -39,6 +41,7 @@ __all__ = ( "AMSMATH_BLOCK", "DOLLARMATH_BLOCK", + "DOLLARMATH_BLOCK_LABEL", "DOLLARMATH_INLINE", "MATERIAL_ADMON_MARKERS", "MATERIAL_CONTENT_TAB_MARKERS", @@ -49,6 +52,7 @@ "PYMD_CAPTIONS_PREFIX", "PYMD_SNIPPET_PREFIX", "PYTHON_MARKDOWN_ATTR_LIST_PREFIX", + "TEXMATH_BLOCK_EQNO", "escape_deflist", "material_admon_plugin", "material_content_tabs_plugin", diff --git a/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py index 98ea5e1..7d0fd9e 100644 --- a/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py +++ b/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py @@ -27,12 +27,14 @@ from markdown_it import MarkdownIt # Token types from the plugins +# Note: dollarmath and texmath share the same token types for inline/block math: +# - "math_inline" is used for both $...$ and \(...\) +# - "math_block" is used for both $$...$$ and \[...\] DOLLARMATH_INLINE = "math_inline" DOLLARMATH_BLOCK = "math_block" +DOLLARMATH_BLOCK_LABEL = "math_block_label" # For $$...$$ (label) syntax +TEXMATH_BLOCK_EQNO = "math_block_eqno" # For \[...\] (label) syntax AMSMATH_BLOCK = "amsmath" -# FIXME: How should these be used? -TEXMATH_INLINE = "math_inline_double" -TEXMATH_BLOCK = "math_block_eqno" def pymd_arithmatex_plugin(md: MarkdownIt) -> None: diff --git a/mdformat_mkdocs/plugin.py b/mdformat_mkdocs/plugin.py index c67c2a7..d04987c 100644 --- a/mdformat_mkdocs/plugin.py +++ b/mdformat_mkdocs/plugin.py @@ -14,6 +14,7 @@ from .mdit_plugins import ( AMSMATH_BLOCK, DOLLARMATH_BLOCK, + DOLLARMATH_BLOCK_LABEL, DOLLARMATH_INLINE, MKDOCSTRINGS_AUTOREFS_PREFIX, MKDOCSTRINGS_CROSSREFERENCE_PREFIX, @@ -22,6 +23,7 @@ PYMD_CAPTIONS_PREFIX, PYMD_SNIPPET_PREFIX, PYTHON_MARKDOWN_ATTR_LIST_PREFIX, + TEXMATH_BLOCK_EQNO, escape_deflist, material_admon_plugin, material_content_tabs_plugin, @@ -66,6 +68,11 @@ def cli_is_align_semantic_breaks_in_lists(options: ContextOptions) -> bool: return bool(get_conf(options, "align_semantic_breaks_in_lists")) or False +def cli_is_no_mkdocs_math(options: ContextOptions) -> bool: + """user-specified flag to disable math/LaTeX rendering.""" + return bool(get_conf(options, "no_mkdocs_math")) or False + + def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: """Add options to the mdformat CLI. @@ -84,12 +91,19 @@ def add_cli_argument_group(group: argparse._ArgumentGroup) -> None: const=True, 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", ) + group.add_argument( + "--no-mkdocs-math", + action="store_const", + const=True, + help="If set, disable math/LaTeX rendering (Arithmatex). By default, math is enabled.", + ) def update_mdit(mdit: MarkdownIt) -> None: """Update the parser.""" mdit.use(material_admon_plugin) - mdit.use(pymd_arithmatex_plugin) + if not cli_is_no_mkdocs_math(mdit.options): + mdit.use(pymd_arithmatex_plugin) mdit.use(pymd_captions_plugin) mdit.use(material_content_tabs_plugin) mdit.use(material_deflist_plugin) @@ -132,6 +146,19 @@ def _render_math_block(node: RenderTreeNode, context: RenderContext) -> str: # return f"$$\n{content.strip()}\n$$" +def _render_math_block_eqno(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 + """Render block math with equation label.""" + markup = node.markup + content = node.content + label = node.info # Label is stored in info field + if markup == "$$": + return f"$$\n{content.strip()}\n$$ ({label})" + if markup == "\\[": + return f"\\[\n{content.strip()}\n\\] ({label})" + # Fallback + return f"$$\n{content.strip()}\n$$ ({label})" + + def _render_amsmath(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001 """Render amsmath environment.""" # Content already includes \begin{} and \end{} @@ -252,6 +279,8 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str: # Math support (from mdit-py-plugins) DOLLARMATH_INLINE: _render_math_inline, DOLLARMATH_BLOCK: _render_math_block, + DOLLARMATH_BLOCK_LABEL: _render_math_block_eqno, + TEXMATH_BLOCK_EQNO: _render_math_block_eqno, AMSMATH_BLOCK: _render_amsmath, # Other plugins PYMD_CAPTIONS_PREFIX: render_pymd_caption, diff --git a/tests/format/fixtures/math_with_mkdocs_features.md b/tests/format/fixtures/math_with_mkdocs_features.md new file mode 100644 index 0000000..fcc2649 --- /dev/null +++ b/tests/format/fixtures/math_with_mkdocs_features.md @@ -0,0 +1,244 @@ +Math with Admonitions +. +!!! note + The equation $E = mc^2$ is famous. + + $$ + E = mc^2 + $$ + +!!! tip "Energy Formula" + Einstein's equation \(E = mc^2\) relates energy and mass. + + \[ + E = mc^2 + \] +. +!!! note + + The equation $E = mc^2$ is famous. + + $$ + E = mc^2 + $$ + +!!! tip "Energy Formula" + + Einstein's equation \(E = mc^2\) relates energy and mass. + + \[ + E = mc^2 + \] +. + +Math with Content Tabs +. +=== "Tab 1" + Formula: $x = y$ + + Block equation: + + $$ + a^2 + b^2 = c^2 + $$ + +=== "Tab 2" + Using parenthesis notation: \(F = ma\) + + \[ + F = ma + \] +. +=== "Tab 1" + + Formula: $x = y$ + + Block equation: + + $$ + a^2 + b^2 = c^2 + $$ + +=== "Tab 2" + + Using parenthesis notation: \(F = ma\) + + \[ + F = ma + \] +. + +Math with Definition Lists +. +Einstein's Equation +: $E = mc^2$ represents energy-mass equivalence. + + $$ + E = mc^2 + $$ + +Newton's Second Law +: The force \(F\) is defined as: + + \[ + F = ma + \] +. +Einstein's Equation + +: $E = mc^2$ represents energy-mass equivalence. + + $$ + E = mc^2 + $$ + +Newton's Second Law + +: The force \(F\) is defined as: + + \[ + F = ma + \] +. + +Math in Nested Admonitions +. +!!! note "Outer Note" + This is the outer admonition with $x = y$. + + !!! warning "Inner Warning" + Nested admonition with \(a = b\). + + $$ + c = d + $$ + + Back to outer with: + + \[ + e = f + \] +. +!!! note "Outer Note" + + This is the outer admonition with $x = y$. + + !!! warning "Inner Warning" + + Nested admonition with \(a = b\). + + $$ + c = d + $$ + + Back to outer with: + + \[ + e = f + \] +. + +Math in Admonitions with Lists +. +!!! info + Key formulas: + + 1. Energy: $E = mc^2$ + 2. Force: $F = ma$ + 3. Momentum: + + $$ + p = mv + $$ + + 4. Power: \(P = W / t\) +. +!!! info + + Key formulas: + + 1. Energy: $E = mc^2$ + + 1. Force: $F = ma$ + + 1. Momentum: + + $$ + p = mv + $$ + + 1. Power: \(P = W / t\) +. + +Math with Abbreviations +. +*[HTML]: Hyper Text Markup Language + +The HTML specification uses math like $x^2$ occasionally. + +*[LaTeX]: Lamport TeX + +LaTeX is great for typesetting \(\int_0^1 f(x) dx\). +. +*[HTML]: Hyper Text Markup Language + +The HTML specification uses math like $x^2$ occasionally. + +*[LaTeX]: Lamport TeX + +LaTeX is great for typesetting \(\int_0^1 f(x) dx\). +. + +Math in Mixed MkDocs Features +. +!!! example "Combining Features" + Consider the definition: + + Energy + : Defined as $E = mc^2$ + + With tabs: + + === "General Form" + $$ + E = mc^2 + $$ + + === "Expanded Form" + Where: + + - $E$ is energy + - $m$ is mass + - $c$ is speed of light + + \[ + c = 3 \times 10^8 m/s + \] +. +!!! example "Combining Features" + + Consider the definition: + + Energy + : Defined as $E = mc^2$ + + With tabs: + + === "General Form" + + $$ + E = mc^2 + $$ + + === "Expanded Form" + + Where: + + - $E$ is energy + - $m$ is mass + - $c$ is speed of light + + \[ + c = 3 \times 10^8 m/s + \] +. diff --git a/tests/format/fixtures/pymd_arithmatex_ams_environments.md b/tests/format/fixtures/pymd_arithmatex_ams_environments.md new file mode 100644 index 0000000..c4b7bcb --- /dev/null +++ b/tests/format/fixtures/pymd_arithmatex_ams_environments.md @@ -0,0 +1,403 @@ +AMS Math - align* (unnumbered) +. +The aligned equations without numbers: + +\begin{align*} +x &= a + b \\ +y &= c + d \\ +z &= e + f +\end{align*} + +These are unnumbered aligned equations. +. +The aligned equations without numbers: + +\begin{align*} +x &= a + b \\ +y &= c + d \\ +z &= e + f +\end{align*} + +These are unnumbered aligned equations. +. + +AMS Math - gather +. +Multiple equations centered: + +\begin{gather} +a = b + c \\ +x = y + z \\ +m = n + p +\end{gather} + +The gather environment centers equations. +. +Multiple equations centered: + +\begin{gather} +a = b + c \\ +x = y + z \\ +m = n + p +\end{gather} + +The gather environment centers equations. +. + +AMS Math - gather* +. +Multiple equations centered without numbers: + +\begin{gather*} +\sin^2 x + \cos^2 x = 1 \\ +e^{i\pi} + 1 = 0 \\ +\nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t} +\end{gather*} + +Unnumbered gathered equations. +. +Multiple equations centered without numbers: + +\begin{gather*} +\sin^2 x + \cos^2 x = 1 \\ +e^{i\pi} + 1 = 0 \\ +\nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t} +\end{gather*} + +Unnumbered gathered equations. +. + +AMS Math - multline +. +Long equation split across multiple lines: + +\begin{multline} +a + b + c + d + e + f \\ ++ g + h + i + j + k + l \\ ++ m + n + o + p = q +\end{multline} + +The multline environment handles long equations. +. +Long equation split across multiple lines: + +\begin{multline} +a + b + c + d + e + f \\ ++ g + h + i + j + k + l \\ ++ m + n + o + p = q +\end{multline} + +The multline environment handles long equations. +. + +AMS Math - split +. +Split within equation environment: + +\begin{equation} +\begin{split} +a &= b + c \\ + &= d + e \\ + &= f +\end{split} +\end{equation} + +Split provides alignment within a single equation number. +. +Split within equation environment: + +\begin{equation} +\begin{split} +a &= b + c \\ + &= d + e \\ + &= f +\end{split} +\end{equation} + +Split provides alignment within a single equation number. +. + +AMS Math - cases +. +Piecewise function definition: + +$$ +f(x) = \begin{cases} +x^2 & \text{if } x \geq 0 \\ +-x^2 & \text{if } x < 0 +\end{cases} +$$ + +The cases environment is useful for piecewise functions. +. +Piecewise function definition: + +$$ +f(x) = \begin{cases} +x^2 & \text{if } x \geq 0 \\ +-x^2 & \text{if } x < 0 +\end{cases} +$$ + +The cases environment is useful for piecewise functions. +. + +AMS Math - matrix +. +Basic matrix: + +$$ +A = \begin{matrix} +a & b \\ +c & d +\end{matrix} +$$ + +Simple matrix without delimiters. +. +Basic matrix: + +$$ +A = \begin{matrix} +a & b \\ +c & d +\end{matrix} +$$ + +Simple matrix without delimiters. +. + +AMS Math - pmatrix (parentheses) +. +Matrix with parentheses: + +$$ +B = \begin{pmatrix} +1 & 2 & 3 \\ +4 & 5 & 6 \\ +7 & 8 & 9 +\end{pmatrix} +$$ + +The pmatrix environment adds parentheses. +. +Matrix with parentheses: + +$$ +B = \begin{pmatrix} +1 & 2 & 3 \\ +4 & 5 & 6 \\ +7 & 8 & 9 +\end{pmatrix} +$$ + +The pmatrix environment adds parentheses. +. + +AMS Math - bmatrix (brackets) +. +Matrix with brackets: + +$$ +C = \begin{bmatrix} +x & y \\ +z & w +\end{bmatrix} +$$ + +The bmatrix environment adds square brackets. +. +Matrix with brackets: + +$$ +C = \begin{bmatrix} +x & y \\ +z & w +\end{bmatrix} +$$ + +The bmatrix environment adds square brackets. +. + +AMS Math - Bmatrix (braces) +. +Matrix with braces: + +$$ +D = \begin{Bmatrix} +\alpha & \beta \\ +\gamma & \delta +\end{Bmatrix} +$$ + +The Bmatrix environment adds curly braces. +. +Matrix with braces: + +$$ +D = \begin{Bmatrix} +\alpha & \beta \\ +\gamma & \delta +\end{Bmatrix} +$$ + +The Bmatrix environment adds curly braces. +. + +AMS Math - vmatrix (vertical bars) +. +Matrix with vertical bars (determinant): + +$$ +\det(E) = \begin{vmatrix} +a & b \\ +c & d +\end{vmatrix} +$$ + +The vmatrix environment adds vertical bars for determinants. +. +Matrix with vertical bars (determinant): + +$$ +\det(E) = \begin{vmatrix} +a & b \\ +c & d +\end{vmatrix} +$$ + +The vmatrix environment adds vertical bars for determinants. +. + +AMS Math - Vmatrix (double vertical bars) +. +Matrix with double vertical bars: + +$$ +\|F\| = \begin{Vmatrix} +x & y \\ +z & w +\end{Vmatrix} +$$ + +The Vmatrix environment adds double vertical bars. +. +Matrix with double vertical bars: + +$$ +\|F\| = \begin{Vmatrix} +x & y \\ +z & w +\end{Vmatrix} +$$ + +The Vmatrix environment adds double vertical bars. +. + +AMS Math - alignat +. +Alignment at multiple points: + +\begin{alignat}{2} +x &= a &&+ b \\ +y &= c &&+ d \\ +z &= e &&+ f +\end{alignat} + +The alignat environment allows multiple alignment points. +. +Alignment at multiple points: + +\begin{alignat}{2} +x &= a &&+ b \\ +y &= c &&+ d \\ +z &= e &&+ f +\end{alignat} + +The alignat environment allows multiple alignment points. +. + +AMS Math - flalign +. +Full-width alignment: + +\begin{flalign} +x &= a + b \\ +y &= c + d +\end{flalign} + +The flalign environment uses full line width. +. +Full-width alignment: + +\begin{flalign} +x &= a + b \\ +y &= c + d +\end{flalign} + +The flalign environment uses full line width. +. + +AMS Math - eqnarray (legacy, but still supported) +. +Legacy equation array: + +\begin{eqnarray} +a &=& b + c \\ +x &=& y + z +\end{eqnarray} + +The eqnarray environment is legacy but still works. +. +Legacy equation array: + +\begin{eqnarray} +a &=& b + c \\ +x &=& y + z +\end{eqnarray} + +The eqnarray environment is legacy but still works. +. + +Mixed AMS Environments +. +Combining different environments: + +\begin{equation} +A = \begin{pmatrix} +1 & 2 \\ +3 & 4 +\end{pmatrix} +\end{equation} + +\begin{align} +\det(A) &= \begin{vmatrix} +1 & 2 \\ +3 & 4 +\end{vmatrix} \\ +&= 1 \cdot 4 - 2 \cdot 3 \\ +&= -2 +\end{align} + +Multiple environments working together. +. +Combining different environments: + +\begin{equation} +A = \begin{pmatrix} +1 & 2 \\ +3 & 4 +\end{pmatrix} +\end{equation} + +\begin{align} +\det(A) &= \begin{vmatrix} +1 & 2 \\ +3 & 4 +\end{vmatrix} \\ +&= 1 \cdot 4 - 2 \cdot 3 \\ +&= -2 +\end{align} + +Multiple environments working together. +. diff --git a/tests/format/fixtures/pymd_arithmatex_edge_cases.md b/tests/format/fixtures/pymd_arithmatex_edge_cases.md new file mode 100644 index 0000000..e4382bf --- /dev/null +++ b/tests/format/fixtures/pymd_arithmatex_edge_cases.md @@ -0,0 +1,243 @@ +Escaped Delimiters +. +This is not math: \$escaped\$ and \\(also escaped\\). + +Literal dollar: I paid \$5.00 for \$3.00 worth. +. +This is not math: $escaped$ and \\(also escaped\\). + +Literal dollar: I paid $5.00 for $3.00 worth. +. + +Math in Lists +. +1. First item with math $x = y$ +2. Second item with block math: + + $$ + E = mc^2 + $$ + +3. Third item with nested list: + - Nested with $a = b$ + - More nested: + + $$ + F = ma + $$ +. +1. First item with math $x = y$ + +1. Second item with block math: + + $$ + E = mc^2 + $$ + +1. Third item with nested list: + + - Nested with $a = b$ + + - More nested: + + $$ + F = ma + $$ +. + +Math in Blockquotes +. +> Einstein said $E = mc^2$ +> +> The full equation: +> +> $$ +> E = mc^2 +> $$ +. +> Einstein said $E = mc^2$ +> +> The full equation: +> +> $$ +> > E = mc^2 +> > +> $$ +. + +Equation Labels with Square Brackets +. +The Pythagorean theorem: + +\[ +a^2 + b^2 = c^2 +\] (eq:pythagoras) + +Reference equation (eq:pythagoras) above. +. +The Pythagorean theorem: + +\[ +a^2 + b^2 = c^2 +\] (eq:pythagoras) + +Reference equation (eq:pythagoras) above. +. + +Equation Labels with Dollar Signs +. +Maxwell's equations: + +$$ +\nabla \cdot \mathbf{E} = \frac{\rho}{\epsilon_0} +$$ (eq:gauss) + +See equation (eq:gauss) for Gauss's law. +. +Maxwell's equations: + +$$ +\nabla \cdot \mathbf{E} = \frac{\rho}{\epsilon_0} +$$ (eq:gauss) + +See equation (eq:gauss) for Gauss's law. +. + +Math with Line Breaks +. +$$ +x = a + b + c + + d + e + + f +$$ +. +$$ +x = a + b + c + + d + e + + f +$$ +. + +Math in Tables +. +| Formula | Description | +| ------- | ----------- | +| $E=mc^2$ | Energy-mass | +| $F=ma$ | Force | +| \(p=mv\) | Momentum | +. +| Formula | Description | +| ------- | ----------- | +| $E=mc^2$ | Energy-mass | +| $F=ma$ | Force | +| \(p=mv\) | Momentum | +. + +Math at Line Boundaries +. +$start of line$ and $end of line$ +$x=y$ +\(a=b\) and \(c=d\) +. +$start of line$ and $end of line$ +$x=y$ +\(a=b\) and \(c=d\) +. + +Special Characters in Math +. +Brackets: $[a, b]$ and braces: $\{x \in X\}$ + +Pipes: $|x|$ and backslash: $\backslash$ + +Underscores: $x_1, x_2, \ldots, x_n$ + +Carets: $x^2 + y^2 = z^2$ +. +Brackets: $[a, b]$ and braces: $\{x \in X\}$ + +Pipes: $|x|$ and backslash: $\backslash$ + +Underscores: $x_1, x_2, \ldots, x_n$ + +Carets: $x^2 + y^2 = z^2$ +. + +Adjacent Math Expressions +. +Multiple inline: $a$ $b$ $c$ + +With text between: $x$ and $y$ and $z$ + +Different delimiters: $a$ \(b\) $c$ +. +Multiple inline: $a$ $b$ $c$ + +With text between: $x$ and $y$ and $z$ + +Different delimiters: $a$ \(b\) $c$ +. + +Math with Leading/Trailing Whitespace +. +$$ + x = y +$$ + +\[ + a = b +\] +. +$$ +x = y +$$ + +\[ +a = b +\] +. + +Multiline Inline Math +. +This has inline math $\frac{a}{b}$ in it. + +Complex fraction: $\frac{\frac{a}{b}}{\frac{c}{d}}$ is nested. +. +This has inline math $\frac{a}{b}$ in it. + +Complex fraction: $\frac{\frac{a}{b}}{\frac{c}{d}}$ is nested. +. + +Math in Code Fences (Should Not Be Parsed) +. +```python +# This should not be parsed as math +cost = $5.00 + $3.00 +energy = "E = mc^2" +``` + +But this should: $E = mc^2$ +. +```python +# This should not be parsed as math +cost = $5.00 + $3.00 +energy = "E = mc^2" +``` + +But this should: $E = mc^2$ +. + +Mixed Math and Text on Same Line +. +The equation $E = mc^2$ was derived by Einstein in 1905. + +Multiple: $a=1$, $b=2$, and $c=3$. + +Parenthesis: \(x=y\) is equivalent to \(y=x\). +. +The equation $E = mc^2$ was derived by Einstein in 1905. + +Multiple: $a=1$, $b=2$, and $c=3$. + +Parenthesis: \(x=y\) is equivalent to \(y=x\). +. diff --git a/tests/format/test_format.py b/tests/format/test_format.py index a0db47f..3868176 100644 --- a/tests/format/test_format.py +++ b/tests/format/test_format.py @@ -24,9 +24,12 @@ def flatten(nested_list: list[list[T]]) -> list[T]: "material_content_tabs.md", "material_deflist.md", "material_math.md", + "math_with_mkdocs_features.md", "mkdocstrings_autorefs.md", "pymd_abbreviations.md", "pymd_arithmatex.md", + "pymd_arithmatex_ams_environments.md", + "pymd_arithmatex_edge_cases.md", "pymd_snippet.md", "python_markdown_attr_list.md", "regression.md", diff --git a/tests/render/fixtures/pymd_arithmatex.md b/tests/render/fixtures/pymd_arithmatex.md new file mode 100644 index 0000000..0a7ebb0 --- /dev/null +++ b/tests/render/fixtures/pymd_arithmatex.md @@ -0,0 +1,114 @@ +Inline Math with Dollar Signs +. +The equation $E = mc^2$ is famous. +. +

The equation E = mc^2 is famous.

+. + +Inline Math with Parentheses +. +Newton's law \(F = ma\) is fundamental. +. +

Newton's law F = ma is fundamental.

+. + +Block Math with Double Dollars +. +The energy equation: + +$$ +E = mc^2 +$$ +. +

The energy equation:

+
+ +E = mc^2 + +
+. + +Block Math with Square Brackets +. +The force equation: + +\[ +F = ma +\] +. +

The force equation:

+
+ +F = ma + +
+. + +Block Math with Equation Label +. +Pythagorean theorem: + +\[ +a^2 + b^2 = c^2 +\] (eq:pythagoras) +. +

Pythagorean theorem:

+
+ +a^2 + b^2 = c^2 + +
+. + +AMS Math - align +. +System of equations: + +\begin{align} +x + y &= 5 \\ +x - y &= 1 +\end{align} +. +

System of equations:

+
+\begin{align} +x + y &= 5 \\ +x - y &= 1 +\end{align} +
+. + +AMS Math - equation +. +Einstein's field equation: + +\begin{equation} +R_{\mu\nu} = 0 +\end{equation} +. +

Einstein's field equation:

+
+\begin{equation} +R_{\mu\nu} = 0 +\end{equation} +
+. + +Mixed Inline and Block +. +For $x = y$, we have: + +$$ +x + 1 = y + 1 +$$ + +Therefore \(x = y\). +. +

For x = y, we have:

+
+ +x + 1 = y + 1 + +
+

Therefore x = y.

+. diff --git a/tests/render/test_render.py b/tests/render/test_render.py index b7ca047..c9fdd11 100644 --- a/tests/render/test_render.py +++ b/tests/render/test_render.py @@ -11,6 +11,7 @@ mkdocstrings_autorefs_plugin, mkdocstrings_crossreference_plugin, pymd_abbreviations_plugin, + pymd_arithmatex_plugin, pymd_captions_plugin, pymd_snippet_plugin, python_markdown_attr_list_plugin, @@ -35,6 +36,7 @@ def with_plugin(filename, plugins): *with_plugin("material_deflist.md", [material_deflist_plugin]), *with_plugin("mkdocstrings_autorefs.md", [mkdocstrings_autorefs_plugin]), *with_plugin("pymd_abbreviations.md", [pymd_abbreviations_plugin]), + *with_plugin("pymd_arithmatex.md", [pymd_arithmatex_plugin]), *with_plugin("pymd_captions.md", [pymd_captions_plugin]), *with_plugin( "mkdocstrings_crossreference.md", From b68cd5627a09341aee06a8589b1690d22620138e Mon Sep 17 00:00:00 2001 From: Kyle King Date: Fri, 28 Nov 2025 06:35:14 -0600 Subject: [PATCH 9/9] docs: clarify the three plugins --- README.md | 11 +++++-- .../pymd_arithmatex_ams_environments.md | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e66d8f8..be01925 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,11 @@ Supports: - [Python Markdown "Attribute Lists"](https://python-markdown.github.io/extensions/attr_list) - Preserves attribute list syntax when using `--wrap` mode - [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)) - - Inline math: `$E = mc^2$` or `\(x + y\)` - - Block math: `$$...$$`, `\[...\]`, or `\begin{env}...\end{env}` - - Supports smart dollar mode (prevents false positives like `$3.00`) + - This plugin combines three math rendering plugins from mdit-py-plugins: + 1. **dollarmath**: Handles `$...$` (inline) and `$$...$$` (block) with smart dollar mode that prevents false positives (e.g., `$3.00` is not treated as math) + 1. **texmath**: Handles `\(...\)` (inline) and `\[...\]` (block) LaTeX bracket notation + 1. **amsmath**: Handles LaTeX environments like `\begin{align}...\end{align}`, `\begin{cases}...\end{cases}`, `\begin{matrix}...\end{matrix}`, etc. + - Can be deactivated entirely with the `--no-mkdocs-math` flag - [Python Markdown "Snippets"\*](https://facelessuser.github.io/pymdown-extensions/extensions/snippets) - \*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 @@ -129,6 +131,8 @@ md.render(text) - `--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 +- `--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. + You can also use the toml configuration (https://mdformat.readthedocs.io/en/stable/users/configuration_file.html): ```toml @@ -137,6 +141,7 @@ You can also use the toml configuration (https://mdformat.readthedocs.io/en/stab [plugin.mkdocs] align_semantic_breaks_in_lists = true ignore_missing_references = true +no_mkdocs_math = true ``` ## Contributing diff --git a/tests/format/fixtures/pymd_arithmatex_ams_environments.md b/tests/format/fixtures/pymd_arithmatex_ams_environments.md index c4b7bcb..d05237c 100644 --- a/tests/format/fixtures/pymd_arithmatex_ams_environments.md +++ b/tests/format/fixtures/pymd_arithmatex_ams_environments.md @@ -1,3 +1,32 @@ +ReLU Function with Mixed Syntax (Issue #45) +. +$$ +ReLU(x) = + \begin{cases} + x &\quad\text{if } x > 0\\\ + 0 &\quad\text{otherwise} + \end{cases} + $$ + +\[ x = \frac{4}{5} \] + +What about inline expressions? $\Delta_{distance}= \text{Speed} \cdot \text{Time}$ +. +$$ +ReLU(x) = + \begin{cases} + x &\quad\text{if } x > 0\\\ + 0 &\quad\text{otherwise} + \end{cases} +$$ + +\[ +x = \frac{4}{5} +\] + +What about inline expressions? $\Delta_{distance}= \text{Speed} \cdot \text{Time}$ +. + AMS Math - align* (unnumbered) . The aligned equations without numbers: