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 fn source_chain(&self) -> Vec<String> {
236 let mut chain = Vec::new();
237 let mut current_source = self
238 .source
239 .as_ref()
240 .map(|b| b.as_ref() as &dyn std::error::Error);
241 while let Some(err) = current_source {
242 chain.push(err.to_string());
243 current_source = err.source();
244 }
245 chain
246 }
247
248 pub fn fmt_pretty(&self) -> String {
250 let mut result = format!(
251 "[{}] {}",
252 match self.severity {
253 Severity::Error => "ERROR",
254 Severity::Warning => "WARN",
255 Severity::Note => "NOTE",
256 },
257 self.message
258 );
259
260 if let Some(ref code) = self.code {
261 result.push_str(&format!(" ({})", code));
262 }
263
264 if let Some(ref loc) = self.primary {
265 result.push_str(&format!("\n --> {}:{}:{}", loc.file, loc.line, loc.col));
266 }
267
268 if let Some(ref hint) = self.hint {
269 result.push_str(&format!("\n hint: {}", hint));
270 }
271
272 result
273 }
274
275 pub fn fmt_pretty_with_source(&self) -> String {
277 let mut result = self.fmt_pretty();
278
279 for (i, cause) in self.source_chain().iter().enumerate() {
280 result.push_str(&format!("\n cause {}: {}", i + 1, cause));
281 }
282
283 result
284 }
285}
286
287impl std::fmt::Display for Diagnostic {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 write!(f, "{}", self.message)
290 }
291}
292
293#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
300pub struct SerializableDiagnostic {
301 pub severity: Severity,
303 pub code: Option<String>,
305 pub message: String,
307 pub primary: Option<Location>,
309 pub hint: Option<String>,
311 pub source_chain: Vec<String>,
313}
314
315impl From<Diagnostic> for SerializableDiagnostic {
316 fn from(diag: Diagnostic) -> Self {
317 let source_chain = diag.source_chain();
318 Self {
319 severity: diag.severity,
320 code: diag.code,
321 message: diag.message,
322 primary: diag.primary,
323 hint: diag.hint,
324 source_chain,
325 }
326 }
327}
328
329impl From<&Diagnostic> for SerializableDiagnostic {
330 fn from(diag: &Diagnostic) -> Self {
331 Self {
332 severity: diag.severity,
333 code: diag.code.clone(),
334 message: diag.message.clone(),
335 primary: diag.primary.clone(),
336 hint: diag.hint.clone(),
337 source_chain: diag.source_chain(),
338 }
339 }
340}
341
342#[derive(thiserror::Error, Debug)]
344pub enum ParseError {
345 #[error("Input too large: {size} bytes (max: {max} bytes)")]
347 InputTooLarge {
348 size: usize,
350 max: usize,
352 },
353
354 #[error("YAML parsing error: {0}")]
356 YamlError(#[from] serde_saphyr::Error),
357
358 #[error("JSON error: {0}")]
360 JsonError(#[from] serde_json::Error),
361
362 #[error("Invalid YAML structure: {0}")]
364 InvalidStructure(String),
365
366 #[error("{}", .diag.message)]
368 MissingCardDirective {
369 diag: Box<Diagnostic>,
371 },
372
373 #[error("YAML error at line {line}: {message}")]
375 YamlErrorWithLocation {
376 message: String,
378 line: usize,
380 block_index: usize,
382 },
383
384 #[error("{0}")]
386 Other(String),
387}
388
389impl ParseError {
390 pub fn missing_card_directive() -> Self {
392 let diag = Diagnostic::new(
393 Severity::Error,
394 "Inline metadata block missing CARD directive".to_string(),
395 )
396 .with_code("parse::missing_card".to_string())
397 .with_hint(
398 "Add 'CARD: <card_type>' to specify which card this block belongs to. \
399 Example:\n---\nCARD: my_card_type\nfield: value\n---"
400 .to_string(),
401 );
402 ParseError::MissingCardDirective {
403 diag: Box::new(diag),
404 }
405 }
406
407 pub fn to_diagnostic(&self) -> Diagnostic {
409 match self {
410 ParseError::MissingCardDirective { diag } => Diagnostic {
411 severity: diag.severity,
412 code: diag.code.clone(),
413 message: diag.message.clone(),
414 primary: diag.primary.clone(),
415 hint: diag.hint.clone(),
416 source: None, },
418 ParseError::InputTooLarge { size, max } => Diagnostic::new(
419 Severity::Error,
420 format!("Input too large: {} bytes (max: {} bytes)", size, max),
421 )
422 .with_code("parse::input_too_large".to_string()),
423 ParseError::YamlError(e) => {
424 Diagnostic::new(Severity::Error, format!("YAML parsing error: {}", e))
425 .with_code("parse::yaml_error".to_string())
426 } ParseError::JsonError(e) => {
428 Diagnostic::new(Severity::Error, format!("JSON conversion error: {}", e))
429 .with_code("parse::json_error".to_string())
430 }
431 ParseError::InvalidStructure(msg) => Diagnostic::new(Severity::Error, msg.clone())
432 .with_code("parse::invalid_structure".to_string()),
433 ParseError::YamlErrorWithLocation {
434 message,
435 line,
436 block_index,
437 } => Diagnostic::new(
438 Severity::Error,
439 format!(
440 "YAML error at line {} (block {}): {}",
441 line, block_index, message
442 ),
443 )
444 .with_code("parse::yaml_error_with_location".to_string()),
445 ParseError::Other(msg) => Diagnostic::new(Severity::Error, msg.clone()),
446 }
447 }
448}
449
450impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
451 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
452 ParseError::Other(err.to_string())
453 }
454}
455
456impl From<String> for ParseError {
457 fn from(msg: String) -> Self {
458 ParseError::Other(msg)
459 }
460}
461
462impl From<&str> for ParseError {
463 fn from(msg: &str) -> Self {
464 ParseError::Other(msg.to_string())
465 }
466}
467
468#[derive(thiserror::Error, Debug)]
470pub enum RenderError {
471 #[error("{diag}")]
473 EngineCreation {
474 diag: Box<Diagnostic>,
476 },
477
478 #[error("{diag}")]
480 InvalidFrontmatter {
481 diag: Box<Diagnostic>,
483 },
484
485 #[error("{diag}")]
487 TemplateFailed {
488 diag: Box<Diagnostic>,
490 },
491
492 #[error("Backend compilation failed with {} error(s)", diags.len())]
494 CompilationFailed {
495 diags: Vec<Diagnostic>,
497 },
498
499 #[error("{diag}")]
501 FormatNotSupported {
502 diag: Box<Diagnostic>,
504 },
505
506 #[error("{diag}")]
508 UnsupportedBackend {
509 diag: Box<Diagnostic>,
511 },
512
513 #[error("{diag}")]
515 DynamicAssetCollision {
516 diag: Box<Diagnostic>,
518 },
519
520 #[error("{diag}")]
522 DynamicFontCollision {
523 diag: Box<Diagnostic>,
525 },
526
527 #[error("{diag}")]
529 InputTooLarge {
530 diag: Box<Diagnostic>,
532 },
533
534 #[error("{diag}")]
536 YamlTooLarge {
537 diag: Box<Diagnostic>,
539 },
540
541 #[error("{diag}")]
543 NestingTooDeep {
544 diag: Box<Diagnostic>,
546 },
547
548 #[error("{diag}")]
550 ValidationFailed {
551 diag: Box<Diagnostic>,
553 },
554
555 #[error("{diag}")]
557 InvalidSchema {
558 diag: Box<Diagnostic>,
560 },
561
562 #[error("{diag}")]
564 QuillConfig {
565 diag: Box<Diagnostic>,
567 },
568
569 #[error("{diag}")]
571 VersionNotFound {
572 diag: Box<Diagnostic>,
574 },
575
576 #[error("{diag}")]
578 QuillNotFound {
579 diag: Box<Diagnostic>,
581 },
582
583 #[error("{diag}")]
585 InvalidVersion {
586 diag: Box<Diagnostic>,
588 },
589}
590
591impl RenderError {
592 pub fn diagnostics(&self) -> Vec<&Diagnostic> {
594 match self {
595 RenderError::CompilationFailed { diags } => diags.iter().collect(),
596 RenderError::EngineCreation { diag }
597 | RenderError::InvalidFrontmatter { diag }
598 | RenderError::TemplateFailed { diag }
599 | RenderError::FormatNotSupported { diag }
600 | RenderError::UnsupportedBackend { diag }
601 | RenderError::DynamicAssetCollision { diag }
602 | RenderError::DynamicFontCollision { diag }
603 | RenderError::InputTooLarge { diag }
604 | RenderError::YamlTooLarge { diag }
605 | RenderError::NestingTooDeep { diag }
606 | RenderError::ValidationFailed { diag }
607 | RenderError::InvalidSchema { diag }
608 | RenderError::QuillConfig { diag }
609 | RenderError::VersionNotFound { diag }
610 | RenderError::QuillNotFound { diag }
611 | RenderError::InvalidVersion { diag } => vec![diag.as_ref()],
612 }
613 }
614}
615
616impl From<ParseError> for RenderError {
618 fn from(err: ParseError) -> Self {
619 RenderError::InvalidFrontmatter {
620 diag: Box::new(
621 Diagnostic::new(Severity::Error, err.to_string())
622 .with_code("parse::error".to_string()),
623 ),
624 }
625 }
626}
627
628#[derive(Debug)]
630pub struct RenderResult {
631 pub artifacts: Vec<crate::Artifact>,
633 pub warnings: Vec<Diagnostic>,
635 pub output_format: OutputFormat,
637}
638
639impl RenderResult {
640 pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
642 Self {
643 artifacts,
644 warnings: Vec::new(),
645 output_format,
646 }
647 }
648
649 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
651 self.warnings.push(warning);
652 self
653 }
654}
655
656pub fn print_errors(err: &RenderError) {
658 match err {
659 RenderError::CompilationFailed { diags } => {
660 for d in diags {
661 eprintln!("{}", d.fmt_pretty());
662 }
663 }
664 RenderError::TemplateFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
665 RenderError::InvalidFrontmatter { diag } => eprintln!("{}", diag.fmt_pretty()),
666 RenderError::EngineCreation { diag } => eprintln!("{}", diag.fmt_pretty()),
667 RenderError::FormatNotSupported { diag } => eprintln!("{}", diag.fmt_pretty()),
668 RenderError::UnsupportedBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
669 RenderError::DynamicAssetCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
670 RenderError::DynamicFontCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
671 RenderError::InputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
672 RenderError::YamlTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
673 RenderError::NestingTooDeep { diag } => eprintln!("{}", diag.fmt_pretty()),
674 RenderError::ValidationFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
675 RenderError::InvalidSchema { diag } => eprintln!("{}", diag.fmt_pretty()),
676 RenderError::QuillConfig { diag } => eprintln!("{}", diag.fmt_pretty()),
677 RenderError::VersionNotFound { diag } => eprintln!("{}", diag.fmt_pretty()),
678 RenderError::QuillNotFound { diag } => eprintln!("{}", diag.fmt_pretty()),
679 RenderError::InvalidVersion { diag } => eprintln!("{}", diag.fmt_pretty()),
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_diagnostic_with_source_chain() {
689 let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
690 let diag = Diagnostic::new(Severity::Error, "Rendering failed".to_string())
691 .with_source(Box::new(root_err));
692
693 let chain = diag.source_chain();
694 assert_eq!(chain.len(), 1);
695 assert!(chain[0].contains("File not found"));
696 }
697
698 #[test]
699 fn test_diagnostic_serialization() {
700 let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
701 .with_code("E001".to_string())
702 .with_location(Location {
703 file: "test.typ".to_string(),
704 line: 10,
705 col: 5,
706 });
707
708 let serializable: SerializableDiagnostic = diag.into();
709 let json = serde_json::to_string(&serializable).unwrap();
710 assert!(json.contains("Test error"));
711 assert!(json.contains("E001"));
712 }
713
714 #[test]
715 fn test_render_error_diagnostics_extraction() {
716 let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
717 let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
718
719 let err = RenderError::CompilationFailed {
720 diags: vec![diag1, diag2],
721 };
722
723 let diags = err.diagnostics();
724 assert_eq!(diags.len(), 2);
725 }
726
727 #[test]
728 fn test_diagnostic_fmt_pretty() {
729 let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
730 .with_code("W001".to_string())
731 .with_location(Location {
732 file: "input.md".to_string(),
733 line: 5,
734 col: 10,
735 })
736 .with_hint("Use the new field name instead".to_string());
737
738 let output = diag.fmt_pretty();
739 assert!(output.contains("[WARN]"));
740 assert!(output.contains("Deprecated field used"));
741 assert!(output.contains("W001"));
742 assert!(output.contains("input.md:5:10"));
743 assert!(output.contains("hint:"));
744 }
745
746 #[test]
747 fn test_diagnostic_fmt_pretty_with_source() {
748 let root_err = std::io::Error::other("Underlying error");
749 let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
750 .with_code("E002".to_string())
751 .with_source(Box::new(root_err));
752
753 let output = diag.fmt_pretty_with_source();
754 assert!(output.contains("[ERROR]"));
755 assert!(output.contains("Top-level error"));
756 assert!(output.contains("cause 1:"));
757 assert!(output.contains("Underlying error"));
758 }
759
760 #[test]
761 fn test_render_result_with_warnings() {
762 let artifacts = vec![];
763 let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
764
765 let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
766
767 assert_eq!(result.warnings.len(), 1);
768 assert_eq!(result.warnings[0].message, "Test warning");
769 }
770}