@@ -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."""
0 commit comments