1use std::io::{IsTerminal, Write};
9
10use crate::color::ColorSystem;
11use crate::protocol::{Highlighter, Renderable};
12use crate::segment::Segment;
13use crate::style::Style;
14use crate::text::Text;
15use crate::theme::Theme;
16
17const DEFAULT_WIDTH: usize = 80;
18const DEFAULT_HEIGHT: usize = 25;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum Justify {
24 #[default]
26 Default,
27 Left,
28 Center,
29 Right,
30 Full,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum Overflow {
37 #[default]
39 Fold,
40 Crop,
42 Ellipsis,
44 Ignore,
46}
47
48#[derive(Debug, Clone)]
53pub struct ConsoleOptions {
54 pub min_width: usize,
55 pub max_width: usize,
56 pub height: Option<usize>,
57 pub justify: Justify,
58 pub overflow: Option<Overflow>,
61 pub no_wrap: Option<bool>,
64}
65
66impl ConsoleOptions {
67 pub fn update_width(&self, width: usize) -> ConsoleOptions {
70 let mut options = self.clone();
73 options.min_width = width;
74 options.max_width = width;
75 options
76 }
77
78 pub fn update_dimensions(&self, width: usize, height: usize) -> ConsoleOptions {
81 let mut options = self.update_width(width);
82 options.height = Some(height);
83 options
84 }
85}
86
87pub struct Console {
90 color_system: Option<ColorSystem>,
91 width: usize,
92 height: usize,
93 is_terminal: bool,
94 no_color: bool,
95 emoji: bool,
96 highlight: bool,
97 legacy_windows: bool,
98 safe_box: bool,
99 ascii_only: bool,
100 theme: Theme,
101 base_style: Style,
102 highlighters: Vec<Box<dyn Highlighter + Send>>,
103 record_buffer: std::cell::RefCell<Vec<Segment>>,
106 capturing: std::cell::Cell<bool>,
107}
108
109impl Default for Console {
110 fn default() -> Self {
111 Console::new()
112 }
113}
114
115impl Console {
116 pub fn new() -> Self {
118 ConsoleBuilder::new().build()
119 }
120
121 pub fn builder() -> ConsoleBuilder {
123 ConsoleBuilder::new()
124 }
125
126 pub fn color_system(&self) -> Option<ColorSystem> {
128 if self.no_color {
129 None
130 } else {
131 self.color_system
132 }
133 }
134
135 pub fn width(&self) -> usize {
137 self.width
138 }
139
140 pub fn height(&self) -> usize {
143 self.height
144 }
145
146 pub fn is_terminal(&self) -> bool {
148 self.is_terminal
149 }
150
151 pub fn legacy_windows(&self) -> bool {
153 self.legacy_windows
154 }
155
156 pub fn safe_box(&self) -> bool {
158 self.safe_box
159 }
160
161 pub fn ascii_only(&self) -> bool {
163 self.ascii_only
164 }
165
166 pub fn theme(&self) -> &Theme {
168 &self.theme
169 }
170
171 pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
174 self.theme.get_style(style)
175 }
176
177 pub fn base_style(&self) -> &Style {
179 &self.base_style
180 }
181
182 pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
186 self.highlighters.push(highlighter);
187 }
188
189 pub fn options(&self) -> ConsoleOptions {
191 ConsoleOptions {
192 min_width: 1,
193 max_width: self.width,
194 height: None,
195 justify: Justify::Default,
196 overflow: None,
197 no_wrap: None,
198 }
199 }
200
201 pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
208 let segments = self.render_segments(renderable);
209 self.segments_to_string(&segments)
210 }
211
212 fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
215 let mut options = self.options();
216 if options.justify == Justify::Default {
217 let measurement = renderable.measure(self, &options);
218 options.max_width = measurement.maximum.min(options.max_width).max(1);
219 }
220 let segments = renderable.rich_render(self, &options);
221 Segment::crop_lines(&segments, self.width)
226 }
227
228 fn emit(&self, segments: Vec<Segment>) {
231 if self.capturing.get() {
232 let mut buffer = self.record_buffer.borrow_mut();
233 buffer.extend(segments);
234 buffer.push(Segment::line());
235 return;
236 }
237 let mut output = self.segments_to_string(&segments);
238 output.push('\n');
239 let stdout = std::io::stdout();
240 let mut lock = stdout.lock();
241 let _ = write!(lock, "{output}");
242 }
243
244 pub fn render_lines(
250 &self,
251 renderable: &dyn Renderable,
252 options: &ConsoleOptions,
253 pad: bool,
254 ) -> Vec<Vec<Segment>> {
255 let segments = renderable.rich_render(self, options);
256 let mut lines = Segment::split_lines(&segments);
257 if pad {
258 for line in &mut lines {
259 *line = Segment::adjust_line_length(line, options.max_width, Some(Style::new()));
260 }
261 }
262 if let Some(height) = options.height {
266 lines.truncate(height);
267 while lines.len() < height {
268 lines.push(if pad {
269 vec![Segment::new(
270 " ".repeat(options.max_width),
271 Some(Style::new()),
272 )]
273 } else {
274 Vec::new()
275 });
276 }
277 }
278 lines
279 }
280
281 pub fn render_export(&self, renderable: &dyn Renderable) -> String {
285 let mut out = self.render_to_string(renderable);
286 out.push('\n');
287 out
288 }
289
290 pub fn print(&self, renderable: &dyn Renderable) {
292 let segments = self.render_segments(renderable);
293 self.emit(segments);
294 }
295
296 pub fn control(&self, control: &crate::control::Control) {
301 if !self.is_terminal {
302 return;
303 }
304 let text = control.as_str();
305 if !text.is_empty() {
306 let stdout = std::io::stdout();
307 let mut lock = stdout.lock();
308 let _ = write!(lock, "{text}");
309 }
310 }
311
312 pub fn show_cursor(&self, show: bool) {
314 self.control(&crate::control::Control::show_cursor(show));
315 }
316
317 pub fn clear(&self) {
319 self.control(&crate::control::Control::clear());
320 }
321
322 pub fn bell(&self) {
324 self.control(&crate::control::Control::bell());
325 }
326
327 pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
334 let segments = self.record(f);
335 self.segments_to_string(&segments)
336 }
337
338 pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
341 let segments = self.record(f);
342 segments_to_plain(&segments)
343 }
344
345 pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
353 self.page_with(&crate::pager::SystemPager, styles, f)
354 }
355
356 pub fn page_with(
359 &self,
360 pager: &dyn crate::pager::Pager,
361 styles: bool,
362 f: impl FnOnce(&Console),
363 ) -> std::io::Result<()> {
364 let segments = self.record(f);
365 let content = if styles {
366 self.segments_to_string(&segments)
367 } else {
368 segments_to_plain(&segments)
369 };
370 pager.show(&content)
371 }
372
373 pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
377 self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
378 }
379
380 pub fn export_html_themed(
386 &self,
387 theme: &crate::terminal_theme::TerminalTheme,
388 f: impl FnOnce(&Console),
389 ) -> String {
390 let segments = self.record(f);
391 crate::export::export_html_inline(&segments, theme)
392 }
393
394 pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
398 self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
399 }
400
401 pub fn export_html_classes_themed(
404 &self,
405 theme: &crate::terminal_theme::TerminalTheme,
406 f: impl FnOnce(&Console),
407 ) -> String {
408 let segments = self.record(f);
409 crate::export::export_html_classes(&segments, theme)
410 }
411
412 pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
423 self.export_svg_themed(
424 &crate::terminal_theme::SVG_EXPORT_THEME,
425 title,
426 unique_id,
427 f,
428 )
429 }
430
431 pub fn export_svg_themed(
434 &self,
435 theme: &crate::terminal_theme::TerminalTheme,
436 title: &str,
437 unique_id: &str,
438 f: impl FnOnce(&Console),
439 ) -> String {
440 let segments = self.record(f);
441 crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
442 }
443
444 pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
461 self.record(f)
462 }
463
464 fn record(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
467 let previous = std::mem::take(&mut *self.record_buffer.borrow_mut());
468 let was_capturing = self.capturing.replace(true);
469 f(self);
470 let captured = std::mem::replace(&mut *self.record_buffer.borrow_mut(), previous);
471 self.capturing.set(was_capturing);
472 captured
473 }
474
475 pub fn print_str(&self, content: &str) {
478 let text = self.build_text(content);
479 self.print(&text);
480 }
481
482 pub fn render_str_to_string(&self, content: &str) -> String {
484 let text = self.build_text(content);
485 self.render_to_string(&text)
486 }
487
488 pub fn build_text(&self, content: &str) -> Text {
492 self.try_build_text(content)
497 .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
498 }
499
500 pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
504 let expanded = self.expand_emoji(content);
505 let markup = Text::from_markup(&expanded)?;
506
507 let mut text = self.decorate(Text::new(markup.plain()));
518 for span in markup.spans() {
519 text.push_span(span.clone());
520 }
521 Ok(text)
522 }
523
524 pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
526 self.print(&self.try_build_text(content)?);
527 Ok(())
528 }
529
530 pub fn try_print_justified(
533 &self,
534 content: &str,
535 justify: Justify,
536 ) -> crate::errors::Result<()> {
537 let text = self.try_build_text(content)?;
538 let mut options = self.options();
539 options.justify = justify;
540 self.emit(text.rich_render(self, &options));
541 Ok(())
542 }
543
544 fn expand_emoji(&self, content: &str) -> String {
547 if self.emoji {
548 crate::emoji::replace(content)
549 } else {
550 content.to_string()
551 }
552 }
553
554 fn decorate(&self, mut text: Text) -> Text {
557 for highlighter in &self.highlighters {
558 highlighter.highlight(&mut text);
559 }
560 if self.highlight {
561 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
562 }
563 text
564 }
565
566 pub fn print_justified(&self, content: &str, justify: Justify) {
569 let text = self.build_text(content);
570 let mut options = self.options();
571 options.justify = justify;
572 let segments = text.rich_render(self, &options);
573 self.emit(segments);
574 }
575
576 pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
581 let text = self.build_text(content);
582 let mut options = self.options();
583 options.justify = justify;
584 let segments = text.rich_render(self, &options);
585 self.segments_to_string(&segments)
586 }
587
588 pub fn segments_to_string(&self, segments: &[Segment]) -> String {
591 let system = self.color_system();
592 let mut out = String::new();
593 for segment in segments {
594 if segment.control && !self.is_terminal {
597 continue;
598 }
599 match (&segment.style, system) {
600 (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
601 _ => out.push_str(&segment.text),
602 }
603 }
604 out
605 }
606}
607
608fn segments_to_plain(segments: &[Segment]) -> String {
611 segments
612 .iter()
613 .filter(|s| !s.control)
614 .map(|s| s.text.as_str())
615 .collect()
616}
617
618impl Renderable for Text {
619 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
620 let justify = if self.get_justify() != Justify::Default {
623 self.get_justify()
624 } else {
625 options.justify
626 };
627 let overflow = self
631 .get_overflow()
632 .or(options.overflow)
633 .unwrap_or(Overflow::Fold);
634 let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
635 self.render_joined_wrapped(
636 console.theme(),
637 console.base_style(),
638 options.max_width,
639 justify,
640 overflow,
641 no_wrap,
642 )
643 }
644
645 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
646 let (minimum, maximum) = self.measurement();
647 crate::measure::Measurement::new(
648 minimum.min(options.max_width),
649 maximum.min(options.max_width),
650 )
651 }
652}
653
654pub struct ConsoleBuilder {
656 force_terminal: Option<bool>,
657 color_system: Option<ColorSystem>,
658 color_system_set: bool,
659 width: Option<usize>,
660 height: Option<usize>,
661 no_color: Option<bool>,
662 emoji: Option<bool>,
663 highlight: Option<bool>,
664 legacy_windows: Option<bool>,
665 safe_box: Option<bool>,
666 ascii_only: Option<bool>,
667 theme: Option<Theme>,
668}
669
670impl ConsoleBuilder {
671 fn new() -> Self {
672 ConsoleBuilder {
673 force_terminal: None,
674 color_system: None,
675 color_system_set: false,
676 width: None,
677 height: None,
678 no_color: None,
679 emoji: None,
680 highlight: None,
681 legacy_windows: None,
682 safe_box: None,
683 ascii_only: None,
684 theme: None,
685 }
686 }
687
688 pub fn force_terminal(mut self, value: bool) -> Self {
689 self.force_terminal = Some(value);
690 self
691 }
692
693 pub fn legacy_windows(mut self, value: bool) -> Self {
695 self.legacy_windows = Some(value);
696 self
697 }
698
699 pub fn safe_box(mut self, value: bool) -> Self {
701 self.safe_box = Some(value);
702 self
703 }
704
705 pub fn ascii_only(mut self, value: bool) -> Self {
707 self.ascii_only = Some(value);
708 self
709 }
710
711 pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
713 self.color_system = system;
714 self.color_system_set = true;
715 self
716 }
717
718 pub fn width(mut self, width: usize) -> Self {
719 self.width = Some(width);
720 self
721 }
722
723 pub fn height(mut self, height: usize) -> Self {
725 self.height = Some(height);
726 self
727 }
728
729 pub fn no_color(mut self, value: bool) -> Self {
730 self.no_color = Some(value);
731 self
732 }
733
734 pub fn emoji(mut self, value: bool) -> Self {
736 self.emoji = Some(value);
737 self
738 }
739
740 pub fn highlight(mut self, value: bool) -> Self {
743 self.highlight = Some(value);
744 self
745 }
746
747 pub fn theme(mut self, theme: Theme) -> Self {
748 self.theme = Some(theme);
749 self
750 }
751
752 pub fn build(self) -> Console {
753 let is_terminal = self
754 .force_terminal
755 .unwrap_or_else(|| std::io::stdout().is_terminal());
756 let no_color = self
761 .no_color
762 .unwrap_or_else(|| std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()));
763 let color_system = if self.color_system_set {
764 self.color_system
765 } else if is_terminal {
766 Some(detect_color_system())
767 } else {
768 None
769 };
770 let width = self.width.unwrap_or_else(detect_width);
771 let height = self.height.unwrap_or_else(detect_height);
772 Console {
773 color_system,
774 width,
775 height,
776 is_terminal,
777 no_color,
778 emoji: self.emoji.unwrap_or(true),
779 highlight: self.highlight.unwrap_or(true),
784 legacy_windows: self.legacy_windows.unwrap_or(false),
785 safe_box: self.safe_box.unwrap_or(true),
786 ascii_only: self.ascii_only.unwrap_or(false),
787 theme: self.theme.unwrap_or_else(Theme::default_theme),
788 base_style: Style::new(),
789 highlighters: Vec::new(),
790 record_buffer: std::cell::RefCell::new(Vec::new()),
791 capturing: std::cell::Cell::new(false),
792 }
793 }
794}
795
796fn detect_color_system() -> ColorSystem {
808 if let Some(colorterm) = std::env::var_os("COLORTERM") {
809 let colorterm = colorterm.to_string_lossy().to_ascii_lowercase();
810 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
811 return ColorSystem::Truecolor;
812 }
813 }
814
815 #[cfg(windows)]
827 {
828 let _ = anstyle_query::windows::enable_ansi_colors();
829 ColorSystem::Truecolor
830 }
831
832 #[cfg(not(windows))]
835 {
836 if let Some(term) = std::env::var_os("TERM") {
837 if term.to_string_lossy().contains("256") {
838 return ColorSystem::EightBit;
839 }
840 }
841 ColorSystem::Standard
842 }
843}
844
845fn detect_width() -> usize {
847 if let Some(columns) = std::env::var_os("COLUMNS") {
848 if let Ok(value) = columns.to_string_lossy().trim().parse::<usize>() {
849 if value > 0 {
850 return value;
851 }
852 }
853 }
854 if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
855 if w > 0 {
856 return w as usize;
857 }
858 }
859 DEFAULT_WIDTH
860}
861
862fn detect_height() -> usize {
864 if let Some(lines) = std::env::var_os("LINES") {
865 if let Ok(value) = lines.to_string_lossy().trim().parse::<usize>() {
866 if value > 0 {
867 return value;
868 }
869 }
870 }
871 if let Some((_, terminal_size::Height(h))) = terminal_size::terminal_size() {
872 if h > 0 {
873 return h as usize;
874 }
875 }
876 DEFAULT_HEIGHT
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 fn test_console() -> Console {
884 Console::builder()
885 .force_terminal(true)
886 .color_system(Some(ColorSystem::Truecolor))
887 .width(80)
888 .no_color(false)
889 .build()
890 }
891
892 #[test]
895 fn try_build_text_reports_bad_markup() {
896 let console = test_console();
897
898 let err = console
899 .try_build_text("[/nope]")
900 .expect_err("an unmatched closing tag must be an error");
901 assert!(
902 matches!(err, crate::errors::RichError::Markup(_)),
903 "{err:?}"
904 );
905 assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
907
908 let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
909 assert_eq!(strict.plain(), "hi");
910 assert_eq!(
911 strict.spans().len(),
912 console.build_text("[bold]hi[/]").spans().len()
913 );
914 }
915
916 #[test]
924 fn unknown_tag_names_render_as_no_ops() {
925 let console = test_console();
926 let text = console
927 .try_build_text("[nope]x[/]")
928 .expect("an unknown tag name is not a syntax error");
929 assert_eq!(console.render_to_string(&text), "x");
930 assert_eq!(
931 console.render_to_string(&console.build_text("[a.b.c]x[/]")),
932 "x"
933 );
934
935 assert!(console.try_build_text("[bold]a[/italic]").is_err());
937 assert!(console.try_build_text("[/nope]").is_err());
938 }
939
940 #[test]
943 fn markup_styles_bind_at_render_not_at_parse() {
944 let themed = |definition: &str| {
945 let mut theme = Theme::default_theme();
946 theme.insert("accent", Style::parse(definition).unwrap());
947 Console::builder()
948 .force_terminal(true)
949 .color_system(Some(ColorSystem::Truecolor))
950 .width(80)
951 .no_color(false)
952 .theme(theme)
953 .build()
954 };
955 let red = themed("bold red");
956 let green = themed("underline green");
957
958 let text = red.build_text("[accent]hi[/]");
960 assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
961 assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
963 }
964
965 #[test]
968 fn try_build_text_expands_emoji_like_build_text() {
969 let console = test_console();
970 assert_eq!(
971 console
972 .try_build_text(":rocket: go")
973 .expect("valid")
974 .plain(),
975 console.build_text(":rocket: go").plain()
976 );
977 }
978
979 #[test]
980 fn renders_markup_string() {
981 let console = test_console();
982 assert_eq!(
983 console.render_str_to_string("[bold red]hi[/]"),
984 "\x1b[1;31mhi\x1b[0m"
985 );
986 }
987
988 #[test]
989 fn print_justify_pads_to_width() {
990 let console = Console::builder()
991 .force_terminal(true)
992 .color_system(Some(ColorSystem::Truecolor))
993 .width(10)
994 .build();
995 assert_eq!(
997 console.render_justified_to_string("hi", Justify::Left),
998 "hi "
999 );
1000 assert_eq!(
1001 console.render_justified_to_string("hi", Justify::Center),
1002 " hi "
1003 );
1004 assert_eq!(
1005 console.render_justified_to_string("hi", Justify::Right),
1006 " hi"
1007 );
1008 }
1009
1010 #[test]
1011 fn capture_records_ansi_instead_of_stdout() {
1012 let console = Console::builder()
1013 .force_terminal(true)
1014 .color_system(Some(ColorSystem::Truecolor))
1015 .width(20)
1016 .build();
1017 let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
1019 assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
1020 }
1021
1022 #[test]
1023 fn themed_exports_use_the_given_palette() {
1024 use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
1025
1026 let console = Console::builder()
1027 .force_terminal(true)
1028 .color_system(Some(ColorSystem::Truecolor))
1029 .width(20)
1030 .no_color(false)
1031 .build();
1032 let render = |c: &Console| c.print_str("hi");
1033
1034 let monokai = console.export_html_themed(&MONOKAI, render);
1037 assert!(
1038 monokai.contains("#0c0c0c"),
1039 "monokai bg missing:\n{monokai}"
1040 );
1041
1042 let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
1043 assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
1044 assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
1045
1046 let classes = console.export_html_classes_themed(&MONOKAI, render);
1048 assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
1049 let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
1050 assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
1051
1052 assert!(console.export_html(render).contains("#ffffff"));
1054 }
1055
1056 #[test]
1057 fn page_with_honors_the_styles_flag() {
1058 use std::sync::Mutex;
1059
1060 #[derive(Default)]
1061 struct Recorder(Mutex<String>);
1062 impl crate::pager::Pager for Recorder {
1063 fn show(&self, content: &str) -> std::io::Result<()> {
1064 *self.0.lock().unwrap() = content.to_string();
1065 Ok(())
1066 }
1067 }
1068
1069 let console = Console::builder()
1070 .force_terminal(true)
1071 .color_system(Some(ColorSystem::Truecolor))
1072 .width(20)
1073 .no_color(false)
1074 .build();
1075
1076 let plain = Recorder::default();
1078 console
1079 .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
1080 .unwrap();
1081 assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
1082
1083 let styled = Recorder::default();
1085 console
1086 .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
1087 .unwrap();
1088 assert_eq!(
1089 styled.0.lock().unwrap().as_str(),
1090 "\x1b[1;31mhi\x1b[0m there\n"
1091 );
1092 }
1093
1094 #[test]
1095 fn export_text_strips_styles() {
1096 let console = Console::builder()
1097 .force_terminal(true)
1098 .color_system(Some(ColorSystem::Truecolor))
1099 .width(20)
1100 .build();
1101 let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
1103 assert_eq!(out, "hi there\n");
1104 }
1105
1106 #[test]
1107 fn export_html_matches_upstream() {
1108 let console = Console::builder()
1109 .force_terminal(true)
1110 .color_system(Some(ColorSystem::Truecolor))
1111 .width(20)
1112 .no_color(false)
1113 .build();
1114 let html = console.export_html(|c| {
1115 c.print_str("[bold red]hi[/] there");
1116 c.print_str("plain line");
1117 });
1118 let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
1122 assert_eq!(html, expected);
1123 }
1124
1125 #[test]
1126 fn export_html_classes_matches_upstream() {
1127 let console = Console::builder()
1128 .force_terminal(true)
1129 .color_system(Some(ColorSystem::Truecolor))
1130 .width(20)
1131 .no_color(false)
1132 .build();
1133 let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
1134 let expected =
1137 include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
1138 assert_eq!(html, expected);
1139 }
1140
1141 #[test]
1142 fn capture_matches_direct_render() {
1143 let console = test_console();
1144 let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
1145 assert_eq!(
1146 console.capture(|c| c.print(&panel)),
1147 console.render_export(&panel)
1148 );
1149 }
1150
1151 #[test]
1152 fn no_color_strips_styles() {
1153 let console = Console::builder()
1154 .force_terminal(true)
1155 .color_system(None)
1156 .build();
1157 assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
1158 }
1159}