1use crate::OutputFormat;
134
135pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
137
138pub const MAX_YAML_SIZE: usize = 1 * 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("{0}")]
356 Other(String),
357}
358
359impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
360 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
361 ParseError::Other(err.to_string())
362 }
363}
364
365impl From<String> for ParseError {
366 fn from(msg: String) -> Self {
367 ParseError::Other(msg)
368 }
369}
370
371#[derive(thiserror::Error, Debug)]
373pub enum RenderError {
374 #[error("{diag}")]
376 EngineCreation {
377 diag: Diagnostic,
379 },
380
381 #[error("{diag}")]
383 InvalidFrontmatter {
384 diag: Diagnostic,
386 },
387
388 #[error("{diag}")]
390 TemplateFailed {
391 diag: Diagnostic,
393 },
394
395 #[error("Backend compilation failed with {} error(s)", diags.len())]
397 CompilationFailed {
398 diags: Vec<Diagnostic>,
400 },
401
402 #[error("{diag}")]
404 FormatNotSupported {
405 diag: Diagnostic,
407 },
408
409 #[error("{diag}")]
411 UnsupportedBackend {
412 diag: Diagnostic,
414 },
415
416 #[error("{diag}")]
418 DynamicAssetCollision {
419 diag: Diagnostic,
421 },
422
423 #[error("{diag}")]
425 DynamicFontCollision {
426 diag: Diagnostic,
428 },
429
430 #[error("{diag}")]
432 InputTooLarge {
433 diag: Diagnostic,
435 },
436
437 #[error("{diag}")]
439 YamlTooLarge {
440 diag: Diagnostic,
442 },
443
444 #[error("{diag}")]
446 NestingTooDeep {
447 diag: Diagnostic,
449 },
450
451 #[error("{diag}")]
453 OutputTooLarge {
454 diag: Diagnostic,
456 },
457
458 #[error("{diag}")]
460 ValidationFailed {
461 diag: Diagnostic,
463 },
464
465 #[error("{diag}")]
467 InvalidSchema {
468 diag: Diagnostic,
470 },
471
472 #[error("{diag}")]
474 QuillConfig {
475 diag: Diagnostic,
477 },
478}
479
480impl RenderError {
481 pub fn diagnostics(&self) -> Vec<&Diagnostic> {
483 match self {
484 RenderError::CompilationFailed { diags } => diags.iter().collect(),
485 RenderError::EngineCreation { diag }
486 | RenderError::InvalidFrontmatter { diag }
487 | RenderError::TemplateFailed { diag }
488 | RenderError::FormatNotSupported { diag }
489 | RenderError::UnsupportedBackend { diag }
490 | RenderError::DynamicAssetCollision { diag }
491 | RenderError::DynamicFontCollision { diag }
492 | RenderError::InputTooLarge { diag }
493 | RenderError::YamlTooLarge { diag }
494 | RenderError::NestingTooDeep { diag }
495 | RenderError::OutputTooLarge { diag }
496 | RenderError::ValidationFailed { diag }
497 | RenderError::InvalidSchema { diag }
498 | RenderError::QuillConfig { diag } => vec![diag],
499 }
500 }
501}
502
503#[derive(Debug)]
505pub struct RenderResult {
506 pub artifacts: Vec<crate::Artifact>,
508 pub warnings: Vec<Diagnostic>,
510 pub output_format: OutputFormat,
512}
513
514impl RenderResult {
515 pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
517 Self {
518 artifacts,
519 warnings: Vec::new(),
520 output_format,
521 }
522 }
523
524 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
526 self.warnings.push(warning);
527 self
528 }
529}
530
531impl From<minijinja::Error> for RenderError {
533 fn from(e: minijinja::Error) -> Self {
534 let loc = e.line().map(|line| Location {
536 file: e.name().unwrap_or("template").to_string(),
537 line: line as u32,
538 col: e.range().map(|r| r.start as u32).unwrap_or(0),
540 });
541
542 let hint = generate_minijinja_hint(&e);
544
545 let mut diag = Diagnostic::new(Severity::Error, e.to_string())
547 .with_code(format!("minijinja::{:?}", e.kind()));
548
549 if let Some(loc) = loc {
550 diag = diag.with_location(loc);
551 }
552
553 if let Some(hint) = hint {
554 diag = diag.with_hint(hint);
555 }
556
557 diag = diag.with_source(Box::new(e));
559
560 RenderError::TemplateFailed { diag }
561 }
562}
563
564fn generate_minijinja_hint(e: &minijinja::Error) -> Option<String> {
566 use minijinja::ErrorKind;
567
568 match e.kind() {
569 ErrorKind::UndefinedError => {
570 Some("Check variable spelling and ensure it's defined in frontmatter".to_string())
571 }
572 ErrorKind::InvalidOperation => {
573 Some("Check that you're using the correct filter or operator for this type".to_string())
574 }
575 ErrorKind::SyntaxError => Some(
576 "Check template syntax - look for unclosed tags or invalid expressions".to_string(),
577 ),
578 _ => e.detail().map(|d| d.to_string()),
579 }
580}
581
582pub fn print_errors(err: &RenderError) {
584 match err {
585 RenderError::CompilationFailed { diags } => {
586 for d in diags {
587 eprintln!("{}", d.fmt_pretty());
588 }
589 }
590 RenderError::TemplateFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
591 RenderError::InvalidFrontmatter { diag } => eprintln!("{}", diag.fmt_pretty()),
592 RenderError::EngineCreation { diag } => eprintln!("{}", diag.fmt_pretty()),
593 RenderError::FormatNotSupported { diag } => eprintln!("{}", diag.fmt_pretty()),
594 RenderError::UnsupportedBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
595 RenderError::DynamicAssetCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
596 RenderError::DynamicFontCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
597 RenderError::InputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
598 RenderError::YamlTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
599 RenderError::NestingTooDeep { diag } => eprintln!("{}", diag.fmt_pretty()),
600 RenderError::OutputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
601 RenderError::ValidationFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
602 RenderError::InvalidSchema { diag } => eprintln!("{}", diag.fmt_pretty()),
603 RenderError::QuillConfig { diag } => eprintln!("{}", diag.fmt_pretty()),
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn test_diagnostic_with_source_chain() {
613 let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
614 let diag = Diagnostic::new(Severity::Error, "Rendering failed".to_string())
615 .with_source(Box::new(root_err));
616
617 let chain = diag.source_chain();
618 assert_eq!(chain.len(), 1);
619 assert!(chain[0].contains("File not found"));
620 }
621
622 #[test]
623 fn test_diagnostic_serialization() {
624 let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
625 .with_code("E001".to_string())
626 .with_location(Location {
627 file: "test.typ".to_string(),
628 line: 10,
629 col: 5,
630 });
631
632 let serializable: SerializableDiagnostic = diag.into();
633 let json = serde_json::to_string(&serializable).unwrap();
634 assert!(json.contains("Test error"));
635 assert!(json.contains("E001"));
636 }
637
638 #[test]
639 fn test_render_error_diagnostics_extraction() {
640 let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
641 let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
642
643 let err = RenderError::CompilationFailed {
644 diags: vec![diag1, diag2],
645 };
646
647 let diags = err.diagnostics();
648 assert_eq!(diags.len(), 2);
649 }
650
651 #[test]
652 fn test_diagnostic_fmt_pretty() {
653 let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
654 .with_code("W001".to_string())
655 .with_location(Location {
656 file: "input.md".to_string(),
657 line: 5,
658 col: 10,
659 })
660 .with_hint("Use the new field name instead".to_string());
661
662 let output = diag.fmt_pretty();
663 assert!(output.contains("[WARN]"));
664 assert!(output.contains("Deprecated field used"));
665 assert!(output.contains("W001"));
666 assert!(output.contains("input.md:5:10"));
667 assert!(output.contains("hint:"));
668 }
669
670 #[test]
671 fn test_diagnostic_fmt_pretty_with_source() {
672 let root_err = std::io::Error::new(std::io::ErrorKind::Other, "Underlying error");
673 let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
674 .with_code("E002".to_string())
675 .with_source(Box::new(root_err));
676
677 let output = diag.fmt_pretty_with_source();
678 assert!(output.contains("[ERROR]"));
679 assert!(output.contains("Top-level error"));
680 assert!(output.contains("cause 1:"));
681 assert!(output.contains("Underlying error"));
682 }
683
684 #[test]
685 fn test_render_result_with_warnings() {
686 let artifacts = vec![];
687 let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
688
689 let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
690
691 assert_eq!(result.warnings.len(), 1);
692 assert_eq!(result.warnings[0].message, "Test warning");
693 }
694
695 #[test]
696 fn test_minijinja_error_conversion() {
697 let template_str = "{{ undefined_var }}";
699 let mut env = minijinja::Environment::new();
700 env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
701
702 let result = env.render_str(template_str, minijinja::context! {});
703 assert!(
704 result.is_err(),
705 "Expected rendering to fail with undefined variable"
706 );
707
708 let minijinja_err = result.unwrap_err();
709 let render_err: RenderError = minijinja_err.into();
710
711 match render_err {
712 RenderError::TemplateFailed { diag } => {
713 assert_eq!(diag.severity, Severity::Error);
714 assert!(diag.code.is_some());
715 assert!(diag.hint.is_some());
716 assert!(diag.source.is_some());
717 }
718 _ => panic!("Expected TemplateFailed error"),
719 }
720 }
721}