1use crate::highlighter::{Highlighter, Language, ansi_to_html};
4use crate::output::OutputMode;
5use miette::{Diagnostic, GraphicalReportHandler, GraphicalTheme};
6use owo_colors::OwoColorize;
7
8#[derive(Debug, Clone, Default)]
10pub struct Provenance {
11 pub commit: Option<String>,
13 pub commit_short: Option<String>,
15 pub timestamp: Option<String>,
17 pub rustc_version: Option<String>,
19 pub github_repo: Option<String>,
21 pub source_file: Option<String>,
23}
24
25impl Provenance {
26 pub fn from_env() -> Self {
36 Self {
37 commit: std::env::var("FACET_SHOWCASE_COMMIT").ok(),
38 commit_short: std::env::var("FACET_SHOWCASE_COMMIT_SHORT").ok(),
39 timestamp: std::env::var("FACET_SHOWCASE_TIMESTAMP").ok(),
40 rustc_version: std::env::var("FACET_SHOWCASE_RUSTC_VERSION").ok(),
41 github_repo: std::env::var("FACET_SHOWCASE_GITHUB_REPO").ok(),
42 source_file: std::env::var("FACET_SHOWCASE_SOURCE_FILE").ok(),
43 }
44 }
45
46 pub fn github_source_url(&self) -> Option<String> {
48 match (&self.github_repo, &self.commit, &self.source_file) {
49 (Some(repo), Some(commit), Some(file)) => {
50 Some(format!("https://github.com/{repo}/blob/{commit}/{file}"))
51 }
52 _ => None,
53 }
54 }
55
56 pub fn has_info(&self) -> bool {
58 self.commit.is_some() || self.timestamp.is_some() || self.rustc_version.is_some()
59 }
60}
61
62pub struct ShowcaseRunner {
64 title: String,
66 slug: Option<String>,
68 mode: OutputMode,
70 highlighter: Highlighter,
72 primary_language: Language,
74 scenario_count: usize,
76 in_section: bool,
78 filter: Option<String>,
80 provenance: Provenance,
82}
83
84impl ShowcaseRunner {
85 pub fn new(title: impl Into<String>) -> Self {
90 Self {
91 title: title.into(),
92 slug: None,
93 mode: OutputMode::from_env(),
94 highlighter: Highlighter::new(),
95 primary_language: Language::Json,
96 scenario_count: 0,
97 in_section: false,
98 filter: std::env::var("SHOWCASE_FILTER").ok(),
99 provenance: Provenance::from_env(),
100 }
101 }
102
103 pub fn filter(mut self, filter: impl Into<String>) -> Self {
107 self.filter = Some(filter.into());
108 self
109 }
110
111 pub fn slug(mut self, slug: impl Into<String>) -> Self {
113 self.slug = Some(slug.into());
114 self
115 }
116
117 pub fn language(mut self, lang: Language) -> Self {
119 self.primary_language = lang;
120 self
121 }
122
123 pub fn with_kdl_syntaxes(mut self, syntax_dir: &str) -> Self {
125 self.highlighter = std::mem::take(&mut self.highlighter).with_kdl_syntaxes(syntax_dir);
126 self
127 }
128
129 pub fn header(&self) {
131 match self.mode {
132 OutputMode::Terminal => {
133 println!();
134 self.print_box(&self.title, "cyan");
135 }
136 OutputMode::Markdown => {
137 println!("+++");
139 println!("title = \"{}\"", self.title);
140 if let Some(ref slug) = self.slug {
141 println!("slug = \"{slug}\"");
142 }
143 println!("+++");
144 println!();
145 println!("<div class=\"showcase\">");
146 }
147 }
148 }
149
150 pub fn intro(&self, text: &str) {
155 match self.mode {
156 OutputMode::Terminal => {
157 println!();
158 println!("{}", text.dimmed());
159 println!();
160 }
161 OutputMode::Markdown => {
162 println!();
163 println!("{text}");
164 println!();
165 }
166 }
167 }
168
169 pub fn scenario(&mut self, name: impl Into<String>) -> Scenario<'_> {
173 let name = name.into();
174 let skipped = match &self.filter {
175 Some(filter) => !name.to_lowercase().contains(&filter.to_lowercase()),
176 None => false,
177 };
178 if !skipped {
179 self.scenario_count += 1;
180 }
181 Scenario::new(self, name, skipped)
182 }
183
184 pub fn section(&mut self, name: &str) {
189 self.in_section = true;
190
191 match self.mode {
192 OutputMode::Terminal => {
193 println!();
194 println!();
195 println!("{}", "━".repeat(78).bold().yellow());
196 println!(" {}", name.bold().yellow());
197 println!("{}", "━".repeat(78).bold().yellow());
198 }
199 OutputMode::Markdown => {
200 println!();
201 println!("## {name}");
202 println!();
203 }
204 }
205 }
206
207 pub fn footer(&self) {
209 match self.mode {
210 OutputMode::Terminal => {
211 println!();
212 self.print_box("END OF SHOWCASE", "green");
213 if self.provenance.has_info() {
214 println!();
215 println!("{}", "Provenance:".dimmed());
216 if let Some(ref commit) = self.provenance.commit_short {
217 println!(" {} {}", "Commit:".dimmed(), commit);
218 }
219 if let Some(ref ts) = self.provenance.timestamp {
220 println!(" {} {}", "Generated:".dimmed(), ts);
221 }
222 if let Some(ref rustc) = self.provenance.rustc_version {
223 println!(" {} {}", "Rustc:".dimmed(), rustc);
224 }
225 if let Some(url) = self.provenance.github_source_url() {
226 println!(" {} {}", "Source:".dimmed(), url);
227 }
228 }
229 }
230 OutputMode::Markdown => {
231 if self.provenance.has_info() {
233 println!();
234 println!("<footer class=\"showcase-provenance\">");
235 println!("<p>This showcase was auto-generated from source code.</p>");
236 println!("<dl>");
237 if let Some(url) = self.provenance.github_source_url()
238 && let Some(ref file) = self.provenance.source_file
239 {
240 println!(
241 "<dt>Source</dt><dd><a href=\"{url}\"><code>{file}</code></a></dd>"
242 );
243 }
244 if let Some(ref commit) = self.provenance.commit_short {
245 if let Some(ref repo) = self.provenance.github_repo {
246 if let Some(ref full_commit) = self.provenance.commit {
247 println!(
248 "<dt>Commit</dt><dd><a href=\"https://github.com/{repo}/commit/{full_commit}\"><code>{commit}</code></a></dd>"
249 );
250 }
251 } else {
252 println!("<dt>Commit</dt><dd><code>{commit}</code></dd>");
253 }
254 }
255 if let Some(ref ts) = self.provenance.timestamp {
256 println!("<dt>Generated</dt><dd><time datetime=\"{ts}\">{ts}</time></dd>");
257 }
258 if let Some(ref rustc) = self.provenance.rustc_version {
259 println!("<dt>Compiler</dt><dd><code>{rustc}</code></dd>");
260 }
261 println!("</dl>");
262 println!("</footer>");
263 }
264 println!("</div>");
265 }
266 }
267 }
268
269 pub fn highlighter(&self) -> &Highlighter {
271 &self.highlighter
272 }
273
274 pub fn mode(&self) -> OutputMode {
276 self.mode
277 }
278
279 pub fn primary_language(&self) -> Language {
281 self.primary_language
282 }
283
284 fn print_box(&self, text: &str, color: &str) {
286 let width = 70;
288 let inner_width = width - 2; let top = format!("╭{}╮", "─".repeat(inner_width));
291 let bottom = format!("╰{}╯", "─".repeat(inner_width));
292 let empty_line = format!("│{}│", " ".repeat(inner_width));
293
294 let text_padding = (inner_width.saturating_sub(text.len())) / 2;
296 let text_line = format!(
297 "│{}{}{}│",
298 " ".repeat(text_padding),
299 text,
300 " ".repeat(inner_width - text_padding - text.len())
301 );
302
303 let output = match color {
304 "cyan" => {
305 format!(
306 "{}\n{}\n{}\n{}\n{}",
307 top.cyan(),
308 empty_line.cyan(),
309 text_line.cyan(),
310 empty_line.cyan(),
311 bottom.cyan()
312 )
313 }
314 "green" => {
315 format!(
316 "{}\n{}\n{}\n{}\n{}",
317 top.green(),
318 empty_line.green(),
319 text_line.green(),
320 empty_line.green(),
321 bottom.green()
322 )
323 }
324 _ => {
325 format!("{top}\n{empty_line}\n{text_line}\n{empty_line}\n{bottom}")
326 }
327 };
328 println!("{output}");
329 }
330}
331
332pub struct Scenario<'a> {
334 runner: &'a mut ShowcaseRunner,
335 name: String,
336 description: Option<String>,
337 printed_header: bool,
338 skipped: bool,
340}
341
342impl<'a> Scenario<'a> {
343 fn new(runner: &'a mut ShowcaseRunner, name: String, skipped: bool) -> Self {
344 Self {
345 runner,
346 name,
347 description: None,
348 printed_header: false,
349 skipped,
350 }
351 }
352
353 pub fn description(mut self, desc: impl Into<String>) -> Self {
355 self.description = Some(desc.into());
356 self
357 }
358
359 fn ensure_header(&mut self) {
361 if self.skipped || self.printed_header {
362 return;
363 }
364 self.printed_header = true;
365
366 match self.runner.mode {
367 OutputMode::Terminal => {
368 println!();
369 println!("{}", "═".repeat(78).dimmed());
370 println!("{} {}", "SCENARIO:".bold().cyan(), self.name.bold().white());
371 println!("{}", "─".repeat(78).dimmed());
372 if let Some(ref desc) = self.description {
373 println!("{}", desc.dimmed());
374 }
375 println!("{}", "═".repeat(78).dimmed());
376 }
377 OutputMode::Markdown => {
378 let heading = if self.runner.in_section { "###" } else { "##" };
381 println!();
382 println!("{} {}", heading, self.name);
383 println!();
384 println!("<section class=\"scenario\">");
385 if let Some(ref desc) = self.description {
386 println!(
387 "<p class=\"description\">{}</p>",
388 markdown_inline_to_html(desc)
389 );
390 }
391 }
392 }
393 }
394
395 pub fn input(mut self, lang: Language, code: &str) -> Self {
397 if self.skipped {
398 return self;
399 }
400 self.ensure_header();
401
402 match self.runner.mode {
403 OutputMode::Terminal => {
404 println!();
405 println!("{}", format!("{} Input:", lang.name()).bold().green());
406 println!("{}", "─".repeat(60).dimmed());
407 print!(
408 "{}",
409 self.runner
410 .highlighter
411 .highlight_to_terminal_with_line_numbers(code, lang)
412 );
413 println!("{}", "─".repeat(60).dimmed());
414 }
415 OutputMode::Markdown => {
416 println!("<div class=\"input\">");
417 println!("<h4>{} Input</h4>", lang.name());
418 println!("{}", self.runner.highlighter.highlight_to_html(code, lang));
420 println!("</div>");
421 }
422 }
423 self
424 }
425
426 pub fn input_value<'b, T: facet::Facet<'b>>(mut self, value: &'b T) -> Self {
428 if self.skipped {
429 return self;
430 }
431 self.ensure_header();
432
433 use facet_pretty::FacetPretty;
434
435 match self.runner.mode {
436 OutputMode::Terminal => {
437 println!();
438 println!("{}", "Value Input:".bold().green());
439 println!("{}", "─".repeat(60).dimmed());
440 println!(" {}", value.pretty());
441 println!("{}", "─".repeat(60).dimmed());
442 }
443 OutputMode::Markdown => {
444 let pretty_output = format!("{}", value.pretty());
445 println!("<div class=\"input\">");
446 println!("<h4>Value Input</h4>");
447 println!("<pre><code>{}</code></pre>", ansi_to_html(&pretty_output));
448 println!("</div>");
449 }
450 }
451 self
452 }
453
454 pub fn serialized_output(mut self, lang: Language, code: &str) -> Self {
456 if self.skipped {
457 return self;
458 }
459 self.ensure_header();
460
461 match self.runner.mode {
462 OutputMode::Terminal => {
463 println!();
464 println!("{}", format!("{} Output:", lang.name()).bold().magenta());
465 println!("{}", "─".repeat(60).dimmed());
466 print!(
467 "{}",
468 self.runner
469 .highlighter
470 .highlight_to_terminal_with_line_numbers(code, lang)
471 );
472 println!("{}", "─".repeat(60).dimmed());
473 }
474 OutputMode::Markdown => {
475 println!("<div class=\"serialized-output\">");
476 println!("<h4>{} Output</h4>", lang.name());
477 println!("{}", self.runner.highlighter.highlight_to_html(code, lang));
479 println!("</div>");
480 }
481 }
482 self
483 }
484
485 pub fn target_type<T: facet::Facet<'static>>(mut self) -> Self {
487 if self.skipped {
488 return self;
489 }
490 self.ensure_header();
491
492 let type_def = facet_pretty::format_shape(T::SHAPE);
493
494 match self.runner.mode {
495 OutputMode::Terminal => {
496 println!();
497 println!("{}", "Target Type:".bold().blue());
498 println!("{}", "─".repeat(60).dimmed());
499 print!(
500 "{}",
501 self.runner
502 .highlighter
503 .highlight_to_terminal(&type_def, Language::Rust)
504 );
505 println!("{}", "─".repeat(60).dimmed());
506 }
507 OutputMode::Markdown => {
508 println!("<details class=\"target-type\">");
509 println!("<summary>Target Type</summary>");
510 println!(
512 "{}",
513 self.runner
514 .highlighter
515 .highlight_to_html(&type_def, Language::Rust)
516 );
517 println!("</details>");
518 }
519 }
520 self
521 }
522
523 pub fn target_type_str(mut self, type_def: &str) -> Self {
525 if self.skipped {
526 return self;
527 }
528 self.ensure_header();
529
530 match self.runner.mode {
531 OutputMode::Terminal => {
532 println!();
533 println!("{}", "Target Type:".bold().blue());
534 println!("{}", "─".repeat(60).dimmed());
535 print!(
536 "{}",
537 self.runner
538 .highlighter
539 .highlight_to_terminal(type_def, Language::Rust)
540 );
541 println!("{}", "─".repeat(60).dimmed());
542 }
543 OutputMode::Markdown => {
544 println!("<details class=\"target-type\">");
545 println!("<summary>Target Type</summary>");
546 println!(
548 "{}",
549 self.runner
550 .highlighter
551 .highlight_to_html(type_def, Language::Rust)
552 );
553 println!("</details>");
554 }
555 }
556 self
557 }
558
559 pub fn error(mut self, err: &dyn Diagnostic) -> Self {
561 if self.skipped {
562 return self;
563 }
564 self.ensure_header();
565
566 match self.runner.mode {
567 OutputMode::Terminal => {
568 println!();
569 println!("{}", "Error:".bold().red());
570
571 let mut output = String::new();
572 let highlighter = self
573 .runner
574 .highlighter
575 .build_miette_highlighter(self.runner.primary_language);
576 let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
577 .with_syntax_highlighting(highlighter);
578 handler.render_report(&mut output, err).unwrap();
579 println!("{output}");
580 }
581 OutputMode::Markdown => {
582 let mut output = String::new();
584 let highlighter = self
585 .runner
586 .highlighter
587 .build_miette_highlighter(self.runner.primary_language);
588 let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
589 .with_syntax_highlighting(highlighter);
590 handler.render_report(&mut output, err).unwrap();
591
592 println!("<div class=\"error\">");
593 println!("<h4>Error</h4>");
594 println!("<pre><code>{}</code></pre>", ansi_to_html(&output));
595 println!("</div>");
596 }
597 }
598 self
599 }
600
601 pub fn compiler_error(mut self, ansi_output: &str) -> Self {
603 if self.skipped {
604 return self;
605 }
606 self.ensure_header();
607
608 match self.runner.mode {
609 OutputMode::Terminal => {
610 println!();
611 println!("{}", "Compiler Error:".bold().red());
612 println!("{ansi_output}");
613 }
614 OutputMode::Markdown => {
615 println!("<div class=\"compiler-error\">");
616 println!("<h4>Compiler Error</h4>");
617 println!("<pre><code>{}</code></pre>", ansi_to_html(ansi_output));
618 println!("</div>");
619 }
620 }
621 self
622 }
623
624 pub fn success<'b, T: facet::Facet<'b>>(mut self, value: &'b T) -> Self {
626 if self.skipped {
627 return self;
628 }
629 self.ensure_header();
630
631 use facet_pretty::FacetPretty;
632
633 match self.runner.mode {
634 OutputMode::Terminal => {
635 println!();
636 println!("{}", "Success:".bold().green());
637 println!(" {}", value.pretty());
638 }
639 OutputMode::Markdown => {
640 let pretty_output = format!("{}", value.pretty());
641 println!("<div class=\"success\">");
642 println!("<h4>Success</h4>");
643 println!("<pre><code>{}</code></pre>", ansi_to_html(&pretty_output));
644 println!("</div>");
645 }
646 }
647 self
648 }
649
650 pub fn result<'b, T: facet::Facet<'b>, E: Diagnostic>(self, result: &'b Result<T, E>) -> Self {
652 match result {
653 Ok(value) => self.success(value),
654 Err(err) => self.error(err),
655 }
656 }
657
658 pub fn finish(mut self) {
660 if self.skipped {
661 return;
662 }
663 self.ensure_header();
664
665 if self.runner.mode == OutputMode::Markdown {
666 println!("</section>");
667 }
668 }
669}
670
671fn markdown_inline_to_html(text: &str) -> String {
673 let mut result = String::new();
674 let chars = text.chars();
675 let mut in_code = false;
676
677 for c in chars {
678 if c == '`' {
679 if in_code {
680 result.push_str("</code>");
681 in_code = false;
682 } else {
683 result.push_str("<code>");
684 in_code = true;
685 }
686 } else if c == '<' {
687 result.push_str("<");
688 } else if c == '>' {
689 result.push_str(">");
690 } else if c == '&' {
691 result.push_str("&");
692 } else if c == '\n' {
693 result.push_str("<br>");
694 } else {
695 result.push(c);
696 }
697 }
698
699 if in_code {
700 result.push_str("</code>");
701 }
702
703 result
704}