1use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::time::Duration;
9
10pub type RenderOptions = MarkdownOptions;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct MarkdownOptions {
18 pub enable_gfm: bool,
20
21 pub enable_footnotes: bool,
23
24 pub enable_tables: bool,
26
27 pub enable_task_lists: bool,
29
30 pub enable_strikethrough: bool,
32
33 pub enable_autolinks: bool,
35
36 pub enable_smart_punctuation: bool,
38
39 pub enable_heading_attributes: bool,
41}
42
43impl Default for MarkdownOptions {
44 fn default() -> Self {
45 Self {
46 enable_gfm: true,
47 enable_footnotes: true,
48 enable_tables: true,
49 enable_task_lists: true,
50 enable_strikethrough: true,
51 enable_autolinks: true,
52 enable_smart_punctuation: true,
53 enable_heading_attributes: true,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
60pub enum OutputFormat {
61 #[default]
63 Html,
64
65 PlainText,
67
68 Ast,
70
71 Markdown,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct RenderResult {
78 pub content: String,
80
81 pub format: OutputFormat,
83
84 pub metadata: RenderMetadata,
86
87 pub stats: RenderStats,
89}
90
91impl RenderResult {
92 pub fn new(content: String, format: OutputFormat) -> Self {
94 Self {
95 content,
96 format,
97 metadata: RenderMetadata::default(),
98 stats: RenderStats::default(),
99 }
100 }
101
102 pub fn with_metadata(mut self, metadata: RenderMetadata) -> Self {
104 self.metadata = metadata;
105 self
106 }
107
108 pub fn with_content(mut self, content: String) -> Self {
110 self.content = content;
111 self
112 }
113
114 pub fn with_stats(mut self, stats: RenderStats) -> Self {
116 self.stats = stats;
117 self
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123pub struct RenderMetadata {
124 pub title: Option<String>,
126
127 pub description: Option<String>,
129
130 pub author: Option<String>,
132
133 pub tags: Vec<String>,
135
136 pub custom: BTreeMap<String, String>,
138
139 pub word_count: usize,
141
142 pub char_count: usize,
144
145 pub heading_count: usize,
147
148 pub code_block_count: usize,
150}
151
152impl RenderMetadata {
153 pub fn new() -> Self {
155 Self::default()
156 }
157
158 pub fn add_tag(&mut self, tag: String) {
160 if !self.tags.contains(&tag) {
161 self.tags.push(tag);
162 }
163 }
164
165 pub fn set_custom(&mut self, key: String, value: String) {
167 self.custom.insert(key, value);
168 }
169
170 pub fn get_custom(&self, key: &str) -> Option<&String> {
172 self.custom.get(key)
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Default)]
178pub struct RenderStats {
179 pub render_time_ms: u64,
181
182 pub cache_hit: bool,
184
185 pub latex_equations: usize,
187
188 pub code_blocks: usize,
190
191 pub template_substitutions: usize,
193
194 pub output_size_bytes: usize,
196}
197
198impl RenderStats {
199 pub fn new() -> Self {
201 Self::default()
202 }
203
204 pub fn with_render_time(mut self, duration: Duration) -> Self {
206 self.render_time_ms = duration.as_millis() as u64;
207 self
208 }
209
210 pub fn with_cache_hit(mut self, hit: bool) -> Self {
212 self.cache_hit = hit;
213 self
214 }
215
216 pub fn increment_latex(&mut self) {
218 self.latex_equations += 1;
219 }
220
221 pub fn increment_code_blocks(&mut self) {
223 self.code_blocks += 1;
224 }
225
226 pub fn increment_template_substitutions(&mut self) {
228 self.template_substitutions += 1;
229 }
230
231 pub fn with_output_size(mut self, size: usize) -> Self {
233 self.output_size_bytes = size;
234 self
235 }
236}
237
238#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
240pub enum SyntaxTheme {
241 Light,
243
244 #[default]
246 Dark,
247
248 HighContrast,
250
251 Custom,
253}
254
255impl SyntaxTheme {
256 pub fn from_theme_name(name: &str) -> Self {
262 match name {
263 "light" | "one-light" | "github" => SyntaxTheme::Light,
264 "high-contrast" | "highcontrast" | "hc" => SyntaxTheme::HighContrast,
265 _ => SyntaxTheme::Dark,
266 }
267 }
268}
269
270#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
276pub enum Language {
277 Rust,
279 Python,
281 JavaScript,
283 TypeScript,
285 Json,
287 Toml,
289 Yaml,
291 Html,
293 Css,
295 Sql,
297 Bash,
299 Markdown,
301}
302
303impl Language {
304 pub fn all() -> &'static [Language] {
306 &[
307 Language::Rust,
308 Language::Python,
309 Language::JavaScript,
310 Language::TypeScript,
311 Language::Json,
312 Language::Toml,
313 Language::Yaml,
314 Language::Html,
315 Language::Css,
316 Language::Sql,
317 Language::Bash,
318 Language::Markdown,
319 ]
320 }
321
322 pub fn from_name(s: &str) -> Option<Self> {
324 match s.to_lowercase().as_str() {
325 "rust" => Some(Language::Rust),
326 "rs" => Some(Language::Rust),
327 "python" => Some(Language::Python),
328 "py" => Some(Language::Python),
329 "javascript" => Some(Language::JavaScript),
330 "js" => Some(Language::JavaScript),
331 "typescript" => Some(Language::TypeScript),
332 "ts" => Some(Language::TypeScript),
333 "json" => Some(Language::Json),
334 "toml" => Some(Language::Toml),
335 "yaml" => Some(Language::Yaml),
336 "yml" => Some(Language::Yaml),
337 "html" => Some(Language::Html),
338 "htm" => Some(Language::Html),
339 "css" => Some(Language::Css),
340 "sql" => Some(Language::Sql),
341 "bash" => Some(Language::Bash),
342 "sh" => Some(Language::Bash),
343 "shell" => Some(Language::Bash),
344 "markdown" | "md" | "markdown-inline" => Some(Language::Markdown),
345 _ => None,
346 }
347 }
348
349 pub fn as_str(&self) -> &'static str {
351 match self {
352 Language::Rust => "rust",
353 Language::Python => "python",
354 Language::JavaScript => "javascript",
355 Language::TypeScript => "typescript",
356 Language::Json => "json",
357 Language::Toml => "toml",
358 Language::Yaml => "yaml",
359 Language::Html => "html",
360 Language::Css => "css",
361 Language::Sql => "sql",
362 Language::Bash => "bash",
363 Language::Markdown => "markdown",
364 }
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_markdown_options_default() {
374 let opts = MarkdownOptions::default();
375 assert!(opts.enable_gfm);
376 assert!(opts.enable_tables);
377 }
378
379 #[test]
380 fn test_language_from_str() {
381 assert_eq!(Language::from_name("rust"), Some(Language::Rust));
382 assert_eq!(Language::from_name("py"), Some(Language::Python));
383 assert_eq!(
384 Language::from_name("markdown-inline"),
385 Some(Language::Markdown)
386 );
387 assert_eq!(Language::from_name("unknown"), None);
388 }
389
390 #[test]
391 fn test_theme_from_name() {
392 assert_eq!(SyntaxTheme::from_theme_name("light"), SyntaxTheme::Light);
393 assert_eq!(SyntaxTheme::from_theme_name("github"), SyntaxTheme::Light);
394 assert_eq!(
395 SyntaxTheme::from_theme_name("high-contrast"),
396 SyntaxTheme::HighContrast
397 );
398 assert_eq!(SyntaxTheme::from_theme_name("dark"), SyntaxTheme::Dark);
399 assert_eq!(SyntaxTheme::from_theme_name("nonsense"), SyntaxTheme::Dark);
400 }
401}