magical markdown slides
at main 6.9 kB view raw
1use std::str::FromStr; 2 3use serde::{Deserialize, Serialize}; 4 5/// A single slide in a presentation 6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 7pub struct Slide { 8 /// The content blocks that make up this slide 9 pub blocks: Vec<Block>, 10 /// Optional speaker notes (not displayed on main slide) 11 pub notes: Option<String>, 12} 13 14impl Slide { 15 pub fn new() -> Self { 16 Self { blocks: Vec::new(), notes: None } 17 } 18 19 pub fn with_blocks(blocks: Vec<Block>) -> Self { 20 Self { blocks, notes: None } 21 } 22 23 pub fn is_empty(&self) -> bool { 24 self.blocks.is_empty() 25 } 26} 27 28impl Default for Slide { 29 fn default() -> Self { 30 Self::new() 31 } 32} 33 34/// Content block types that can appear in a slide 35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 36pub enum Block { 37 /// Heading with level (1-6) and text spans 38 Heading { level: u8, spans: Vec<TextSpan> }, 39 /// Paragraph of text spans 40 Paragraph { spans: Vec<TextSpan> }, 41 /// Code block with optional language and content 42 Code(CodeBlock), 43 /// Ordered or unordered list 44 List(List), 45 /// Horizontal rule/divider 46 Rule, 47 /// Block quote 48 BlockQuote { blocks: Vec<Block> }, 49 /// Table 50 Table(Table), 51 /// Admonition/alert box with type, optional title, and content 52 Admonition(Admonition), 53 /// Image with path and alt text 54 Image { path: String, alt: String }, 55} 56 57/// Styled text span within a block 58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 59pub struct TextSpan { 60 pub text: String, 61 pub style: TextStyle, 62} 63 64impl TextSpan { 65 pub fn plain(text: impl Into<String>) -> Self { 66 Self { text: text.into(), style: TextStyle::default() } 67 } 68 69 pub fn bold(text: impl Into<String>) -> Self { 70 Self { text: text.into(), style: TextStyle { bold: true, ..Default::default() } } 71 } 72 73 pub fn italic(text: impl Into<String>) -> Self { 74 Self { text: text.into(), style: TextStyle { italic: true, ..Default::default() } } 75 } 76 77 pub fn code(text: impl Into<String>) -> Self { 78 Self { text: text.into(), style: TextStyle { code: true, ..Default::default() } } 79 } 80} 81 82/// Text styling flags 83#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] 84pub struct TextStyle { 85 pub bold: bool, 86 pub italic: bool, 87 pub strikethrough: bool, 88 pub code: bool, 89} 90 91/// Code block with language and content 92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 93pub struct CodeBlock { 94 /// Programming language for syntax highlighting 95 pub language: Option<String>, 96 /// Raw code content 97 pub code: String, 98} 99 100impl CodeBlock { 101 pub fn new(code: impl Into<String>) -> Self { 102 Self { language: None, code: code.into() } 103 } 104 105 pub fn with_language(language: impl Into<String>, code: impl Into<String>) -> Self { 106 Self { language: Some(language.into()), code: code.into() } 107 } 108} 109 110/// List (ordered or unordered) 111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 112pub struct List { 113 pub ordered: bool, 114 pub items: Vec<ListItem>, 115} 116 117/// Single list item that can contain blocks 118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 119pub struct ListItem { 120 pub spans: Vec<TextSpan>, 121 pub nested: Option<Box<List>>, 122} 123 124/// Table with headers and rows 125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 126pub struct Table { 127 pub headers: Vec<Vec<TextSpan>>, 128 pub rows: Vec<Vec<Vec<TextSpan>>>, 129 pub alignments: Vec<Alignment>, 130} 131 132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 133pub enum Alignment { 134 Left, 135 Center, 136 Right, 137} 138 139/// Admonition type determines styling and icon 140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 141#[serde(rename_all = "lowercase")] 142pub enum AdmonitionType { 143 Note, 144 Tip, 145 Important, 146 Warning, 147 Caution, 148 Danger, 149 Error, 150 Info, 151 Success, 152 Question, 153 Example, 154 Quote, 155 Abstract, 156 Todo, 157 Bug, 158 Failure, 159} 160 161/// Error type for parsing AdmonitionType 162#[derive(Debug, Clone, PartialEq, Eq)] 163pub struct ParseAdmonitionTypeError; 164 165impl std::fmt::Display for ParseAdmonitionTypeError { 166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 167 write!(f, "invalid admonition type") 168 } 169} 170 171impl std::error::Error for ParseAdmonitionTypeError {} 172 173impl FromStr for AdmonitionType { 174 type Err = ParseAdmonitionTypeError; 175 176 /// Parse admonition type from string (case-insensitive) 177 /// 178 /// Supports GitHub and Obsidian aliases 179 fn from_str(s: &str) -> Result<Self, Self::Err> { 180 match s.to_lowercase().as_str() { 181 "note" => Ok(Self::Note), 182 "tip" | "hint" => Ok(Self::Tip), 183 "important" => Ok(Self::Important), 184 "warning" | "caution" | "attention" => Ok(Self::Warning), 185 "danger" | "error" => Ok(Self::Danger), 186 "info" => Ok(Self::Info), 187 "success" | "check" | "done" => Ok(Self::Success), 188 "question" | "help" | "faq" => Ok(Self::Question), 189 "example" => Ok(Self::Example), 190 "quote" => Ok(Self::Quote), 191 "abstract" | "summary" | "tldr" => Ok(Self::Abstract), 192 "todo" => Ok(Self::Todo), 193 "bug" => Ok(Self::Bug), 194 "failure" | "fail" | "missing" => Ok(Self::Failure), 195 _ => Err(ParseAdmonitionTypeError), 196 } 197 } 198} 199 200/// Admonition/alert box with styled content 201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 202pub struct Admonition { 203 pub admonition_type: AdmonitionType, 204 pub title: Option<String>, 205 pub blocks: Vec<Block>, 206} 207 208#[cfg(test)] 209mod tests { 210 use super::*; 211 212 #[test] 213 fn slide_creation() { 214 let slide = Slide::new(); 215 assert!(slide.is_empty()); 216 assert_eq!(slide.blocks.len(), 0); 217 } 218 219 #[test] 220 fn slide_with_blocks() { 221 let blocks = vec![Block::Paragraph { spans: vec![TextSpan::plain("Hello")] }]; 222 let slide = Slide::with_blocks(blocks.clone()); 223 assert!(!slide.is_empty()); 224 assert_eq!(slide.blocks.len(), 1); 225 } 226 227 #[test] 228 fn text_span_styles() { 229 let plain = TextSpan::plain("text"); 230 assert!(!plain.style.bold); 231 assert!(!plain.style.italic); 232 233 let bold = TextSpan::bold("text"); 234 assert!(bold.style.bold); 235 236 let italic = TextSpan::italic("text"); 237 assert!(italic.style.italic); 238 239 let code = TextSpan::code("text"); 240 assert!(code.style.code); 241 } 242 243 #[test] 244 fn code_block_creation() { 245 let code = CodeBlock::new("fn main() {}"); 246 assert_eq!(code.language, None); 247 assert_eq!(code.code, "fn main() {}"); 248 249 let rust_code = CodeBlock::with_language("rust", "fn main() {}"); 250 assert_eq!(rust_code.language, Some("rust".to_string())); 251 } 252}