1use crate::OutputFormat;
134
135pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
137
138pub const MAX_YAML_SIZE: usize = 1024 * 1024;
140
141pub const MAX_NESTING_DEPTH: usize = 100;
143
144pub const MAX_TEMPLATE_OUTPUT: usize = 50 * 1024 * 1024;
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149pub enum Severity {
150 Error,
152 Warning,
154 Note,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
160pub struct Location {
161 pub file: String,
163 pub line: u32,
165 pub col: u32,
167}
168
169#[derive(Debug, serde::Serialize)]
171pub struct Diagnostic {
172 pub severity: Severity,
174 pub code: Option<String>,
176 pub message: String,
178 pub primary: Option<Location>,
180 pub hint: Option<String>,
182 #[serde(skip)]
186 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
187}
188
189impl Diagnostic {
190 pub fn new(severity: Severity, message: String) -> Self {
192 Self {
193 severity,
194 code: None,
195 message,
196 primary: None,
197 hint: None,
198 source: None,
199 }
200 }
201
202 pub fn with_code(mut self, code: String) -> Self {
204 self.code = Some(code);
205 self
206 }
207
208 pub fn with_location(mut self, location: Location) -> Self {
210 self.primary = Some(location);
211 self
212 }
213
214 pub fn with_hint(mut self, hint: String) -> Self {
216 self.hint = Some(hint);
217 self
218 }
219
220 pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
222 self.source = Some(source);
223 self
224 }
225
226 pub fn source_chain(&self) -> Vec<String> {
228 let mut chain = Vec::new();
229 let mut current_source = self
230 .source
231 .as_ref()
232 .map(|b| b.as_ref() as &dyn std::error::Error);
233 while let Some(err) = current_source {
234 chain.push(err.to_string());
235 current_source = err.source();
236 }
237 chain
238 }
239
240 pub fn fmt_pretty(&self) -> String {
242 let mut result = format!(
243 "[{}] {}",
244 match self.severity {
245 Severity::Error => "ERROR",
246 Severity::Warning => "WARN",
247 Severity::Note => "NOTE",
248 },
249 self.message
250 );
251
252 if let Some(ref code) = self.code {
253 result.push_str(&format!(" ({})", code));
254 }
255
256 if let Some(ref loc) = self.primary {
257 result.push_str(&format!("\n --> {}:{}:{}", loc.file, loc.line, loc.col));
258 }
259
260 if let Some(ref hint) = self.hint {
261 result.push_str(&format!("\n hint: {}", hint));
262 }
263
264 result
265 }
266
267 pub fn fmt_pretty_with_source(&self) -> String {
269 let mut result = self.fmt_pretty();
270
271 for (i, cause) in self.source_chain().iter().enumerate() {
272 result.push_str(&format!("\n cause {}: {}", i + 1, cause));
273 }
274
275 result
276 }
277}
278
279impl std::fmt::Display for Diagnostic {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 write!(f, "{}", self.message)
282 }
283}
284
285#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
292pub struct SerializableDiagnostic {
293 pub severity: Severity,
295 pub code: Option<String>,
297 pub message: String,
299 pub primary: Option<Location>,
301 pub hint: Option<String>,
303 pub source_chain: Vec<String>,
305}
306
307impl From<Diagnostic> for SerializableDiagnostic {
308 fn from(diag: Diagnostic) -> Self {
309 let source_chain = diag.source_chain();
310 Self {
311 severity: diag.severity,
312 code: diag.code,
313 message: diag.message,
314 primary: diag.primary,
315 hint: diag.hint,
316 source_chain,
317 }
318 }
319}
320
321impl From<&Diagnostic> for SerializableDiagnostic {
322 fn from(diag: &Diagnostic) -> Self {
323 Self {
324 severity: diag.severity,
325 code: diag.code.clone(),
326 message: diag.message.clone(),
327 primary: diag.primary.clone(),
328 hint: diag.hint.clone(),
329 source_chain: diag.source_chain(),
330 }
331 }
332}
333
334#[derive(thiserror::Error, Debug)]
336pub enum ParseError {
337 #[error("Input too large: {size} bytes (max: {max} bytes)")]
339 InputTooLarge {
340 size: usize,
342 max: usize,
344 },
345
346 #[error("YAML parsing error: {0}")]
348 YamlError(#[from] serde_yaml::Error),
349
350 #[error("Invalid YAML structure: {0}")]
352 InvalidStructure(String),
353
354 #[error("{}", .diag.message)]
356 MissingCardDirective {
357 diag: Diagnostic,
359 },
360
361 #[error("{0}")]
363 Other(String),
364}
365
366impl ParseError {
367 pub fn missing_card_directive() -> Self {
369 let diag = Diagnostic::new(
370 Severity::Error,
371 "Inline metadata block missing CARD directive".to_string(),
372 )
373 .with_code("parse::missing_card".to_string())
374 .with_hint(
375 "Add 'CARD: <card_type>' to specify which card this block belongs to. \
376 Example:\n---\nCARD: my_card_type\nfield: value\n---"
377 .to_string(),
378 );
379 ParseError::MissingCardDirective { diag }
380 }
381}
382
383impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
384 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
385 ParseError::Other(err.to_string())
386 }
387}
388
389impl From<String> for ParseError {
390 fn from(msg: String) -> Self {
391 ParseError::Other(msg)
392 }
393}
394
395#[derive(thiserror::Error, Debug)]
397pub enum RenderError {
398 #[error("{diag}")]
400 EngineCreation {
401 diag: Diagnostic,
403 },
404
405 #[error("{diag}")]
407 InvalidFrontmatter {
408 diag: Diagnostic,
410 },
411
412 #[error("{diag}")]
414 TemplateFailed {
415 diag: Diagnostic,
417 },
418
419 #[error("Backend compilation failed with {} error(s)", diags.len())]
421 CompilationFailed {
422 diags: Vec<Diagnostic>,
424 },
425
426 #[error("{diag}")]
428 FormatNotSupported {
429 diag: Diagnostic,
431 },
432
433 #[error("{diag}")]
435 UnsupportedBackend {
436 diag: Diagnostic,
438 },
439
440 #[error("{diag}")]
442 DynamicAssetCollision {
443 diag: Diagnostic,
445 },
446
447 #[error("{diag}")]
449 DynamicFontCollision {
450 diag: Diagnostic,
452 },
453
454 #[error("{diag}")]
456 InputTooLarge {
457 diag: Diagnostic,
459 },
460
461 #[error("{diag}")]
463 YamlTooLarge {
464 diag: Diagnostic,
466 },
467
468 #[error("{diag}")]
470 NestingTooDeep {
471 diag: Diagnostic,
473 },
474
475 #[error("{diag}")]
477 OutputTooLarge {
478 diag: Diagnostic,
480 },
481
482 #[error("{diag}")]
484 ValidationFailed {
485 diag: Diagnostic,
487 },
488
489 #[error("{diag}")]
491 InvalidSchema {
492 diag: Diagnostic,
494 },
495
496 #[error("{diag}")]
498 QuillConfig {
499 diag: Diagnostic,
501 },
502}
503
504impl RenderError {
505 pub fn diagnostics(&self) -> Vec<&Diagnostic> {
507 match self {
508 RenderError::CompilationFailed { diags } => diags.iter().collect(),
509 RenderError::EngineCreation { diag }
510 | RenderError::InvalidFrontmatter { diag }
511 | RenderError::TemplateFailed { diag }
512 | RenderError::FormatNotSupported { diag }
513 | RenderError::UnsupportedBackend { diag }
514 | RenderError::DynamicAssetCollision { diag }
515 | RenderError::DynamicFontCollision { diag }
516 | RenderError::InputTooLarge { diag }
517 | RenderError::YamlTooLarge { diag }
518 | RenderError::NestingTooDeep { diag }
519 | RenderError::OutputTooLarge { diag }
520 | RenderError::ValidationFailed { diag }
521 | RenderError::InvalidSchema { diag }
522 | RenderError::QuillConfig { diag } => vec![diag],
523 }
524 }
525}
526
527#[derive(Debug)]
529pub struct RenderResult {
530 pub artifacts: Vec<crate::Artifact>,
532 pub warnings: Vec<Diagnostic>,
534 pub output_format: OutputFormat,
536}
537
538impl RenderResult {
539 pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
541 Self {
542 artifacts,
543 warnings: Vec::new(),
544 output_format,
545 }
546 }
547
548 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
550 self.warnings.push(warning);
551 self
552 }
553}
554
555impl From<minijinja::Error> for RenderError {
557 fn from(e: minijinja::Error) -> Self {
558 let loc = e.line().map(|line| Location {
560 file: e.name().unwrap_or("template").to_string(),
561 line: line as u32,
562 col: e.range().map(|r| r.start as u32).unwrap_or(0),
564 });
565
566 let hint = generate_minijinja_hint(&e);
568
569 let mut diag = Diagnostic::new(Severity::Error, e.to_string())
571 .with_code(format!("minijinja::{:?}", e.kind()));
572
573 if let Some(loc) = loc {
574 diag = diag.with_location(loc);
575 }
576
577 if let Some(hint) = hint {
578 diag = diag.with_hint(hint);
579 }
580
581 diag = diag.with_source(Box::new(e));
583
584 RenderError::TemplateFailed { diag }
585 }
586}
587
588fn generate_minijinja_hint(e: &minijinja::Error) -> Option<String> {
590 use minijinja::ErrorKind;
591
592 match e.kind() {
593 ErrorKind::UndefinedError => {
594 Some("Check variable spelling and ensure it's defined in frontmatter".to_string())
595 }
596 ErrorKind::InvalidOperation => {
597 Some("Check that you're using the correct filter or operator for this type".to_string())
598 }
599 ErrorKind::SyntaxError => Some(
600 "Check template syntax - look for unclosed tags or invalid expressions".to_string(),
601 ),
602 _ => e.detail().map(|d| d.to_string()),
603 }
604}
605
606pub fn print_errors(err: &RenderError) {
608 match err {
609 RenderError::CompilationFailed { diags } => {
610 for d in diags {
611 eprintln!("{}", d.fmt_pretty());
612 }
613 }
614 RenderError::TemplateFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
615 RenderError::InvalidFrontmatter { diag } => eprintln!("{}", diag.fmt_pretty()),
616 RenderError::EngineCreation { diag } => eprintln!("{}", diag.fmt_pretty()),
617 RenderError::FormatNotSupported { diag } => eprintln!("{}", diag.fmt_pretty()),
618 RenderError::UnsupportedBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
619 RenderError::DynamicAssetCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
620 RenderError::DynamicFontCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
621 RenderError::InputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
622 RenderError::YamlTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
623 RenderError::NestingTooDeep { diag } => eprintln!("{}", diag.fmt_pretty()),
624 RenderError::OutputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
625 RenderError::ValidationFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
626 RenderError::InvalidSchema { diag } => eprintln!("{}", diag.fmt_pretty()),
627 RenderError::QuillConfig { diag } => eprintln!("{}", diag.fmt_pretty()),
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn test_diagnostic_with_source_chain() {
637 let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
638 let diag = Diagnostic::new(Severity::Error, "Rendering failed".to_string())
639 .with_source(Box::new(root_err));
640
641 let chain = diag.source_chain();
642 assert_eq!(chain.len(), 1);
643 assert!(chain[0].contains("File not found"));
644 }
645
646 #[test]
647 fn test_diagnostic_serialization() {
648 let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
649 .with_code("E001".to_string())
650 .with_location(Location {
651 file: "test.typ".to_string(),
652 line: 10,
653 col: 5,
654 });
655
656 let serializable: SerializableDiagnostic = diag.into();
657 let json = serde_json::to_string(&serializable).unwrap();
658 assert!(json.contains("Test error"));
659 assert!(json.contains("E001"));
660 }
661
662 #[test]
663 fn test_render_error_diagnostics_extraction() {
664 let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
665 let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
666
667 let err = RenderError::CompilationFailed {
668 diags: vec![diag1, diag2],
669 };
670
671 let diags = err.diagnostics();
672 assert_eq!(diags.len(), 2);
673 }
674
675 #[test]
676 fn test_diagnostic_fmt_pretty() {
677 let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
678 .with_code("W001".to_string())
679 .with_location(Location {
680 file: "input.md".to_string(),
681 line: 5,
682 col: 10,
683 })
684 .with_hint("Use the new field name instead".to_string());
685
686 let output = diag.fmt_pretty();
687 assert!(output.contains("[WARN]"));
688 assert!(output.contains("Deprecated field used"));
689 assert!(output.contains("W001"));
690 assert!(output.contains("input.md:5:10"));
691 assert!(output.contains("hint:"));
692 }
693
694 #[test]
695 fn test_diagnostic_fmt_pretty_with_source() {
696 let root_err = std::io::Error::new(std::io::ErrorKind::Other, "Underlying error");
697 let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
698 .with_code("E002".to_string())
699 .with_source(Box::new(root_err));
700
701 let output = diag.fmt_pretty_with_source();
702 assert!(output.contains("[ERROR]"));
703 assert!(output.contains("Top-level error"));
704 assert!(output.contains("cause 1:"));
705 assert!(output.contains("Underlying error"));
706 }
707
708 #[test]
709 fn test_render_result_with_warnings() {
710 let artifacts = vec![];
711 let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
712
713 let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
714
715 assert_eq!(result.warnings.len(), 1);
716 assert_eq!(result.warnings[0].message, "Test warning");
717 }
718
719 #[test]
720 fn test_minijinja_error_conversion() {
721 let template_str = "{{ undefined_var }}";
723 let mut env = minijinja::Environment::new();
724 env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
725
726 let result = env.render_str(template_str, minijinja::context! {});
727 assert!(
728 result.is_err(),
729 "Expected rendering to fail with undefined variable"
730 );
731
732 let minijinja_err = result.unwrap_err();
733 let render_err: RenderError = minijinja_err.into();
734
735 match render_err {
736 RenderError::TemplateFailed { diag } => {
737 assert_eq!(diag.severity, Severity::Error);
738 assert!(diag.code.is_some());
739 assert!(diag.hint.is_some());
740 assert!(diag.source.is_some());
741 }
742 _ => panic!("Expected TemplateFailed error"),
743 }
744 }
745}