mdformat-mkdocs 5.1.4__tar.gz → 5.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/PKG-INFO +21 -1
  2. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/README.md +19 -0
  3. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/__init__.py +2 -1
  4. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/_helpers.py +1 -1
  5. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/_normalize_list.py +77 -17
  6. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/_synced/admon_factories/_whitespace_admon_factories.py +4 -2
  7. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/__init__.py +11 -0
  8. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_material_deflist.py +0 -1
  9. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_mkdocstrings_autorefs.py +1 -0
  10. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_mkdocstrings_crossreference.py +1 -0
  11. mdformat_mkdocs-5.2.0/mdformat_mkdocs/mdit_plugins/_mkdocstrings_injection.py +78 -0
  12. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_python_markdown_attr_list.py +6 -5
  13. mdformat_mkdocs-5.2.0/mdformat_mkdocs/mdit_plugins/_spaced_url_link.py +78 -0
  14. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/plugin.py +116 -40
  15. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/pyproject.toml +10 -2
  16. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/LICENSE +0 -0
  17. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/_postprocess_inline.py +0 -0
  18. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/_synced/__init__.py +0 -0
  19. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/_synced/admon_factories/README.md +0 -0
  20. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/_synced/admon_factories/__init__.py +0 -0
  21. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_material_admon.py +0 -0
  22. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_material_content_tabs.py +0 -0
  23. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_pymd_abbreviations.py +0 -0
  24. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_pymd_admon.py +0 -0
  25. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_pymd_arithmatex.py +0 -0
  26. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_pymd_captions.py +0 -0
  27. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/mdit_plugins/_pymd_snippet.py +0 -0
  28. {mdformat_mkdocs-5.1.4 → mdformat_mkdocs-5.2.0}/mdformat_mkdocs/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdformat_mkdocs
3
- Version: 5.1.4
3
+ Version: 5.2.0
4
4
  Summary: An mdformat plugin for mkdocs and Material for MkDocs
5
5
  Keywords: markdown,markdown-it,mdformat,mdformat_plugin_template
6
6
  Author: kyleking
@@ -32,6 +32,7 @@ Requires-Dist: mdformat-hooks>=0.1.0 ; extra == 'recommended-mdsf'
32
32
  Requires-Dist: mdformat-simple-breaks>=0.0.1 ; extra == 'recommended-mdsf'
33
33
  Requires-Dist: mdformat-wikilink>=0.2.0 ; extra == 'recommended-mdsf'
34
34
  Requires-Dist: beartype>=0.21.0 ; extra == 'test'
35
+ Requires-Dist: hypothesis>=6.100.0 ; extra == 'test'
35
36
  Requires-Dist: pytest>=9.0.1 ; extra == 'test'
36
37
  Requires-Dist: pytest-beartype>=0.2.0 ; extra == 'test'
37
38
  Requires-Dist: pytest-cov>=7.0.0 ; extra == 'test'
@@ -62,6 +63,8 @@ Supports:
62
63
  - [MkDocs-Material Content Tabs\*](https://squidfunk.github.io/mkdocs-material/reference/content-tabs)
63
64
  - \*Note: the markup (HTML) rendered by this plugin is sufficient for formatting but not for viewing in a browser. Please open an issue if you have a need to generate valid HTML.
64
65
  - [MkDocs-Material Definition Lists](https://squidfunk.github.io/mkdocs-material/reference/lists/#using-definition-lists)
66
+ - [mkdocstrings Injection Blocks](https://mkdocstrings.github.io/usage/)
67
+ - Preserves `:::` identifier blocks and their indented YAML options verbatim, including when nested inside lists with `--align-semantic-breaks-in-lists`
65
68
  - [mkdocstrings Anchors (autorefs)](https://mkdocstrings.github.io/autorefs/#markdown-anchors)
66
69
  - [mkdocstrings Cross-References](https://mkdocstrings.github.io/usage/#cross-references)
67
70
  - [Python Markdown "Abbreviations"\*](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations)
@@ -77,6 +80,23 @@ Supports:
77
80
  - [Python Markdown "Snippets"\*](https://facelessuser.github.io/pymdown-extensions/extensions/snippets)
78
81
  - \*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
79
82
 
83
+ ### Features with Implicit Support
84
+
85
+ The following MkDocs/Material/PyMdown syntax passes through mdformat-mkdocs unchanged — it is not modified or corrupted, but it is also not actively normalized:
86
+
87
+ - [PyMdown Keys](https://facelessuser.github.io/pymdown-extensions/extensions/keys/) — `++ctrl+alt+del++` (not a markdown construct, preserved as-is)
88
+ - [PyMdown Critic Markup](https://facelessuser.github.io/pymdown-extensions/extensions/critic/) — `{--deleted--}`, `{++added++}`, `{~~old~>new~~}`, `{==highlight==}`, `{>>comment<<}`
89
+ - [PyMdown Highlight](https://facelessuser.github.io/pymdown-extensions/extensions/mark/) — `==marked text==`
90
+ - [PyMdown Caret / Tilde](https://facelessuser.github.io/pymdown-extensions/extensions/caret/) — `H^2^O`, `CH~3~OH`
91
+ - [PyMdown Emoji](https://facelessuser.github.io/pymdown-extensions/extensions/emoji/) — `:smile:`, `:material-icon:`
92
+ - [PyMdown InlineHilite](https://facelessuser.github.io/pymdown-extensions/extensions/inlinehilite/) — language hints inside backtick spans (`:::python code`) are never modified
93
+ - [PyMdown SmartSymbols](https://facelessuser.github.io/pymdown-extensions/extensions/smartsymbols/) — `(c)`, `(tm)`, `--`, `-->` are plain ASCII in source and not touched
94
+ - [PyMdown MagicLink](https://facelessuser.github.io/pymdown-extensions/extensions/magiclink/) — `@username`, `#123` are plain text and pass through
95
+ - [Material Grids](https://squidfunk.github.io/mkdocs-material/reference/grids/) — `<div class="grid cards" markdown>` is an HTML block; content is preserved but markdown inside is not reformatted
96
+ - [Mermaid / Superfences](https://squidfunk.github.io/mkdocs-material/reference/diagrams/) — diagram code inside fenced blocks is never modified
97
+
98
+ **Note on [PyMdown ProgressBar](https://facelessuser.github.io/pymdown-extensions/extensions/progressbar/)**: The syntax `[=50% "50%"]` resembles an undefined link reference and will be escaped to `\[=50% "50%"\]` by default. Use `--ignore-missing-references` to preserve it, or avoid this extension if you use mdformat without that flag.
99
+
80
100
  See the example test files, [./tests/pre-commit-test.md](https://raw.githubusercontent.com/KyleKing/mdformat-mkdocs/main/tests/pre-commit-test.md) and [./tests/format/fixtures.md](https://raw.githubusercontent.com/KyleKing/mdformat-mkdocs/main/tests/format/fixtures.md)
81
101
 
82
102
  ## `mdformat` Usage
@@ -15,6 +15,8 @@ Supports:
15
15
  - [MkDocs-Material Content Tabs\*](https://squidfunk.github.io/mkdocs-material/reference/content-tabs)
16
16
  - \*Note: the markup (HTML) rendered by this plugin is sufficient for formatting but not for viewing in a browser. Please open an issue if you have a need to generate valid HTML.
17
17
  - [MkDocs-Material Definition Lists](https://squidfunk.github.io/mkdocs-material/reference/lists/#using-definition-lists)
18
+ - [mkdocstrings Injection Blocks](https://mkdocstrings.github.io/usage/)
19
+ - Preserves `:::` identifier blocks and their indented YAML options verbatim, including when nested inside lists with `--align-semantic-breaks-in-lists`
18
20
  - [mkdocstrings Anchors (autorefs)](https://mkdocstrings.github.io/autorefs/#markdown-anchors)
19
21
  - [mkdocstrings Cross-References](https://mkdocstrings.github.io/usage/#cross-references)
20
22
  - [Python Markdown "Abbreviations"\*](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations)
@@ -30,6 +32,23 @@ Supports:
30
32
  - [Python Markdown "Snippets"\*](https://facelessuser.github.io/pymdown-extensions/extensions/snippets)
31
33
  - \*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
32
34
 
35
+ ### Features with Implicit Support
36
+
37
+ The following MkDocs/Material/PyMdown syntax passes through mdformat-mkdocs unchanged — it is not modified or corrupted, but it is also not actively normalized:
38
+
39
+ - [PyMdown Keys](https://facelessuser.github.io/pymdown-extensions/extensions/keys/) — `++ctrl+alt+del++` (not a markdown construct, preserved as-is)
40
+ - [PyMdown Critic Markup](https://facelessuser.github.io/pymdown-extensions/extensions/critic/) — `{--deleted--}`, `{++added++}`, `{~~old~>new~~}`, `{==highlight==}`, `{>>comment<<}`
41
+ - [PyMdown Highlight](https://facelessuser.github.io/pymdown-extensions/extensions/mark/) — `==marked text==`
42
+ - [PyMdown Caret / Tilde](https://facelessuser.github.io/pymdown-extensions/extensions/caret/) — `H^2^O`, `CH~3~OH`
43
+ - [PyMdown Emoji](https://facelessuser.github.io/pymdown-extensions/extensions/emoji/) — `:smile:`, `:material-icon:`
44
+ - [PyMdown InlineHilite](https://facelessuser.github.io/pymdown-extensions/extensions/inlinehilite/) — language hints inside backtick spans (`:::python code`) are never modified
45
+ - [PyMdown SmartSymbols](https://facelessuser.github.io/pymdown-extensions/extensions/smartsymbols/) — `(c)`, `(tm)`, `--`, `-->` are plain ASCII in source and not touched
46
+ - [PyMdown MagicLink](https://facelessuser.github.io/pymdown-extensions/extensions/magiclink/) — `@username`, `#123` are plain text and pass through
47
+ - [Material Grids](https://squidfunk.github.io/mkdocs-material/reference/grids/) — `<div class="grid cards" markdown>` is an HTML block; content is preserved but markdown inside is not reformatted
48
+ - [Mermaid / Superfences](https://squidfunk.github.io/mkdocs-material/reference/diagrams/) — diagram code inside fenced blocks is never modified
49
+
50
+ **Note on [PyMdown ProgressBar](https://facelessuser.github.io/pymdown-extensions/extensions/progressbar/)**: The syntax `[=50% "50%"]` resembles an undefined link reference and will be escaped to `\[=50% "50%"\]` by default. Use `--ignore-missing-references` to preserve it, or avoid this extension if you use mdformat without that flag.
51
+
33
52
  See the example test files, [./tests/pre-commit-test.md](https://raw.githubusercontent.com/KyleKing/mdformat-mkdocs/main/tests/pre-commit-test.md) and [./tests/format/fixtures.md](https://raw.githubusercontent.com/KyleKing/mdformat-mkdocs/main/tests/format/fixtures.md)
34
53
 
35
54
  ## `mdformat` Usage
@@ -1,6 +1,7 @@
1
+ # ruff: noqa: RUF067
1
2
  """An mdformat plugin for `mkdocs`."""
2
3
 
3
- __version__ = "5.1.4"
4
+ __version__ = "5.2.0"
4
5
 
5
6
  __plugin_name__ = "mkdocs"
6
7
 
@@ -15,7 +15,7 @@ EOL = "\n"
15
15
  MKDOCS_INDENT_COUNT = 4
16
16
  """Use 4-spaces for mkdocs."""
17
17
 
18
- FILLER_CHAR = "𝕏" # noqa: RUF001
18
+ FILLER_CHAR = ""
19
19
  """A spacer that is inserted and then removed to ensure proper word wrap."""
20
20
 
21
21
 
@@ -19,7 +19,11 @@ from ._helpers import (
19
19
  rstrip_result,
20
20
  separate_indent,
21
21
  )
22
- from .mdit_plugins import MATERIAL_ADMON_MARKERS, MATERIAL_CONTENT_TAB_MARKERS
22
+ from .mdit_plugins import (
23
+ INJECTION_PATTERN,
24
+ MATERIAL_ADMON_MARKERS,
25
+ MATERIAL_CONTENT_TAB_MARKERS,
26
+ )
23
27
 
24
28
  if TYPE_CHECKING:
25
29
  from collections.abc import Mapping
@@ -71,6 +75,7 @@ class Syntax(Enum):
71
75
  START_MARKED = "START_MARKED"
72
76
  EDGE_CODE = "EDGE_CODE"
73
77
  HTML = "HTML"
78
+ INJECTION = "INJECTION"
74
79
 
75
80
  @classmethod
76
81
  def from_content(cls, content: str) -> Syntax | None:
@@ -85,18 +90,25 @@ class Syntax(Enum):
85
90
  if match["item"].startswith("```"):
86
91
  return cls.CODE_NUMBERED if is_numbered else cls.CODE_BULLETED
87
92
  return cls.LIST_NUMBERED if is_numbered else cls.LIST_BULLETED
93
+
94
+ result = None
88
95
  if any(content.startswith(f"{marker} ") for marker in MARKERS):
89
- return cls.START_MARKED
90
- if content.startswith("```"):
91
- return cls.EDGE_CODE
92
- if content.startswith("<"):
93
- return cls.HTML
94
- return None
96
+ result = cls.START_MARKED
97
+ elif content.startswith("```"):
98
+ result = cls.EDGE_CODE
99
+ elif content.startswith("<"):
100
+ result = cls.HTML
101
+ elif INJECTION_PATTERN.match(content):
102
+ result = cls.INJECTION
103
+ return result
95
104
 
96
105
 
97
106
  SYNTAX_CODE_LIST = {Syntax.CODE_BULLETED, Syntax.CODE_NUMBERED}
98
107
  """The start of a code block, which is also the start of a list."""
99
108
 
109
+ SYNTAX_LIST = {Syntax.LIST_BULLETED, Syntax.LIST_NUMBERED, *SYNTAX_CODE_LIST}
110
+ """Any line that begins a list item."""
111
+
100
112
 
101
113
  class ParsedLine(NamedTuple):
102
114
  """Parsed Line of text."""
@@ -212,7 +224,7 @@ class BlockIndent(NamedTuple):
212
224
  start_line: int
213
225
  raw_indent: str
214
226
  indent_depth: int
215
- kind: Literal["code", "HTML"]
227
+ kind: Literal["code", "HTML", "injection"]
216
228
 
217
229
 
218
230
  def _parse_code_block(last: BlockIndent | None, line: LineResult) -> BlockIndent | None:
@@ -251,9 +263,44 @@ def _parse_html_line(last: BlockIndent | None, line: LineResult) -> BlockIndent
251
263
  elif last and not line.parsed.content:
252
264
  # Stop tracking an HTML block on a line break
253
265
  result = None
266
+ elif (
267
+ last
268
+ and line.parsed.syntax in SYNTAX_LIST
269
+ and len(line.parsed.indent) < len(last.raw_indent)
270
+ ):
271
+ # A list item that dedents below the block is a sibling, not HTML
272
+ # content. This guards against an inline autolink (or comment) on a
273
+ # continuation line being mistaken for a block that swallows siblings.
274
+ result = None
254
275
  return result
255
276
 
256
277
 
278
+ def _parse_injection_block(
279
+ last: BlockIndent | None,
280
+ line: LineResult,
281
+ ) -> BlockIndent | None:
282
+ """Identify mkdocstrings injection sections."""
283
+ if last is not None:
284
+ if line.parsed.content and len(line.parsed.indent) <= len(last.raw_indent):
285
+ if line.parsed.syntax == Syntax.INJECTION:
286
+ return BlockIndent(
287
+ start_line=line.parsed.line_num,
288
+ raw_indent=line.parsed.indent,
289
+ indent_depth=len(line.parents),
290
+ kind="injection",
291
+ )
292
+ return None
293
+ return last
294
+ if line.parsed.syntax == Syntax.INJECTION:
295
+ return BlockIndent(
296
+ start_line=line.parsed.line_num,
297
+ raw_indent=line.parsed.indent,
298
+ indent_depth=len(line.parents),
299
+ kind="injection",
300
+ )
301
+ return None
302
+
303
+
257
304
  # ======================================================================================
258
305
  # Semantic Indent Handling
259
306
 
@@ -274,12 +321,11 @@ def _parse_semantic_indent(
274
321
  tin: tuple[LineResult, BlockIndent | None],
275
322
  ) -> SemanticIndent:
276
323
  """Conditionally evaluate when semantic indents are necessary."""
277
- # PLANNED: This works, but is very confusing
278
- line, code_indent = tin
324
+ line, block_indent = tin
279
325
 
280
326
  if (
281
327
  not line.parsed.content
282
- or code_indent is not None
328
+ or block_indent is not None
283
329
  or line.parsed.syntax in SYNTAX_CODE_LIST
284
330
  ):
285
331
  result = SemanticIndent.EMPTY
@@ -420,22 +466,36 @@ def parse_text(
420
466
  indent if (indent and code_indents[indent.start_line] is None) else None
421
467
  for indent in map_lookback(_parse_html_line, lines, None)
422
468
  ]
423
- # When both, code_indents take precedence
469
+ injection_indents = [
470
+ # Any indents initiated from within a `code_block_indents` should be ignored
471
+ indent if (indent and code_indents[indent.start_line] is None) else None
472
+ for indent in map_lookback(_parse_injection_block, lines, None)
473
+ ]
474
+ # When multiple match, code_indents take precedence, then html_indents
424
475
  block_indents = [
425
- c_ or h_ for c_, h_ in zip(code_indents, html_indents, strict=True)
476
+ c_ or h_ or i_
477
+ for c_, h_, i_ in zip(
478
+ code_indents, html_indents, injection_indents, strict=True
479
+ )
426
480
  ]
427
481
  new_indents = [*starmap(_format_new_indent, zip(lines, block_indents, strict=True))]
428
482
 
429
483
  new_contents = [
430
- _format_new_content(line, inc_numbers, ci is not None)
431
- for line, ci in zip(lines, code_indents, strict=True)
484
+ _format_new_content(
485
+ line,
486
+ inc_numbers,
487
+ block_indent is not None and block_indent.kind in {"code", "injection"},
488
+ )
489
+ for line, block_indent in zip(lines, block_indents, strict=True)
432
490
  ]
433
491
 
434
492
  if use_sem_break:
435
493
  semantic_indents = map_lookback(
436
494
  _parse_semantic_indent,
437
- [*zip(lines, code_indents, strict=True)],
438
- _parse_semantic_indent(SemanticIndent.INITIAL, (lines[0], code_indents[0])),
495
+ [*zip(lines, block_indents, strict=True)],
496
+ _parse_semantic_indent(
497
+ SemanticIndent.INITIAL, (lines[0], block_indents[0])
498
+ ),
439
499
  )
440
500
  new_indents = [
441
501
  _trim_semantic_indent(indent, s_i, in_defbody)
@@ -189,8 +189,10 @@ def new_token(
189
189
  kind: str,
190
190
  ) -> Generator[Token, None, None]:
191
191
  """Create scoped token."""
192
- yield state.push(f"{name}_open", kind, 1)
193
- state.push(f"{name}_close", kind, -1)
192
+ try:
193
+ yield state.push(f"{name}_open", kind, 1)
194
+ finally:
195
+ state.push(f"{name}_close", kind, -1)
194
196
 
195
197
 
196
198
  def default_render(
@@ -21,6 +21,11 @@ from ._mkdocstrings_crossreference import (
21
21
  MKDOCSTRINGS_CROSSREFERENCE_PREFIX,
22
22
  mkdocstrings_crossreference_plugin,
23
23
  )
24
+ from ._mkdocstrings_injection import (
25
+ INJECTION_PATTERN,
26
+ MKDOCSTRINGS_INJECTION_PREFIX,
27
+ mkdocstrings_injection_plugin,
28
+ )
24
29
  from ._pymd_abbreviations import PYMD_ABBREVIATIONS_PREFIX, pymd_abbreviations_plugin
25
30
  from ._pymd_admon import pymd_admon_plugin
26
31
  from ._pymd_arithmatex import (
@@ -37,21 +42,25 @@ from ._python_markdown_attr_list import (
37
42
  PYTHON_MARKDOWN_ATTR_LIST_PREFIX,
38
43
  python_markdown_attr_list_plugin,
39
44
  )
45
+ from ._spaced_url_link import SPACED_URL_LINK_PREFIX, spaced_url_link_plugin
40
46
 
41
47
  __all__ = (
42
48
  "AMSMATH_BLOCK",
43
49
  "DOLLARMATH_BLOCK",
44
50
  "DOLLARMATH_BLOCK_LABEL",
45
51
  "DOLLARMATH_INLINE",
52
+ "INJECTION_PATTERN",
46
53
  "MATERIAL_ADMON_MARKERS",
47
54
  "MATERIAL_CONTENT_TAB_MARKERS",
48
55
  "MKDOCSTRINGS_AUTOREFS_PREFIX",
49
56
  "MKDOCSTRINGS_CROSSREFERENCE_PREFIX",
50
57
  "MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX",
58
+ "MKDOCSTRINGS_INJECTION_PREFIX",
51
59
  "PYMD_ABBREVIATIONS_PREFIX",
52
60
  "PYMD_CAPTIONS_PREFIX",
53
61
  "PYMD_SNIPPET_PREFIX",
54
62
  "PYTHON_MARKDOWN_ATTR_LIST_PREFIX",
63
+ "SPACED_URL_LINK_PREFIX",
55
64
  "TEXMATH_BLOCK_EQNO",
56
65
  "escape_deflist",
57
66
  "material_admon_plugin",
@@ -59,6 +68,7 @@ __all__ = (
59
68
  "material_deflist_plugin",
60
69
  "mkdocstrings_autorefs_plugin",
61
70
  "mkdocstrings_crossreference_plugin",
71
+ "mkdocstrings_injection_plugin",
62
72
  "pymd_abbreviations_plugin",
63
73
  "pymd_admon_plugin",
64
74
  "pymd_arithmatex_plugin",
@@ -68,4 +78,5 @@ __all__ = (
68
78
  "render_material_definition_body",
69
79
  "render_material_definition_list",
70
80
  "render_material_definition_term",
81
+ "spaced_url_link_plugin",
71
82
  )
@@ -35,7 +35,6 @@ from markdown_it import MarkdownIt
35
35
  from mdit_py_plugins.deflist import deflist_plugin
36
36
 
37
37
  if TYPE_CHECKING:
38
- from markdown_it import MarkdownIt
39
38
  from mdformat.renderer import RenderContext, RenderTreeNode
40
39
  from mdformat.renderer.typing import Render
41
40
 
@@ -35,6 +35,7 @@ def _mkdocstrings_autorefs_plugin(state: StateInline, silent: bool) -> bool:
35
35
  return False
36
36
 
37
37
  if silent:
38
+ state.pos += match.end() # skipToken only auto-advances when ok=False
38
39
  return True
39
40
 
40
41
  anchor = match["anchor"]
@@ -28,6 +28,7 @@ def _mkdocstrings_crossreference(state: StateInline, silent: bool) -> bool:
28
28
  return False
29
29
 
30
30
  if silent:
31
+ state.pos += match.end() # skipToken only auto-advances when ok=False
31
32
  return True
32
33
 
33
34
  original_pos = state.pos
@@ -0,0 +1,78 @@
1
+ """mkdocstrings injection blocks.
2
+
3
+ Matches:
4
+
5
+ ```md
6
+ ::: package.module.Class
7
+ options:
8
+ heading_level: 2
9
+ ```
10
+
11
+ Docs: https://mkdocstrings.github.io/usage/
12
+
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from typing import TYPE_CHECKING
19
+
20
+ from mdit_py_plugins.utils import is_code_block
21
+
22
+ if TYPE_CHECKING:
23
+ from markdown_it import MarkdownIt
24
+ from markdown_it.rules_block import StateBlock
25
+
26
+ INJECTION_PATTERN = re.compile(r"^:::\s+\S")
27
+ MKDOCSTRINGS_INJECTION_PREFIX = "mkdocstrings_injection"
28
+
29
+
30
+ def _get_line(state: StateBlock, line: int, base_indent: int) -> str:
31
+ return state.src[state.bMarks[line] + base_indent : state.eMarks[line]]
32
+
33
+
34
+ def _mkdocstrings_injection(
35
+ state: StateBlock,
36
+ start_line: int,
37
+ end_line: int,
38
+ silent: bool,
39
+ ) -> bool:
40
+ if is_code_block(state, start_line):
41
+ return False
42
+
43
+ base_indent = state.tShift[start_line]
44
+ header = _get_line(state, start_line, base_indent)
45
+ if not INJECTION_PATTERN.match(header):
46
+ return False
47
+
48
+ if silent:
49
+ return True
50
+
51
+ lines = [header]
52
+ next_line = start_line + 1
53
+ while next_line < end_line:
54
+ if not state.isEmpty(next_line) and state.tShift[next_line] <= base_indent:
55
+ break
56
+ lines.append(_get_line(state, next_line, base_indent))
57
+ next_line += 1
58
+
59
+ while len(lines) > 1 and not lines[-1].strip():
60
+ lines.pop()
61
+ next_line -= 1
62
+
63
+ token = state.push(MKDOCSTRINGS_INJECTION_PREFIX, "", 0)
64
+ token.content = "\n".join(lines)
65
+ token.block = True
66
+ token.map = [start_line, next_line]
67
+
68
+ state.line = next_line
69
+ return True
70
+
71
+
72
+ def mkdocstrings_injection_plugin(md: MarkdownIt) -> None:
73
+ md.block.ruler.before(
74
+ "paragraph",
75
+ MKDOCSTRINGS_INJECTION_PREFIX,
76
+ _mkdocstrings_injection,
77
+ {"alt": ["paragraph"]},
78
+ )
@@ -17,7 +17,6 @@ from markdown_it import MarkdownIt
17
17
  from mdformat_mkdocs._synced.admon_factories import new_token
18
18
 
19
19
  if TYPE_CHECKING:
20
- from markdown_it import MarkdownIt
21
20
  from markdown_it.rules_inline import StateInline
22
21
 
23
22
  _ATTR_LIST_PATTERN = re.compile(r"{:? (?P<attrs>[^}]+) }")
@@ -38,20 +37,22 @@ def _python_markdown_attr_list(state: StateInline, silent: bool) -> bool:
38
37
  if state.linkLevel > 0:
39
38
  return False
40
39
 
41
- # Look backwards for unclosed '['
42
- search_start = max(0, state.pos - 100) # Limit backwards search
43
- text_before = state.src[search_start:state.pos]
40
+ # Look backwards for unclosed '[' — no length cap; any fixed cap fails for long URLs.
41
+ text_before = state.src[: state.pos]
44
42
  open_brackets = text_before.count("[") - text_before.count("]")
45
43
  if open_brackets > 0:
46
44
  # We might be inside a link, check if there's '](' after our match
47
45
  match_end_pos = state.pos + match.end()
48
46
  if match_end_pos < len(state.src):
49
- lookahead = state.src[match_end_pos:min(match_end_pos + 100, len(state.src))]
47
+ lookahead = state.src[
48
+ match_end_pos : min(match_end_pos + 100, len(state.src))
49
+ ]
50
50
  if "](" in lookahead:
51
51
  # Very likely inside link text, don't match
52
52
  return False
53
53
 
54
54
  if silent:
55
+ state.pos += match.end() # skipToken only auto-advances when ok=False
55
56
  return True
56
57
 
57
58
  original_pos = state.pos
@@ -0,0 +1,78 @@
1
+ """Inline rule: parse ``[text](url with spaces)`` as a link.
2
+
3
+ CommonMark requires angle brackets for link destinations containing spaces.
4
+ Python-Markdown/MkDocs users write these without angle brackets. Without
5
+ this rule, markdown-it treats the whole construct as plain text, and
6
+ mdformat's HTML stability check fires.
7
+
8
+ This rule fires BEFORE markdown-it's built-in link rule. When it detects a
9
+ URL with at least one literal space (and no angle brackets), it emits proper
10
+ link tokens with the space percent-encoded in the href, matching the HTML
11
+ that mdformat outputs for the corrected form ``[text](<url with spaces>)``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING
17
+
18
+ from markdown_it import MarkdownIt
19
+
20
+ if TYPE_CHECKING:
21
+ from markdown_it.rules_inline import StateInline
22
+
23
+ SPACED_URL_LINK_PREFIX = "spaced_url_link"
24
+
25
+
26
+ def _spaced_url_link(state: StateInline, silent: bool) -> bool:
27
+ src = state.src
28
+ pos = state.pos
29
+
30
+ if src[pos] != "[":
31
+ return False
32
+
33
+ bracket_end = src.find("]", pos + 1)
34
+ if bracket_end == -1 or bracket_end + 1 >= len(src) or src[bracket_end + 1] != "(":
35
+ return False
36
+
37
+ paren_start = bracket_end + 2
38
+ paren_end = src.find(")", paren_start)
39
+ if paren_end == -1:
40
+ return False
41
+
42
+ url = src[paren_start:paren_end]
43
+ if " " not in url or url.startswith("<"):
44
+ return False
45
+
46
+ first_space = url.index(" ")
47
+ char_after_space = url[first_space + 1] if first_space + 1 < len(url) else ""
48
+ if char_after_space in {'"', "'", "("}:
49
+ return False
50
+
51
+ if not silent:
52
+ old_pos_max = state.posMax
53
+
54
+ token = state.push("link_open", "a", 1)
55
+ token.attrs = {"href": url.replace(" ", "%20")}
56
+ token.markup = ""
57
+ token.info = ""
58
+
59
+ state.pos = pos + 1
60
+ state.posMax = bracket_end
61
+ state.linkLevel += 1
62
+ state.md.inline.tokenize(state)
63
+ state.linkLevel -= 1
64
+ state.posMax = old_pos_max
65
+
66
+ state.push("link_close", "a", -1)
67
+
68
+ state.pos = paren_end + 1
69
+ return True
70
+
71
+
72
+ def spaced_url_link_plugin(md: MarkdownIt) -> None:
73
+ """Register the spaced-URL inline rule before the built-in link rule."""
74
+ md.inline.ruler.before(
75
+ "link",
76
+ SPACED_URL_LINK_PREFIX,
77
+ _spaced_url_link,
78
+ )
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import textwrap
6
7
  from functools import partial
7
8
  from typing import TYPE_CHECKING
@@ -10,7 +11,7 @@ from mdformat.renderer import DEFAULT_RENDERERS, RenderContext, RenderTreeNode
10
11
 
11
12
  from ._helpers import ContextOptions, get_conf
12
13
  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
14
15
  from .mdit_plugins import (
15
16
  AMSMATH_BLOCK,
16
17
  DOLLARMATH_BLOCK,
@@ -19,6 +20,7 @@ from .mdit_plugins import (
19
20
  MKDOCSTRINGS_AUTOREFS_PREFIX,
20
21
  MKDOCSTRINGS_CROSSREFERENCE_PREFIX,
21
22
  MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX,
23
+ MKDOCSTRINGS_INJECTION_PREFIX,
22
24
  PYMD_ABBREVIATIONS_PREFIX,
23
25
  PYMD_CAPTIONS_PREFIX,
24
26
  PYMD_SNIPPET_PREFIX,
@@ -30,6 +32,7 @@ from .mdit_plugins import (
30
32
  material_deflist_plugin,
31
33
  mkdocstrings_autorefs_plugin,
32
34
  mkdocstrings_crossreference_plugin,
35
+ mkdocstrings_injection_plugin,
33
36
  pymd_abbreviations_plugin,
34
37
  pymd_admon_plugin,
35
38
  pymd_arithmatex_plugin,
@@ -39,6 +42,7 @@ from .mdit_plugins import (
39
42
  render_material_definition_body,
40
43
  render_material_definition_list,
41
44
  render_material_definition_term,
45
+ spaced_url_link_plugin,
42
46
  )
43
47
 
44
48
  if TYPE_CHECKING:
@@ -108,10 +112,12 @@ def update_mdit(mdit: MarkdownIt) -> None:
108
112
  mdit.use(material_content_tabs_plugin)
109
113
  mdit.use(material_deflist_plugin)
110
114
  mdit.use(mkdocstrings_autorefs_plugin)
115
+ mdit.use(mkdocstrings_injection_plugin)
111
116
  mdit.use(pymd_abbreviations_plugin)
112
117
  mdit.use(pymd_admon_plugin)
113
118
  mdit.use(pymd_snippet_plugin)
114
119
  mdit.use(python_markdown_attr_list_plugin)
120
+ mdit.use(spaced_url_link_plugin)
115
121
 
116
122
  if cli_is_ignore_missing_references(mdit.options):
117
123
  mdit.use(mkdocstrings_crossreference_plugin)
@@ -134,29 +140,42 @@ def _render_math_inline(node: RenderTreeNode, context: RenderContext) -> str: #
134
140
  return f"${content}$"
135
141
 
136
142
 
143
+ def _strip_blockquote_markers(content: str) -> str:
144
+ """Strip blockquote markers from math block content.
145
+
146
+ markdown-it includes "> " prefixes when block math appears inside blockquotes.
147
+ """
148
+ lines = content.split("\n")
149
+ return "\n".join(
150
+ line.removeprefix("> ") if line.startswith("> ") else line for line in lines
151
+ ).strip()
152
+
153
+
137
154
  def _render_math_block(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
138
155
  """Render block math with original delimiters."""
139
156
  markup = node.markup
140
- content = node.content
157
+ cleaned_content = _strip_blockquote_markers(node.content)
158
+
141
159
  if markup == "$$":
142
- return f"$$\n{content.strip()}\n$$"
160
+ return f"$$\n{cleaned_content}\n$$"
143
161
  if markup == "\\[":
144
- return f"\\[\n{content.strip()}\n\\]"
162
+ return f"\\[\n{cleaned_content}\n\\]"
145
163
  # Fallback
146
- return f"$$\n{content.strip()}\n$$"
164
+ return f"$$\n{cleaned_content}\n$$"
147
165
 
148
166
 
149
167
  def _render_math_block_eqno(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
150
168
  """Render block math with equation label."""
151
169
  markup = node.markup
152
- content = node.content
153
- label = node.info # Label is stored in info field
170
+ label = node.info
171
+ cleaned_content = _strip_blockquote_markers(node.content)
172
+
154
173
  if markup == "$$":
155
- return f"$$\n{content.strip()}\n$$ ({label})"
174
+ return f"$$\n{cleaned_content}\n$$ ({label})"
156
175
  if markup == "\\[":
157
- return f"\\[\n{content.strip()}\n\\] ({label})"
176
+ return f"\\[\n{cleaned_content}\n\\] ({label})"
158
177
  # Fallback
159
- return f"$$\n{content.strip()}\n$$ ({label})"
178
+ return f"$$\n{cleaned_content}\n$$ ({label})"
160
179
 
161
180
 
162
181
  def _render_amsmath(node: RenderTreeNode, context: RenderContext) -> str: # noqa: ARG001
@@ -176,40 +195,82 @@ def _render_inline_content(node: RenderTreeNode, context: RenderContext) -> str:
176
195
  return inline.content
177
196
 
178
197
 
179
- def _render_code_inline(node: RenderTreeNode, context: RenderContext) -> str:
180
- r"""Render inline code, cleaning up whitespace from newline normalization.
198
+ _ESCAPED_LINK_SPACED_URL = re.compile(r"\\\[([^\]]*)\\\]\(([^)]*[ ][^)]*)\)")
199
+ _PERCENT_ENCODED_URL_LINK = re.compile(r"\[([^\]]*)\]\(([^)]*%20[^)]*)\)")
200
+ _ANGLE_BRACKET_SPACED_URL_SOURCE = re.compile(r"\]\(<([^>]* [^>]*)>\)")
201
+ _UNBRACKETED_SPACED_URL_SOURCE = re.compile(r"\]\(([^<>()]*\s[^<>()]*)\)")
202
+
203
+
204
+ def _fix_links_with_spaced_urls(
205
+ text: str,
206
+ node: RenderTreeNode,
207
+ context: RenderContext, # noqa: ARG001
208
+ ) -> str:
209
+ r"""Rewrite links with space-containing URLs to angle-bracket syntax.
210
+
211
+ CommonMark requires link destinations with spaces to use angle brackets.
212
+ Handles three cases:
213
+
214
+ 1. markdown-it fails to parse [text](url space) as a link and mdformat
215
+ escapes the brackets — detects ``\\[text\\](url space)`` and rewrites to
216
+ ``[text](<url space>)``. (Fallback for edge cases not caught by the
217
+ spaced_url_link mdit rule.)
218
+ 2. markdown-it parses [text](<url space>) correctly but percent-encodes
219
+ the space in the href — detects [text](url%20space) and restores to
220
+ [text](<url space>) using the original source in node.content.
221
+ 3. The spaced_url_link mdit rule parses [text](url space) as a link with
222
+ percent-encoded href — same rewrite as case 2, but source has no
223
+ angle brackets so a separate pattern extracts the original URL.
224
+
225
+ Addresses: https://github.com/KyleKing/mdformat-mkdocs/issues/80
226
+
227
+ """
228
+ text = _ESCAPED_LINK_SPACED_URL.sub(
229
+ lambda m: f"[{m.group(1)}](<{m.group(2)}>)",
230
+ text,
231
+ )
232
+
233
+ spaced_urls_from_source = {
234
+ m.group(1) for m in _ANGLE_BRACKET_SPACED_URL_SOURCE.finditer(node.content)
235
+ } | {m.group(1) for m in _UNBRACKETED_SPACED_URL_SOURCE.finditer(node.content)}
236
+ if spaced_urls_from_source:
181
237
 
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.
238
+ def _restore_spaced_url(m: re.Match[str]) -> str:
239
+ link_text, encoded_url = m.group(1), m.group(2)
240
+ decoded_url = encoded_url.replace("%20", " ")
241
+ if decoded_url in spaced_urls_from_source:
242
+ return f"[{link_text}](<{decoded_url}>)"
243
+ return m.group(0)
186
244
 
187
- Example: `code\n` (newline) → `code ` (parsed) → `code` (rendered)
245
+ text = _PERCENT_ENCODED_URL_LINK.sub(_restore_spaced_url, text)
246
+
247
+ return text
188
248
 
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
191
249
 
250
+ def _render_text(node: RenderTreeNode, context: RenderContext) -> str:
251
+ r"""Re-escape dollar signs that mdformat core stripped.
252
+
253
+ mdformat removes "unnecessary" backslash escapes (\$ -> $), but with math enabled
254
+ those bare $ become math delimiters. Compares text content against the parent
255
+ inline token (which preserves backslashes) to detect and restore the escapes.
256
+
257
+ Related: https://github.com/KyleKing/mdformat-mkdocs/issues/77
192
258
  """
193
- default_renderer = DEFAULT_RENDERERS.get("code_inline")
259
+ default_renderer = DEFAULT_RENDERERS.get("text")
194
260
  if default_renderer is None:
195
261
  return node.content
196
262
 
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
263
+ text = default_renderer(node, context)
202
264
 
203
- content = result[1:-1] # Strip opening and closing backticks
204
- has_leading_space = content.startswith(" ")
205
- has_trailing_space = content.endswith(" ")
265
+ if cli_is_no_mkdocs_math(context.options):
266
+ return text
206
267
 
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(' ')}`"
268
+ if node.parent and node.parent.type == "inline":
269
+ parent_content = node.parent.content
270
+ if "$" in text and r"\$" in parent_content:
271
+ text = re.sub(r"(?<!\\)\$", r"\$", text)
211
272
 
212
- return result
273
+ return text
213
274
 
214
275
 
215
276
  def _render_heading_autoref(node: RenderTreeNode, context: RenderContext) -> str:
@@ -307,12 +368,12 @@ RENDERERS: Mapping[str, Render] = {
307
368
  "admonition_title": render_admon_title,
308
369
  "admonition_mkdocs": add_extra_admon_newline,
309
370
  "admonition_mkdocs_title": render_admon_title,
310
- "code_inline": _render_code_inline,
311
371
  "content_tab_mkdocs": add_extra_admon_newline,
312
372
  "content_tab_mkdocs_title": render_admon_title,
373
+ "dd": render_material_definition_body,
313
374
  "dl": render_material_definition_list,
314
375
  "dt": render_material_definition_term,
315
- "dd": render_material_definition_body,
376
+ "text": _render_text,
316
377
  # Math support (from mdit-py-plugins)
317
378
  DOLLARMATH_INLINE: _render_math_inline,
318
379
  DOLLARMATH_BLOCK: _render_math_block,
@@ -323,6 +384,7 @@ RENDERERS: Mapping[str, Render] = {
323
384
  PYMD_CAPTIONS_PREFIX: render_pymd_caption,
324
385
  MKDOCSTRINGS_AUTOREFS_PREFIX: _render_meta_content,
325
386
  MKDOCSTRINGS_CROSSREFERENCE_PREFIX: _render_cross_reference,
387
+ MKDOCSTRINGS_INJECTION_PREFIX: _render_node_content,
326
388
  MKDOCSTRINGS_HEADING_AUTOREFS_PREFIX: _render_heading_autoref,
327
389
  PYMD_ABBREVIATIONS_PREFIX: _render_inline_content,
328
390
  PYMD_SNIPPET_PREFIX: _render_inline_content,
@@ -330,10 +392,24 @@ RENDERERS: Mapping[str, Render] = {
330
392
  }
331
393
 
332
394
 
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
- )
395
+ if TYPE_CHECKING:
396
+ normalize_list: Postprocess
397
+ postprocess_inline: Postprocess
398
+ else:
399
+ normalize_list = partial(
400
+ unbounded_normalize_list,
401
+ check_if_align_semantic_breaks_in_lists=cli_is_align_semantic_breaks_in_lists,
402
+ )
403
+
404
+ def postprocess_inline(
405
+ text: str,
406
+ node: RenderTreeNode,
407
+ context: RenderContext,
408
+ ) -> str:
409
+ """Run all inline postprocessors in sequence."""
410
+ text = _postprocess_list_wrap(text, node, context)
411
+ return _fix_links_with_spaced_urls(text, node, context)
412
+
337
413
 
338
414
  # A mapping from `RenderTreeNode.type` to a `Postprocess` that does
339
415
  # postprocessing for the output of the `Render` function. Unlike
@@ -342,7 +418,7 @@ normalize_list = partial(
342
418
  # will run in series.
343
419
  POSTPROCESSORS: Mapping[str, Postprocess] = {
344
420
  "bullet_list": normalize_list,
345
- "inline": postprocess_list_wrap, # type: ignore[has-type]
421
+ "inline": postprocess_inline,
346
422
  "ordered_list": normalize_list,
347
423
  "paragraph": escape_deflist,
348
424
  }
@@ -25,7 +25,7 @@ license-files = ["LICENSE"]
25
25
  name = "mdformat_mkdocs"
26
26
  readme = "README.md"
27
27
  requires-python = ">=3.10.0"
28
- version = "5.1.4"
28
+ version = "5.2.0"
29
29
 
30
30
  [project.entry-points."mdformat.parser_extension"]
31
31
  mkdocs = "mdformat_mkdocs"
@@ -56,6 +56,7 @@ recommended-mdsf = [
56
56
  ]
57
57
  test = [
58
58
  "beartype >= 0.21.0",
59
+ "hypothesis >= 6.100.0",
59
60
  "pytest >= 9.0.1",
60
61
  "pytest-beartype >= 0.2.0",
61
62
  "pytest-cov >= 7.0.0",
@@ -69,7 +70,7 @@ homepage = "https://github.com/kyleking/mdformat-mkdocs"
69
70
 
70
71
  [tool.commitizen]
71
72
  tag_format = "v${version}"
72
- version = "5.1.4"
73
+ version = "5.2.0"
73
74
  version_files = ["mdformat_mkdocs/__init__.py", "pyproject.toml:^version"]
74
75
 
75
76
  [tool.mypy]
@@ -183,6 +184,13 @@ isolated_build = true
183
184
  requires = ["tox>=4.32.0"]
184
185
  skip_missing_interpreters = false
185
186
 
187
+ [tool.tox.env.canary]
188
+ basepython = ["py314"]
189
+ changedir = "{tox_root}"
190
+ commands = [["python", "scripts/canary.py", {default = [], extend = true, replace = "posargs"}]]
191
+ description = "Run mdformat --check against real downstream repos. Optionally specify: '-- ruff uv' to test a subset."
192
+ skip_install = false
193
+
186
194
  [tool.tox.env.cz]
187
195
  basepython = ["py314"]
188
196
  commands = [["cz", "bump", {default = [], extend = true, replace = "posargs"}]]
File without changes