1use crate::OutputFormat;
83
84pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
86
87pub const MAX_YAML_SIZE: usize = 1024 * 1024;
89
90pub const MAX_NESTING_DEPTH: usize = 100;
92
93pub use crate::document::limits::MAX_YAML_DEPTH;
98
99pub const MAX_CARD_COUNT: usize = 1000;
102
103pub const MAX_FIELD_COUNT: usize = 1000;
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
109#[serde(rename_all = "lowercase")]
110pub enum Severity {
111 Error,
113 Warning,
115 Note,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct Location {
123 pub file: String,
125 pub line: u32,
127 pub column: u32,
129}
130
131#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct Diagnostic {
140 pub severity: Severity,
142 #[serde(skip_serializing_if = "Option::is_none", default)]
144 pub code: Option<String>,
145 pub message: String,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
149 pub location: Option<Location>,
150 #[serde(skip_serializing_if = "Option::is_none", default)]
152 pub hint: Option<String>,
153 #[serde(skip_serializing_if = "Vec::is_empty", default)]
155 pub source_chain: Vec<String>,
156}
157
158impl Diagnostic {
159 pub fn new(severity: Severity, message: String) -> Self {
161 Self {
162 severity,
163 code: None,
164 message,
165 location: None,
166 hint: None,
167 source_chain: Vec::new(),
168 }
169 }
170
171 pub fn with_code(mut self, code: String) -> Self {
173 self.code = Some(code);
174 self
175 }
176
177 pub fn with_location(mut self, location: Location) -> Self {
179 self.location = Some(location);
180 self
181 }
182
183 pub fn with_hint(mut self, hint: String) -> Self {
185 self.hint = Some(hint);
186 self
187 }
188
189 pub fn with_source(mut self, source: &(dyn std::error::Error + 'static)) -> Self {
191 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(source);
192 while let Some(err) = current {
193 self.source_chain.push(err.to_string());
194 current = err.source();
195 }
196 self
197 }
198
199 pub fn fmt_pretty(&self) -> String {
201 let mut result = format!(
202 "[{}] {}",
203 match self.severity {
204 Severity::Error => "ERROR",
205 Severity::Warning => "WARN",
206 Severity::Note => "NOTE",
207 },
208 self.message
209 );
210
211 if let Some(ref code) = self.code {
212 result.push_str(&format!(" ({})", code));
213 }
214
215 if let Some(ref loc) = self.location {
216 result.push_str(&format!("\n --> {}:{}:{}", loc.file, loc.line, loc.column));
217 }
218
219 if let Some(ref hint) = self.hint {
220 result.push_str(&format!("\n hint: {}", hint));
221 }
222
223 result
224 }
225
226 pub fn fmt_pretty_with_source(&self) -> String {
228 let mut result = self.fmt_pretty();
229
230 for (i, cause) in self.source_chain.iter().enumerate() {
231 result.push_str(&format!("\n cause {}: {}", i + 1, cause));
232 }
233
234 result
235 }
236}
237
238impl std::fmt::Display for Diagnostic {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 write!(f, "{}", self.message)
241 }
242}
243
244#[derive(thiserror::Error, Debug)]
246pub enum ParseError {
247 #[error("Input too large: {size} bytes (max: {max} bytes)")]
249 InputTooLarge {
250 size: usize,
252 max: usize,
254 },
255
256 #[error("Invalid YAML structure: {0}")]
258 InvalidStructure(String),
259
260 #[error("{0}")]
265 EmptyInput(String),
266
267 #[error("{0}")]
272 MissingQuillField(String),
273
274 #[error("YAML error at line {line}: {message}")]
276 YamlErrorWithLocation {
277 message: String,
279 line: usize,
281 block_index: usize,
283 },
284
285 #[error("{0}")]
287 Other(String),
288}
289
290impl ParseError {
291 pub fn to_diagnostic(&self) -> Diagnostic {
293 match self {
294 ParseError::InputTooLarge { size, max } => Diagnostic::new(
295 Severity::Error,
296 format!("Input too large: {} bytes (max: {} bytes)", size, max),
297 )
298 .with_code("parse::input_too_large".to_string()),
299 ParseError::InvalidStructure(msg) => Diagnostic::new(Severity::Error, msg.clone())
300 .with_code("parse::invalid_structure".to_string()),
301 ParseError::EmptyInput(msg) => Diagnostic::new(Severity::Error, msg.clone())
302 .with_code("parse::empty_input".to_string()),
303 ParseError::MissingQuillField(msg) => Diagnostic::new(Severity::Error, msg.clone())
304 .with_code("parse::missing_quill_field".to_string()),
305 ParseError::YamlErrorWithLocation {
306 message,
307 line,
308 block_index,
309 } => Diagnostic::new(
310 Severity::Error,
311 format!(
312 "YAML error at line {} (block {}): {}",
313 line, block_index, message
314 ),
315 )
316 .with_code("parse::yaml_error_with_location".to_string()),
317 ParseError::Other(msg) => Diagnostic::new(Severity::Error, msg.clone()),
318 }
319 }
320}
321
322impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
323 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
324 ParseError::Other(err.to_string())
325 }
326}
327
328impl From<String> for ParseError {
329 fn from(msg: String) -> Self {
330 ParseError::Other(msg)
331 }
332}
333
334impl From<&str> for ParseError {
335 fn from(msg: &str) -> Self {
336 ParseError::Other(msg.to_string())
337 }
338}
339
340#[derive(thiserror::Error, Debug)]
342pub enum RenderError {
343 #[error("{diag}")]
345 EngineCreation {
346 diag: Box<Diagnostic>,
348 },
349
350 #[error("{diag}")]
352 InvalidFrontmatter {
353 diag: Box<Diagnostic>,
355 },
356
357 #[error("Backend compilation failed with {} error(s)", diags.len())]
359 CompilationFailed {
360 diags: Vec<Diagnostic>,
362 },
363
364 #[error("{diag}")]
366 FormatNotSupported {
367 diag: Box<Diagnostic>,
369 },
370
371 #[error("{diag}")]
373 UnsupportedBackend {
374 diag: Box<Diagnostic>,
376 },
377
378 #[error("{diag}")]
380 ValidationFailed {
381 diag: Box<Diagnostic>,
383 },
384
385 #[error("Quill configuration failed with {} error(s)", diags.len())]
388 QuillConfig {
389 diags: Vec<Diagnostic>,
391 },
392}
393
394impl RenderError {
395 pub fn diagnostics(&self) -> Vec<&Diagnostic> {
397 match self {
398 RenderError::CompilationFailed { diags } | RenderError::QuillConfig { diags } => {
399 diags.iter().collect()
400 }
401 RenderError::EngineCreation { diag }
402 | RenderError::InvalidFrontmatter { diag }
403 | RenderError::FormatNotSupported { diag }
404 | RenderError::UnsupportedBackend { diag }
405 | RenderError::ValidationFailed { diag } => vec![diag.as_ref()],
406 }
407 }
408}
409
410impl From<ParseError> for RenderError {
412 fn from(err: ParseError) -> Self {
413 RenderError::InvalidFrontmatter {
414 diag: Box::new(
415 Diagnostic::new(Severity::Error, err.to_string())
416 .with_code("parse::error".to_string()),
417 ),
418 }
419 }
420}
421
422#[derive(Debug)]
424pub struct RenderResult {
425 pub artifacts: Vec<crate::Artifact>,
427 pub warnings: Vec<Diagnostic>,
429 pub output_format: OutputFormat,
431}
432
433impl RenderResult {
434 pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
436 Self {
437 artifacts,
438 warnings: Vec::new(),
439 output_format,
440 }
441 }
442
443 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
445 self.warnings.push(warning);
446 self
447 }
448}
449
450pub fn print_errors(err: &RenderError) {
452 for d in err.diagnostics() {
453 eprintln!("{}", d.fmt_pretty());
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 #[test]
462 fn test_diagnostic_with_source_chain() {
463 let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
464 let diag =
465 Diagnostic::new(Severity::Error, "Rendering failed".to_string()).with_source(&root_err);
466
467 assert_eq!(diag.source_chain.len(), 1);
468 assert!(diag.source_chain[0].contains("File not found"));
469 }
470
471 #[test]
472 fn test_diagnostic_serialization() {
473 let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
474 .with_code("E001".to_string())
475 .with_location(Location {
476 file: "test.typ".to_string(),
477 line: 10,
478 column: 5,
479 });
480
481 let json = serde_json::to_string(&diag).unwrap();
482 assert!(json.contains("Test error"));
483 assert!(json.contains("E001"));
484 assert!(json.contains("\"severity\":\"error\""));
485 assert!(json.contains("\"column\":5"));
486 }
487
488 #[test]
489 fn test_render_error_diagnostics_extraction() {
490 let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
491 let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
492
493 let err = RenderError::CompilationFailed {
494 diags: vec![diag1, diag2],
495 };
496
497 let diags = err.diagnostics();
498 assert_eq!(diags.len(), 2);
499 }
500
501 #[test]
502 fn test_diagnostic_fmt_pretty() {
503 let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
504 .with_code("W001".to_string())
505 .with_location(Location {
506 file: "input.md".to_string(),
507 line: 5,
508 column: 10,
509 })
510 .with_hint("Use the new field name instead".to_string());
511
512 let output = diag.fmt_pretty();
513 assert!(output.contains("[WARN]"));
514 assert!(output.contains("Deprecated field used"));
515 assert!(output.contains("W001"));
516 assert!(output.contains("input.md:5:10"));
517 assert!(output.contains("hint:"));
518 }
519
520 #[test]
521 fn test_diagnostic_fmt_pretty_with_source() {
522 let root_err = std::io::Error::other("Underlying error");
523 let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
524 .with_code("E002".to_string())
525 .with_source(&root_err);
526
527 let output = diag.fmt_pretty_with_source();
528 assert!(output.contains("[ERROR]"));
529 assert!(output.contains("Top-level error"));
530 assert!(output.contains("cause 1:"));
531 assert!(output.contains("Underlying error"));
532 }
533
534 #[test]
535 fn test_render_result_with_warnings() {
536 let artifacts = vec![];
537 let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
538
539 let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
540
541 assert_eq!(result.warnings.len(), 1);
542 assert_eq!(result.warnings[0].message, "Test warning");
543 }
544}