magical markdown slides
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}