quillmark_core/error.rs
1//! # Error Handling
2//!
3//! Structured error handling with diagnostics and source location tracking.
4//!
5//! ## Overview
6//!
7//! The `error` module provides error types and diagnostic types for actionable
8//! error reporting with source location tracking.
9//!
10//! ## Key Types
11//!
12//! - [`RenderError`]: Main error enum for rendering operations
13//! - [`crate::TemplateError`]: Template-specific errors
14//! - [`Diagnostic`]: Structured diagnostic information
15//! - [`Location`]: Source file location (file, line, column)
16//! - [`Severity`]: Error severity levels (Error, Warning, Note)
17//! - [`RenderResult`]: Result type with artifacts and warnings
18//!
19//! ## Error Hierarchy
20//!
21//! ### RenderError Variants
22//!
23//! - [`RenderError::EngineCreation`]: Failed to create rendering engine
24//! - [`RenderError::InvalidFrontmatter`]: Malformed YAML frontmatter
25//! - [`RenderError::TemplateFailed`]: Template rendering error
26//! - [`RenderError::CompilationFailed`]: Backend compilation errors
27//! - [`RenderError::FormatNotSupported`]: Requested format not supported
28//! - [`RenderError::UnsupportedBackend`]: Backend not registered
29//! - [`RenderError::DynamicAssetCollision`]: Asset filename collision
30//! - [`RenderError::Internal`]: Internal error
31//! - [`RenderError::Other`]: Other errors
32//! - [`RenderError::Template`]: Template error
33//!
34//! ## Examples
35//!
36//! ### Error Handling
37//!
38//! ```no_run
39//! use quillmark_core::{RenderError, error::print_errors};
40//! # use quillmark_core::RenderResult;
41//! # struct Workflow;
42//! # impl Workflow {
43//! # fn render(&self, _: &str, _: Option<()>) -> Result<RenderResult, RenderError> {
44//! # Ok(RenderResult::new(vec![]))
45//! # }
46//! # }
47//! # let workflow = Workflow;
48//! # let markdown = "";
49//!
50//! match workflow.render(markdown, None) {
51//! Ok(result) => {
52//! // Process artifacts
53//! for artifact in result.artifacts {
54//! std::fs::write(
55//! format!("output.{:?}", artifact.output_format),
56//! &artifact.bytes
57//! )?;
58//! }
59//! }
60//! Err(e) => {
61//! // Print structured diagnostics
62//! print_errors(&e);
63//!
64//! // Match specific error types
65//! match e {
66//! RenderError::CompilationFailed(count, diags) => {
67//! eprintln!("Compilation failed with {} errors:", count);
68//! for diag in diags {
69//! eprintln!("{}", diag.fmt_pretty());
70//! }
71//! }
72//! RenderError::InvalidFrontmatter { diag, .. } => {
73//! eprintln!("Frontmatter error: {}", diag.message);
74//! }
75//! _ => eprintln!("Error: {}", e),
76//! }
77//! }
78//! }
79//! # Ok::<(), Box<dyn std::error::Error>>(())
80//! ```
81//!
82//! ### Creating Diagnostics
83//!
84//! ```
85//! use quillmark_core::{Diagnostic, Location, Severity};
86//!
87//! let diag = Diagnostic::new(Severity::Error, "Undefined variable".to_string())
88//! .with_code("E001".to_string())
89//! .with_location(Location {
90//! file: "template.typ".to_string(),
91//! line: 10,
92//! col: 5,
93//! })
94//! .with_hint("Check variable spelling".to_string());
95//!
96//! println!("{}", diag.fmt_pretty());
97//! ```
98//!
99//! Example output:
100//! ```text
101//! [ERROR] Undefined variable (E001) at template.typ:10:5
102//! hint: Check variable spelling
103//! ```
104//!
105//! ### Result with Warnings
106//!
107//! ```no_run
108//! # use quillmark_core::{RenderResult, Diagnostic, Severity};
109//! # let artifacts = vec![];
110//! let result = RenderResult::new(artifacts)
111//! .with_warning(Diagnostic::new(
112//! Severity::Warning,
113//! "Deprecated field used".to_string(),
114//! ));
115//! ```
116//!
117//! ## Pretty Printing
118//!
119//! The [`Diagnostic`] type provides [`Diagnostic::fmt_pretty()`] for human-readable output with error code, location, and hints.
120//!
121//! ## Machine-Readable Output
122//!
123//! All diagnostic types implement `serde::Serialize` for JSON export:
124//!
125//! ```no_run
126//! # use quillmark_core::{Diagnostic, Severity};
127//! # let diagnostic = Diagnostic::new(Severity::Error, "Test".to_string());
128//! let json = serde_json::to_string(&diagnostic).unwrap();
129//! ```
130
131use crate::OutputFormat;
132
133/// Maximum input size for markdown (10 MB)
134pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
135
136/// Maximum YAML size (1 MB)
137pub const MAX_YAML_SIZE: usize = 1 * 1024 * 1024;
138
139/// Maximum nesting depth for markdown structures (100 levels)
140pub const MAX_NESTING_DEPTH: usize = 100;
141
142/// Maximum template output size (50 MB)
143pub const MAX_TEMPLATE_OUTPUT: usize = 50 * 1024 * 1024;
144
145/// Error severity levels
146#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
147pub enum Severity {
148 /// Fatal error that prevents completion
149 Error,
150 /// Non-fatal issue that may need attention
151 Warning,
152 /// Informational message
153 Note,
154}
155
156/// Location information for diagnostics
157#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
158pub struct Location {
159 /// Source file name (e.g., "glue.typ", "template.typ", "input.md")
160 pub file: String,
161 /// Line number (1-indexed)
162 pub line: u32,
163 /// Column number (1-indexed)
164 pub col: u32,
165}
166
167/// Structured diagnostic information
168#[derive(Debug, Clone, serde::Serialize)]
169pub struct Diagnostic {
170 /// Error severity level
171 pub severity: Severity,
172 /// Optional error code (e.g., "E001", "typst::syntax")
173 pub code: Option<String>,
174 /// Human-readable error message
175 pub message: String,
176 /// Primary source location
177 pub primary: Option<Location>,
178 /// Related source locations for context
179 pub related: Vec<Location>,
180 /// Optional hint for fixing the error
181 pub hint: Option<String>,
182}
183
184impl Diagnostic {
185 /// Create a new diagnostic
186 pub fn new(severity: Severity, message: String) -> Self {
187 Self {
188 severity,
189 code: None,
190 message,
191 primary: None,
192 related: Vec::new(),
193 hint: None,
194 }
195 }
196
197 /// Set the error code
198 pub fn with_code(mut self, code: String) -> Self {
199 self.code = Some(code);
200 self
201 }
202
203 /// Set the primary location
204 pub fn with_location(mut self, location: Location) -> Self {
205 self.primary = Some(location);
206 self
207 }
208
209 /// Add a related location
210 pub fn with_related(mut self, location: Location) -> Self {
211 self.related.push(location);
212 self
213 }
214
215 /// Set a hint
216 pub fn with_hint(mut self, hint: String) -> Self {
217 self.hint = Some(hint);
218 self
219 }
220
221 /// Format diagnostic for pretty printing
222 pub fn fmt_pretty(&self) -> String {
223 let mut result = format!(
224 "[{}] {}",
225 match self.severity {
226 Severity::Error => "ERROR",
227 Severity::Warning => "WARN",
228 Severity::Note => "NOTE",
229 },
230 self.message
231 );
232
233 if let Some(ref code) = self.code {
234 result.push_str(&format!(" ({})", code));
235 }
236
237 if let Some(ref loc) = self.primary {
238 result.push_str(&format!("\n --> {}:{}:{}", loc.file, loc.line, loc.col));
239 }
240
241 // Add related locations (trace)
242 for (i, related) in self.related.iter().enumerate() {
243 result.push_str(&format!(
244 "\n {} {}:{}:{}",
245 if i == 0 { "trace:" } else { " " },
246 related.file,
247 related.line,
248 related.col
249 ));
250 }
251
252 if let Some(ref hint) = self.hint {
253 result.push_str(&format!("\n hint: {}", hint));
254 }
255
256 result
257 }
258}
259
260/// Main error type for rendering operations
261#[derive(thiserror::Error, Debug)]
262pub enum RenderError {
263 /// Failed to create rendering engine
264 #[error("Engine creation failed")]
265 EngineCreation {
266 /// Diagnostic information
267 diag: Diagnostic,
268 #[source]
269 /// Optional source error
270 source: Option<anyhow::Error>,
271 },
272
273 /// Invalid YAML frontmatter in markdown document
274 #[error("Invalid YAML frontmatter")]
275 InvalidFrontmatter {
276 /// Diagnostic information
277 diag: Diagnostic,
278 #[source]
279 /// Optional source error
280 source: Option<anyhow::Error>,
281 },
282
283 /// Template rendering failed
284 #[error("Template rendering failed")]
285 TemplateFailed {
286 #[source]
287 /// MiniJinja error
288 source: minijinja::Error,
289 /// Diagnostic information
290 diag: Diagnostic,
291 },
292
293 /// Backend compilation failed with one or more errors
294 #[error("Backend compilation failed with {0} error(s)")]
295 CompilationFailed(
296 /// Number of errors
297 usize,
298 /// List of diagnostics
299 Vec<Diagnostic>,
300 ),
301
302 /// Requested output format not supported by backend
303 #[error("{format:?} not supported by {backend}")]
304 FormatNotSupported {
305 /// Backend identifier
306 backend: String,
307 /// Requested format
308 format: OutputFormat,
309 },
310
311 /// Backend not registered with engine
312 #[error("Unsupported backend: {0}")]
313 UnsupportedBackend(String),
314
315 /// Dynamic asset filename collision
316 #[error("Dynamic asset collision: {filename}")]
317 DynamicAssetCollision {
318 /// Filename that collided
319 filename: String,
320 /// Error message
321 message: String,
322 },
323
324 /// Internal error (wraps anyhow::Error)
325 #[error(transparent)]
326 Internal(#[from] anyhow::Error),
327
328 /// Other errors (boxed trait object)
329 #[error("{0}")]
330 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
331
332 /// Template-related error
333 #[error("Template error: {0}")]
334 Template(#[from] crate::templating::TemplateError),
335
336 /// Input size exceeded maximum allowed
337 #[error("Input too large: {size} bytes (max: {max} bytes)")]
338 InputTooLarge {
339 /// Actual size
340 size: usize,
341 /// Maximum allowed size
342 max: usize,
343 },
344
345 /// YAML size exceeded maximum allowed
346 #[error("YAML block too large: {size} bytes (max: {max} bytes)")]
347 YamlTooLarge {
348 /// Actual size
349 size: usize,
350 /// Maximum allowed size
351 max: usize,
352 },
353
354 /// Nesting depth exceeded maximum allowed
355 #[error("Nesting too deep: {depth} levels (max: {max} levels)")]
356 NestingTooDeep {
357 /// Actual depth
358 depth: usize,
359 /// Maximum allowed depth
360 max: usize,
361 },
362
363 /// Template output exceeded maximum size
364 #[error("Template output too large: {size} bytes (max: {max} bytes)")]
365 OutputTooLarge {
366 /// Actual size
367 size: usize,
368 /// Maximum allowed size
369 max: usize,
370 },
371}
372
373/// Result type containing artifacts and warnings
374#[derive(Debug)]
375pub struct RenderResult {
376 /// Generated output artifacts
377 pub artifacts: Vec<crate::Artifact>,
378 /// Non-fatal diagnostic messages
379 pub warnings: Vec<Diagnostic>,
380}
381
382impl RenderResult {
383 /// Create a new result with artifacts
384 pub fn new(artifacts: Vec<crate::Artifact>) -> Self {
385 Self {
386 artifacts,
387 warnings: Vec::new(),
388 }
389 }
390
391 /// Add a warning to the result
392 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
393 self.warnings.push(warning);
394 self
395 }
396}
397
398/// Convert minijinja errors to RenderError
399impl From<minijinja::Error> for RenderError {
400 fn from(e: minijinja::Error) -> Self {
401 // Extract location with proper range information
402 let loc = e.line().map(|line| {
403 Location {
404 file: e.name().unwrap_or("template").to_string(),
405 line: line as u32,
406 // MiniJinja provides range, extract approximate column
407 col: e.range().map(|r| r.start as u32).unwrap_or(0),
408 }
409 });
410
411 // Generate helpful hints based on error kind
412 let hint = generate_minijinja_hint(&e);
413
414 let diag = Diagnostic {
415 severity: Severity::Error,
416 code: Some(format!("minijinja::{:?}", e.kind())),
417 message: e.to_string(),
418 primary: loc,
419 related: vec![],
420 hint,
421 };
422
423 RenderError::TemplateFailed { source: e, diag }
424 }
425}
426
427/// Generate helpful hints for common MiniJinja errors
428fn generate_minijinja_hint(e: &minijinja::Error) -> Option<String> {
429 use minijinja::ErrorKind;
430
431 match e.kind() {
432 ErrorKind::UndefinedError => {
433 Some("Check variable spelling and ensure it's defined in frontmatter".to_string())
434 }
435 ErrorKind::InvalidOperation => {
436 Some("Check that you're using the correct filter or operator for this type".to_string())
437 }
438 ErrorKind::SyntaxError => Some(
439 "Check template syntax - look for unclosed tags or invalid expressions".to_string(),
440 ),
441 _ => e.detail().map(|d| d.to_string()),
442 }
443}
444
445/// Helper to print structured errors
446pub fn print_errors(err: &RenderError) {
447 match err {
448 RenderError::CompilationFailed(_, diags) => {
449 for d in diags {
450 eprintln!("{}", d.fmt_pretty());
451 }
452 }
453 RenderError::TemplateFailed { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
454 RenderError::InvalidFrontmatter { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
455 RenderError::EngineCreation { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
456 RenderError::FormatNotSupported { backend, format } => {
457 eprintln!(
458 "[ERROR] Format {:?} not supported by {} backend",
459 format, backend
460 );
461 }
462 RenderError::UnsupportedBackend(name) => {
463 eprintln!("[ERROR] Unsupported backend: {}", name);
464 }
465 RenderError::DynamicAssetCollision { filename, message } => {
466 eprintln!(
467 "[ERROR] Dynamic asset collision: {}\n {}",
468 filename, message
469 );
470 }
471 RenderError::Internal(e) => {
472 eprintln!("[ERROR] Internal error: {}", e);
473 }
474 RenderError::Template(e) => {
475 eprintln!("[ERROR] Template error: {}", e);
476 }
477 RenderError::Other(e) => {
478 eprintln!("[ERROR] {}", e);
479 }
480 RenderError::InputTooLarge { size, max } => {
481 eprintln!(
482 "[ERROR] Input too large: {} bytes (maximum: {} bytes)",
483 size, max
484 );
485 }
486 RenderError::YamlTooLarge { size, max } => {
487 eprintln!(
488 "[ERROR] YAML block too large: {} bytes (maximum: {} bytes)",
489 size, max
490 );
491 }
492 RenderError::NestingTooDeep { depth, max } => {
493 eprintln!(
494 "[ERROR] Nesting too deep: {} levels (maximum: {} levels)",
495 depth, max
496 );
497 }
498 RenderError::OutputTooLarge { size, max } => {
499 eprintln!(
500 "[ERROR] Template output too large: {} bytes (maximum: {} bytes)",
501 size, max
502 );
503 }
504 }
505}