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("JSON error: {0}")]
352 JsonError(#[from] serde_json::Error),
353
354 #[error("Invalid YAML structure: {0}")]
356 InvalidStructure(String),
357
358 #[error("{}", .diag.message)]
360 MissingCardDirective {
361 diag: Diagnostic,
363 },
364
365 #[error("{0}")]
367 Other(String),
368}
369
370impl ParseError {
371 pub fn missing_card_directive() -> Self {
373 let diag = Diagnostic::new(
374 Severity::Error,
375 "Inline metadata block missing CARD directive".to_string(),
376 )
377 .with_code("parse::missing_card".to_string())
378 .with_hint(
379 "Add 'CARD: <card_type>' to specify which card this block belongs to. \
380 Example:\n---\nCARD: my_card_type\nfield: value\n---"
381 .to_string(),
382 );
383 ParseError::MissingCardDirective { diag }
384 }
385
386 pub fn to_diagnostic(&self) -> Diagnostic {
388 match self {
389 ParseError::MissingCardDirective { diag } => Diagnostic {
390 severity: diag.severity,
391 code: diag.code.clone(),
392 message: diag.message.clone(),
393 primary: diag.primary.clone(),
394 hint: diag.hint.clone(),
395 source: None, },
397 ParseError::InputTooLarge { size, max } => Diagnostic::new(
398 Severity::Error,
399 format!("Input too large: {} bytes (max: {} bytes)", size, max),
400 )
401 .with_code("parse::input_too_large".to_string()),
402 ParseError::YamlError(e) => {
403 Diagnostic::new(Severity::Error, format!("YAML parsing error: {}", e))
404 .with_code("parse::yaml_error".to_string())
405 } ParseError::JsonError(e) => {
407 Diagnostic::new(Severity::Error, format!("JSON conversion error: {}", e))
408 .with_code("parse::json_error".to_string())
409 }
410 ParseError::InvalidStructure(msg) => Diagnostic::new(Severity::Error, msg.clone())
411 .with_code("parse::invalid_structure".to_string()),
412 ParseError::Other(msg) => Diagnostic::new(Severity::Error, msg.clone()),
413 }
414 }
415}
416
417impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
418 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
419 ParseError::Other(err.to_string())
420 }
421}
422
423impl From<String> for ParseError {
424 fn from(msg: String) -> Self {
425 ParseError::Other(msg)
426 }
427}
428
429impl From<&str> for ParseError {
430 fn from(msg: &str) -> Self {
431 ParseError::Other(msg.to_string())
432 }
433}
434
435#[derive(thiserror::Error, Debug)]
437pub enum RenderError {
438 #[error("{diag}")]
440 EngineCreation {
441 diag: Diagnostic,
443 },
444
445 #[error("{diag}")]
447 InvalidFrontmatter {
448 diag: Diagnostic,
450 },
451
452 #[error("{diag}")]
454 TemplateFailed {
455 diag: Diagnostic,
457 },
458
459 #[error("Backend compilation failed with {} error(s)", diags.len())]
461 CompilationFailed {
462 diags: Vec<Diagnostic>,
464 },
465
466 #[error("{diag}")]
468 FormatNotSupported {
469 diag: Diagnostic,
471 },
472
473 #[error("{diag}")]
475 UnsupportedBackend {
476 diag: Diagnostic,
478 },
479
480 #[error("{diag}")]
482 DynamicAssetCollision {
483 diag: Diagnostic,
485 },
486
487 #[error("{diag}")]
489 DynamicFontCollision {
490 diag: Diagnostic,
492 },
493
494 #[error("{diag}")]
496 InputTooLarge {
497 diag: Diagnostic,
499 },
500
501 #[error("{diag}")]
503 YamlTooLarge {
504 diag: Diagnostic,
506 },
507
508 #[error("{diag}")]
510 NestingTooDeep {
511 diag: Diagnostic,
513 },
514
515 #[error("{diag}")]
517 OutputTooLarge {
518 diag: Diagnostic,
520 },
521
522 #[error("{diag}")]
524 ValidationFailed {
525 diag: Diagnostic,
527 },
528
529 #[error("{diag}")]
531 InvalidSchema {
532 diag: Diagnostic,
534 },
535
536 #[error("{diag}")]
538 QuillConfig {
539 diag: Diagnostic,
541 },
542}
543
544impl RenderError {
545 pub fn diagnostics(&self) -> Vec<&Diagnostic> {
547 match self {
548 RenderError::CompilationFailed { diags } => diags.iter().collect(),
549 RenderError::EngineCreation { diag }
550 | RenderError::InvalidFrontmatter { diag }
551 | RenderError::TemplateFailed { diag }
552 | RenderError::FormatNotSupported { diag }
553 | RenderError::UnsupportedBackend { diag }
554 | RenderError::DynamicAssetCollision { diag }
555 | RenderError::DynamicFontCollision { diag }
556 | RenderError::InputTooLarge { diag }
557 | RenderError::YamlTooLarge { diag }
558 | RenderError::NestingTooDeep { diag }
559 | RenderError::OutputTooLarge { diag }
560 | RenderError::ValidationFailed { diag }
561 | RenderError::InvalidSchema { diag }
562 | RenderError::QuillConfig { diag } => vec![diag],
563 }
564 }
565}
566
567#[derive(Debug)]
569pub struct RenderResult {
570 pub artifacts: Vec<crate::Artifact>,
572 pub warnings: Vec<Diagnostic>,
574 pub output_format: OutputFormat,
576}
577
578impl RenderResult {
579 pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
581 Self {
582 artifacts,
583 warnings: Vec::new(),
584 output_format,
585 }
586 }
587
588 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
590 self.warnings.push(warning);
591 self
592 }
593}
594
595impl From<minijinja::Error> for RenderError {
597 fn from(e: minijinja::Error) -> Self {
598 let loc = e.line().map(|line| Location {
600 file: e.name().unwrap_or("template").to_string(),
601 line: line as u32,
602 col: e.range().map(|r| r.start as u32).unwrap_or(0),
604 });
605
606 let hint = generate_minijinja_hint(&e);
608
609 let mut diag = Diagnostic::new(Severity::Error, e.to_string())
611 .with_code(format!("minijinja::{:?}", e.kind()));
612
613 if let Some(loc) = loc {
614 diag = diag.with_location(loc);
615 }
616
617 if let Some(hint) = hint {
618 diag = diag.with_hint(hint);
619 }
620
621 diag = diag.with_source(Box::new(e));
623
624 RenderError::TemplateFailed { diag }
625 }
626}
627
628fn generate_minijinja_hint(e: &minijinja::Error) -> Option<String> {
630 use minijinja::ErrorKind;
631
632 match e.kind() {
633 ErrorKind::UndefinedError => {
634 Some("Check variable spelling and ensure it's defined in frontmatter".to_string())
635 }
636 ErrorKind::InvalidOperation => {
637 Some("Check that you're using the correct filter or operator for this type".to_string())
638 }
639 ErrorKind::SyntaxError => Some(
640 "Check template syntax - look for unclosed tags or invalid expressions".to_string(),
641 ),
642 _ => e.detail().map(|d| d.to_string()),
643 }
644}
645
646pub fn print_errors(err: &RenderError) {
648 match err {
649 RenderError::CompilationFailed { diags } => {
650 for d in diags {
651 eprintln!("{}", d.fmt_pretty());
652 }
653 }
654 RenderError::TemplateFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
655 RenderError::InvalidFrontmatter { diag } => eprintln!("{}", diag.fmt_pretty()),
656 RenderError::EngineCreation { diag } => eprintln!("{}", diag.fmt_pretty()),
657 RenderError::FormatNotSupported { diag } => eprintln!("{}", diag.fmt_pretty()),
658 RenderError::UnsupportedBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
659 RenderError::DynamicAssetCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
660 RenderError::DynamicFontCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
661 RenderError::InputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
662 RenderError::YamlTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
663 RenderError::NestingTooDeep { diag } => eprintln!("{}", diag.fmt_pretty()),
664 RenderError::OutputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
665 RenderError::ValidationFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
666 RenderError::InvalidSchema { diag } => eprintln!("{}", diag.fmt_pretty()),
667 RenderError::QuillConfig { diag } => eprintln!("{}", diag.fmt_pretty()),
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
676 fn test_diagnostic_with_source_chain() {
677 let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
678 let diag = Diagnostic::new(Severity::Error, "Rendering failed".to_string())
679 .with_source(Box::new(root_err));
680
681 let chain = diag.source_chain();
682 assert_eq!(chain.len(), 1);
683 assert!(chain[0].contains("File not found"));
684 }
685
686 #[test]
687 fn test_diagnostic_serialization() {
688 let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
689 .with_code("E001".to_string())
690 .with_location(Location {
691 file: "test.typ".to_string(),
692 line: 10,
693 col: 5,
694 });
695
696 let serializable: SerializableDiagnostic = diag.into();
697 let json = serde_json::to_string(&serializable).unwrap();
698 assert!(json.contains("Test error"));
699 assert!(json.contains("E001"));
700 }
701
702 #[test]
703 fn test_render_error_diagnostics_extraction() {
704 let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
705 let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
706
707 let err = RenderError::CompilationFailed {
708 diags: vec![diag1, diag2],
709 };
710
711 let diags = err.diagnostics();
712 assert_eq!(diags.len(), 2);
713 }
714
715 #[test]
716 fn test_diagnostic_fmt_pretty() {
717 let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
718 .with_code("W001".to_string())
719 .with_location(Location {
720 file: "input.md".to_string(),
721 line: 5,
722 col: 10,
723 })
724 .with_hint("Use the new field name instead".to_string());
725
726 let output = diag.fmt_pretty();
727 assert!(output.contains("[WARN]"));
728 assert!(output.contains("Deprecated field used"));
729 assert!(output.contains("W001"));
730 assert!(output.contains("input.md:5:10"));
731 assert!(output.contains("hint:"));
732 }
733
734 #[test]
735 fn test_diagnostic_fmt_pretty_with_source() {
736 let root_err = std::io::Error::new(std::io::ErrorKind::Other, "Underlying error");
737 let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
738 .with_code("E002".to_string())
739 .with_source(Box::new(root_err));
740
741 let output = diag.fmt_pretty_with_source();
742 assert!(output.contains("[ERROR]"));
743 assert!(output.contains("Top-level error"));
744 assert!(output.contains("cause 1:"));
745 assert!(output.contains("Underlying error"));
746 }
747
748 #[test]
749 fn test_render_result_with_warnings() {
750 let artifacts = vec![];
751 let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
752
753 let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
754
755 assert_eq!(result.warnings.len(), 1);
756 assert_eq!(result.warnings[0].message, "Test warning");
757 }
758
759 #[test]
760 fn test_minijinja_error_conversion() {
761 let template_str = "{{ undefined_var }}";
763 let mut env = minijinja::Environment::new();
764 env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
765
766 let result = env.render_str(template_str, minijinja::context! {});
767 assert!(
768 result.is_err(),
769 "Expected rendering to fail with undefined variable"
770 );
771
772 let minijinja_err = result.unwrap_err();
773 let render_err: RenderError = minijinja_err.into();
774
775 match render_err {
776 RenderError::TemplateFailed { diag } => {
777 assert_eq!(diag.severity, Severity::Error);
778 assert!(diag.code.is_some());
779 assert!(diag.hint.is_some());
780 assert!(diag.source.is_some());
781 }
782 _ => panic!("Expected TemplateFailed error"),
783 }
784 }
785}