Skip to content

Commit 6ea68e6

Browse files
authored
fix: Narrow application of snippets trailing spaces
1 parent b384ce1 commit 6ea68e6

10 files changed

Lines changed: 256 additions & 63 deletions

File tree

mdformat_mkdocs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# ruff: noqa: RUF067
12
"""An mdformat plugin for `mkdocs`."""
23

34
__version__ = "5.1.4"

mdformat_mkdocs/mdit_plugins/_python_markdown_attr_list.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ def _python_markdown_attr_list(state: StateInline, silent: bool) -> bool:
4040

4141
# Look backwards for unclosed '['
4242
search_start = max(0, state.pos - 100) # Limit backwards search
43-
text_before = state.src[search_start:state.pos]
43+
text_before = state.src[search_start : state.pos]
4444
open_brackets = text_before.count("[") - text_before.count("]")
4545
if open_brackets > 0:
4646
# We might be inside a link, check if there's '](' after our match
4747
match_end_pos = state.pos + match.end()
4848
if match_end_pos < len(state.src):
49-
lookahead = state.src[match_end_pos:min(match_end_pos + 100, len(state.src))]
49+
lookahead = state.src[
50+
match_end_pos : min(match_end_pos + 100, len(state.src))
51+
]
5052
if "](" in lookahead:
5153
# Very likely inside link text, don't match
5254
return False

mdformat_mkdocs/plugin.py

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import re
56
import textwrap
67
from functools import partial
78
from typing import TYPE_CHECKING
@@ -10,7 +11,7 @@
1011

1112
from ._helpers import ContextOptions, get_conf
1213
from ._normalize_list import normalize_list as unbounded_normalize_list
13-
from ._postprocess_inline import postprocess_list_wrap
14+
from ._postprocess_inline import postprocess_list_wrap as _postprocess_list_wrap
1415
from .mdit_plugins import (
1516
AMSMATH_BLOCK,
1617
DOLLARMATH_BLOCK,
@@ -134,29 +135,42 @@ def _render_math_inline(node: RenderTreeNode, context: RenderContext) -> str: #
134135
return f"${content}$"
135136

136137

138+
def _strip_blockquote_markers(content: str) -> str:
139+
"""Strip blockquote markers from math block content.
140+
141+
markdown-it includes "> " prefixes when block math appears inside blockquotes.
142+
"""
143+
lines = content.split("\n")
144+
return "\n".join(
145+
line.removeprefix("> ") if line.startswith("> ") else line for line in lines
146+
).strip()
147+
148+
137149
def _render_math_block(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
138150
"""Render block math with original delimiters."""
139151
markup = node.markup
140-
content = node.content
152+
cleaned_content = _strip_blockquote_markers(node.content)
153+
141154
if markup == "$$":
142-
return f"$$\n{content.strip()}\n$$"
155+
return f"$$\n{cleaned_content}\n$$"
143156
if markup == "\\[":
144-
return f"\\[\n{content.strip()}\n\\]"
157+
return f"\\[\n{cleaned_content}\n\\]"
145158
# Fallback
146-
return f"$$\n{content.strip()}\n$$"
159+
return f"$$\n{cleaned_content}\n$$"
147160

148161

149162
def _render_math_block_eqno(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
150163
"""Render block math with equation label."""
151164
markup = node.markup
152-
content = node.content
153-
label = node.info # Label is stored in info field
165+
label = node.info
166+
cleaned_content = _strip_blockquote_markers(node.content)
167+
154168
if markup == "$$":
155-
return f"$$\n{content.strip()}\n$$ ({label})"
169+
return f"$$\n{cleaned_content}\n$$ ({label})"
156170
if markup == "\\[":
157-
return f"\\[\n{content.strip()}\n\\] ({label})"
171+
return f"\\[\n{cleaned_content}\n\\] ({label})"
158172
# Fallback
159-
return f"$$\n{content.strip()}\n$$ ({label})"
173+
return f"$$\n{cleaned_content}\n$$ ({label})"
160174

161175

162176
def _render_amsmath(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
@@ -176,40 +190,30 @@ def _render_inline_content(node: RenderTreeNode, context: RenderContext) -> str:
176190
return inline.content
177191

178192

179-
def _render_code_inline(node: RenderTreeNode, context: RenderContext) -> str:
180-
r"""Render inline code, cleaning up whitespace from newline normalization.
181-
182-
`markdown-it` normalizes newlines in inline code to spaces. This can result in
183-
unintended trailing spaces from original newlines before closing backticks.
184-
Per mdformat's own logic, trailing spaces are only intentional if there are
185-
also leading spaces. So we strip trailing spaces when there's no leading space.
193+
def _render_text(node: RenderTreeNode, context: RenderContext) -> str:
194+
r"""Re-escape dollar signs that mdformat core stripped.
186195
187-
Example: `code\n` (newline) → `code ` (parsed) → `code` (rendered)
188-
189-
This could break at any time, so this is a best effort to resolve issues like:
190-
https://github.com/KyleKing/mdformat-mkdocs/issues/34#issuecomment-3589835341
196+
mdformat removes "unnecessary" backslash escapes (\$ -> $), but with math enabled
197+
those bare $ become math delimiters. Compares text content against the parent
198+
inline token (which preserves backslashes) to detect and restore the escapes.
191199
200+
Related: https://github.com/KyleKing/mdformat-mkdocs/issues/77
192201
"""
193-
default_renderer = DEFAULT_RENDERERS.get("code_inline")
202+
default_renderer = DEFAULT_RENDERERS.get("text")
194203
if default_renderer is None:
195204
return node.content
196205

197-
result = default_renderer(node, context)
198-
199-
# Only process single-backtick code (not double-backtick code with embedded backticks)
200-
if not (result.startswith("`") and result.endswith("`") and "``" not in result):
201-
return result
206+
text = default_renderer(node, context)
202207

203-
content = result[1:-1] # Strip opening and closing backticks
204-
has_leading_space = content.startswith(" ")
205-
has_trailing_space = content.endswith(" ")
208+
if cli_is_no_mkdocs_math(context.options):
209+
return text
206210

207-
# Strip trailing space only if there's no leading space and content is not all whitespace
208-
# This preserves the mdformat rule: spaces are only intentional when both are present
209-
if has_trailing_space and not has_leading_space and content.strip():
210-
return f"`{content.rstrip(' ')}`"
211+
if node.parent and node.parent.type == "inline":
212+
parent_content = node.parent.content
213+
if "$" in text and r"\$" in parent_content:
214+
text = re.sub(r"(?<!\\)\$", r"\$", text)
211215

212-
return result
216+
return text
213217

214218

215219
def _render_heading_autoref(node: RenderTreeNode, context: RenderContext) -> str:
@@ -307,12 +311,12 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str:
307311
"admonition_title": render_admon_title,
308312
"admonition_mkdocs": add_extra_admon_newline,
309313
"admonition_mkdocs_title": render_admon_title,
310-
"code_inline": _render_code_inline,
311314
"content_tab_mkdocs": add_extra_admon_newline,
312315
"content_tab_mkdocs_title": render_admon_title,
316+
"dd": render_material_definition_body,
313317
"dl": render_material_definition_list,
314318
"dt": render_material_definition_term,
315-
"dd": render_material_definition_body,
319+
"text": _render_text,
316320
# Math support (from mdit-py-plugins)
317321
DOLLARMATH_INLINE: _render_math_inline,
318322
DOLLARMATH_BLOCK: _render_math_block,
@@ -330,10 +334,15 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str:
330334
}
331335

332336

333-
normalize_list = partial(
334-
unbounded_normalize_list, # type: ignore[has-type]
335-
check_if_align_semantic_breaks_in_lists=cli_is_align_semantic_breaks_in_lists,
336-
)
337+
if TYPE_CHECKING:
338+
normalize_list: Postprocess
339+
postprocess_list_wrap: Postprocess
340+
else:
341+
normalize_list = partial(
342+
unbounded_normalize_list,
343+
check_if_align_semantic_breaks_in_lists=cli_is_align_semantic_breaks_in_lists,
344+
)
345+
postprocess_list_wrap = _postprocess_list_wrap
337346

338347
# A mapping from `RenderTreeNode.type` to a `Postprocess` that does
339348
# postprocessing for the output of the `Render` function. Unlike
@@ -342,7 +351,7 @@ def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str:
342351
# will run in series.
343352
POSTPROCESSORS: Mapping[str, Postprocess] = {
344353
"bullet_list": normalize_list,
345-
"inline": postprocess_list_wrap, # type: ignore[has-type]
354+
"inline": postprocess_list_wrap,
346355
"ordered_list": normalize_list,
347356
"paragraph": escape_deflist,
348357
}

tests/__init__.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1 @@
1-
"""Ignore beartype warnings from tests."""
2-
3-
from contextlib import suppress
4-
5-
with suppress(ImportError): # Only suppress beartype warnings when installed
6-
from warnings import filterwarnings
7-
8-
from beartype.roar import (
9-
BeartypeClawDecorWarning, # Too many False Positives using NamedTuples
10-
BeartypeDecorHintPep585DeprecationWarning,
11-
)
12-
13-
filterwarnings("ignore", category=BeartypeClawDecorWarning)
14-
filterwarnings("ignore", category=BeartypeDecorHintPep585DeprecationWarning)
1+
"""Tests for mdformat-mkdocs."""

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Pytest configuration for suppressing beartype warnings."""
2+
3+
from contextlib import suppress
4+
5+
with suppress(ImportError): # Only suppress beartype warnings when installed
6+
from warnings import filterwarnings
7+
8+
from beartype.roar import (
9+
BeartypeClawDecorWarning, # Too many False Positives using NamedTuples
10+
BeartypeDecorHintPep585DeprecationWarning,
11+
)
12+
13+
filterwarnings("ignore", category=BeartypeClawDecorWarning)
14+
filterwarnings("ignore", category=BeartypeDecorHintPep585DeprecationWarning)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
Trailing space only (no leading space) - preserved
2+
.
3+
`code `
4+
.
5+
`code `
6+
.
7+
Leading space only - preserved
8+
.
9+
` code`
10+
.
11+
` code`
12+
.
13+
Both leading and trailing spaces - mdformat strips both
14+
.
15+
` code `
16+
.
17+
`code`
18+
.
19+
Multiple trailing spaces - preserved
20+
.
21+
`code `
22+
.
23+
`code `
24+
.
25+
Trailing tab - preserved (tabs not normalized)
26+
.
27+
`code `
28+
.
29+
`code `
30+
.
31+
All whitespace - preserved
32+
.
33+
` `
34+
.
35+
` `
36+
.
37+
Empty code - gets escaped
38+
.
39+
``
40+
.
41+
\`\`
42+
.
43+
Newline before closing backtick (snippet case) - preserved as space
44+
.
45+
`--8<-- "somesnippet.sh"
46+
`
47+
.
48+
`--8<-- "somesnippet.sh" `
49+
.
50+
Code with internal spaces - preserved
51+
.
52+
`foo bar baz`
53+
.
54+
`foo bar baz`
55+
.
56+
Code with leading and internal spaces - preserved
57+
.
58+
` foo bar`
59+
.
60+
` foo bar`
61+
.
62+
Code with trailing and internal spaces - preserved
63+
.
64+
`foo bar `
65+
.
66+
`foo bar `
67+
.
68+
Double backticks with trailing space - preserved differently
69+
.
70+
``code` ``
71+
.
72+
`` code` ``
73+
.
74+
Trailing space before horizontal rule - preserved (rule normalized to underscores)
75+
.
76+
`test `
77+
78+
---
79+
.
80+
`test `
81+
82+
______________________________________________________________________
83+
.
84+
Trailing space before heading - preserved
85+
.
86+
`test `
87+
88+
# Heading
89+
.
90+
`test `
91+
92+
# Heading
93+
.
94+
Trailing space in middle of paragraph - preserved
95+
.
96+
This is `code ` in text.
97+
.
98+
This is `code ` in text.
99+
.
100+
Multiple inline codes with trailing spaces - all preserved
101+
.
102+
`first ` and `second ` and `third `
103+
.
104+
`first ` and `second ` and `third `
105+
.
106+
Trailing space in list item - preserved
107+
.
108+
- Item with `code `
109+
.
110+
- Item with `code `
111+
.
112+
Trailing space in blockquote - preserved
113+
.
114+
> Quote with `code `
115+
.
116+
> Quote with `code `
117+
.
118+
Trailing space in admonition - preserved
119+
.
120+
!!! note
121+
122+
Content with `code `
123+
.
124+
!!! note
125+
126+
Content with `code `
127+
.

tests/format/fixtures/pymd_arithmatex_edge_cases.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ This is not math: \$escaped\$ and \\(also escaped\\).
44

55
Literal dollar: I paid \$5.00 for \$3.00 worth.
66
.
7-
This is not math: $escaped$ and \\(also escaped\\).
7+
This is not math: \$escaped\$ and \\(also escaped\\).
88

9-
Literal dollar: I paid $5.00 for $3.00 worth.
9+
Literal dollar: I paid \$5.00 for \$3.00 worth.
1010
.
1111

1212
Escaped Bracket Notation (Issue #72)
@@ -125,8 +125,7 @@ Math in Blockquotes
125125
> The full equation:
126126
>
127127
> $$
128-
> > E = mc^2
129-
> >
128+
> E = mc^2
130129
> $$
131130
.
132131

tests/format/fixtures/regression.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ Inline snippet with newline before closing backtick
99
`--8<-- "somesnippet.sh"
1010
`
1111
.
12-
`--8<-- "somesnippet.sh"`
12+
`--8<-- "somesnippet.sh" `
1313
.

0 commit comments

Comments
 (0)