Slicer 5.9
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
github_alerts.py
Go to the documentation of this file.
1from __future__ import annotations
2import re
3from docutils import nodes
4from sphinx.transforms.post_transforms import SphinxPostTransform
5from sphinx.util import logging
6
7logger = logging.getLogger(__name__)
8
9# Matches "[!KIND]" optionally followed by an inline title on the same line.
10# Examples:
11# [!NOTE]
12# [!WARNING] Read this first
13_ALERT_HEADER = re.compile(r"^\s*\[\!(?P<kind>[A-Za-z]+)\]\s*(?P<title>.*)")
14
15# Map GitHub kinds to Docutils admonition node classes (extend as you like)
16_KIND_MAP = {
17 "note": nodes.note,
18 "tip": nodes.tip,
19 "hint": nodes.hint,
20 "important": nodes.important,
21 "warning": nodes.warning,
22 "caution": nodes.caution,
23 "attention": nodes.attention,
24 "danger": nodes.danger,
25 "error": nodes.error,
26 # Aliases
27 "info": nodes.note,
28 "success": nodes.tip,
29}
30
31def _convert_blockquotes(doctree: nodes.document, *, debug: bool = False) -> int:
32 """Convert GitHub alert blockquotes into admonitions. Returns count converted."""
33 converted = 0
34
35 candidates = list(doctree.traverse(nodes.block_quote))
36 for cont in doctree.traverse(nodes.container):
37 if "quote" in cont.get("classes", []):
38 candidates.append(cont)
39
40 seen = set()
41 for bq in candidates:
42 if id(bq) in seen or not bq.children:
43 continue
44 seen.add(id(bq))
45
46 first_para = next((c for c in bq.children if isinstance(c, nodes.paragraph)), None)
47 if not first_para:
48 continue
49
50 head_text = first_para.astext().strip()
51 m = _ALERT_HEADER.match(head_text)
52 if not m:
53 continue
54
55 kind_raw = m.group("kind") # preserve original case for prefix removal
56 kind = kind_raw.lower()
57
58 NodeClass = _KIND_MAP.get(kind)
59 admon = nodes.admonition() if NodeClass is None else NodeClass()
60 if NodeClass is None:
61 admon += nodes.title(text=kind.title())
62
63 # Inline text = everything after the marker on the first paragraph (possibly multi-line)
64 prefix = f"[!{kind_raw}]"
65 inline_text = head_text.removeprefix(prefix)
66
67 # Keep inline text as the first paragraph of the body (if present)
68 if inline_text:
69 admon += nodes.paragraph(text=inline_text)
70
71 # Append remaining children from the blockquote (skip the first paragraph)
72 for child in bq.children[1:]:
73 admon += child.deepcopy()
74
75 if debug:
76 logger.info(
77 f"[github_alerts] converted {kind!r} (inline->body: {bool(inline_text)})",
78 )
79
80 bq.replace_self(admon)
81 converted += 1
82
83 return converted
84
85class GithubAlertsToAdmonitions(SphinxPostTransform):
86 default_priority = 700 # after MyST parsing, before writers
87
88 def run(self):
89 debug = bool(getattr(self.app.config, "github_alerts_debug", False))
90 _convert_blockquotes(self.document, debug=debug)
91
92def setup(app):
93 app.add_config_value("github_alerts_debug", False, "env")
94 app.add_post_transform(GithubAlertsToAdmonitions)
95
96 return {
97 "version": "0.1",
98 "parallel_read_safe": True,
99 "parallel_write_safe": True,
100 }
int _convert_blockquotes(nodes.document doctree, *, bool debug=False)