1#![allow(dead_code)]
13
14use std::collections::HashMap;
15use std::fmt::Write as FmtWrite;
16
17#[derive(Debug, Clone, PartialEq)]
23pub enum ColumnType {
24 Integer,
26 Float,
28 Text,
30 Boolean,
32}
33
34impl ColumnType {
35 pub fn label(&self) -> &'static str {
37 match self {
38 ColumnType::Integer => "integer",
39 ColumnType::Float => "float",
40 ColumnType::Text => "text",
41 ColumnType::Boolean => "boolean",
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq)]
52pub enum CellValue {
53 Int(i64),
55 Float(f64, Option<usize>),
57 Text(String),
59 Bool(bool),
61 Empty,
63}
64
65impl CellValue {
66 pub fn to_display(&self) -> String {
68 match self {
69 CellValue::Int(v) => v.to_string(),
70 CellValue::Float(v, Some(prec)) => format!("{:.prec$}", v, prec = prec),
71 CellValue::Float(v, None) => format!("{v:.6}"),
72 CellValue::Text(s) => s.clone(),
73 CellValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
74 CellValue::Empty => String::new(),
75 }
76 }
77
78 pub fn to_html(&self) -> String {
80 html_escape(&self.to_display())
81 }
82
83 pub fn to_latex(&self) -> String {
85 latex_escape(&self.to_display())
86 }
87}
88
89#[derive(Debug, Clone, Default)]
95pub struct DataTable {
96 pub caption: String,
98 pub headers: Vec<String>,
100 pub column_types: Vec<ColumnType>,
102 pub rows: Vec<Vec<CellValue>>,
104 pub row_highlights: HashMap<usize, String>,
106}
107
108impl DataTable {
109 pub fn new(caption: impl Into<String>, headers: Vec<String>) -> Self {
111 let ncols = headers.len();
112 Self {
113 caption: caption.into(),
114 headers,
115 column_types: vec![ColumnType::Text; ncols],
116 rows: Vec::new(),
117 row_highlights: HashMap::new(),
118 }
119 }
120
121 pub fn add_row(&mut self, row: Vec<CellValue>) {
123 debug_assert!(
124 self.headers.is_empty() || row.len() == self.headers.len(),
125 "row width {} != header count {}",
126 row.len(),
127 self.headers.len()
128 );
129 self.rows.push(row);
130 }
131
132 pub fn set_column_type(&mut self, idx: usize, ct: ColumnType) {
134 if idx < self.column_types.len() {
135 self.column_types[idx] = ct;
136 }
137 }
138
139 pub fn highlight_row(&mut self, idx: usize, label: impl Into<String>) {
141 self.row_highlights.insert(idx, label.into());
142 }
143
144 pub fn row_count(&self) -> usize {
146 self.rows.len()
147 }
148
149 pub fn col_count(&self) -> usize {
151 self.headers.len()
152 }
153
154 pub fn is_empty(&self) -> bool {
156 self.rows.is_empty()
157 }
158
159 pub fn column_widths(&self) -> Vec<usize> {
161 let n = self.headers.len();
162 let mut widths: Vec<usize> = self.headers.iter().map(|h| h.len()).collect();
163 for row in &self.rows {
164 for (i, cell) in row.iter().enumerate() {
165 if i < n {
166 widths[i] = widths[i].max(cell.to_display().len());
167 }
168 }
169 }
170 widths
171 }
172}
173
174#[derive(Debug, Clone)]
180pub struct FigureRef {
181 pub label: String,
183 pub path: String,
185 pub caption: String,
187 pub alt: Option<String>,
189 pub width: Option<String>,
191}
192
193impl FigureRef {
194 pub fn new(
196 label: impl Into<String>,
197 path: impl Into<String>,
198 caption: impl Into<String>,
199 ) -> Self {
200 Self {
201 label: label.into(),
202 path: path.into(),
203 caption: caption.into(),
204 alt: None,
205 width: None,
206 }
207 }
208
209 pub fn with_alt(mut self, alt: impl Into<String>) -> Self {
211 self.alt = Some(alt.into());
212 self
213 }
214
215 pub fn with_width(mut self, width: impl Into<String>) -> Self {
217 self.width = Some(width.into());
218 self
219 }
220}
221
222#[derive(Debug, Clone, Default)]
228pub struct ReportSection {
229 pub title: String,
231 pub text: String,
233 pub tables: Vec<DataTable>,
235 pub figures: Vec<FigureRef>,
237 pub level: u8,
239}
240
241impl ReportSection {
242 pub fn new(title: impl Into<String>) -> Self {
244 Self {
245 title: title.into(),
246 level: 0,
247 ..Default::default()
248 }
249 }
250
251 pub fn subsection(title: impl Into<String>) -> Self {
253 Self {
254 title: title.into(),
255 level: 1,
256 ..Default::default()
257 }
258 }
259
260 pub fn append_text(&mut self, text: impl Into<String>) {
262 let t = text.into();
263 if self.text.is_empty() {
264 self.text = t;
265 } else {
266 self.text.push_str("\n\n");
267 self.text.push_str(&t);
268 }
269 }
270
271 pub fn add_table(&mut self, table: DataTable) {
273 self.tables.push(table);
274 }
275
276 pub fn add_figure(&mut self, figure: FigureRef) {
278 self.figures.push(figure);
279 }
280}
281
282#[derive(Debug, Clone, Default)]
289pub struct SimulationReport {
290 pub title: String,
292 pub description: String,
294 pub parameters: HashMap<String, String>,
296 pub results: HashMap<String, String>,
298 pub sections: Vec<ReportSection>,
300 pub figures: Vec<FigureRef>,
302 pub tables: Vec<DataTable>,
304 pub author: String,
306 pub date: String,
308 pub version: Option<String>,
310}
311
312impl SimulationReport {
313 pub fn new(title: impl Into<String>) -> Self {
315 Self {
316 title: title.into(),
317 ..Default::default()
318 }
319 }
320
321 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
323 self.description = desc.into();
324 self
325 }
326
327 pub fn with_author(mut self, author: impl Into<String>) -> Self {
329 self.author = author.into();
330 self
331 }
332
333 pub fn with_date(mut self, date: impl Into<String>) -> Self {
335 self.date = date.into();
336 self
337 }
338
339 pub fn with_version(mut self, version: impl Into<String>) -> Self {
341 self.version = Some(version.into());
342 self
343 }
344
345 pub fn add_parameter(&mut self, key: impl Into<String>, value: impl Into<String>) {
347 self.parameters.insert(key.into(), value.into());
348 }
349
350 pub fn add_result(&mut self, key: impl Into<String>, value: impl Into<String>) {
352 self.results.insert(key.into(), value.into());
353 }
354
355 pub fn add_section(&mut self, section: ReportSection) {
357 self.sections.push(section);
358 }
359
360 pub fn add_figure(&mut self, figure: FigureRef) {
362 self.figures.push(figure);
363 }
364
365 pub fn add_table(&mut self, table: DataTable) {
367 self.tables.push(table);
368 }
369
370 pub fn total_tables(&self) -> usize {
372 self.tables.len() + self.sections.iter().map(|s| s.tables.len()).sum::<usize>()
373 }
374
375 pub fn total_figures(&self) -> usize {
377 self.figures.len() + self.sections.iter().map(|s| s.figures.len()).sum::<usize>()
378 }
379}
380
381fn html_escape(s: &str) -> String {
387 s.replace('&', "&")
388 .replace('<', "<")
389 .replace('>', ">")
390 .replace('"', """)
391}
392
393fn latex_escape(s: &str) -> String {
395 let mut out = String::with_capacity(s.len() + 8);
396 for ch in s.chars() {
397 match ch {
398 '&' => out.push_str(r"\&"),
399 '%' => out.push_str(r"\%"),
400 '$' => out.push_str(r"\$"),
401 '#' => out.push_str(r"\#"),
402 '_' => out.push_str(r"\_"),
403 '{' => out.push_str(r"\{"),
404 '}' => out.push_str(r"\}"),
405 '~' => out.push_str(r"\textasciitilde{}"),
406 '^' => out.push_str(r"\textasciicircum{}"),
407 '\\' => out.push_str(r"\textbackslash{}"),
408 other => out.push(other),
409 }
410 }
411 out
412}
413
414fn markdown_table(table: &DataTable) -> String {
416 let mut out = String::new();
417 if !table.caption.is_empty() {
419 let _ = writeln!(out, "**{}**\n", table.caption);
420 }
421 if table.headers.is_empty() {
422 return out;
423 }
424 let _ = write!(out, "|");
426 for h in &table.headers {
427 let _ = write!(out, " {} |", h);
428 }
429 out.push('\n');
430 let _ = write!(out, "|");
432 for _ in &table.headers {
433 let _ = write!(out, " --- |");
434 }
435 out.push('\n');
436 for row in &table.rows {
438 let _ = write!(out, "|");
439 for cell in row {
440 let _ = write!(out, " {} |", cell.to_display());
441 }
442 out.push('\n');
443 }
444 out
445}
446
447fn html_table(table: &DataTable) -> String {
449 let mut out = String::new();
450 let _ = write!(out, "<table class=\"data-table\">");
451 if !table.caption.is_empty() {
452 let _ = write!(out, "<caption>{}</caption>", html_escape(&table.caption));
453 }
454 if !table.headers.is_empty() {
456 let _ = write!(out, "<thead><tr>");
457 for h in &table.headers {
458 let _ = write!(out, "<th>{}</th>", html_escape(h));
459 }
460 let _ = write!(out, "</tr></thead>");
461 }
462 let _ = write!(out, "<tbody>");
464 for (i, row) in table.rows.iter().enumerate() {
465 let class = if let Some(lbl) = table.row_highlights.get(&i) {
466 format!(" class=\"{}\"", html_escape(lbl))
467 } else {
468 String::new()
469 };
470 let _ = write!(out, "<tr{}>", class);
471 for cell in row {
472 let _ = write!(out, "<td>{}</td>", cell.to_html());
473 }
474 let _ = write!(out, "</tr>");
475 }
476 let _ = write!(out, "</tbody></table>");
477 out
478}
479
480fn latex_tabular(table: &DataTable) -> String {
482 let ncols = table.headers.len();
483 let col_spec: String = std::iter::repeat_n("l", ncols)
484 .collect::<Vec<_>>()
485 .join("|");
486 let mut out = String::new();
487 if !table.caption.is_empty() {
488 let _ = writeln!(out, "\\begin{{table}}[htbp]");
489 let _ = writeln!(out, "\\centering");
490 let _ = writeln!(out, "\\caption{{{}}}", latex_escape(&table.caption));
491 }
492 let _ = writeln!(out, "\\begin{{tabular}}{{|{}|}}", col_spec);
493 let _ = writeln!(out, "\\hline");
494 if !table.headers.is_empty() {
496 let header_row = table
497 .headers
498 .iter()
499 .map(|h| format!("\\textbf{{{}}}", latex_escape(h)))
500 .collect::<Vec<_>>()
501 .join(" & ");
502 let _ = writeln!(out, "{} \\\\", header_row);
503 let _ = writeln!(out, "\\hline");
504 }
505 for row in &table.rows {
507 let row_str = row
508 .iter()
509 .map(|c| c.to_latex())
510 .collect::<Vec<_>>()
511 .join(" & ");
512 let _ = writeln!(out, "{} \\\\", row_str);
513 }
514 let _ = writeln!(out, "\\hline");
515 let _ = writeln!(out, "\\end{{tabular}}");
516 if !table.caption.is_empty() {
517 let _ = writeln!(out, "\\end{{table}}");
518 }
519 out
520}
521
522#[derive(Debug, Clone, Default)]
537pub struct HtmlReportWriter {
538 pub embed_css: bool,
540 pub extra_css: String,
542}
543
544impl HtmlReportWriter {
545 pub fn new() -> Self {
547 Self {
548 embed_css: true,
549 extra_css: String::new(),
550 }
551 }
552
553 pub fn without_css(mut self) -> Self {
555 self.embed_css = false;
556 self
557 }
558
559 pub fn with_extra_css(mut self, css: impl Into<String>) -> Self {
561 self.extra_css = css.into();
562 self
563 }
564
565 pub fn write(&self, report: &SimulationReport) -> String {
567 let mut out = String::new();
568 let _ = writeln!(out, "<!DOCTYPE html>");
569 let _ = writeln!(out, "<html lang=\"en\">");
570 let _ = writeln!(out, "<head>");
571 let _ = writeln!(out, "<meta charset=\"UTF-8\">");
572 let _ = writeln!(
573 out,
574 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
575 );
576 let _ = writeln!(out, "<title>{}</title>", html_escape(&report.title));
577 if self.embed_css {
578 let _ = writeln!(out, "<style>");
579 out.push_str(DEFAULT_CSS);
580 if !self.extra_css.is_empty() {
581 out.push_str(&self.extra_css);
582 }
583 let _ = writeln!(out, "</style>");
584 }
585 let _ = writeln!(out, "</head>");
586 let _ = writeln!(out, "<body>");
587 let _ = writeln!(out, "<div class=\"container\">");
588
589 let _ = writeln!(
591 out,
592 "<h1 class=\"report-title\">{}</h1>",
593 html_escape(&report.title)
594 );
595 if !report.author.is_empty() || !report.date.is_empty() {
596 let _ = write!(out, "<p class=\"report-meta\">");
597 if !report.author.is_empty() {
598 let _ = write!(
599 out,
600 "<strong>Author:</strong> {} ",
601 html_escape(&report.author)
602 );
603 }
604 if !report.date.is_empty() {
605 let _ = write!(out, "<strong>Date:</strong> {}", html_escape(&report.date));
606 }
607 if let Some(ref ver) = report.version {
608 let _ = write!(out, " <strong>Version:</strong> {}", html_escape(ver));
609 }
610 let _ = writeln!(out, "</p>");
611 }
612
613 if !report.description.is_empty() {
615 let _ = writeln!(out, "<div class=\"abstract\">");
616 let _ = writeln!(out, "<h2>Abstract</h2>");
617 let _ = writeln!(out, "<p>{}</p>", html_escape(&report.description));
618 let _ = writeln!(out, "</div>");
619 }
620
621 if !report.parameters.is_empty() {
623 let _ = writeln!(out, "<h2>Parameters</h2>");
624 let _ = writeln!(out, "<table class=\"data-table\">");
625 let _ = writeln!(
626 out,
627 "<thead><tr><th>Parameter</th><th>Value</th></tr></thead>"
628 );
629 let _ = writeln!(out, "<tbody>");
630 let mut params: Vec<(&String, &String)> = report.parameters.iter().collect();
631 params.sort_by_key(|(k, _)| k.as_str());
632 for (k, v) in params {
633 let _ = writeln!(
634 out,
635 "<tr><td>{}</td><td>{}</td></tr>",
636 html_escape(k),
637 html_escape(v)
638 );
639 }
640 let _ = writeln!(out, "</tbody></table>");
641 }
642
643 if !report.results.is_empty() {
645 let _ = writeln!(out, "<h2>Results</h2>");
646 let _ = writeln!(out, "<table class=\"data-table\">");
647 let _ = writeln!(out, "<thead><tr><th>Metric</th><th>Value</th></tr></thead>");
648 let _ = writeln!(out, "<tbody>");
649 let mut res: Vec<(&String, &String)> = report.results.iter().collect();
650 res.sort_by_key(|(k, _)| k.as_str());
651 for (k, v) in res {
652 let _ = writeln!(
653 out,
654 "<tr><td>{}</td><td>{}</td></tr>",
655 html_escape(k),
656 html_escape(v)
657 );
658 }
659 let _ = writeln!(out, "</tbody></table>");
660 }
661
662 for table in &report.tables {
664 out.push_str(&html_table(table));
665 out.push('\n');
666 }
667
668 for fig in &report.figures {
670 self.write_html_figure(&mut out, fig);
671 }
672
673 for section in &report.sections {
675 self.write_html_section(&mut out, section);
676 }
677
678 let _ = writeln!(out, "</div>"); let _ = writeln!(out, "</body></html>");
680 out
681 }
682
683 fn write_html_section(&self, out: &mut String, section: &ReportSection) {
684 let tag = if section.level == 0 { "h2" } else { "h3" };
685 let _ = writeln!(out, "<{tag}>{}</{tag}>", html_escape(§ion.title));
686 if !section.text.is_empty() {
687 for para in section.text.split("\n\n") {
688 let _ = writeln!(out, "<p>{}</p>", html_escape(para.trim()));
689 }
690 }
691 for table in §ion.tables {
692 out.push_str(&html_table(table));
693 out.push('\n');
694 }
695 for fig in §ion.figures {
696 self.write_html_figure(out, fig);
697 }
698 }
699
700 fn write_html_figure(&self, out: &mut String, fig: &FigureRef) {
701 let _ = writeln!(out, "<figure id=\"{}\">", html_escape(&fig.label));
702 let alt = fig.alt.as_deref().unwrap_or(&fig.caption);
703 let width_attr = if let Some(ref w) = fig.width {
704 format!(" width=\"{}\"", html_escape(w))
705 } else {
706 String::new()
707 };
708 let _ = writeln!(
709 out,
710 "<img src=\"{}\" alt=\"{}\"{} />",
711 html_escape(&fig.path),
712 html_escape(alt),
713 width_attr
714 );
715 let _ = writeln!(
716 out,
717 "<figcaption>{}</figcaption>",
718 html_escape(&fig.caption)
719 );
720 let _ = writeln!(out, "</figure>");
721 }
722}
723
724const DEFAULT_CSS: &str = r#"
726body {
727 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
728 line-height: 1.6;
729 color: #333;
730 background: #fafafa;
731 margin: 0;
732 padding: 0;
733}
734.container {
735 max-width: 960px;
736 margin: 2rem auto;
737 background: #fff;
738 padding: 2rem 3rem;
739 box-shadow: 0 2px 8px rgba(0,0,0,.12);
740 border-radius: 6px;
741}
742.report-title {
743 font-size: 2rem;
744 border-bottom: 2px solid #3b82f6;
745 padding-bottom: .4rem;
746 margin-bottom: .5rem;
747}
748.report-meta {
749 color: #555;
750 font-size: .9rem;
751 margin-bottom: 1.5rem;
752}
753.abstract {
754 background: #eff6ff;
755 border-left: 4px solid #3b82f6;
756 padding: .8rem 1rem;
757 margin-bottom: 1.5rem;
758 border-radius: 0 4px 4px 0;
759}
760h2 { font-size: 1.4rem; margin-top: 2rem; border-bottom: 1px solid #e5e7eb; padding-bottom: .3rem; }
761h3 { font-size: 1.15rem; margin-top: 1.5rem; }
762table.data-table {
763 border-collapse: collapse;
764 width: 100%;
765 margin: 1rem 0;
766 font-size: .9rem;
767}
768table.data-table th, table.data-table td {
769 border: 1px solid #d1d5db;
770 padding: .5rem .75rem;
771 text-align: left;
772}
773table.data-table thead tr { background: #3b82f6; color: #fff; }
774table.data-table tbody tr:nth-child(even) { background: #f3f4f6; }
775table.data-table caption { caption-side: top; font-weight: bold; margin-bottom: .4rem; }
776table.data-table tr.highlight { background: #fef9c3; }
777figure { margin: 1.5rem 0; text-align: center; }
778figure img { max-width: 100%; border-radius: 4px; }
779figcaption { color: #555; font-size: .85rem; margin-top: .4rem; font-style: italic; }
780"#;
781
782#[derive(Debug, Clone, Default)]
796pub struct MarkdownReportWriter {
797 pub section_dividers: bool,
799}
800
801impl MarkdownReportWriter {
802 pub fn new() -> Self {
804 Self {
805 section_dividers: true,
806 }
807 }
808
809 pub fn write(&self, report: &SimulationReport) -> String {
811 let mut out = String::new();
812
813 let _ = writeln!(out, "# {}", report.title);
815 out.push('\n');
816
817 if !report.author.is_empty() {
819 let _ = writeln!(out, "**Author:** {} ", report.author);
820 }
821 if !report.date.is_empty() {
822 let _ = writeln!(out, "**Date:** {} ", report.date);
823 }
824 if let Some(ref ver) = report.version {
825 let _ = writeln!(out, "**Version:** {} ", ver);
826 }
827 if !report.author.is_empty() || !report.date.is_empty() {
828 out.push('\n');
829 }
830
831 if !report.description.is_empty() {
833 let _ = writeln!(out, "## Abstract");
834 out.push('\n');
835 let _ = writeln!(out, "{}", report.description);
836 out.push('\n');
837 }
838
839 if !report.parameters.is_empty() {
841 let _ = writeln!(out, "## Parameters");
842 out.push('\n');
843 let _ = writeln!(out, "| Parameter | Value |");
844 let _ = writeln!(out, "| --- | --- |");
845 let mut params: Vec<(&String, &String)> = report.parameters.iter().collect();
846 params.sort_by_key(|(k, _)| k.as_str());
847 for (k, v) in params {
848 let _ = writeln!(out, "| {} | {} |", k, v);
849 }
850 out.push('\n');
851 }
852
853 if !report.results.is_empty() {
855 let _ = writeln!(out, "## Results");
856 out.push('\n');
857 let _ = writeln!(out, "| Metric | Value |");
858 let _ = writeln!(out, "| --- | --- |");
859 let mut res: Vec<(&String, &String)> = report.results.iter().collect();
860 res.sort_by_key(|(k, _)| k.as_str());
861 for (k, v) in res {
862 let _ = writeln!(out, "| {} | {} |", k, v);
863 }
864 out.push('\n');
865 }
866
867 for table in &report.tables {
869 out.push_str(&markdown_table(table));
870 out.push('\n');
871 }
872
873 for fig in &report.figures {
875 let alt = fig.alt.as_deref().unwrap_or(&fig.caption);
876 let _ = writeln!(out, "", alt, fig.path);
877 let _ = writeln!(out, "*Figure: {}*", fig.caption);
878 out.push('\n');
879 }
880
881 for section in &report.sections {
883 if self.section_dividers {
884 let _ = writeln!(out, "---");
885 out.push('\n');
886 }
887 self.write_md_section(&mut out, section);
888 }
889
890 out
891 }
892
893 fn write_md_section(&self, out: &mut String, section: &ReportSection) {
894 let prefix = if section.level == 0 { "##" } else { "###" };
895 let _ = writeln!(out, "{} {}", prefix, section.title);
896 out.push('\n');
897
898 if !section.text.is_empty() {
899 let _ = writeln!(out, "{}", section.text);
900 out.push('\n');
901 }
902
903 for table in §ion.tables {
904 out.push_str(&markdown_table(table));
905 out.push('\n');
906 }
907
908 for fig in §ion.figures {
909 let alt = fig.alt.as_deref().unwrap_or(&fig.caption);
910 let _ = writeln!(out, "", alt, fig.path);
911 let _ = writeln!(out, "*Figure: {}*", fig.caption);
912 out.push('\n');
913 }
914 }
915}
916
917#[derive(Debug, Clone)]
931pub struct LatexReportWriter {
932 pub paper: String,
934 pub font_size: String,
936 pub use_booktabs: bool,
938 pub use_graphicx: bool,
940}
941
942impl Default for LatexReportWriter {
943 fn default() -> Self {
944 Self {
945 paper: "a4paper".to_string(),
946 font_size: "11pt".to_string(),
947 use_booktabs: true,
948 use_graphicx: true,
949 }
950 }
951}
952
953impl LatexReportWriter {
954 pub fn new() -> Self {
956 Self::default()
957 }
958
959 pub fn write(&self, report: &SimulationReport) -> String {
961 let mut out = String::new();
962
963 let _ = writeln!(
965 out,
966 "\\documentclass[{},{}]{{article}}",
967 self.font_size, self.paper
968 );
969 let _ = writeln!(out, "\\usepackage[utf8]{{inputenc}}");
970 let _ = writeln!(out, "\\usepackage[T1]{{fontenc}}");
971 let _ = writeln!(out, "\\usepackage{{geometry}}");
972 let _ = writeln!(out, "\\geometry{{margin=2.5cm}}");
973 let _ = writeln!(out, "\\usepackage{{hyperref}}");
974 if self.use_booktabs {
975 let _ = writeln!(out, "\\usepackage{{booktabs}}");
976 }
977 if self.use_graphicx {
978 let _ = writeln!(out, "\\usepackage{{graphicx}}");
979 }
980 let _ = writeln!(out, "\\usepackage{{array}}");
981 let _ = writeln!(out, "\\usepackage{{colortbl}}");
982 let _ = writeln!(out, "\\usepackage{{xcolor}}");
983 out.push('\n');
984
985 let _ = writeln!(out, "\\title{{{}}}", latex_escape(&report.title));
987 if !report.author.is_empty() {
988 let _ = writeln!(out, "\\author{{{}}}", latex_escape(&report.author));
989 }
990 if !report.date.is_empty() {
991 let _ = writeln!(out, "\\date{{{}}}", latex_escape(&report.date));
992 } else {
993 let _ = writeln!(out, "\\date{{\\today}}");
994 }
995 out.push('\n');
996
997 let _ = writeln!(out, "\\begin{{document}}");
999 let _ = writeln!(out, "\\maketitle");
1000 if let Some(ref ver) = report.version {
1001 let _ = writeln!(
1002 out,
1003 "\\begin{{center}}\\small Version: {}\\end{{center}}",
1004 latex_escape(ver)
1005 );
1006 }
1007 out.push('\n');
1008
1009 if !report.description.is_empty() {
1011 let _ = writeln!(out, "\\begin{{abstract}}");
1012 let _ = writeln!(out, "{}", latex_escape(&report.description));
1013 let _ = writeln!(out, "\\end{{abstract}}");
1014 out.push('\n');
1015 }
1016
1017 let _ = writeln!(out, "\\tableofcontents");
1019 let _ = writeln!(out, "\\newpage");
1020 out.push('\n');
1021
1022 if !report.parameters.is_empty() {
1024 let _ = writeln!(out, "\\section{{Parameters}}");
1025 let mut params: Vec<(&String, &String)> = report.parameters.iter().collect();
1026 params.sort_by_key(|(k, _)| k.as_str());
1027 let _ = writeln!(out, "\\begin{{table}}[htbp]\\centering");
1028 let _ = writeln!(out, "\\caption{{Simulation Parameters}}");
1029 let _ = writeln!(out, "\\begin{{tabular}}{{|l|l|}}\\hline");
1030 let _ = writeln!(out, "\\textbf{{Parameter}} & \\textbf{{Value}} \\\\\\hline");
1031 for (k, v) in params {
1032 let _ = writeln!(out, "{} & {} \\\\", latex_escape(k), latex_escape(v));
1033 }
1034 let _ = writeln!(out, "\\hline\\end{{tabular}}\\end{{table}}");
1035 out.push('\n');
1036 }
1037
1038 if !report.results.is_empty() {
1040 let _ = writeln!(out, "\\section{{Results Summary}}");
1041 let mut res: Vec<(&String, &String)> = report.results.iter().collect();
1042 res.sort_by_key(|(k, _)| k.as_str());
1043 let _ = writeln!(out, "\\begin{{table}}[htbp]\\centering");
1044 let _ = writeln!(out, "\\caption{{Result Metrics}}");
1045 let _ = writeln!(out, "\\begin{{tabular}}{{|l|l|}}\\hline");
1046 let _ = writeln!(out, "\\textbf{{Metric}} & \\textbf{{Value}} \\\\\\hline");
1047 for (k, v) in res {
1048 let _ = writeln!(out, "{} & {} \\\\", latex_escape(k), latex_escape(v));
1049 }
1050 let _ = writeln!(out, "\\hline\\end{{tabular}}\\end{{table}}");
1051 out.push('\n');
1052 }
1053
1054 for table in &report.tables {
1056 out.push_str(&latex_tabular(table));
1057 out.push('\n');
1058 }
1059
1060 for fig in &report.figures {
1062 self.write_latex_figure(&mut out, fig);
1063 }
1064
1065 for section in &report.sections {
1067 self.write_latex_section(&mut out, section);
1068 }
1069
1070 let _ = writeln!(out, "\\end{{document}}");
1071 out
1072 }
1073
1074 fn write_latex_section(&self, out: &mut String, section: &ReportSection) {
1075 let cmd = if section.level == 0 {
1076 "\\section"
1077 } else {
1078 "\\subsection"
1079 };
1080 let _ = writeln!(out, "{}{{{}}}", cmd, latex_escape(§ion.title));
1081 out.push('\n');
1082
1083 if !section.text.is_empty() {
1084 for para in section.text.split("\n\n") {
1085 let _ = writeln!(out, "{}\n", latex_escape(para.trim()));
1086 }
1087 }
1088
1089 for table in §ion.tables {
1090 out.push_str(&latex_tabular(table));
1091 out.push('\n');
1092 }
1093
1094 for fig in §ion.figures {
1095 self.write_latex_figure(out, fig);
1096 }
1097 }
1098
1099 fn write_latex_figure(&self, out: &mut String, fig: &FigureRef) {
1100 let width = fig.width.as_deref().unwrap_or("0.8\\linewidth");
1101 let _ = writeln!(out, "\\begin{{figure}}[htbp]");
1102 let _ = writeln!(out, "\\centering");
1103 let _ = writeln!(
1104 out,
1105 "\\includegraphics[width={}]{{{}}}",
1106 width,
1107 latex_escape(&fig.path)
1108 );
1109 let _ = writeln!(out, "\\caption{{{}}}", latex_escape(&fig.caption));
1110 let _ = writeln!(out, "\\label{{fig:{}}}", latex_escape(&fig.label));
1111 let _ = writeln!(out, "\\end{{figure}}");
1112 }
1113}
1114
1115#[derive(Debug, Default)]
1122pub struct ReportBuilder {
1123 report: SimulationReport,
1124}
1125
1126impl ReportBuilder {
1127 pub fn new(title: impl Into<String>) -> Self {
1129 Self {
1130 report: SimulationReport::new(title),
1131 }
1132 }
1133
1134 pub fn author(mut self, author: impl Into<String>) -> Self {
1136 self.report.author = author.into();
1137 self
1138 }
1139
1140 pub fn date(mut self, date: impl Into<String>) -> Self {
1142 self.report.date = date.into();
1143 self
1144 }
1145
1146 pub fn description(mut self, desc: impl Into<String>) -> Self {
1148 self.report.description = desc.into();
1149 self
1150 }
1151
1152 pub fn parameter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1154 self.report.add_parameter(key, value);
1155 self
1156 }
1157
1158 pub fn result(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1160 self.report.add_result(key, value);
1161 self
1162 }
1163
1164 pub fn section(mut self, section: ReportSection) -> Self {
1166 self.report.add_section(section);
1167 self
1168 }
1169
1170 pub fn build(self) -> SimulationReport {
1172 self.report
1173 }
1174
1175 pub fn to_html(self) -> String {
1177 HtmlReportWriter::new().write(&self.report)
1178 }
1179
1180 pub fn to_markdown(self) -> String {
1182 MarkdownReportWriter::new().write(&self.report)
1183 }
1184
1185 pub fn to_latex(self) -> String {
1187 LatexReportWriter::new().write(&self.report)
1188 }
1189}
1190
1191#[cfg(test)]
1196mod tests {
1197 use super::*;
1198
1199 #[test]
1202 fn test_cell_int_display() {
1203 assert_eq!(CellValue::Int(42).to_display(), "42");
1204 }
1205
1206 #[test]
1207 fn test_cell_float_default_precision() {
1208 let s = CellValue::Float(2.54321, None).to_display();
1209 assert!(s.starts_with("2.54321"), "got {s}");
1210 }
1211
1212 #[test]
1213 fn test_cell_float_custom_precision() {
1214 let s = CellValue::Float(2.54321, Some(2)).to_display();
1215 assert_eq!(s, "2.54");
1216 }
1217
1218 #[test]
1219 fn test_cell_text_display() {
1220 assert_eq!(CellValue::Text("hello".into()).to_display(), "hello");
1221 }
1222
1223 #[test]
1224 fn test_cell_bool_true() {
1225 assert_eq!(CellValue::Bool(true).to_display(), "true");
1226 }
1227
1228 #[test]
1229 fn test_cell_bool_false() {
1230 assert_eq!(CellValue::Bool(false).to_display(), "false");
1231 }
1232
1233 #[test]
1234 fn test_cell_empty_display() {
1235 assert_eq!(CellValue::Empty.to_display(), "");
1236 }
1237
1238 #[test]
1239 fn test_cell_html_escape() {
1240 let s = CellValue::Text("<b>bold & italic</b>".into()).to_html();
1241 assert!(s.contains("<"), "got {s}");
1242 assert!(s.contains("&"), "got {s}");
1243 }
1244
1245 #[test]
1246 fn test_cell_latex_escape() {
1247 let s = CellValue::Text("x_1 & y_2".into()).to_latex();
1248 assert!(s.contains(r"\_"), "got {s}");
1249 assert!(s.contains(r"\&"), "got {s}");
1250 }
1251
1252 #[test]
1255 fn test_data_table_add_row() {
1256 let mut t = DataTable::new("T", vec!["A".into(), "B".into()]);
1257 t.add_row(vec![CellValue::Int(1), CellValue::Int(2)]);
1258 assert_eq!(t.row_count(), 1);
1259 }
1260
1261 #[test]
1262 fn test_data_table_col_count() {
1263 let t = DataTable::new("T", vec!["A".into(), "B".into(), "C".into()]);
1264 assert_eq!(t.col_count(), 3);
1265 }
1266
1267 #[test]
1268 fn test_data_table_is_empty() {
1269 let t = DataTable::new("T", vec!["A".into()]);
1270 assert!(t.is_empty());
1271 }
1272
1273 #[test]
1274 fn test_data_table_highlight_row() {
1275 let mut t = DataTable::new("T", vec!["A".into()]);
1276 t.add_row(vec![CellValue::Int(1)]);
1277 t.highlight_row(0, "highlight");
1278 assert_eq!(t.row_highlights.get(&0).unwrap(), "highlight");
1279 }
1280
1281 #[test]
1282 fn test_data_table_column_widths() {
1283 let mut t = DataTable::new("T", vec!["Name".into(), "Val".into()]);
1284 t.add_row(vec![CellValue::Text("Alice".into()), CellValue::Int(100)]);
1285 let widths = t.column_widths();
1286 assert_eq!(widths[0], 5); assert!(widths[1] >= 3); }
1289
1290 #[test]
1291 fn test_data_table_set_column_type() {
1292 let mut t = DataTable::new("T", vec!["A".into(), "B".into()]);
1293 t.set_column_type(0, ColumnType::Integer);
1294 assert_eq!(t.column_types[0], ColumnType::Integer);
1295 }
1296
1297 #[test]
1300 fn test_figure_ref_new() {
1301 let f = FigureRef::new("fig1", "plot.png", "My Plot");
1302 assert_eq!(f.label, "fig1");
1303 assert_eq!(f.path, "plot.png");
1304 assert_eq!(f.caption, "My Plot");
1305 assert!(f.alt.is_none());
1306 }
1307
1308 #[test]
1309 fn test_figure_ref_with_alt_and_width() {
1310 let f = FigureRef::new("f", "a.png", "cap")
1311 .with_alt("alt text")
1312 .with_width("80%");
1313 assert_eq!(f.alt.as_deref(), Some("alt text"));
1314 assert_eq!(f.width.as_deref(), Some("80%"));
1315 }
1316
1317 #[test]
1320 fn test_report_section_new() {
1321 let s = ReportSection::new("Intro");
1322 assert_eq!(s.title, "Intro");
1323 assert_eq!(s.level, 0);
1324 assert!(s.tables.is_empty());
1325 }
1326
1327 #[test]
1328 fn test_report_section_subsection_level() {
1329 let s = ReportSection::subsection("Sub");
1330 assert_eq!(s.level, 1);
1331 }
1332
1333 #[test]
1334 fn test_report_section_append_text() {
1335 let mut s = ReportSection::new("S");
1336 s.append_text("Para 1.");
1337 s.append_text("Para 2.");
1338 assert!(s.text.contains("Para 1."));
1339 assert!(s.text.contains("Para 2."));
1340 assert!(s.text.contains("\n\n"));
1341 }
1342
1343 #[test]
1344 fn test_report_section_add_table() {
1345 let mut s = ReportSection::new("S");
1346 s.add_table(DataTable::new("T", vec!["X".into()]));
1347 assert_eq!(s.tables.len(), 1);
1348 }
1349
1350 #[test]
1351 fn test_report_section_add_figure() {
1352 let mut s = ReportSection::new("S");
1353 s.add_figure(FigureRef::new("f", "a.png", "cap"));
1354 assert_eq!(s.figures.len(), 1);
1355 }
1356
1357 #[test]
1360 fn test_report_new() {
1361 let r = SimulationReport::new("Test Report");
1362 assert_eq!(r.title, "Test Report");
1363 assert!(r.parameters.is_empty());
1364 assert!(r.results.is_empty());
1365 }
1366
1367 #[test]
1368 fn test_report_builder_chain() {
1369 let r = SimulationReport::new("T")
1370 .with_description("desc")
1371 .with_author("Alice")
1372 .with_date("2026-01-01")
1373 .with_version("1.0.0");
1374 assert_eq!(r.description, "desc");
1375 assert_eq!(r.author, "Alice");
1376 assert_eq!(r.version.as_deref(), Some("1.0.0"));
1377 }
1378
1379 #[test]
1380 fn test_report_add_parameter() {
1381 let mut r = SimulationReport::new("T");
1382 r.add_parameter("dt", "0.001");
1383 assert_eq!(r.parameters.get("dt").map(|s| s.as_str()), Some("0.001"));
1384 }
1385
1386 #[test]
1387 fn test_report_add_result() {
1388 let mut r = SimulationReport::new("T");
1389 r.add_result("energy", "42.5");
1390 assert_eq!(r.results.get("energy").map(|s| s.as_str()), Some("42.5"));
1391 }
1392
1393 #[test]
1394 fn test_report_total_tables() {
1395 let mut r = SimulationReport::new("T");
1396 r.add_table(DataTable::new("T1", vec![]));
1397 let mut sec = ReportSection::new("S");
1398 sec.add_table(DataTable::new("T2", vec![]));
1399 r.add_section(sec);
1400 assert_eq!(r.total_tables(), 2);
1401 }
1402
1403 #[test]
1404 fn test_report_total_figures() {
1405 let mut r = SimulationReport::new("T");
1406 r.add_figure(FigureRef::new("f1", "a.png", "A"));
1407 let mut sec = ReportSection::new("S");
1408 sec.add_figure(FigureRef::new("f2", "b.png", "B"));
1409 r.add_section(sec);
1410 assert_eq!(r.total_figures(), 2);
1411 }
1412
1413 #[test]
1416 fn test_html_writer_contains_doctype() {
1417 let r = SimulationReport::new("Test");
1418 let html = HtmlReportWriter::new().write(&r);
1419 assert!(html.contains("<!DOCTYPE html>"), "no doctype in:\n{html}");
1420 }
1421
1422 #[test]
1423 fn test_html_writer_contains_title() {
1424 let r = SimulationReport::new("My Report");
1425 let html = HtmlReportWriter::new().write(&r);
1426 assert!(html.contains("My Report"), "title missing");
1427 }
1428
1429 #[test]
1430 fn test_html_writer_contains_css() {
1431 let r = SimulationReport::new("T");
1432 let html = HtmlReportWriter::new().write(&r);
1433 assert!(html.contains("<style>"), "CSS missing");
1434 }
1435
1436 #[test]
1437 fn test_html_writer_no_css_when_disabled() {
1438 let r = SimulationReport::new("T");
1439 let html = HtmlReportWriter::new().without_css().write(&r);
1440 assert!(!html.contains("<style>"), "CSS should be absent");
1441 }
1442
1443 #[test]
1444 fn test_html_writer_parameters_table() {
1445 let mut r = SimulationReport::new("T");
1446 r.add_parameter("alpha", "0.5");
1447 let html = HtmlReportWriter::new().write(&r);
1448 assert!(html.contains("alpha"), "param key missing");
1449 assert!(html.contains("0.5"), "param value missing");
1450 }
1451
1452 #[test]
1453 fn test_html_writer_results_table() {
1454 let mut r = SimulationReport::new("T");
1455 r.add_result("max_stress", "1.23e9");
1456 let html = HtmlReportWriter::new().write(&r);
1457 assert!(html.contains("max_stress"));
1458 assert!(html.contains("1.23e9"));
1459 }
1460
1461 #[test]
1462 fn test_html_writer_section_heading() {
1463 let mut r = SimulationReport::new("T");
1464 r.add_section(ReportSection::new("Methodology"));
1465 let html = HtmlReportWriter::new().write(&r);
1466 assert!(html.contains("Methodology"));
1467 }
1468
1469 #[test]
1470 fn test_html_writer_figure() {
1471 let mut r = SimulationReport::new("T");
1472 r.add_figure(FigureRef::new("f1", "energy.png", "Energy over time"));
1473 let html = HtmlReportWriter::new().write(&r);
1474 assert!(html.contains("energy.png"));
1475 assert!(html.contains("Energy over time"));
1476 }
1477
1478 #[test]
1479 fn test_html_writer_data_table() {
1480 let mut r = SimulationReport::new("T");
1481 let mut tbl = DataTable::new("Results", vec!["Step".into(), "E".into()]);
1482 tbl.add_row(vec![CellValue::Int(1), CellValue::Float(1.5, Some(2))]);
1483 r.add_table(tbl);
1484 let html = HtmlReportWriter::new().write(&r);
1485 assert!(html.contains("<table"), "table tag missing");
1486 assert!(html.contains("Step"));
1487 assert!(html.contains("1.50"));
1488 }
1489
1490 #[test]
1491 fn test_html_writer_html_escape_in_title() {
1492 let r = SimulationReport::new("Report <1> & <2>");
1493 let html = HtmlReportWriter::new().write(&r);
1494 assert!(html.contains("<1>"), "angle brackets not escaped");
1495 assert!(html.contains("&"), "ampersand not escaped");
1496 }
1497
1498 #[test]
1501 fn test_md_writer_title_heading() {
1502 let r = SimulationReport::new("My Sim");
1503 let md = MarkdownReportWriter::new().write(&r);
1504 assert!(md.starts_with("# My Sim"), "title heading missing:\n{md}");
1505 }
1506
1507 #[test]
1508 fn test_md_writer_parameters_table() {
1509 let mut r = SimulationReport::new("T");
1510 r.add_parameter("dt", "0.01");
1511 let md = MarkdownReportWriter::new().write(&r);
1512 assert!(md.contains("## Parameters"), "header missing");
1513 assert!(md.contains("| dt |"), "param row missing");
1514 }
1515
1516 #[test]
1517 fn test_md_writer_results_table() {
1518 let mut r = SimulationReport::new("T");
1519 r.add_result("energy", "5.0");
1520 let md = MarkdownReportWriter::new().write(&r);
1521 assert!(md.contains("## Results"));
1522 assert!(md.contains("| energy |"));
1523 }
1524
1525 #[test]
1526 fn test_md_writer_section_heading() {
1527 let mut r = SimulationReport::new("T");
1528 r.add_section(ReportSection::new("Discussion"));
1529 let md = MarkdownReportWriter::new().write(&r);
1530 assert!(md.contains("## Discussion"));
1531 }
1532
1533 #[test]
1534 fn test_md_writer_subsection_heading() {
1535 let mut r = SimulationReport::new("T");
1536 r.add_section(ReportSection::subsection("Sub Details"));
1537 let md = MarkdownReportWriter::new().write(&r);
1538 assert!(md.contains("### Sub Details"));
1539 }
1540
1541 #[test]
1542 fn test_md_writer_dividers() {
1543 let mut r = SimulationReport::new("T");
1544 r.add_section(ReportSection::new("A"));
1545 r.add_section(ReportSection::new("B"));
1546 let md = MarkdownReportWriter::new().write(&r);
1547 assert!(md.contains("---"), "divider missing");
1548 }
1549
1550 #[test]
1551 fn test_md_writer_no_dividers_when_disabled() {
1552 let mut r = SimulationReport::new("T");
1553 r.add_section(ReportSection::new("A"));
1554 let w = MarkdownReportWriter {
1555 section_dividers: false,
1556 };
1557 let md = w.write(&r);
1558 assert!(!md.contains("\n---\n"), "divider should be absent");
1559 }
1560
1561 #[test]
1562 fn test_md_writer_figure() {
1563 let mut r = SimulationReport::new("T");
1564 r.add_figure(FigureRef::new("f", "plot.png", "Energy").with_alt("alt"));
1565 let md = MarkdownReportWriter::new().write(&r);
1566 assert!(md.contains(""));
1567 }
1568
1569 #[test]
1570 fn test_md_writer_data_table() {
1571 let mut r = SimulationReport::new("T");
1572 let mut tbl = DataTable::new("T1", vec!["X".into(), "Y".into()]);
1573 tbl.add_row(vec![
1574 CellValue::Float(1.0, Some(1)),
1575 CellValue::Float(2.0, Some(1)),
1576 ]);
1577 r.add_table(tbl);
1578 let md = MarkdownReportWriter::new().write(&r);
1579 assert!(md.contains("| X |"), "header missing");
1580 assert!(md.contains("| 1.0 |"), "data missing");
1581 }
1582
1583 #[test]
1584 fn test_md_writer_author_and_date() {
1585 let r = SimulationReport::new("T")
1586 .with_author("Bob")
1587 .with_date("2026-03-24");
1588 let md = MarkdownReportWriter::new().write(&r);
1589 assert!(md.contains("**Author:** Bob"));
1590 assert!(md.contains("**Date:** 2026-03-24"));
1591 }
1592
1593 #[test]
1596 fn test_latex_writer_contains_documentclass() {
1597 let r = SimulationReport::new("T");
1598 let tex = LatexReportWriter::new().write(&r);
1599 assert!(tex.contains("\\documentclass"), "no documentclass");
1600 }
1601
1602 #[test]
1603 fn test_latex_writer_begin_document() {
1604 let r = SimulationReport::new("T");
1605 let tex = LatexReportWriter::new().write(&r);
1606 assert!(tex.contains("\\begin{document}"));
1607 assert!(tex.contains("\\end{document}"));
1608 }
1609
1610 #[test]
1611 fn test_latex_writer_title() {
1612 let r = SimulationReport::new("Simulation Report");
1613 let tex = LatexReportWriter::new().write(&r);
1614 assert!(tex.contains("\\title{Simulation Report}"));
1615 }
1616
1617 #[test]
1618 fn test_latex_writer_parameters_section() {
1619 let mut r = SimulationReport::new("T");
1620 r.add_parameter("dt", "0.001");
1621 let tex = LatexReportWriter::new().write(&r);
1622 assert!(tex.contains("\\section{Parameters}"));
1623 assert!(tex.contains("dt"));
1624 }
1625
1626 #[test]
1627 fn test_latex_writer_results_section() {
1628 let mut r = SimulationReport::new("T");
1629 r.add_result("E_max", "42.0");
1630 let tex = LatexReportWriter::new().write(&r);
1631 assert!(tex.contains("\\section{Results Summary}"));
1632 assert!(tex.contains("E"), "result key missing");
1633 }
1634
1635 #[test]
1636 fn test_latex_writer_section() {
1637 let mut r = SimulationReport::new("T");
1638 r.add_section(ReportSection::new("Methodology"));
1639 let tex = LatexReportWriter::new().write(&r);
1640 assert!(tex.contains("\\section{Methodology}"));
1641 }
1642
1643 #[test]
1644 fn test_latex_writer_subsection() {
1645 let mut r = SimulationReport::new("T");
1646 r.add_section(ReportSection::subsection("Numerical Setup"));
1647 let tex = LatexReportWriter::new().write(&r);
1648 assert!(tex.contains("\\subsection{Numerical Setup}"));
1649 }
1650
1651 #[test]
1652 fn test_latex_writer_figure() {
1653 let mut r = SimulationReport::new("T");
1654 r.add_figure(FigureRef::new("fig1", "energy.pdf", "Energy Plot"));
1655 let tex = LatexReportWriter::new().write(&r);
1656 assert!(tex.contains("\\begin{figure}"));
1657 assert!(tex.contains("energy.pdf"));
1658 assert!(tex.contains("Energy Plot"));
1659 }
1660
1661 #[test]
1662 fn test_latex_writer_data_table() {
1663 let mut r = SimulationReport::new("T");
1664 let mut tbl = DataTable::new("Results", vec!["A".into(), "B".into()]);
1665 tbl.add_row(vec![CellValue::Int(1), CellValue::Float(2.5, Some(1))]);
1666 r.add_table(tbl);
1667 let tex = LatexReportWriter::new().write(&r);
1668 assert!(tex.contains("\\begin{tabular}"), "tabular missing");
1669 assert!(tex.contains("2.5"));
1670 }
1671
1672 #[test]
1673 fn test_latex_escape_special_chars() {
1674 assert_eq!(latex_escape("x_1 & y"), r"x\_1 \& y");
1675 assert_eq!(latex_escape("100%"), r"100\%");
1676 assert_eq!(latex_escape("$10"), r"\$10");
1677 }
1678
1679 #[test]
1680 fn test_latex_writer_version() {
1681 let r = SimulationReport::new("T").with_version("2.0");
1682 let tex = LatexReportWriter::new().write(&r);
1683 assert!(tex.contains("Version: 2.0"));
1684 }
1685
1686 #[test]
1689 fn test_report_builder_to_html() {
1690 let html = ReportBuilder::new("Builder Test")
1691 .author("Alice")
1692 .description("A test report")
1693 .parameter("n", "100")
1694 .result("max_err", "1e-5")
1695 .to_html();
1696 assert!(html.contains("Builder Test"));
1697 assert!(html.contains("Alice"));
1698 assert!(html.contains("max_err"));
1699 }
1700
1701 #[test]
1702 fn test_report_builder_to_markdown() {
1703 let md = ReportBuilder::new("MD Report")
1704 .description("Desc")
1705 .parameter("dt", "0.01")
1706 .to_markdown();
1707 assert!(md.starts_with("# MD Report"));
1708 assert!(md.contains("dt"));
1709 }
1710
1711 #[test]
1712 fn test_report_builder_to_latex() {
1713 let tex = ReportBuilder::new("LaTeX Report")
1714 .description("Abstract text")
1715 .to_latex();
1716 assert!(tex.contains("\\documentclass"));
1717 assert!(tex.contains("Abstract text"));
1718 }
1719
1720 #[test]
1721 fn test_report_builder_section_in_output() {
1722 let mut sec = ReportSection::new("Methods");
1723 sec.append_text("We used finite differences.");
1724 let html = ReportBuilder::new("Report").section(sec).to_html();
1725 assert!(html.contains("Methods"));
1726 assert!(html.contains("finite differences"));
1727 }
1728
1729 #[test]
1732 fn test_html_escape_ampersand() {
1733 assert_eq!(html_escape("a & b"), "a & b");
1734 }
1735
1736 #[test]
1737 fn test_html_escape_angle_brackets() {
1738 assert_eq!(html_escape("<tag>"), "<tag>");
1739 }
1740
1741 #[test]
1742 fn test_html_escape_quote() {
1743 assert_eq!(html_escape("\"quoted\""), ""quoted"");
1744 }
1745
1746 #[test]
1747 fn test_html_escape_plain_text() {
1748 assert_eq!(html_escape("hello world"), "hello world");
1749 }
1750
1751 #[test]
1754 fn test_markdown_table_empty_headers() {
1755 let t = DataTable::new("Cap", vec![]);
1756 let md = markdown_table(&t);
1757 assert!(md.contains("Cap") || md.is_empty() || md.contains("**Cap**"));
1759 }
1760
1761 #[test]
1762 fn test_markdown_table_with_data() {
1763 let mut t = DataTable::new("T", vec!["A".into(), "B".into()]);
1764 t.add_row(vec![CellValue::Text("x".into()), CellValue::Int(99)]);
1765 let md = markdown_table(&t);
1766 assert!(md.contains("| A |"), "header A missing");
1767 assert!(md.contains("| x |"), "cell x missing");
1768 assert!(md.contains("99"));
1769 }
1770
1771 #[test]
1772 fn test_html_table_with_highlight() {
1773 let mut t = DataTable::new("T", vec!["A".into()]);
1774 t.add_row(vec![CellValue::Int(1)]);
1775 t.highlight_row(0, "highlight");
1776 let html = html_table(&t);
1777 assert!(
1778 html.contains("class=\"highlight\""),
1779 "highlight class missing: {html}"
1780 );
1781 }
1782
1783 #[test]
1786 fn test_column_type_labels() {
1787 assert_eq!(ColumnType::Integer.label(), "integer");
1788 assert_eq!(ColumnType::Float.label(), "float");
1789 assert_eq!(ColumnType::Text.label(), "text");
1790 assert_eq!(ColumnType::Boolean.label(), "boolean");
1791 }
1792}