Skip to content

Commit fede1b2

Browse files
bittoralaBittor Alaña
andauthored
feat(#51): add caption support (#52)
Co-authored-by: Bittor Alaña <[email protected]>
1 parent f1eeb2b commit fede1b2

File tree

7 files changed

+221
-0
lines changed

7 files changed

+221
-0
lines changed

mdformat_mkdocs/mdit_plugins/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717
from ._pymd_abbreviations import PYMD_ABBREVIATIONS_PREFIX, pymd_abbreviations_plugin
1818
from ._pymd_admon import pymd_admon_plugin
19+
from ._pymd_captions import PYMD_CAPTIONS_PREFIX, pymd_captions_plugin
1920
from ._pymd_snippet import PYMD_SNIPPET_PREFIX, pymd_snippet_plugin
2021
from ._python_markdown_attr_list import (
2122
PYTHON_MARKDOWN_ATTR_LIST_PREFIX,
@@ -29,6 +30,7 @@
2930
"MKDOCSTRINGS_CROSSREFERENCE_PREFIX",
3031
"MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX",
3132
"PYMD_ABBREVIATIONS_PREFIX",
33+
"PYMD_CAPTIONS_PREFIX",
3234
"PYMD_SNIPPET_PREFIX",
3335
"PYTHON_MARKDOWN_ATTR_LIST_PREFIX",
3436
"material_admon_plugin",
@@ -37,6 +39,7 @@
3739
"mkdocstrings_crossreference_plugin",
3840
"pymd_abbreviations_plugin",
3941
"pymd_admon_plugin",
42+
"pymd_captions_plugin",
4043
"pymd_snippet_plugin",
4144
"python_markdown_attr_list_plugin",
4245
)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Python-Markdown Extensions Captions.
2+
3+
Matches:
4+
5+
```md
6+
/// caption
7+
Default values for config variables.
8+
///
9+
```
10+
11+
Docs:
12+
https://github.com/facelessuser/pymdown-extensions/blob/main/pymdownx/blocks/caption.py
13+
14+
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import re
20+
from typing import TYPE_CHECKING
21+
22+
from mdit_py_plugins.utils import is_code_block
23+
24+
from mdformat_mkdocs._synced.admon_factories._whitespace_admon_factories import (
25+
new_token,
26+
)
27+
28+
if TYPE_CHECKING:
29+
from markdown_it import MarkdownIt
30+
from markdown_it.rules_block import StateBlock
31+
32+
_CAPTION_START_PATTERN = re.compile(
33+
r"\s*///\s*(?P<type>figure-|table-|)caption\s*(\|\s*(?P<number>[\d\.]+))?",
34+
)
35+
_CAPTION_END_PATTERN = re.compile(r"^\s*///\s*$")
36+
_CAPTION_ATTRS_PATTERN = re.compile(r"^s*(?P<attrs>attrs:\s*\{[^}]*\})\s*$")
37+
PYMD_CAPTIONS_PREFIX = "mkdocs_caption"
38+
39+
40+
def _src_in_line(state: StateBlock, line: int) -> tuple[str, int, int]:
41+
"""Get the source in a given line number."""
42+
start_pos = state.bMarks[line] + state.tShift[line]
43+
end_pos = state.eMarks[line]
44+
return state.src[start_pos:end_pos], start_pos, end_pos
45+
46+
47+
def _parse(
48+
state: StateBlock,
49+
first_line_max_pos: int,
50+
start_line: int,
51+
end_line: int,
52+
) -> tuple[int, str, str | None]:
53+
"""Parse a caption block: optionally read attrs and extract content."""
54+
end_match = None
55+
max_line = start_line + 1
56+
end_pos = -1
57+
attrs_text, _, attrs_max_pos = _src_in_line(state, max_line)
58+
caption_attrs_match = _CAPTION_ATTRS_PATTERN.match(attrs_text)
59+
content_start_pos = (
60+
first_line_max_pos + 1 if caption_attrs_match is None else attrs_max_pos + 1
61+
)
62+
attrs = (
63+
caption_attrs_match.group("attrs") if caption_attrs_match is not None else None
64+
)
65+
if not isinstance(attrs, str):
66+
attrs = None
67+
68+
while end_match is None and max_line <= end_line:
69+
line_text, end_pos, _ = _src_in_line(state, max_line)
70+
if _CAPTION_END_PATTERN.match(line_text) is None:
71+
max_line += 1
72+
else:
73+
end_match = max_line
74+
75+
return max_line, state.src[content_start_pos:end_pos], attrs
76+
77+
78+
def _material_captions(
79+
state: StateBlock,
80+
start_line: int,
81+
end_line: int,
82+
silent: bool,
83+
) -> bool:
84+
"""Detect caption blocks and wrap them in a token."""
85+
if is_code_block(state, start_line):
86+
return False
87+
88+
first_line_text, _, first_line_max_pos = _src_in_line(state, start_line)
89+
start_match = _CAPTION_START_PATTERN.match(first_line_text)
90+
if start_match is None:
91+
return False
92+
93+
if silent:
94+
return True
95+
96+
max_line, content, attrs = _parse(state, first_line_max_pos, start_line, end_line)
97+
98+
with (
99+
new_token(state, PYMD_CAPTIONS_PREFIX, "figcaption") as token,
100+
new_token(state, "", "p"),
101+
):
102+
token.info = start_match.group("type") + "caption"
103+
token.meta = {"number": start_match.group("number")}
104+
if attrs is not None:
105+
token.meta["attrs"] = attrs
106+
tkn_inline = state.push("inline", "", 0)
107+
tkn_inline.content = content.strip()
108+
tkn_inline.map = [start_line, max_line]
109+
tkn_inline.children = []
110+
111+
state.line = max_line + 1
112+
113+
return True
114+
115+
116+
def pymd_captions_plugin(md: MarkdownIt) -> None:
117+
md.block.ruler.before(
118+
"fence",
119+
PYMD_CAPTIONS_PREFIX,
120+
_material_captions,
121+
{"alt": ["paragraph"]},
122+
)

mdformat_mkdocs/plugin.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
MKDOCSTRINGS_CROSSREFERENCE_PREFIX,
1717
MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX,
1818
PYMD_ABBREVIATIONS_PREFIX,
19+
PYMD_CAPTIONS_PREFIX,
1920
PYMD_SNIPPET_PREFIX,
2021
PYTHON_MARKDOWN_ATTR_LIST_PREFIX,
2122
material_admon_plugin,
@@ -24,6 +25,7 @@
2425
mkdocstrings_crossreference_plugin,
2526
pymd_abbreviations_plugin,
2627
pymd_admon_plugin,
28+
pymd_captions_plugin,
2729
pymd_snippet_plugin,
2830
python_markdown_attr_list_plugin,
2931
)
@@ -78,6 +80,7 @@ def add_cli_argument_group(group: argparse._ArgumentGroup) -> None:
7880
def update_mdit(mdit: MarkdownIt) -> None:
7981
"""Update the parser."""
8082
mdit.use(material_admon_plugin)
83+
mdit.use(pymd_captions_plugin)
8184
mdit.use(material_content_tabs_plugin)
8285
mdit.use(mkdocstrings_autorefs_plugin)
8386
mdit.use(pymd_abbreviations_plugin)
@@ -179,6 +182,19 @@ def add_extra_admon_newline(node: RenderTreeNode, context: RenderContext) -> str
179182
return f"{title}\n\n{''.join(content)}"
180183

181184

185+
def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str:
186+
"""Render caption with normalized format."""
187+
caption_type = node.info or "caption"
188+
attrs = node.meta.get("attrs")
189+
number = node.meta.get("number")
190+
rendered_content = "".join(
191+
child.render(context) for child in node.children[0].children
192+
)
193+
caption_number = f" | {number}" if number else ""
194+
caption_attrs = f"\n {attrs}" if attrs else ""
195+
return f"/// {caption_type}{caption_number}{caption_attrs}\n{rendered_content}\n///"
196+
197+
182198
# A mapping from syntax tree node type to a function that renders it.
183199
# This can be used to overwrite renderer functions of existing syntax
184200
# or add support for new syntax.
@@ -189,6 +205,7 @@ def add_extra_admon_newline(node: RenderTreeNode, context: RenderContext) -> str
189205
"admonition_mkdocs_title": render_admon_title,
190206
"content_tab_mkdocs": add_extra_admon_newline,
191207
"content_tab_mkdocs_title": render_admon_title,
208+
PYMD_CAPTIONS_PREFIX: render_pymd_caption,
192209
MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content,
193210
MKDOCSTRINGS_CROSSREFERENCE_PREFIX: _render_cross_reference,
194211
MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX: _render_heading_autoref,

tests/format/fixtures/text.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,3 +1735,22 @@ Don't wrap long URLs (fixes: https://github.com/KyleKing/mdformat-mkdocs/issues/
17351735
- a [with space](https://github.com/python/mypy/blob/a3ce6d5307e99a1b6c181eaa7c5cf134c53b7d/test-data/check-protocols)
17361736
- b
17371737
.
1738+
1739+
Format captions correctly
1740+
.
1741+
|a|b|
1742+
|-|-|
1743+
|c|d|
1744+
1745+
/// table-caption | 1.5.2
1746+
A table with letters.
1747+
///
1748+
.
1749+
|a|b|
1750+
|-|-|
1751+
|c|d|
1752+
1753+
/// table-caption | 1.5.2
1754+
A table with letters.
1755+
///
1756+
.

tests/format/test_wrap.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,39 @@
188188
{: .class1 .class2 .class3 .class4 .class5 .class6 .class7 .class8 .class9 .class10 .class11 .class12 .class13 .class14 .class15 .class16 .class17 .class18 .class19 .class20 }
189189
"""
190190

191+
CASE_CAPTION_WRAP = """
192+
This line is longer than 40 characters and should be wrapped.
193+
194+
```
195+
def gcd(a, b):
196+
if a == 0: return b
197+
elif b == 0: return a
198+
if a > b: return gcd(a % b, b)
199+
else: return gcd(a, b % a)
200+
```
201+
202+
/// caption
203+
Greatest common divisor algorithm.
204+
///
205+
"""
206+
207+
CASE_CAPTION_WRAP_TRUE_40 = """
208+
This line is longer than 40 characters
209+
and should be wrapped.
210+
211+
```
212+
def gcd(a, b):
213+
if a == 0: return b
214+
elif b == 0: return a
215+
if a > b: return gcd(a % b, b)
216+
else: return gcd(a, b % a)
217+
```
218+
219+
/// caption
220+
Greatest common divisor algorithm.
221+
///
222+
"""
223+
191224

192225
@pytest.mark.parametrize(
193226
("text", "expected", "align_lists", "wrap"),
@@ -200,6 +233,7 @@
200233
(WITH_CODE, WITH_CODE_TRUE_80, True, 80),
201234
(WITH_ATTR_LIST, WITH_ATTR_LIST_TRUE_80, True, 80),
202235
(CASE_ATTR_LIST_WRAP, CASE_ATTR_LIST_WRAP_TRUE_80, True, 80),
236+
(CASE_CAPTION_WRAP, CASE_CAPTION_WRAP_TRUE_40, True, 40),
203237
],
204238
ids=[
205239
"CASE_1_FALSE_40",
@@ -210,6 +244,7 @@
210244
"WITH_CODE_TRUE_80",
211245
"WITH_ATTR_LIST_TRUE_80",
212246
"CASE_ATTR_LIST_WRAP_TRUE_80",
247+
"CASE_CAPTION_WRAP_TRUE_40",
213248
],
214249
)
215250
def test_wrap(text: str, expected: str, align_lists: bool, wrap: int):
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
pymdown captions (https://github.com/KyleKing/mdformat-mkdocs/issues/51)
2+
.
3+
# Captions
4+
5+
Captioned content.
6+
7+
/// caption
8+
First caption.
9+
///
10+
11+
/// caption
12+
Second caption.
13+
///
14+
.
15+
<h1>Captions</h1>
16+
<p>Captioned content.</p>
17+
<figcaption>
18+
<p>First caption.</p>
19+
</figcaption>
20+
<figcaption>
21+
<p>Second caption.</p>
22+
</figcaption>
23+
.

tests/render/test_render.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
mkdocstrings_autorefs_plugin,
1111
mkdocstrings_crossreference_plugin,
1212
pymd_abbreviations_plugin,
13+
pymd_captions_plugin,
1314
pymd_snippet_plugin,
1415
python_markdown_attr_list_plugin,
1516
)
@@ -32,6 +33,7 @@ def with_plugin(filename, plugins):
3233
),
3334
*with_plugin("mkdocstrings_autorefs.md", [mkdocstrings_autorefs_plugin]),
3435
*with_plugin("pymd_abbreviations.md", [pymd_abbreviations_plugin]),
36+
*with_plugin("pymd_captions.md", [pymd_captions_plugin]),
3537
*with_plugin(
3638
"mkdocstrings_crossreference.md",
3739
[mkdocstrings_crossreference_plugin],

0 commit comments

Comments
 (0)