Skip to content

Commit b72add0

Browse files
committed
Add GitHub-style callouts support in md2term.py and update example.md
Implement functionality to detect and render GitHub-style callouts in the TerminalRenderer class. Enhance the rendering process to include various callout types with custom titles and rich content. Update example.md to showcase the new callout features, providing users with clear examples of usage.
1 parent 601e351 commit b72add0

File tree

3 files changed

+244
-7
lines changed

3 files changed

+244
-7
lines changed

example.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,51 @@ python md2term.py README.md
101101
curl -s https://raw.githubusercontent.com/user/repo/main/README.md | python md2term.py
102102
```
103103

104+
# Test Callouts
105+
106+
## GitHub-style callouts
107+
108+
> [!NOTE]
109+
> This is a note callout.
110+
111+
> [!TIP]
112+
> This is a tip callout.
113+
114+
> [!WARNING]
115+
> This is a warning callout.
116+
117+
> [!IMPORTANT]
118+
> This is an important callout.
119+
120+
> [!CAUTION]
121+
> This is a caution callout.
122+
123+
## Callouts with custom titles
124+
125+
> [!NOTE] Custom Note Title
126+
>
127+
> This is a note with a custom title.
128+
129+
> [!TIP] Pro Tip
130+
>
131+
> This is a tip with a custom title.
132+
133+
## Callouts with rich content
134+
135+
> [!WARNING] Complex Warning
136+
>
137+
> This callout contains **bold text**, _italic text_, and `inline code`.
138+
>
139+
> It can also contain:
140+
>
141+
> - Bullet points
142+
> - Multiple paragraphs
143+
> - Even [links](https://example.com)
144+
>
145+
> ```python
146+
> # And code blocks!
147+
> def example():
148+
> return "Hello from a callout!"
149+
> ```
150+
104151
That's all folks!

md2term.py

Lines changed: 135 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def _render_code_block(self, token: Dict[str, Any]) -> None:
128128
self.console.print(panel)
129129

130130
def _render_blockquote(self, token: Dict[str, Any]) -> None:
131-
"""Render a blockquote with indentation and styling."""
131+
"""Render a blockquote with indentation and styling, including GitHub-style callouts."""
132132
# Render children into a string buffer
133133
old_console = self.console
134134
buffer = StringIO()
@@ -141,13 +141,141 @@ def _render_blockquote(self, token: Dict[str, Any]) -> None:
141141
self.console = old_console
142142
content = buffer.getvalue().rstrip()
143143

144-
# Create GitHub-style blockquote with left border only
145-
lines = content.split('\n')
146-
for line in lines:
147-
if line.strip(): # Only add border to non-empty lines
148-
self.console.print(f"[dim blue]│[/] [italic dim blue]{line}[/]")
144+
# Check if this is a GitHub-style callout
145+
callout_info = self._detect_callout(content)
146+
147+
if callout_info:
148+
self._render_callout(content, callout_info)
149+
else:
150+
# Create GitHub-style blockquote with left border only
151+
lines = content.split('\n')
152+
for line in lines:
153+
if line.strip(): # Only add border to non-empty lines
154+
self.console.print(f"[dim blue]│[/] [italic dim blue]{line}[/]")
155+
else:
156+
self.console.print(f"[dim blue]│[/]")
157+
158+
def _detect_callout(self, content: str) -> Optional[Dict[str, str]]:
159+
"""Detect GitHub-style callouts in blockquote content."""
160+
lines = content.strip().split('\n')
161+
if not lines:
162+
return None
163+
164+
first_line = lines[0].strip()
165+
166+
# Match patterns like "[!NOTE]" or "[!NOTE] Custom Title"
167+
callout_pattern = r'^\[!([A-Z]+)\](?:\s+(.+))?$'
168+
match = re.match(callout_pattern, first_line)
169+
170+
if match:
171+
callout_type = match.group(1).lower()
172+
custom_title = match.group(2)
173+
174+
# Get the content after the callout declaration
175+
content_lines = lines[1:] if len(lines) > 1 else []
176+
callout_content = '\n'.join(content_lines).strip()
177+
178+
return {
179+
'type': callout_type,
180+
'title': custom_title,
181+
'content': callout_content
182+
}
183+
184+
return None
185+
186+
def _render_callout(self, content: str, callout_info: Dict[str, str]) -> None:
187+
"""Render a GitHub-style callout with emoji and appropriate styling."""
188+
callout_type = callout_info['type']
189+
custom_title = callout_info['title']
190+
callout_content = callout_info['content']
191+
192+
# Define callout styles and emojis
193+
callout_styles = {
194+
'note': {
195+
'emoji': '📝',
196+
'title': 'Note',
197+
'border_style': 'blue',
198+
'title_style': 'bold blue',
199+
'content_style': 'blue'
200+
},
201+
'tip': {
202+
'emoji': '💡',
203+
'title': 'Tip',
204+
'border_style': 'green',
205+
'title_style': 'bold green',
206+
'content_style': 'green'
207+
},
208+
'warning': {
209+
'emoji': '⚠️',
210+
'title': 'Warning',
211+
'border_style': 'yellow',
212+
'title_style': 'bold yellow',
213+
'content_style': 'yellow'
214+
},
215+
'important': {
216+
'emoji': '❗',
217+
'title': 'Important',
218+
'border_style': 'magenta',
219+
'title_style': 'bold magenta',
220+
'content_style': 'magenta'
221+
},
222+
'caution': {
223+
'emoji': '🚨',
224+
'title': 'Caution',
225+
'border_style': 'red',
226+
'title_style': 'bold red',
227+
'content_style': 'red'
228+
}
229+
}
230+
231+
# Get style info, default to note if unknown type
232+
style = callout_styles.get(callout_type, callout_styles['note'])
233+
234+
# Use custom title if provided, otherwise use default
235+
title = custom_title if custom_title else style['title']
236+
237+
# Create the title line with emoji and two extra spaces
238+
title_line = f"{style['emoji']} {title}"
239+
240+
# Render the callout as a panel
241+
if callout_content:
242+
# Parse and render the content with markdown
243+
old_console = self.console
244+
content_buffer = StringIO()
245+
temp_console = Console(file=content_buffer, width=self.console.size.width - 6)
246+
self.console = temp_console
247+
248+
# Parse the content as markdown
249+
import mistune
250+
markdown = mistune.create_markdown(renderer=None)
251+
try:
252+
tokens = markdown(callout_content)
253+
temp_renderer = TerminalRenderer(temp_console)
254+
temp_renderer.render(tokens)
255+
except Exception:
256+
# Fallback to plain text if parsing fails
257+
temp_console.print(callout_content)
258+
259+
self.console = old_console
260+
rendered_content = content_buffer.getvalue().rstrip()
261+
262+
# Create panel with title and content properly separated
263+
if rendered_content:
264+
panel_content = f"[{style['title_style']}]{title_line}[/]\n\n{rendered_content}"
149265
else:
150-
self.console.print(f"[dim blue]│[/]")
266+
panel_content = f"[{style['title_style']}]{title_line}[/]"
267+
else:
268+
# Just the title if no content
269+
panel_content = f"[{style['title_style']}]{title_line}[/]"
270+
271+
# Create and display the panel
272+
panel = Panel(
273+
panel_content,
274+
border_style=style['border_style'],
275+
padding=(0, 1),
276+
expand=False
277+
)
278+
self.console.print(panel)
151279

152280
def _render_list(self, token: Dict[str, Any]) -> None:
153281
"""Render ordered or unordered lists."""

tests/__snapshots__/test_md2term.ambr

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,68 @@
303303
│ curl -s https://raw.githubusercontent.com/user/repo/main/README.md | python  │
304304
╰──────────────────────────────────────────────────────────────────────────────╯
305305

306+
────────────────────────────────────────────────────────────────────────────────
307+
 Test Callouts 
308+
────────────────────────────────────────────────────────────────────────────────
309+
310+
GitHub-style callouts
311+
────────────────────────────────────────────────────────────────────────────────
312+
313+
╭─────────────────────────────╮
314+
│ 📝 This is a note callout. │
315+
╰─────────────────────────────╯
316+
317+
╭────────────────────────────╮
318+
│ 💡 This is a tip callout. │
319+
╰────────────────────────────╯
320+
321+
╭───────────────────────────────╮
322+
│ ⚠️ This is a warning callout. │
323+
╰───────────────────────────────╯
324+
325+
╭───────────────────────────────────╮
326+
│ ❗ This is an important callout. │
327+
╰───────────────────────────────────╯
328+
329+
╭────────────────────────────────╮
330+
│ 🚨 This is a caution callout. │
331+
╰────────────────────────────────╯
332+
333+
Callouts with custom titles
334+
────────────────────────────────────────────────────────────────────────────────
335+
336+
╭─────────────────────────────────────╮
337+
│ 📝 Custom Note Title │
338+
│ │
339+
│ This is a note with a custom title. │
340+
╰─────────────────────────────────────╯
341+
342+
╭────────────────────────────────────╮
343+
│ 💡 Pro Tip │
344+
│ │
345+
│ This is a tip with a custom title. │
346+
╰────────────────────────────────────╯
347+
348+
Callouts with rich content
349+
────────────────────────────────────────────────────────────────────────────────
350+
351+
╭────────────────────────────────────────────────────────────────────────────╮
352+
│ ⚠️ Complex Warning │
353+
│ │
354+
│ This callout contains bold text, italic text, and inline code. │
355+
│ │
356+
│ It can also contain: │
357+
│ │
358+
│ • Bullet points • Multiple paragraphs • Even links (https://example.com) │
359+
│ ╭───────────────────────────────────────────────────────────────────────── │
360+
│ ─╮ │ # And code blocks! │
361+
│ │ │ def example(): │
362+
│ │ │ return "Hello from a callout!" │
363+
│ │ │
364+
│ ╰───────────────────────────────────────────────────────────────────────── │
365+
│ ─╯ │
366+
╰────────────────────────────────────────────────────────────────────────────╯
367+
306368
That's all folks!
307369

308370
'''

0 commit comments

Comments
 (0)