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