quillmark_core/
error.rs

1use crate::OutputFormat;
2
3/// Error severity levels
4#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
5pub enum Severity {
6    Error,
7    Warning,
8    Note,
9}
10
11/// Location information for diagnostics
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
13pub struct Location {
14    pub file: String, // e.g., "glue.typ", "template.typ", "input.md"
15    pub line: u32,
16    pub col: u32,
17}
18
19/// Structured diagnostic information
20#[derive(Debug, Clone, serde::Serialize)]
21pub struct Diagnostic {
22    pub severity: Severity,
23    pub code: Option<String>,
24    pub message: String,
25    pub primary: Option<Location>,
26    pub related: Vec<Location>,
27    pub hint: Option<String>,
28}
29
30impl Diagnostic {
31    /// Create a new diagnostic
32    pub fn new(severity: Severity, message: String) -> Self {
33        Self {
34            severity,
35            code: None,
36            message,
37            primary: None,
38            related: Vec::new(),
39            hint: None,
40        }
41    }
42
43    /// Set the error code
44    pub fn with_code(mut self, code: String) -> Self {
45        self.code = Some(code);
46        self
47    }
48
49    /// Set the primary location
50    pub fn with_location(mut self, location: Location) -> Self {
51        self.primary = Some(location);
52        self
53    }
54
55    /// Add a related location
56    pub fn with_related(mut self, location: Location) -> Self {
57        self.related.push(location);
58        self
59    }
60
61    /// Set a hint
62    pub fn with_hint(mut self, hint: String) -> Self {
63        self.hint = Some(hint);
64        self
65    }
66
67    /// Format diagnostic for pretty printing
68    pub fn fmt_pretty(&self) -> String {
69        let mut result = format!(
70            "[{}] {}",
71            match self.severity {
72                Severity::Error => "ERROR",
73                Severity::Warning => "WARN",
74                Severity::Note => "NOTE",
75            },
76            self.message
77        );
78
79        if let Some(ref code) = self.code {
80            result.push_str(&format!(" ({})", code));
81        }
82
83        if let Some(ref loc) = self.primary {
84            result.push_str(&format!(" at {}:{}:{}", loc.file, loc.line, loc.col));
85        }
86
87        if let Some(ref hint) = self.hint {
88            result.push_str(&format!("\n  hint: {}", hint));
89        }
90
91        result
92    }
93}
94
95/// Main error type for rendering operations
96#[derive(thiserror::Error, Debug)]
97pub enum RenderError {
98    #[error("Engine creation failed")]
99    EngineCreation {
100        diag: Diagnostic,
101        #[source]
102        source: Option<anyhow::Error>,
103    },
104
105    #[error("Invalid YAML frontmatter")]
106    InvalidFrontmatter {
107        diag: Diagnostic,
108        #[source]
109        source: Option<anyhow::Error>,
110    },
111
112    #[error("Template rendering failed")]
113    TemplateFailed {
114        #[source]
115        source: minijinja::Error,
116        diag: Diagnostic,
117    },
118
119    #[error("Backend compilation failed with {0} error(s)")]
120    CompilationFailed(usize, Vec<Diagnostic>),
121
122    #[error("{format:?} not supported by {backend}")]
123    FormatNotSupported {
124        backend: String,
125        format: OutputFormat,
126    },
127
128    #[error("Unsupported backend: {0}")]
129    UnsupportedBackend(String),
130
131    #[error("Dynamic asset collision: {filename}")]
132    DynamicAssetCollision { filename: String, message: String },
133
134    #[error(transparent)]
135    Internal(#[from] anyhow::Error),
136
137    #[error("{0}")]
138    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
139
140    #[error("Template error: {0}")]
141    Template(#[from] crate::templating::TemplateError),
142}
143
144/// Result type containing artifacts and warnings
145#[derive(Debug)]
146pub struct RenderResult {
147    pub artifacts: Vec<crate::Artifact>,
148    pub warnings: Vec<Diagnostic>,
149}
150
151impl RenderResult {
152    /// Create a new result with artifacts
153    pub fn new(artifacts: Vec<crate::Artifact>) -> Self {
154        Self {
155            artifacts,
156            warnings: Vec::new(),
157        }
158    }
159
160    /// Add a warning to the result
161    pub fn with_warning(mut self, warning: Diagnostic) -> Self {
162        self.warnings.push(warning);
163        self
164    }
165}
166
167/// Convert minijinja errors to RenderError
168impl From<minijinja::Error> for RenderError {
169    fn from(e: minijinja::Error) -> Self {
170        let loc = e.line().map(|line| Location {
171            file: e.name().unwrap_or("template").to_string(),
172            line: line as u32,
173            col: 0, // MiniJinja doesn't provide column info
174        });
175
176        let diag = Diagnostic {
177            severity: Severity::Error,
178            code: Some(format!("minijinja::{:?}", e.kind())),
179            message: e.to_string(),
180            primary: loc,
181            related: vec![],
182            hint: None,
183        };
184
185        RenderError::TemplateFailed { source: e, diag }
186    }
187}
188
189/// Helper to print structured errors
190pub fn print_errors(err: &RenderError) {
191    match err {
192        RenderError::CompilationFailed(_, diags) => {
193            for d in diags {
194                eprintln!("{}", d.fmt_pretty());
195            }
196        }
197        RenderError::TemplateFailed { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
198        RenderError::InvalidFrontmatter { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
199        RenderError::EngineCreation { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
200        _ => eprintln!("{}", err),
201    }
202}