Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mdformat_mkdocs/mdit_plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from ._pymd_abbreviations import PYMD_ABBREVIATIONS_PREFIX, pymd_abbreviations_plugin
from ._pymd_admon import pymd_admon_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 (
PYTHON_MARKDOWN_ATTR_LIST_PREFIX,
Expand All @@ -29,6 +30,7 @@
"MKDOCSTRINGS_CROSSREFERENCE_PREFIX",
"MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX",
"PYMD_ABBREVIATIONS_PREFIX",
"PYMD_CAPTIONS_PREFIX",
"PYMD_SNIPPET_PREFIX",
"PYTHON_MARKDOWN_ATTR_LIST_PREFIX",
"material_admon_plugin",
Expand All @@ -37,6 +39,7 @@
"mkdocstrings_crossreference_plugin",
"pymd_abbreviations_plugin",
"pymd_admon_plugin",
"pymd_captions_plugin",
"pymd_snippet_plugin",
"python_markdown_attr_list_plugin",
)
122 changes: 122 additions & 0 deletions mdformat_mkdocs/mdit_plugins/_pymd_captions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Python-Markdown Extensions Captions.

Matches:

```md
/// caption
Default values for config variables.
///
```

Docs:
https://github.com/facelessuser/pymdown-extensions/blob/main/pymdownx/blocks/caption.py


"""

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._whitespace_admon_factories import (
new_token,
)

if TYPE_CHECKING:
from markdown_it import MarkdownIt
from markdown_it.rules_block import StateBlock

_CAPTION_START_PATTERN = re.compile(
r"\s*///\s*(?P<type>figure-|table-|)caption\s*(\|\s*(?P<number>[\d\.]+))?",
)
_CAPTION_END_PATTERN = re.compile(r"^\s*///\s*$")
_CAPTION_ATTRS_PATTERN = re.compile(r"^s*(?P<attrs>attrs:\s*\{[^}]*\})\s*$")
PYMD_CAPTIONS_PREFIX = "mkdocs_caption"


def _src_in_line(state: StateBlock, line: int) -> tuple[str, int, int]:
"""Get the source in a given line number."""
start_pos = state.bMarks[line] + state.tShift[line]
end_pos = state.eMarks[line]
return state.src[start_pos:end_pos], start_pos, end_pos


def _parse(
state: StateBlock,
first_line_max_pos: int,
start_line: int,
end_line: int,
) -> tuple[int, str, str | None]:
"""Parse a caption block: optionally read attrs and extract content."""
end_match = None
max_line = start_line + 1
end_pos = -1
attrs_text, _, attrs_max_pos = _src_in_line(state, max_line)
caption_attrs_match = _CAPTION_ATTRS_PATTERN.match(attrs_text)
content_start_pos = (
first_line_max_pos + 1 if caption_attrs_match is None else attrs_max_pos + 1
)
attrs = (
caption_attrs_match.group("attrs") if caption_attrs_match is not None else None
)
if not isinstance(attrs, str):
attrs = None

while end_match is None and max_line <= end_line:
line_text, end_pos, _ = _src_in_line(state, max_line)
if _CAPTION_END_PATTERN.match(line_text) is None:
max_line += 1
else:
end_match = max_line

return max_line, state.src[content_start_pos:end_pos], attrs


def _material_captions(
state: StateBlock,
start_line: int,
end_line: int,
silent: bool,
) -> bool:
"""Detect caption blocks and wrap them in a token."""
if is_code_block(state, start_line):
return False

first_line_text, _, first_line_max_pos = _src_in_line(state, start_line)
start_match = _CAPTION_START_PATTERN.match(first_line_text)
if start_match is None:
return False

if silent:
return True

max_line, content, attrs = _parse(state, first_line_max_pos, start_line, end_line)

with (
new_token(state, PYMD_CAPTIONS_PREFIX, "figcaption") as token,
new_token(state, "", "p"),
):
token.info = start_match.group("type") + "caption"
token.meta = {"number": start_match.group("number")}
if attrs is not None:
token.meta["attrs"] = attrs
tkn_inline = state.push("inline", "", 0)
tkn_inline.content = content.strip()
tkn_inline.map = [start_line, max_line]
tkn_inline.children = []

state.line = max_line + 1

return True


def pymd_captions_plugin(md: MarkdownIt) -> None:
md.block.ruler.before(
"fence",
PYMD_CAPTIONS_PREFIX,
_material_captions,
{"alt": ["paragraph"]},
)
17 changes: 17 additions & 0 deletions mdformat_mkdocs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
MKDOCSTRINGS_CROSSREFERENCE_PREFIX,
MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX,
PYMD_ABBREVIATIONS_PREFIX,
PYMD_CAPTIONS_PREFIX,
PYMD_SNIPPET_PREFIX,
PYTHON_MARKDOWN_ATTR_LIST_PREFIX,
material_admon_plugin,
Expand All @@ -24,6 +25,7 @@
mkdocstrings_crossreference_plugin,
pymd_abbreviations_plugin,
pymd_admon_plugin,
pymd_captions_plugin,
pymd_snippet_plugin,
python_markdown_attr_list_plugin,
)
Expand Down Expand Up @@ -78,6 +80,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_captions_plugin)
mdit.use(material_content_tabs_plugin)
mdit.use(mkdocstrings_autorefs_plugin)
mdit.use(pymd_abbreviations_plugin)
Expand Down Expand Up @@ -179,6 +182,19 @@ def add_extra_admon_newline(node: RenderTreeNode, context: RenderContext) -> str
return f"{title}\n\n{''.join(content)}"


def render_pymd_caption(node: RenderTreeNode, context: RenderContext) -> str:
"""Render caption with normalized format."""
caption_type = node.info or "caption"
attrs = node.meta.get("attrs")
number = node.meta.get("number")
rendered_content = "".join(
child.render(context) for child in node.children[0].children
)
caption_number = f" | {number}" if number else ""
caption_attrs = f"\n {attrs}" if attrs else ""
return f"/// {caption_type}{caption_number}{caption_attrs}\n{rendered_content}\n///"


# A mapping from syntax tree node type to a function that renders it.
# This can be used to overwrite renderer functions of existing syntax
# or add support for new syntax.
Expand All @@ -189,6 +205,7 @@ def add_extra_admon_newline(node: RenderTreeNode, context: RenderContext) -> str
"admonition_mkdocs_title": render_admon_title,
"content_tab_mkdocs": add_extra_admon_newline,
"content_tab_mkdocs_title": render_admon_title,
PYMD_CAPTIONS_PREFIX: render_pymd_caption,
MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content,
MKDOCSTRINGS_CROSSREFERENCE_PREFIX: _render_cross_reference,
MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX: _render_heading_autoref,
Expand Down
19 changes: 19 additions & 0 deletions tests/format/fixtures/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -1735,3 +1735,22 @@ Don't wrap long URLs (fixes: https://github.com/KyleKing/mdformat-mkdocs/issues/
- a [with space](https://github.com/python/mypy/blob/a3ce6d5307e99a1b6c181eaa7c5cf134c53b7d/test-data/check-protocols)
- b
.

Format captions correctly
.
|a|b|
|-|-|
|c|d|

/// table-caption | 1.5.2
A table with letters.
///
.
|a|b|
|-|-|
|c|d|

/// table-caption | 1.5.2
A table with letters.
///
.
35 changes: 35 additions & 0 deletions tests/format/test_wrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,39 @@
{: .class1 .class2 .class3 .class4 .class5 .class6 .class7 .class8 .class9 .class10 .class11 .class12 .class13 .class14 .class15 .class16 .class17 .class18 .class19 .class20 }
"""

CASE_CAPTION_WRAP = """
This line is longer than 40 characters and should be wrapped.

```
def gcd(a, b):
if a == 0: return b
elif b == 0: return a
if a > b: return gcd(a % b, b)
else: return gcd(a, b % a)
```

/// caption
Greatest common divisor algorithm.
///
"""

CASE_CAPTION_WRAP_TRUE_40 = """
This line is longer than 40 characters
and should be wrapped.

```
def gcd(a, b):
if a == 0: return b
elif b == 0: return a
if a > b: return gcd(a % b, b)
else: return gcd(a, b % a)
```

/// caption
Greatest common divisor algorithm.
///
"""


@pytest.mark.parametrize(
("text", "expected", "align_lists", "wrap"),
Expand All @@ -200,6 +233,7 @@
(WITH_CODE, WITH_CODE_TRUE_80, True, 80),
(WITH_ATTR_LIST, WITH_ATTR_LIST_TRUE_80, True, 80),
(CASE_ATTR_LIST_WRAP, CASE_ATTR_LIST_WRAP_TRUE_80, True, 80),
(CASE_CAPTION_WRAP, CASE_CAPTION_WRAP_TRUE_40, True, 40),
],
ids=[
"CASE_1_FALSE_40",
Expand All @@ -210,6 +244,7 @@
"WITH_CODE_TRUE_80",
"WITH_ATTR_LIST_TRUE_80",
"CASE_ATTR_LIST_WRAP_TRUE_80",
"CASE_CAPTION_WRAP_TRUE_40",
],
)
def test_wrap(text: str, expected: str, align_lists: bool, wrap: int):
Expand Down
23 changes: 23 additions & 0 deletions tests/render/fixtures/pymd_captions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pymdown captions (https://github.com/KyleKing/mdformat-mkdocs/issues/51)
.
# Captions

Captioned content.

/// caption
First caption.
///

/// caption
Second caption.
///
.
<h1>Captions</h1>
<p>Captioned content.</p>
<figcaption>
<p>First caption.</p>
</figcaption>
<figcaption>
<p>Second caption.</p>
</figcaption>
.
2 changes: 2 additions & 0 deletions tests/render/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
mkdocstrings_autorefs_plugin,
mkdocstrings_crossreference_plugin,
pymd_abbreviations_plugin,
pymd_captions_plugin,
pymd_snippet_plugin,
python_markdown_attr_list_plugin,
)
Expand All @@ -32,6 +33,7 @@ def with_plugin(filename, plugins):
),
*with_plugin("mkdocstrings_autorefs.md", [mkdocstrings_autorefs_plugin]),
*with_plugin("pymd_abbreviations.md", [pymd_abbreviations_plugin]),
*with_plugin("pymd_captions.md", [pymd_captions_plugin]),
*with_plugin(
"mkdocstrings_crossreference.md",
[mkdocstrings_crossreference_plugin],
Expand Down
Loading