Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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