1use crate::OutputFormat;
133
134pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
136
137pub const MAX_YAML_SIZE: usize = 1024 * 1024;
139
140pub const MAX_NESTING_DEPTH: usize = 100;
142
143pub const MAX_YAML_DEPTH: usize = 100;
146
147pub const MAX_CARD_COUNT: usize = 1000;
150
151pub const MAX_FIELD_COUNT: usize = 1000;
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
157pub enum Severity {
158 Error,
160 Warning,
162 Note,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
168pub struct Location {
169 pub file: String,
171 pub line: u32,
173 pub col: u32,
175}
176
177#[derive(Debug, serde::Serialize)]
179pub struct Diagnostic {
180 pub severity: Severity,
182 pub code: Option<String>,
184 pub message: String,
186 pub primary: Option<Location>,
188 pub hint: Option<String>,
190 #[serde(skip)]
194 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
195}
196
197impl Diagnostic {
198 pub fn new(severity: Severity, message: String) -> Self {
200 Self {
201 severity,
202 code: None,
203 message,
204 primary: None,
205 hint: None,
206 source: None,
207 }
208 }
209
210 pub fn with_code(mut self, code: String) -> Self {
212 self.code = Some(code);
213 self
214 }
215
216 pub fn with_location(mut self, location: Location) -> Self {
218 self.primary = Some(location);
219 self
220 }
221
222 pub fn with_hint(mut self, hint: String) -> Self {
224 self.hint = Some(hint);
225 self
226 }
227
228 pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
230 self.source = Some(source);
231 self
232 }
233
234 pub(crate) fn clone_without_source(&self) -> Self {
236 Self {
237 severity: self.severity,
238 code: self.code.clone(),
239 message: self.message.clone(),
240 primary: self.primary.clone(),
241 hint: self.hint.clone(),
242 source: None,
243 }
244 }
245
246 pub fn source_chain(&self) -> Vec<String> {
248 let mut chain = Vec::new();
249 let mut current_source = self
250 .source
251 .as_ref()
252 .map(|b| b.as_ref() as &dyn std::error::Error);
253 while let Some(err) = current_source {
254 chain.push(err.to_string());
255 current_source = err.source();
256 }
257 chain
258 }
259
260 pub fn fmt_pretty(&self) -> String {
262 let mut result = format!(
263 "[{}] {}",
264 match self.severity {
265 Severity::Error => "ERROR",
266 Severity::Warning => "WARN",
267 Severity::Note => "NOTE",
268 },
269 self.message
270 );
271
272 if let Some(ref code) = self.code {
273 result.push_str(&format!(" ({})", code));
274 }
275
276 if let Some(ref loc) = self.primary {
277 result.push_str(&format!("\n --> {}:{}:{}", loc.file, loc.line, loc.col));
278 }
279
280 if let Some(ref hint) = self.hint {
281 result.push_str(&format!("\n hint: {}", hint));
282 }
283
284 result
285 }
286
287 pub fn fmt_pretty_with_source(&self) -> String {
289 let mut result = self.fmt_pretty();
290
291 for (i, cause) in self.source_chain().iter().enumerate() {
292 result.push_str(&format!("\n cause {}: {}", i + 1, cause));
293 }
294
295 result
296 }
297}
298
299impl std::fmt::Display for Diagnostic {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 write!(f, "{}", self.message)
302 }
303}
304
305#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
312pub struct SerializableDiagnostic {
313 pub severity: Severity,
315 pub code: Option<String>,
317 pub message: String,
319 pub primary: Option<Location>,
321 pub hint: Option<String>,
323 pub source_chain: Vec<String>,
325}
326
327impl From<Diagnostic> for SerializableDiagnostic {
328 fn from(diag: Diagnostic) -> Self {
329 let source_chain = diag.source_chain();
330 Self {
331 severity: diag.severity,
332 code: diag.code,
333 message: diag.message,
334 primary: diag.primary,
335 hint: diag.hint,
336 source_chain,
337 }
338 }
339}
340
341impl From<&Diagnostic> for SerializableDiagnostic {
342 fn from(diag: &Diagnostic) -> Self {
343 Self {
344 severity: diag.severity,
345 code: diag.code.clone(),
346 message: diag.message.clone(),
347 primary: diag.primary.clone(),
348 hint: diag.hint.clone(),
349 source_chain: diag.source_chain(),
350 }
351 }
352}
353
354#[derive(thiserror::Error, Debug)]
356pub enum ParseError {
357 #[error("Input too large: {size} bytes (max: {max} bytes)")]
359 InputTooLarge {
360 size: usize,
362 max: usize,
364 },
365
366 #[error("YAML parsing error: {0}")]
368 YamlError(#[from] serde_saphyr::Error),
369
370 #[error("JSON error: {0}")]
372 JsonError(#[from] serde_json::Error),
373
374 #[error("Invalid YAML structure: {0}")]
376 InvalidStructure(String),
377
378 #[error("{}", .diag.message)]
380 MissingCardDirective {
381 diag: Box<Diagnostic>,
383 },
384
385 #[error("YAML error at line {line}: {message}")]
387 YamlErrorWithLocation {
388 message: String,
390 line: usize,
392 block_index: usize,
394 },
395
396 #[error("{0}")]
398 Other(String),
399}
400
401impl ParseError {
402 pub fn missing_card_directive() -> Self {
404 let diag = Diagnostic::new(
405 Severity::Error,
406 "Inline metadata block missing CARD directive".to_string(),
407 )
408 .with_code("parse::missing_card".to_string())
409 .with_hint(
410 "Add 'CARD: <card_type>' to specify which card this block belongs to. \
411 Example:\n---\nCARD: my_card_type\nfield: value\n---"
412 .to_string(),
413 );
414 ParseError::MissingCardDirective {
415 diag: Box::new(diag),
416 }
417 }
418
419 pub fn to_diagnostic(&self) -> Diagnostic {
421 match self {
422 ParseError::MissingCardDirective { diag } => Diagnostic {
423 severity: diag.severity,
424 code: diag.code.clone(),
425 message: diag.message.clone(),
426 primary: diag.primary.clone(),
427 hint: diag.hint.clone(),
428 source: None, },
430 ParseError::InputTooLarge { size, max } => Diagnostic::new(
431 Severity::Error,
432 format!("Input too large: {} bytes (max: {} bytes)", size, max),
433 )
434 .with_code("parse::input_too_large".to_string()),
435 ParseError::YamlError(e) => {
436 Diagnostic::new(Severity::Error, format!("YAML parsing error: {}", e))
437 .with_code("parse::yaml_error".to_string())
438 } ParseError::JsonError(e) => {
440 Diagnostic::new(Severity::Error, format!("JSON conversion error: {}", e))
441 .with_code("parse::json_error".to_string())
442 }
443 ParseError::InvalidStructure(msg) => Diagnostic::new(Severity::Error, msg.clone())
444 .with_code("parse::invalid_structure".to_string()),
445 ParseError::YamlErrorWithLocation {
446 message,
447 line,
448 block_index,
449 } => Diagnostic::new(
450 Severity::Error,
451 format!(
452 "YAML error at line {} (block {}): {}",
453 line, block_index, message
454 ),
455 )
456 .with_code("parse::yaml_error_with_location".to_string()),
457 ParseError::Other(msg) => Diagnostic::new(Severity::Error, msg.clone()),
458 }
459 }
460}
461
462impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
463 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
464 ParseError::Other(err.to_string())
465 }
466}
467
468impl From<String> for ParseError {
469 fn from(msg: String) -> Self {
470 ParseError::Other(msg)
471 }
472}
473
474impl From<&str> for ParseError {
475 fn from(msg: &str) -> Self {
476 ParseError::Other(msg.to_string())
477 }
478}
479
480#[derive(thiserror::Error, Debug)]
482pub enum RenderError {
483 #[error("{diag}")]
485 EngineCreation {
486 diag: Box<Diagnostic>,
488 },
489
490 #[error("{diag}")]
492 InvalidFrontmatter {
493 diag: Box<Diagnostic>,
495 },
496
497 #[error("{diag}")]
499 TemplateFailed {
500 diag: Box<Diagnostic>,
502 },
503
504 #[error("Backend compilation failed with {} error(s)", diags.len())]
506 CompilationFailed {
507 diags: Vec<Diagnostic>,
509 },
510
511 #[error("{diag}")]
513 FormatNotSupported {
514 diag: Box<Diagnostic>,
516 },
517
518 #[error("{diag}")]
520 UnsupportedBackend {
521 diag: Box<Diagnostic>,
523 },
524
525 #[error("{diag}")]
527 DynamicAssetCollision {
528 diag: Box<Diagnostic>,
530 },
531
532 #[error("{diag}")]
534 DynamicFontCollision {
535 diag: Box<Diagnostic>,
537 },
538
539 #[error("{diag}")]
541 InputTooLarge {
542 diag: Box<Diagnostic>,
544 },
545
546 #[error("{diag}")]
548 YamlTooLarge {
549 diag: Box<Diagnostic>,
551 },
552
553 #[error("{diag}")]
555 NestingTooDeep {
556 diag: Box<Diagnostic>,
558 },
559
560 #[error("{diag}")]
562 ValidationFailed {
563 diag: Box<Diagnostic>,
565 },
566
567 #[error("{diag}")]
569 InvalidSchema {
570 diag: Box<Diagnostic>,
572 },
573
574 #[error("{diag}")]
576 QuillConfig {
577 diag: Box<Diagnostic>,
579 },
580
581 #[error("{diag}")]
583 VersionNotFound {
584 diag: Box<Diagnostic>,
586 },
587
588 #[error("{diag}")]
590 QuillNotFound {
591 diag: Box<Diagnostic>,
593 },
594
595 #[error("{diag}")]
597 InvalidVersion {
598 diag: Box<Diagnostic>,
600 },
601
602 #[error("{diag}")]
604 NoBackend {
605 diag: Box<Diagnostic>,
607 },
608}
609
610impl RenderError {
611 pub fn diagnostics(&self) -> Vec<&Diagnostic> {
613 match self {
614 RenderError::CompilationFailed { diags } => diags.iter().collect(),
615 RenderError::EngineCreation { diag }
616 | RenderError::InvalidFrontmatter { diag }
617 | RenderError::TemplateFailed { diag }
618 | RenderError::FormatNotSupported { diag }
619 | RenderError::UnsupportedBackend { diag }
620 | RenderError::DynamicAssetCollision { diag }
621 | RenderError::DynamicFontCollision { diag }
622 | RenderError::InputTooLarge { diag }
623 | RenderError::YamlTooLarge { diag }
624 | RenderError::NestingTooDeep { diag }
625 | RenderError::ValidationFailed { diag }
626 | RenderError::InvalidSchema { diag }
627 | RenderError::QuillConfig { diag }
628 | RenderError::VersionNotFound { diag }
629 | RenderError::QuillNotFound { diag }
630 | RenderError::InvalidVersion { diag }
631 | RenderError::NoBackend { diag } => vec![diag.as_ref()],
632 }
633 }
634}
635
636impl From<ParseError> for RenderError {
638 fn from(err: ParseError) -> Self {
639 RenderError::InvalidFrontmatter {
640 diag: Box::new(
641 Diagnostic::new(Severity::Error, err.to_string())
642 .with_code("parse::error".to_string()),
643 ),
644 }
645 }
646}
647
648#[derive(Debug)]
650pub struct RenderResult {
651 pub artifacts: Vec<crate::Artifact>,
653 pub warnings: Vec<Diagnostic>,
655 pub output_format: OutputFormat,
657}
658
659impl RenderResult {
660 pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
662 Self {
663 artifacts,
664 warnings: Vec::new(),
665 output_format,
666 }
667 }
668
669 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
671 self.warnings.push(warning);
672 self
673 }
674}
675
676pub fn print_errors(err: &RenderError) {
678 match err {
679 RenderError::CompilationFailed { diags } => {
680 for d in diags {
681 eprintln!("{}", d.fmt_pretty());
682 }
683 }
684 RenderError::TemplateFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
685 RenderError::InvalidFrontmatter { diag } => eprintln!("{}", diag.fmt_pretty()),
686 RenderError::EngineCreation { diag } => eprintln!("{}", diag.fmt_pretty()),
687 RenderError::FormatNotSupported { diag } => eprintln!("{}", diag.fmt_pretty()),
688 RenderError::UnsupportedBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
689 RenderError::DynamicAssetCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
690 RenderError::DynamicFontCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
691 RenderError::InputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
692 RenderError::YamlTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
693 RenderError::NestingTooDeep { diag } => eprintln!("{}", diag.fmt_pretty()),
694 RenderError::ValidationFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
695 RenderError::InvalidSchema { diag } => eprintln!("{}", diag.fmt_pretty()),
696 RenderError::QuillConfig { diag } => eprintln!("{}", diag.fmt_pretty()),
697 RenderError::VersionNotFound { diag } => eprintln!("{}", diag.fmt_pretty()),
698 RenderError::QuillNotFound { diag } => eprintln!("{}", diag.fmt_pretty()),
699 RenderError::InvalidVersion { diag } => eprintln!("{}", diag.fmt_pretty()),
700 RenderError::NoBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 #[test]
709 fn test_diagnostic_with_source_chain() {
710 let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
711 let diag = Diagnostic::new(Severity::Error, "Rendering failed".to_string())
712 .with_source(Box::new(root_err));
713
714 let chain = diag.source_chain();
715 assert_eq!(chain.len(), 1);
716 assert!(chain[0].contains("File not found"));
717 }
718
719 #[test]
720 fn test_diagnostic_serialization() {
721 let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
722 .with_code("E001".to_string())
723 .with_location(Location {
724 file: "test.typ".to_string(),
725 line: 10,
726 col: 5,
727 });
728
729 let serializable: SerializableDiagnostic = diag.into();
730 let json = serde_json::to_string(&serializable).unwrap();
731 assert!(json.contains("Test error"));
732 assert!(json.contains("E001"));
733 }
734
735 #[test]
736 fn test_render_error_diagnostics_extraction() {
737 let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
738 let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
739
740 let err = RenderError::CompilationFailed {
741 diags: vec![diag1, diag2],
742 };
743
744 let diags = err.diagnostics();
745 assert_eq!(diags.len(), 2);
746 }
747
748 #[test]
749 fn test_diagnostic_fmt_pretty() {
750 let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
751 .with_code("W001".to_string())
752 .with_location(Location {
753 file: "input.md".to_string(),
754 line: 5,
755 col: 10,
756 })
757 .with_hint("Use the new field name instead".to_string());
758
759 let output = diag.fmt_pretty();
760 assert!(output.contains("[WARN]"));
761 assert!(output.contains("Deprecated field used"));
762 assert!(output.contains("W001"));
763 assert!(output.contains("input.md:5:10"));
764 assert!(output.contains("hint:"));
765 }
766
767 #[test]
768 fn test_diagnostic_fmt_pretty_with_source() {
769 let root_err = std::io::Error::other("Underlying error");
770 let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
771 .with_code("E002".to_string())
772 .with_source(Box::new(root_err));
773
774 let output = diag.fmt_pretty_with_source();
775 assert!(output.contains("[ERROR]"));
776 assert!(output.contains("Top-level error"));
777 assert!(output.contains("cause 1:"));
778 assert!(output.contains("Underlying error"));
779 }
780
781 #[test]
782 fn test_render_result_with_warnings() {
783 let artifacts = vec![];
784 let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
785
786 let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
787
788 assert_eq!(result.warnings.len(), 1);
789 assert_eq!(result.warnings[0].message, "Test warning");
790 }
791}