1#![cfg_attr(feature = "document-features", doc = "\n# Features")]
7#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
8use std::sync::LazyLock;
34use std::vec;
35
36#[cfg(feature = "highlight-code")]
37use ansi_to_tui::IntoText;
38use itertools::{Itertools, Position};
39use pulldown_cmark::{
40 BlockQuoteKind, CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag, TagEnd,
41};
42use ratatui::style::{Style, Stylize};
43use ratatui::text::{Line, Span, Text};
44#[cfg(feature = "highlight-code")]
45use syntect::{
46 easy::HighlightLines,
47 highlighting::ThemeSet,
48 parsing::SyntaxSet,
49 util::{as_24_bit_terminal_escaped, LinesWithEndings},
50};
51use tracing::{debug, instrument, warn};
52
53pub fn from_str(input: &str) -> Text<'_> {
54 let mut options = Options::empty();
55 options.insert(Options::ENABLE_STRIKETHROUGH);
56 options.insert(Options::ENABLE_TASKLISTS);
57 let parser = Parser::new_ext(input, options);
58 let mut writer = TextWriter::new(parser);
59 writer.run();
60 writer.text
61}
62
63struct TextWriter<'a, I> {
64 iter: I,
66
67 text: Text<'a>,
69
70 inline_styles: Vec<Style>,
74
75 line_prefixes: Vec<Span<'a>>,
77
78 line_styles: Vec<Style>,
80
81 #[cfg(feature = "highlight-code")]
83 code_highlighter: Option<HighlightLines<'a>>,
84
85 list_indices: Vec<Option<u64>>,
87
88 link: Option<CowStr<'a>>,
90
91 needs_newline: bool,
92}
93
94#[cfg(feature = "highlight-code")]
95static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
96#[cfg(feature = "highlight-code")]
97static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
98
99impl<'a, I> TextWriter<'a, I>
100where
101 I: Iterator<Item = Event<'a>>,
102{
103 fn new(iter: I) -> Self {
104 Self {
105 iter,
106 text: Text::default(),
107 inline_styles: vec![],
108 line_styles: vec![],
109 line_prefixes: vec![],
110 list_indices: vec![],
111 needs_newline: false,
112 #[cfg(feature = "highlight-code")]
113 code_highlighter: None,
114 link: None,
115 }
116 }
117
118 fn run(&mut self) {
119 debug!("Running text writer");
120 while let Some(event) = self.iter.next() {
121 self.handle_event(event);
122 }
123 }
124
125 #[instrument(level = "debug", skip(self))]
126 fn handle_event(&mut self, event: Event<'a>) {
127 match event {
128 Event::Start(tag) => self.start_tag(tag),
129 Event::End(tag) => self.end_tag(tag),
130 Event::Text(text) => self.text(text),
131 Event::Code(code) => self.code(code),
132 Event::Html(_html) => warn!("Html not yet supported"),
133 Event::InlineHtml(_html) => warn!("Inline html not yet supported"),
134 Event::FootnoteReference(_) => warn!("Footnote reference not yet supported"),
135 Event::SoftBreak => self.soft_break(),
136 Event::HardBreak => self.hard_break(),
137 Event::Rule => warn!("Rule not yet supported"),
138 Event::TaskListMarker(checked) => self.task_list_marker(checked),
139 Event::InlineMath(_) => warn!("Inline math not yet supported"),
140 Event::DisplayMath(_) => warn!("Display math not yet supported"),
141 }
142 }
143
144 fn start_tag(&mut self, tag: Tag<'a>) {
145 match tag {
146 Tag::Paragraph => self.start_paragraph(),
147 Tag::Heading { level, .. } => self.start_heading(level),
148 Tag::BlockQuote(kind) => self.start_blockquote(kind),
149 Tag::CodeBlock(kind) => self.start_codeblock(kind),
150 Tag::HtmlBlock => warn!("Html block not yet supported"),
151 Tag::List(start_index) => self.start_list(start_index),
152 Tag::Item => self.start_item(),
153 Tag::FootnoteDefinition(_) => warn!("Footnote definition not yet supported"),
154 Tag::Table(_) => warn!("Table not yet supported"),
155 Tag::TableHead => warn!("Table head not yet supported"),
156 Tag::TableRow => warn!("Table row not yet supported"),
157 Tag::TableCell => warn!("Table cell not yet supported"),
158 Tag::Emphasis => self.push_inline_style(Style::new().italic()),
159 Tag::Strong => self.push_inline_style(Style::new().bold()),
160 Tag::Strikethrough => self.push_inline_style(Style::new().crossed_out()),
161 Tag::Subscript => warn!("Subscript not yet supported"),
162 Tag::Superscript => warn!("Superscript not yet supported"),
163 Tag::Link { dest_url, .. } => self.push_link(dest_url),
164 Tag::Image { .. } => warn!("Image not yet supported"),
165 Tag::MetadataBlock(_) => warn!("Metadata block not yet supported"),
166 Tag::DefinitionList => warn!("Definition list not yet supported"),
167 Tag::DefinitionListTitle => warn!("Definition list title not yet supported"),
168 Tag::DefinitionListDefinition => warn!("Definition list definition not yet supported"),
169 }
170 }
171
172 fn end_tag(&mut self, tag: TagEnd) {
173 match tag {
174 TagEnd::Paragraph => self.end_paragraph(),
175 TagEnd::Heading(_) => self.end_heading(),
176 TagEnd::BlockQuote(_) => self.end_blockquote(),
177 TagEnd::CodeBlock => self.end_codeblock(),
178 TagEnd::HtmlBlock => {}
179 TagEnd::List(_is_ordered) => self.end_list(),
180 TagEnd::Item => {}
181 TagEnd::FootnoteDefinition => {}
182 TagEnd::Table => {}
183 TagEnd::TableHead => {}
184 TagEnd::TableRow => {}
185 TagEnd::TableCell => {}
186 TagEnd::Emphasis => self.pop_inline_style(),
187 TagEnd::Strong => self.pop_inline_style(),
188 TagEnd::Strikethrough => self.pop_inline_style(),
189 TagEnd::Subscript => {}
190 TagEnd::Superscript => {}
191 TagEnd::Link => self.pop_link(),
192 TagEnd::Image => {}
193 TagEnd::MetadataBlock(_) => {}
194 TagEnd::DefinitionList => {}
195 TagEnd::DefinitionListTitle => {}
196 TagEnd::DefinitionListDefinition => {}
197 }
198 }
199
200 fn start_paragraph(&mut self) {
201 if self.needs_newline {
203 self.push_line(Line::default());
204 }
205 self.push_line(Line::default());
206 self.needs_newline = false;
207 }
208
209 fn end_paragraph(&mut self) {
210 self.needs_newline = true
211 }
212
213 fn start_heading(&mut self, level: HeadingLevel) {
214 if self.needs_newline {
215 self.push_line(Line::default());
216 }
217 let style = match level {
218 HeadingLevel::H1 => styles::H1,
219 HeadingLevel::H2 => styles::H2,
220 HeadingLevel::H3 => styles::H3,
221 HeadingLevel::H4 => styles::H4,
222 HeadingLevel::H5 => styles::H5,
223 HeadingLevel::H6 => styles::H6,
224 };
225 let content = format!("{} ", "#".repeat(level as usize));
226 self.push_line(Line::styled(content, style));
227 self.needs_newline = false;
228 }
229
230 fn end_heading(&mut self) {
231 self.needs_newline = true
232 }
233
234 fn start_blockquote(&mut self, _kind: Option<BlockQuoteKind>) {
235 if self.needs_newline {
236 self.push_line(Line::default());
237 self.needs_newline = false;
238 }
239 self.line_prefixes.push(Span::from(">"));
240 self.line_styles.push(styles::BLOCKQUOTE);
241 }
242
243 fn end_blockquote(&mut self) {
244 self.line_prefixes.pop();
245 self.line_styles.pop();
246 self.needs_newline = true;
247 }
248
249 fn text(&mut self, text: CowStr<'a>) {
250 #[cfg(feature = "highlight-code")]
251 if let Some(highlighter) = &mut self.code_highlighter {
252 let text: Text = LinesWithEndings::from(&text)
253 .filter_map(|line| highlighter.highlight_line(line, &SYNTAX_SET).ok())
254 .filter_map(|part| as_24_bit_terminal_escaped(&part, false).into_text().ok())
255 .flatten()
256 .collect();
257
258 for line in text.lines {
259 self.text.push_line(line);
260 }
261 self.needs_newline = false;
262 return;
263 }
264
265 for (position, line) in text.lines().with_position() {
266 if self.needs_newline {
267 self.push_line(Line::default());
268 self.needs_newline = false;
269 }
270 if matches!(position, Position::Middle | Position::Last) {
271 self.push_line(Line::default());
272 }
273
274 let style = self.inline_styles.last().copied().unwrap_or_default();
275
276 let span = Span::styled(line.to_owned(), style);
277
278 self.push_span(span);
279 }
280 self.needs_newline = false;
281 }
282
283 fn code(&mut self, code: CowStr<'a>) {
284 let span = Span::styled(code, styles::CODE);
285 self.push_span(span);
286 }
287
288 fn hard_break(&mut self) {
289 self.push_line(Line::default());
290 }
291
292 fn start_list(&mut self, index: Option<u64>) {
293 if self.list_indices.is_empty() && self.needs_newline {
294 self.push_line(Line::default());
295 }
296 self.list_indices.push(index);
297 }
298
299 fn end_list(&mut self) {
300 self.list_indices.pop();
301 self.needs_newline = true;
302 }
303
304 fn start_item(&mut self) {
305 self.push_line(Line::default());
306 let width = self.list_indices.len() * 4 - 3;
307 if let Some(last_index) = self.list_indices.last_mut() {
308 let span = match last_index {
309 None => Span::from(" ".repeat(width - 1) + "- "),
310 Some(index) => {
311 *index += 1;
312 format!("{:width$}. ", *index - 1).light_blue()
313 }
314 };
315 self.push_span(span);
316 }
317 self.needs_newline = false;
318 }
319
320 fn task_list_marker(&mut self, checked: bool) {
321 let marker = if checked { 'x' } else { ' ' };
322 let marker_span = Span::from(format!("[{}] ", marker));
323 if let Some(line) = self.text.lines.last_mut() {
324 if let Some(first_span) = line.spans.first_mut() {
325 let content = first_span.content.to_mut();
326 if content.ends_with("- ") {
327 let len = content.len();
328 content.truncate(len - 2);
329 content.push_str("- [");
330 content.push(marker);
331 content.push_str("] ");
332 return;
333 }
334 }
335 line.spans.insert(1, marker_span);
336 } else {
337 self.push_span(marker_span);
338 }
339 }
340
341 fn soft_break(&mut self) {
342 self.push_line(Line::default());
343 }
344
345 fn start_codeblock(&mut self, kind: CodeBlockKind<'_>) {
346 if !self.text.lines.is_empty() {
347 self.push_line(Line::default());
348 }
349 let lang = match kind {
350 CodeBlockKind::Fenced(ref lang) => lang.as_ref(),
351 CodeBlockKind::Indented => "",
352 };
353
354 #[cfg(not(feature = "highlight-code"))]
355 self.line_styles.push(styles::CODE);
356
357 #[cfg(feature = "highlight-code")]
358 self.set_code_highlighter(lang);
359
360 let span = Span::from(format!("```{lang}"));
361 self.push_line(span.into());
362 self.needs_newline = true;
363 }
364
365 fn end_codeblock(&mut self) {
366 let span = Span::from("```");
367 self.push_line(span.into());
368 self.needs_newline = true;
369
370 #[cfg(not(feature = "highlight-code"))]
371 self.line_styles.pop();
372
373 #[cfg(feature = "highlight-code")]
374 self.clear_code_highlighter();
375 }
376
377 #[cfg(feature = "highlight-code")]
378 #[instrument(level = "trace", skip(self))]
379 fn set_code_highlighter(&mut self, lang: &str) {
380 if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(lang) {
381 debug!("Starting code block with syntax: {:?}", lang);
382 let theme = &THEME_SET.themes["base16-ocean.dark"];
383 let highlighter = HighlightLines::new(syntax, theme);
384 self.code_highlighter = Some(highlighter);
385 } else {
386 warn!("Could not find syntax for code block: {:?}", lang);
387 }
388 }
389
390 #[cfg(feature = "highlight-code")]
391 #[instrument(level = "trace", skip(self))]
392 fn clear_code_highlighter(&mut self) {
393 self.code_highlighter = None;
394 }
395
396 #[instrument(level = "trace", skip(self))]
397 fn push_inline_style(&mut self, style: Style) {
398 let current_style = self.inline_styles.last().copied().unwrap_or_default();
399 let style = current_style.patch(style);
400 self.inline_styles.push(style);
401 debug!("Pushed inline style: {:?}", style);
402 debug!("Current inline styles: {:?}", self.inline_styles);
403 }
404
405 #[instrument(level = "trace", skip(self))]
406 fn pop_inline_style(&mut self) {
407 self.inline_styles.pop();
408 }
409
410 #[instrument(level = "trace", skip(self))]
411 fn push_line(&mut self, line: Line<'a>) {
412 let style = self.line_styles.last().copied().unwrap_or_default();
413 let mut line = line.patch_style(style);
414
415 let line_prefixes = self.line_prefixes.iter().cloned().collect_vec();
417 let has_prefixes = !line_prefixes.is_empty();
418 if has_prefixes {
419 line.spans.insert(0, " ".into());
420 }
421 for prefix in line_prefixes.iter().rev().cloned() {
422 line.spans.insert(0, prefix);
423 }
424 self.text.lines.push(line);
425 }
426
427 #[instrument(level = "trace", skip(self))]
428 fn push_span(&mut self, span: Span<'a>) {
429 if let Some(line) = self.text.lines.last_mut() {
430 line.push_span(span);
431 } else {
432 self.push_line(Line::from(vec![span]));
433 }
434 }
435
436 #[instrument(level = "trace", skip(self))]
438 fn push_link(&mut self, dest_url: CowStr<'a>) {
439 self.link = Some(dest_url);
440 }
441
442 #[instrument(level = "trace", skip(self))]
444 fn pop_link(&mut self) {
445 if let Some(link) = self.link.take() {
446 self.push_span(" (".into());
447 self.push_span(Span::styled(link, styles::LINK));
448 self.push_span(")".into());
449 }
450 }
451}
452
453mod styles {
454 use ratatui::style::{Color, Modifier, Style};
455
456 pub const H1: Style = Style::new()
457 .bg(Color::Cyan)
458 .add_modifier(Modifier::BOLD)
459 .add_modifier(Modifier::UNDERLINED);
460 pub const H2: Style = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
461 pub const H3: Style = Style::new()
462 .fg(Color::Cyan)
463 .add_modifier(Modifier::BOLD)
464 .add_modifier(Modifier::ITALIC);
465 pub const H4: Style = Style::new()
466 .fg(Color::LightCyan)
467 .add_modifier(Modifier::ITALIC);
468 pub const H5: Style = Style::new()
469 .fg(Color::LightCyan)
470 .add_modifier(Modifier::ITALIC);
471 pub const H6: Style = Style::new()
472 .fg(Color::LightCyan)
473 .add_modifier(Modifier::ITALIC);
474 pub const BLOCKQUOTE: Style = Style::new().fg(Color::Green);
475 pub const CODE: Style = Style::new().fg(Color::White).bg(Color::Black);
476 pub const LINK: Style = Style::new()
477 .fg(Color::Blue)
478 .add_modifier(Modifier::UNDERLINED);
479}
480
481#[cfg(test)]
482mod tests {
483 use indoc::indoc;
484 use pretty_assertions::assert_eq;
485 use rstest::{fixture, rstest};
486 use tracing::level_filters::LevelFilter;
487 use tracing::subscriber::{self, DefaultGuard};
488 use tracing_subscriber::fmt::format::FmtSpan;
489 use tracing_subscriber::fmt::time::Uptime;
490
491 use super::*;
492
493 #[fixture]
494 fn with_tracing() -> DefaultGuard {
495 let subscriber = tracing_subscriber::fmt()
496 .with_test_writer()
497 .with_timer(Uptime::default())
498 .with_max_level(LevelFilter::TRACE)
499 .with_span_events(FmtSpan::ENTER)
500 .finish();
501 subscriber::set_default(subscriber)
502 }
503
504 #[rstest]
505 fn empty(_with_tracing: DefaultGuard) {
506 assert_eq!(from_str(""), Text::default());
507 }
508
509 #[rstest]
510 fn paragraph_single(_with_tracing: DefaultGuard) {
511 assert_eq!(from_str("Hello, world!"), Text::from("Hello, world!"));
512 }
513
514 #[rstest]
515 fn paragraph_soft_break(_with_tracing: DefaultGuard) {
516 assert_eq!(
517 from_str(indoc! {"
518 Hello
519 World
520 "}),
521 Text::from_iter(["Hello", "World"])
522 );
523 }
524
525 #[rstest]
526 fn paragraph_multiple(_with_tracing: DefaultGuard) {
527 assert_eq!(
528 from_str(indoc! {"
529 Paragraph 1
530
531 Paragraph 2
532 "}),
533 Text::from_iter(["Paragraph 1", "", "Paragraph 2",])
534 );
535 }
536
537 #[rstest]
538 fn headings(_with_tracing: DefaultGuard) {
539 assert_eq!(
540 from_str(indoc! {"
541 # Heading 1
542 ## Heading 2
543 ### Heading 3
544 #### Heading 4
545 ##### Heading 5
546 ###### Heading 6
547 "}),
548 Text::from_iter([
549 Line::from_iter(["# ", "Heading 1"]).style(styles::H1),
550 Line::default(),
551 Line::from_iter(["## ", "Heading 2"]).style(styles::H2),
552 Line::default(),
553 Line::from_iter(["### ", "Heading 3"]).style(styles::H3),
554 Line::default(),
555 Line::from_iter(["#### ", "Heading 4"]).style(styles::H4),
556 Line::default(),
557 Line::from_iter(["##### ", "Heading 5"]).style(styles::H5),
558 Line::default(),
559 Line::from_iter(["###### ", "Heading 6"]).style(styles::H6),
560 ])
561 );
562 }
563
564 #[rstest]
567 fn blockquote_after_paragraph(_with_tracing: DefaultGuard) {
568 assert_eq!(
569 from_str(indoc! {"
570 Hello, world!
571
572 > Blockquote
573 "}),
574 Text::from_iter([
575 Line::from("Hello, world!"),
576 Line::default(),
577 Line::from_iter([">", " ", "Blockquote"]).style(styles::BLOCKQUOTE),
578 ])
579 );
580 }
581 #[rstest]
582 fn blockquote_single(_with_tracing: DefaultGuard) {
583 assert_eq!(
584 from_str("> Blockquote"),
585 Text::from(Line::from_iter([">", " ", "Blockquote"]).style(styles::BLOCKQUOTE))
586 );
587 }
588
589 #[rstest]
590 fn blockquote_soft_break(_with_tracing: DefaultGuard) {
591 assert_eq!(
592 from_str(indoc! {"
593 > Blockquote 1
594 > Blockquote 2
595 "}),
596 Text::from_iter([
597 Line::from_iter([">", " ", "Blockquote 1"]).style(styles::BLOCKQUOTE),
598 Line::from_iter([">", " ", "Blockquote 2"]).style(styles::BLOCKQUOTE),
599 ])
600 );
601 }
602
603 #[rstest]
604 fn blockquote_multiple(_with_tracing: DefaultGuard) {
605 assert_eq!(
606 from_str(indoc! {"
607 > Blockquote 1
608 >
609 > Blockquote 2
610 "}),
611 Text::from_iter([
612 Line::from_iter([">", " ", "Blockquote 1"]).style(styles::BLOCKQUOTE),
613 Line::from_iter([">", " "]).style(styles::BLOCKQUOTE),
614 Line::from_iter([">", " ", "Blockquote 2"]).style(styles::BLOCKQUOTE),
615 ])
616 );
617 }
618
619 #[rstest]
620 fn blockquote_multiple_with_break(_with_tracing: DefaultGuard) {
621 assert_eq!(
622 from_str(indoc! {"
623 > Blockquote 1
624
625 > Blockquote 2
626 "}),
627 Text::from_iter([
628 Line::from_iter([">", " ", "Blockquote 1"]).style(styles::BLOCKQUOTE),
629 Line::default(),
630 Line::from_iter([">", " ", "Blockquote 2"]).style(styles::BLOCKQUOTE),
631 ])
632 );
633 }
634
635 #[rstest]
636 fn blockquote_nested(_with_tracing: DefaultGuard) {
637 assert_eq!(
638 from_str(indoc! {"
639 > Blockquote 1
640 >> Nested Blockquote
641 "}),
642 Text::from_iter([
643 Line::from_iter([">", " ", "Blockquote 1"]).style(styles::BLOCKQUOTE),
644 Line::from_iter([">", " "]).style(styles::BLOCKQUOTE),
645 Line::from_iter([">", ">", " ", "Nested Blockquote"]).style(styles::BLOCKQUOTE),
646 ])
647 );
648 }
649
650 #[rstest]
651 fn list_single(_with_tracing: DefaultGuard) {
652 assert_eq!(
653 from_str(indoc! {"
654 - List item 1
655 "}),
656 Text::from_iter([Line::from_iter(["- ", "List item 1"])])
657 );
658 }
659
660 #[rstest]
661 fn list_multiple(_with_tracing: DefaultGuard) {
662 assert_eq!(
663 from_str(indoc! {"
664 - List item 1
665 - List item 2
666 "}),
667 Text::from_iter([
668 Line::from_iter(["- ", "List item 1"]),
669 Line::from_iter(["- ", "List item 2"]),
670 ])
671 );
672 }
673
674 #[rstest]
675 fn list_ordered(_with_tracing: DefaultGuard) {
676 assert_eq!(
677 from_str(indoc! {"
678 1. List item 1
679 2. List item 2
680 "}),
681 Text::from_iter([
682 Line::from_iter(["1. ".light_blue(), "List item 1".into()]),
683 Line::from_iter(["2. ".light_blue(), "List item 2".into()]),
684 ])
685 );
686 }
687
688 #[rstest]
689 fn list_nested(_with_tracing: DefaultGuard) {
690 assert_eq!(
691 from_str(indoc! {"
692 - List item 1
693 - Nested list item 1
694 "}),
695 Text::from_iter([
696 Line::from_iter(["- ", "List item 1"]),
697 Line::from_iter([" - ", "Nested list item 1"]),
698 ])
699 );
700 }
701
702 #[rstest]
703 fn list_task_items(_with_tracing: DefaultGuard) {
704 assert_eq!(
705 from_str(indoc! {"
706 - [ ] Incomplete
707 - [x] Complete
708 "}),
709 Text::from_iter([
710 Line::from_iter(["- [ ] ", "Incomplete"]),
711 Line::from_iter(["- [x] ", "Complete"]),
712 ])
713 );
714 }
715
716 #[rstest]
717 fn list_task_items_ordered(_with_tracing: DefaultGuard) {
718 assert_eq!(
719 from_str(indoc! {"
720 1. [ ] Incomplete
721 2. [x] Complete
722 "}),
723 Text::from_iter([
724 Line::from_iter(["1. ".light_blue(), "[ ] ".into(), "Incomplete".into(),]),
725 Line::from_iter(["2. ".light_blue(), "[x] ".into(), "Complete".into(),]),
726 ])
727 );
728 }
729
730 #[cfg_attr(not(feature = "highlight-code"), ignore)]
731 #[rstest]
732 fn highlighted_code(_with_tracing: DefaultGuard) {
733 let highlighted_code = from_str(indoc! {"
735 ```rust
736 fn main() {
737 println!(\"Hello, highlighted code!\");
738 }
739 ```"});
740
741 insta::assert_snapshot!(highlighted_code);
742 insta::assert_debug_snapshot!(highlighted_code);
743 }
744
745 #[cfg_attr(not(feature = "highlight-code"), ignore)]
746 #[rstest]
747 fn highlighted_code_with_indentation(_with_tracing: DefaultGuard) {
748 let highlighted_code_indented = from_str(indoc! {"
750 ```rust
751 fn main() {
752 // This is a comment
753 HelloWorldBuilder::new()
754 .with_text(\"Hello, highlighted code!\")
755 .build()
756 .show();
757
758 }
759 ```"});
760
761 insta::assert_snapshot!(highlighted_code_indented);
762 insta::assert_debug_snapshot!(highlighted_code_indented);
763 }
764
765 #[cfg_attr(feature = "highlight-code", ignore)]
766 #[rstest]
767 fn unhighlighted_code(_with_tracing: DefaultGuard) {
768 let unhiglighted_code = from_str(indoc! {"
770 ```rust
771 fn main() {
772 println!(\"Hello, unhighlighted code!\");
773 }
774 ```"});
775
776 insta::assert_snapshot!(unhiglighted_code);
777
778 insta::assert_debug_snapshot!(unhiglighted_code);
780 }
781
782 #[rstest]
783 fn inline_code(_with_tracing: DefaultGuard) {
784 let text = from_str("Example of `Inline code`");
785 insta::assert_snapshot!(text);
786
787 assert_eq!(
788 text,
789 Line::from_iter([
790 Span::from("Example of "),
791 Span::styled("Inline code", styles::CODE)
792 ])
793 .into()
794 );
795 }
796
797 #[rstest]
798 fn strong(_with_tracing: DefaultGuard) {
799 assert_eq!(
800 from_str("**Strong**"),
801 Text::from(Line::from("Strong".bold()))
802 );
803 }
804
805 #[rstest]
806 fn emphasis(_with_tracing: DefaultGuard) {
807 assert_eq!(
808 from_str("*Emphasis*"),
809 Text::from(Line::from("Emphasis".italic()))
810 );
811 }
812
813 #[rstest]
814 fn strikethrough(_with_tracing: DefaultGuard) {
815 assert_eq!(
816 from_str("~~Strikethrough~~"),
817 Text::from(Line::from("Strikethrough".crossed_out()))
818 );
819 }
820
821 #[rstest]
822 fn strong_emphasis(_with_tracing: DefaultGuard) {
823 assert_eq!(
824 from_str("**Strong *emphasis***"),
825 Text::from(Line::from_iter([
826 "Strong ".bold(),
827 "emphasis".bold().italic()
828 ]))
829 );
830 }
831
832 #[rstest]
833 fn link(_with_tracing: DefaultGuard) {
834 assert_eq!(
835 from_str("[Link](https://example.com)"),
836 Text::from(Line::from_iter([
837 Span::from("Link"),
838 Span::from(" ("),
839 Span::from("https://example.com").blue().underlined(),
840 Span::from(")")
841 ]))
842 );
843 }
844}