Skip to main content

docs_pipeline/
types.rs

1//! Shared types for the docs pipeline.
2//!
3//! This module defines render options, output formats, render results,
4//! metadata, syntax highlighting themes, and supported languages.
5
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::time::Duration;
9
10/// Markdown parsing options.
11///
12/// This is the primary options type for [`crate::MarkdownParser`].
13pub type RenderOptions = MarkdownOptions;
14
15/// Markdown parsing options
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct MarkdownOptions {
18    /// Enable GitHub Flavored Markdown (GFM)
19    pub enable_gfm: bool,
20
21    /// Enable footnotes extension
22    pub enable_footnotes: bool,
23
24    /// Enable tables extension
25    pub enable_tables: bool,
26
27    /// Enable task lists (checkboxes)
28    pub enable_task_lists: bool,
29
30    /// Enable strikethrough text
31    pub enable_strikethrough: bool,
32
33    /// Enable autolinks (convert URLs to links)
34    pub enable_autolinks: bool,
35
36    /// Enable smart punctuation (quotes, dashes, etc.)
37    pub enable_smart_punctuation: bool,
38
39    /// Enable heading attributes
40    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/// Output format for rendering
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
60pub enum OutputFormat {
61    /// HTML output
62    #[default]
63    Html,
64
65    /// Plain text output
66    PlainText,
67
68    /// AST (Abstract Syntax Tree) representation
69    Ast,
70
71    /// Markdown (pass-through)
72    Markdown,
73}
74
75/// Render result containing the rendered content and metadata
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct RenderResult {
78    /// The rendered content
79    pub content: String,
80
81    /// Output format
82    pub format: OutputFormat,
83
84    /// Metadata extracted during rendering
85    pub metadata: RenderMetadata,
86
87    /// Rendering statistics
88    pub stats: RenderStats,
89}
90
91impl RenderResult {
92    /// Create a new render result
93    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    /// Set metadata
103    pub fn with_metadata(mut self, metadata: RenderMetadata) -> Self {
104        self.metadata = metadata;
105        self
106    }
107
108    /// Set content
109    pub fn with_content(mut self, content: String) -> Self {
110        self.content = content;
111        self
112    }
113
114    /// Set statistics
115    pub fn with_stats(mut self, stats: RenderStats) -> Self {
116        self.stats = stats;
117        self
118    }
119}
120
121/// Metadata extracted during rendering
122#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123pub struct RenderMetadata {
124    /// Document title
125    pub title: Option<String>,
126
127    /// Document description
128    pub description: Option<String>,
129
130    /// Author information
131    pub author: Option<String>,
132
133    /// Document tags
134    pub tags: Vec<String>,
135
136    /// Custom metadata key-value pairs
137    pub custom: BTreeMap<String, String>,
138
139    /// Word count
140    pub word_count: usize,
141
142    /// Character count
143    pub char_count: usize,
144
145    /// Number of headings
146    pub heading_count: usize,
147
148    /// Number of code blocks
149    pub code_block_count: usize,
150}
151
152impl RenderMetadata {
153    /// Create a new render metadata
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Add a tag
159    pub fn add_tag(&mut self, tag: String) {
160        if !self.tags.contains(&tag) {
161            self.tags.push(tag);
162        }
163    }
164
165    /// Set custom metadata
166    pub fn set_custom(&mut self, key: String, value: String) {
167        self.custom.insert(key, value);
168    }
169
170    /// Get custom metadata
171    pub fn get_custom(&self, key: &str) -> Option<&String> {
172        self.custom.get(key)
173    }
174}
175
176/// Rendering statistics
177#[derive(Debug, Clone, Serialize, Deserialize, Default)]
178pub struct RenderStats {
179    /// Time taken to render in milliseconds
180    pub render_time_ms: u64,
181
182    /// Whether cache was used
183    pub cache_hit: bool,
184
185    /// Number of LaTeX equations rendered
186    pub latex_equations: usize,
187
188    /// Number of code blocks highlighted
189    pub code_blocks: usize,
190
191    /// Number of template substitutions
192    pub template_substitutions: usize,
193
194    /// Size of rendered output in bytes
195    pub output_size_bytes: usize,
196}
197
198impl RenderStats {
199    /// Create a new render stats
200    pub fn new() -> Self {
201        Self::default()
202    }
203
204    /// Set render time
205    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    /// Set cache hit
211    pub fn with_cache_hit(mut self, hit: bool) -> Self {
212        self.cache_hit = hit;
213        self
214    }
215
216    /// Increment LaTeX equation count
217    pub fn increment_latex(&mut self) {
218        self.latex_equations += 1;
219    }
220
221    /// Increment code block count
222    pub fn increment_code_blocks(&mut self) {
223        self.code_blocks += 1;
224    }
225
226    /// Increment template substitution count
227    pub fn increment_template_substitutions(&mut self) {
228        self.template_substitutions += 1;
229    }
230
231    /// Set output size
232    pub fn with_output_size(mut self, size: usize) -> Self {
233        self.output_size_bytes = size;
234        self
235    }
236}
237
238/// Syntax highlighting theme
239#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
240pub enum SyntaxTheme {
241    /// Light theme
242    Light,
243
244    /// Dark theme
245    #[default]
246    Dark,
247
248    /// High contrast theme
249    HighContrast,
250
251    /// Custom theme (user-defined)
252    Custom,
253}
254
255impl SyntaxTheme {
256    /// Map a theme name (e.g. from a site config) to a theme.
257    ///
258    /// Recognized names: `light`, `one-light`, `github` map to [`SyntaxTheme::Light`];
259    /// `high-contrast`, `highcontrast`, `hc` map to [`SyntaxTheme::HighContrast`];
260    /// everything else (including `dark`, `one-dark`) maps to [`SyntaxTheme::Dark`].
261    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/// Supported programming languages for syntax highlighting.
271///
272/// Every variant is listed here regardless of which `lang-*` cargo features
273/// are enabled; use [`SyntaxHighlighter::is_language_supported`] to check
274/// availability at runtime.
275#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
276pub enum Language {
277    /// Rust
278    Rust,
279    /// Python
280    Python,
281    /// JavaScript
282    JavaScript,
283    /// TypeScript
284    TypeScript,
285    /// JSON
286    Json,
287    /// TOML
288    Toml,
289    /// YAML
290    Yaml,
291    /// HTML
292    Html,
293    /// CSS
294    Css,
295    /// SQL (grammar not bundled; falls back to plain output)
296    Sql,
297    /// Bash / shell
298    Bash,
299    /// Markdown
300    Markdown,
301}
302
303impl Language {
304    /// Get all supported languages
305    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    /// Parse language from string
323    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    /// Get language as string
350    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}