//! Markdown rendering component using pulldown-cmark. //! //! More robust than eye-declare's built-in Markdown component: //! uses a proper CommonMark parser rather than line-by-line regex. use eye_declare::{Component, props}; use pulldown_cmark::{Event, Parser, Tag, TagEnd}; use ratatui_core::{ buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span, Text}, widgets::Widget, }; use ratatui_widgets::paragraph::{Paragraph, Wrap}; /// A markdown rendering component backed by pulldown-cmark. #[props] pub struct Markdown { pub source: String, } impl Markdown { pub fn new(source: impl Into) -> Self { Self { source: source.into(), } } } /// Style configuration for markdown rendering. pub struct MarkdownStyles { pub base: Style, pub code_inline: Style, pub code_block: Style, pub bold: Style, pub italic: Style, pub heading: Style, } impl MarkdownStyles { pub fn new() -> Self { let base = Style::default(); Self { base, code_inline: Style::default().fg(Color::Yellow), code_block: Style::default().fg(Color::Green), bold: base.add_modifier(Modifier::BOLD), italic: base.add_modifier(Modifier::ITALIC), heading: Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), } } } impl Default for MarkdownStyles { fn default() -> Self { Self::new() } } impl Component for Markdown { type State = MarkdownStyles; fn render(&self, area: Rect, buf: &mut Buffer, state: &Self::State) { if self.source.is_empty() || area.width == 0 || area.height == 0 { return; } let text = parse_markdown(&self.source, state); Paragraph::new(text) .wrap(Wrap { trim: false }) .render(area, buf); } fn desired_height(&self, width: u16, state: &Self::State) -> Option { if self.source.is_empty() || width == 0 { return Some(0); } let text = parse_markdown(&self.source, state); Some( Paragraph::new(text) .wrap(Wrap { trim: false }) .line_count(width) as u16, ) } fn initial_state(&self) -> Option { Some(MarkdownStyles::new()) } } /// Parse markdown source into styled ratatui Text using pulldown-cmark. fn parse_markdown<'a>(source: &'a str, styles: &'a MarkdownStyles) -> Text<'static> { let parser = Parser::new(source); let mut lines: Vec>> = vec![Vec::new()]; let mut current_line = 0; let mut style_stack: Vec