#![cfg_attr(feature = "document-features", doc = "\n# Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
use std::sync::LazyLock;
use std::vec;
#[cfg(feature = "highlight-code")]
use ansi_to_tui::IntoText;
use itertools::{Itertools, Position};
use pulldown_cmark::{
BlockQuoteKind, CodeBlockKind, CowStr, Event, HeadingLevel, Options as ParseOptions, Parser,
Tag, TagEnd,
};
use ratatui_core::style::{Style, Stylize};
use ratatui_core::text::{Line, Span, Text};
#[cfg(feature = "highlight-code")]
use syntect::{
easy::HighlightLines,
highlighting::ThemeSet,
parsing::SyntaxSet,
util::{as_24_bit_terminal_escaped, LinesWithEndings},
};
use tracing::{debug, instrument, warn};
pub use crate::options::Options;
pub use crate::style_sheet::{DefaultStyleSheet, StyleSheet};
mod options;
mod style_sheet;
pub fn from_str(input: &str) -> Text<'_> {
from_str_with_options(input, &Options::default())
}
pub fn from_str_with_options<'a, S>(input: &'a str, options: &Options<S>) -> Text<'a>
where
S: StyleSheet,
{
let mut parse_opts = ParseOptions::empty();
parse_opts.insert(ParseOptions::ENABLE_STRIKETHROUGH);
parse_opts.insert(ParseOptions::ENABLE_TASKLISTS);
parse_opts.insert(ParseOptions::ENABLE_HEADING_ATTRIBUTES);
parse_opts.insert(ParseOptions::ENABLE_YAML_STYLE_METADATA_BLOCKS);
parse_opts.insert(ParseOptions::ENABLE_SUPERSCRIPT);
parse_opts.insert(ParseOptions::ENABLE_SUBSCRIPT);
let parser = Parser::new_ext(input, parse_opts);
let mut writer = TextWriter::new(parser, options.styles.clone());
writer.run();
writer.text
}
struct HeadingMeta<'a> {
id: Option<CowStr<'a>>,
classes: Vec<CowStr<'a>>,
attrs: Vec<(CowStr<'a>, Option<CowStr<'a>>)>,
}
impl<'a> HeadingMeta<'a> {
fn into_option(self) -> Option<Self> {
let has_id = self.id.is_some();
let has_classes = !self.classes.is_empty();
let has_attrs = !self.attrs.is_empty();
if has_id || has_classes || has_attrs {
Some(self)
} else {
None
}
}
fn to_suffix(&self) -> Option<String> {
let mut parts = Vec::new();
if let Some(id) = &self.id {
parts.push(format!("#{}", id));
}
for class in &self.classes {
parts.push(format!(".{}", class));
}
for (key, value) in &self.attrs {
match value {
Some(value) => parts.push(format!("{}={}", key, value)),
None => parts.push(key.to_string()),
}
}
if parts.is_empty() {
None
} else {
Some(format!(" {{{}}}", parts.join(" ")))
}
}
}
struct TextWriter<'a, I, S: StyleSheet> {
iter: I,
text: Text<'a>,
inline_styles: Vec<Style>,
line_prefixes: Vec<Span<'a>>,
line_styles: Vec<Style>,
#[cfg(feature = "highlight-code")]
code_highlighter: Option<HighlightLines<'a>>,
list_indices: Vec<Option<u64>>,
link: Option<CowStr<'a>>,
styles: S,
heading_meta: Option<HeadingMeta<'a>>,
in_metadata_block: bool,
needs_newline: bool,
}
#[cfg(feature = "highlight-code")]
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
#[cfg(feature = "highlight-code")]
static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
impl<'a, I, S> TextWriter<'a, I, S>
where
I: Iterator<Item = Event<'a>>,
S: StyleSheet,
{
fn new(iter: I, styles: S) -> Self {
Self {
iter,
text: Text::default(),
inline_styles: vec![],
line_styles: vec![],
line_prefixes: vec![],
list_indices: vec![],
needs_newline: false,
#[cfg(feature = "highlight-code")]
code_highlighter: None,
link: None,
styles,
heading_meta: None,
in_metadata_block: false,
}
}
fn run(&mut self) {
debug!("Running text writer");
while let Some(event) = self.iter.next() {
self.handle_event(event);
}
}
#[instrument(level = "debug", skip(self))]
fn handle_event(&mut self, event: Event<'a>) {
match event {
Event::Start(tag) => self.start_tag(tag),
Event::End(tag) => self.end_tag(tag),
Event::Text(text) => self.text(text),
Event::Code(code) => self.code(code),
Event::Html(_) => warn!("Html not yet supported"),
Event::InlineHtml(_) => warn!("Inline html not yet supported"),
Event::FootnoteReference(_) => warn!("Footnote reference not yet supported"),
Event::SoftBreak => self.soft_break(),
Event::HardBreak => self.hard_break(),
Event::Rule => self.rule(),
Event::TaskListMarker(checked) => self.task_list_marker(checked),
Event::InlineMath(_) => warn!("Inline math not yet supported"),
Event::DisplayMath(_) => warn!("Display math not yet supported"),
}
}
fn start_tag(&mut self, tag: Tag<'a>) {
match tag {
Tag::Paragraph => self.start_paragraph(),
Tag::Heading {
level,
id,
classes,
attrs,
} => self.start_heading(level, HeadingMeta { id, classes, attrs }),
Tag::BlockQuote(kind) => self.start_blockquote(kind),
Tag::CodeBlock(kind) => self.start_codeblock(kind),
Tag::HtmlBlock => warn!("Html block not yet supported"),
Tag::List(start_index) => self.start_list(start_index),
Tag::Item => self.start_item(),
Tag::FootnoteDefinition(_) => warn!("Footnote definition not yet supported"),
Tag::Table(_) => warn!("Table not yet supported"),
Tag::TableHead => warn!("Table head not yet supported"),
Tag::TableRow => warn!("Table row not yet supported"),
Tag::TableCell => warn!("Table cell not yet supported"),
Tag::Emphasis => self.push_inline_style(Style::new().italic()),
Tag::Strong => self.push_inline_style(Style::new().bold()),
Tag::Strikethrough => self.push_inline_style(Style::new().crossed_out()),
Tag::Subscript => self.push_inline_style(Style::new().dim().italic()),
Tag::Superscript => self.push_inline_style(Style::new().dim().italic()),
Tag::Link { dest_url, .. } => self.push_link(dest_url),
Tag::Image { .. } => warn!("Image not yet supported"),
Tag::MetadataBlock(_) => self.start_metadata_block(),
Tag::DefinitionList => warn!("Definition list not yet supported"),
Tag::DefinitionListTitle => warn!("Definition list title not yet supported"),
Tag::DefinitionListDefinition => warn!("Definition list definition not yet supported"),
}
}
fn end_tag(&mut self, tag: TagEnd) {
match tag {
TagEnd::Paragraph => self.end_paragraph(),
TagEnd::Heading(_) => self.end_heading(),
TagEnd::BlockQuote(_) => self.end_blockquote(),
TagEnd::CodeBlock => self.end_codeblock(),
TagEnd::HtmlBlock => {}
TagEnd::List(_is_ordered) => self.end_list(),
TagEnd::Item => {}
TagEnd::FootnoteDefinition => {}
TagEnd::Table => {}
TagEnd::TableHead => {}
TagEnd::TableRow => {}
TagEnd::TableCell => {}
TagEnd::Emphasis => self.pop_inline_style(),
TagEnd::Strong => self.pop_inline_style(),
TagEnd::Strikethrough => self.pop_inline_style(),
TagEnd::Subscript => self.pop_inline_style(),
TagEnd::Superscript => self.pop_inline_style(),
TagEnd::Link => self.pop_link(),
TagEnd::Image => {}
TagEnd::MetadataBlock(_) => self.end_metadata_block(),
TagEnd::DefinitionList => {}
TagEnd::DefinitionListTitle => {}
TagEnd::DefinitionListDefinition => {}
}
}
fn start_paragraph(&mut self) {
if self.needs_newline {
self.push_line(Line::default());
}
self.push_line(Line::default());
self.needs_newline = false;
}
fn end_paragraph(&mut self) {
self.needs_newline = true
}
fn start_heading(&mut self, level: HeadingLevel, heading_meta: HeadingMeta<'a>) {
if self.needs_newline {
self.push_line(Line::default());
}
let heading_level = match level {
HeadingLevel::H1 => 1,
HeadingLevel::H2 => 2,
HeadingLevel::H3 => 3,
HeadingLevel::H4 => 4,
HeadingLevel::H5 => 5,
HeadingLevel::H6 => 6,
};
let style = self.styles.heading(heading_level);
let content = format!("{} ", "#".repeat(heading_level as usize));
self.push_line(Line::styled(content, style));
self.heading_meta = heading_meta.into_option();
self.needs_newline = false;
}
fn end_heading(&mut self) {
if let Some(meta) = self.heading_meta.take() {
if let Some(suffix) = meta.to_suffix() {
self.push_span(Span::styled(suffix, self.styles.heading_meta()));
}
}
self.needs_newline = true
}
fn start_blockquote(&mut self, _kind: Option<BlockQuoteKind>) {
if self.needs_newline {
self.push_line(Line::default());
self.needs_newline = false;
}
self.line_prefixes.push(Span::from(">"));
self.line_styles.push(self.styles.blockquote());
}
fn end_blockquote(&mut self) {
self.line_prefixes.pop();
self.line_styles.pop();
self.needs_newline = true;
}
fn text(&mut self, text: CowStr<'a>) {
#[cfg(feature = "highlight-code")]
if let Some(highlighter) = &mut self.code_highlighter {
let text: Text = LinesWithEndings::from(&text)
.filter_map(|line| highlighter.highlight_line(line, &SYNTAX_SET).ok())
.filter_map(|part| as_24_bit_terminal_escaped(&part, false).into_text().ok())
.flatten()
.collect();
for line in text.lines {
self.text.push_line(line);
}
self.needs_newline = false;
return;
}
for (position, line) in text.lines().with_position() {
if self.needs_newline {
self.push_line(Line::default());
self.needs_newline = false;
}
if matches!(position, Position::Middle | Position::Last) {
self.push_line(Line::default());
}
let style = self.inline_styles.last().copied().unwrap_or_default();
let span = Span::styled(line.to_owned(), style);
self.push_span(span);
}
self.needs_newline = false;
}
fn code(&mut self, code: CowStr<'a>) {
let span = Span::styled(code, self.styles.code());
self.push_span(span);
}
fn hard_break(&mut self) {
self.push_line(Line::default());
}
fn start_metadata_block(&mut self) {
if self.needs_newline {
self.push_line(Line::default());
}
self.line_styles.push(self.styles.metadata_block());
self.push_line(Line::from("---"));
self.push_line(Line::default());
self.in_metadata_block = true;
}
fn end_metadata_block(&mut self) {
if self.in_metadata_block {
self.push_line(Line::from("---"));
self.line_styles.pop();
self.in_metadata_block = false;
self.needs_newline = true;
}
}
fn rule(&mut self) {
if self.needs_newline {
self.push_line(Line::default());
}
self.push_line(Line::from("---"));
self.needs_newline = true;
}
fn start_list(&mut self, index: Option<u64>) {
if self.list_indices.is_empty() && self.needs_newline {
self.push_line(Line::default());
}
self.list_indices.push(index);
}
fn end_list(&mut self) {
self.list_indices.pop();
self.needs_newline = true;
}
fn start_item(&mut self) {
self.push_line(Line::default());
let width = self.list_indices.len() * 4 - 3;
if let Some(last_index) = self.list_indices.last_mut() {
let span = match last_index {
None => Span::from(" ".repeat(width - 1) + "- "),
Some(index) => {
*index += 1;
format!("{:width$}. ", *index - 1).light_blue()
}
};
self.push_span(span);
}
self.needs_newline = false;
}
fn task_list_marker(&mut self, checked: bool) {
let marker = if checked { 'x' } else { ' ' };
let marker_span = Span::from(format!("[{}] ", marker));
if let Some(line) = self.text.lines.last_mut() {
if let Some(first_span) = line.spans.first_mut() {
let content = first_span.content.to_mut();
if content.ends_with("- ") {
let len = content.len();
content.truncate(len - 2);
content.push_str("- [");
content.push(marker);
content.push_str("] ");
return;
}
}
line.spans.insert(1, marker_span);
} else {
self.push_span(marker_span);
}
}
fn soft_break(&mut self) {
if self.in_metadata_block {
self.hard_break();
} else {
self.push_span(Span::raw(" "));
}
}
fn start_codeblock(&mut self, kind: CodeBlockKind<'_>) {
if !self.text.lines.is_empty() {
self.push_line(Line::default());
}
let lang = match kind {
CodeBlockKind::Fenced(ref lang) => lang.as_ref(),
CodeBlockKind::Indented => "",
};
#[cfg(not(feature = "highlight-code"))]
self.line_styles.push(self.styles.code());
#[cfg(feature = "highlight-code")]
self.set_code_highlighter(lang);
let span = Span::from(format!("```{lang}"));
self.push_line(span.into());
self.needs_newline = true;
}
fn end_codeblock(&mut self) {
let span = Span::from("```");
self.push_line(span.into());
self.needs_newline = true;
#[cfg(not(feature = "highlight-code"))]
self.line_styles.pop();
#[cfg(feature = "highlight-code")]
self.clear_code_highlighter();
}
#[cfg(feature = "highlight-code")]
#[instrument(level = "trace", skip(self))]
fn set_code_highlighter(&mut self, lang: &str) {
if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(lang) {
debug!("Starting code block with syntax: {:?}", lang);
let theme = &THEME_SET.themes["base16-ocean.dark"];
let highlighter = HighlightLines::new(syntax, theme);
self.code_highlighter = Some(highlighter);
} else {
warn!("Could not find syntax for code block: {:?}", lang);
}
}
#[cfg(feature = "highlight-code")]
#[instrument(level = "trace", skip(self))]
fn clear_code_highlighter(&mut self) {
self.code_highlighter = None;
}
#[instrument(level = "trace", skip(self))]
fn push_inline_style(&mut self, style: Style) {
let current_style = self.inline_styles.last().copied().unwrap_or_default();
let style = current_style.patch(style);
self.inline_styles.push(style);
debug!("Pushed inline style: {:?}", style);
debug!("Current inline styles: {:?}", self.inline_styles);
}
#[instrument(level = "trace", skip(self))]
fn pop_inline_style(&mut self) {
self.inline_styles.pop();
}
#[instrument(level = "trace", skip(self))]
fn push_line(&mut self, line: Line<'a>) {
let style = self.line_styles.last().copied().unwrap_or_default();
let mut line = line.patch_style(style);
let line_prefixes = self.line_prefixes.iter().cloned().collect_vec();
let has_prefixes = !line_prefixes.is_empty();
if has_prefixes {
line.spans.insert(0, " ".into());
}
for prefix in line_prefixes.iter().rev().cloned() {
line.spans.insert(0, prefix);
}
self.text.lines.push(line);
}
#[instrument(level = "trace", skip(self))]
fn push_span(&mut self, span: Span<'a>) {
if let Some(line) = self.text.lines.last_mut() {
line.push_span(span);
} else {
self.push_line(Line::from(vec![span]));
}
}
#[instrument(level = "trace", skip(self))]
fn push_link(&mut self, dest_url: CowStr<'a>) {
self.link = Some(dest_url);
}
#[instrument(level = "trace", skip(self))]
fn pop_link(&mut self) {
if let Some(link) = self.link.take() {
self.push_span(" (".into());
self.push_span(Span::styled(link, self.styles.link()));
self.push_span(")".into());
}
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use pretty_assertions::assert_eq;
use rstest::{fixture, rstest};
use tracing::level_filters::LevelFilter;
use tracing::subscriber::{self, DefaultGuard};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::fmt::time::Uptime;
use super::*;
#[fixture]
fn with_tracing() -> DefaultGuard {
let subscriber = tracing_subscriber::fmt()
.with_test_writer()
.with_timer(Uptime::default())
.with_max_level(LevelFilter::TRACE)
.with_span_events(FmtSpan::ENTER)
.finish();
subscriber::set_default(subscriber)
}
#[rstest]
fn empty(_with_tracing: DefaultGuard) {
assert_eq!(from_str(""), Text::default());
}
#[rstest]
fn paragraph_single(_with_tracing: DefaultGuard) {
assert_eq!(from_str("Hello, world!"), Text::from("Hello, world!"));
}
#[rstest]
fn paragraph_soft_break(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
Hello
World
"}),
Text::from(Line::from_iter([
Span::from("Hello"),
Span::from(" "),
Span::from("World"),
]))
);
}
#[rstest]
fn paragraph_multiple(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
Paragraph 1
Paragraph 2
"}),
Text::from_iter(["Paragraph 1", "", "Paragraph 2",])
);
}
#[rstest]
fn rule(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
Paragraph 1
---
Paragraph 2
"}),
Text::from_iter(["Paragraph 1", "", "---", "", "Paragraph 2"])
);
}
#[rstest]
fn headings(_with_tracing: DefaultGuard) {
let h1 = Style::new().on_cyan().bold().underlined();
let h2 = Style::new().cyan().bold();
let h3 = Style::new().cyan().bold().italic();
let h4 = Style::new().light_cyan().italic();
let h5 = Style::new().light_cyan().italic();
let h6 = Style::new().light_cyan().italic();
assert_eq!(
from_str(indoc! {"
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
"}),
Text::from_iter([
Line::from_iter(["# ", "Heading 1"]).style(h1),
Line::default(),
Line::from_iter(["## ", "Heading 2"]).style(h2),
Line::default(),
Line::from_iter(["### ", "Heading 3"]).style(h3),
Line::default(),
Line::from_iter(["#### ", "Heading 4"]).style(h4),
Line::default(),
Line::from_iter(["##### ", "Heading 5"]).style(h5),
Line::default(),
Line::from_iter(["###### ", "Heading 6"]).style(h6),
])
);
}
#[rstest]
fn heading_attributes(_with_tracing: DefaultGuard) {
let h1 = Style::new().on_cyan().bold().underlined();
let meta = Style::new().dim();
assert_eq!(
from_str("# Heading {#title .primary data-kind=doc}"),
Text::from(
Line::from_iter([
Span::from("# "),
Span::from("Heading"),
Span::styled(" {#title .primary data-kind=doc}", meta),
])
.style(h1)
)
);
}
mod blockquote {
use pretty_assertions::assert_eq;
use ratatui::style::Color;
use super::*;
const STYLE: Style = Style::new().fg(Color::Green);
#[rstest]
fn after_paragraph(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
Hello, world!
> Blockquote
"}),
Text::from_iter([
Line::from("Hello, world!"),
Line::default(),
Line::from_iter([">", " ", "Blockquote"]).style(STYLE),
])
);
}
#[rstest]
fn single(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("> Blockquote"),
Text::from(Line::from_iter([">", " ", "Blockquote"]).style(STYLE))
);
}
#[rstest]
fn soft_break(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
> Blockquote 1
> Blockquote 2
"}),
Text::from(
Line::from_iter([">", " ", "Blockquote 1", " ", "Blockquote 2"]).style(STYLE)
)
);
}
#[rstest]
fn multiple(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
> Blockquote 1
>
> Blockquote 2
"}),
Text::from_iter([
Line::from_iter([">", " ", "Blockquote 1"]).style(STYLE),
Line::from_iter([">", " "]).style(STYLE),
Line::from_iter([">", " ", "Blockquote 2"]).style(STYLE),
])
);
}
#[rstest]
fn multiple_with_break(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
> Blockquote 1
> Blockquote 2
"}),
Text::from_iter([
Line::from_iter([">", " ", "Blockquote 1"]).style(STYLE),
Line::default(),
Line::from_iter([">", " ", "Blockquote 2"]).style(STYLE),
])
);
}
#[rstest]
fn nested(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
> Blockquote 1
>> Nested Blockquote
"}),
Text::from_iter([
Line::from_iter([">", " ", "Blockquote 1"]).style(STYLE),
Line::from_iter([">", " "]).style(STYLE),
Line::from_iter([">", ">", " ", "Nested Blockquote"]).style(STYLE),
])
);
}
}
#[rstest]
fn list_single(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
- List item 1
"}),
Text::from_iter([Line::from_iter(["- ", "List item 1"])])
);
}
#[rstest]
fn list_multiple(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
- List item 1
- List item 2
"}),
Text::from_iter([
Line::from_iter(["- ", "List item 1"]),
Line::from_iter(["- ", "List item 2"]),
])
);
}
#[rstest]
fn list_ordered(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
1. List item 1
2. List item 2
"}),
Text::from_iter([
Line::from_iter(["1. ".light_blue(), "List item 1".into()]),
Line::from_iter(["2. ".light_blue(), "List item 2".into()]),
])
);
}
#[rstest]
fn list_nested(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
- List item 1
- Nested list item 1
"}),
Text::from_iter([
Line::from_iter(["- ", "List item 1"]),
Line::from_iter([" - ", "Nested list item 1"]),
])
);
}
#[rstest]
fn list_task_items(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
- [ ] Incomplete
- [x] Complete
"}),
Text::from_iter([
Line::from_iter(["- [ ] ", "Incomplete"]),
Line::from_iter(["- [x] ", "Complete"]),
])
);
}
#[rstest]
fn list_task_items_ordered(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
1. [ ] Incomplete
2. [x] Complete
"}),
Text::from_iter([
Line::from_iter(["1. ".light_blue(), "[ ] ".into(), "Incomplete".into(),]),
Line::from_iter(["2. ".light_blue(), "[x] ".into(), "Complete".into(),]),
])
);
}
#[cfg_attr(not(feature = "highlight-code"), ignore)]
#[rstest]
fn highlighted_code(_with_tracing: DefaultGuard) {
let highlighted_code = from_str(indoc! {"
```rust
fn main() {
println!(\"Hello, highlighted code!\");
}
```"});
insta::assert_snapshot!(highlighted_code);
insta::assert_debug_snapshot!(highlighted_code);
}
#[cfg_attr(not(feature = "highlight-code"), ignore)]
#[rstest]
fn highlighted_code_with_indentation(_with_tracing: DefaultGuard) {
let highlighted_code_indented = from_str(indoc! {"
```rust
fn main() {
// This is a comment
HelloWorldBuilder::new()
.with_text(\"Hello, highlighted code!\")
.build()
.show();
}
```"});
insta::assert_snapshot!(highlighted_code_indented);
insta::assert_debug_snapshot!(highlighted_code_indented);
}
#[cfg_attr(feature = "highlight-code", ignore)]
#[rstest]
fn unhighlighted_code(_with_tracing: DefaultGuard) {
let unhiglighted_code = from_str(indoc! {"
```rust
fn main() {
println!(\"Hello, unhighlighted code!\");
}
```"});
insta::assert_snapshot!(unhiglighted_code);
insta::assert_debug_snapshot!(unhiglighted_code);
}
#[rstest]
fn inline_code(_with_tracing: DefaultGuard) {
let text = from_str("Example of `Inline code`");
insta::assert_snapshot!(text);
assert_eq!(
text,
Line::from_iter([
Span::from("Example of "),
Span::styled("Inline code", Style::new().white().on_black())
])
.into()
);
}
#[rstest]
fn superscript(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("H ^2^ O"),
Text::from(Line::from_iter([
Span::from("H "),
Span::styled("2", Style::new().dim().italic()),
Span::from(" O"),
]))
);
}
#[rstest]
fn subscript(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("H ~2~ O"),
Text::from(Line::from_iter([
Span::from("H "),
Span::styled("2", Style::new().dim().italic()),
Span::from(" O"),
]))
);
}
#[rstest]
fn metadata_block(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(indoc! {"
---
title: Demo
---
Body
"}),
Text::from_iter([
Line::from("---").style(Style::new().light_yellow()),
Line::from("title: Demo").style(Style::new().light_yellow()),
Line::from("---").style(Style::new().light_yellow()),
Line::default(),
Line::from("Body"),
])
);
}
#[rstest]
fn strong(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("**Strong**"),
Text::from(Line::from("Strong".bold()))
);
}
#[rstest]
fn emphasis(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("*Emphasis*"),
Text::from(Line::from("Emphasis".italic()))
);
}
#[rstest]
fn strikethrough(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("~~Strikethrough~~"),
Text::from(Line::from("Strikethrough".crossed_out()))
);
}
#[rstest]
fn strong_emphasis(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("**Strong *emphasis***"),
Text::from(Line::from_iter([
"Strong ".bold(),
"emphasis".bold().italic()
]))
);
}
#[rstest]
fn link(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("[Link](https://example.com)"),
Text::from(Line::from_iter([
Span::from("Link"),
Span::from(" ("),
Span::from("https://example.com").blue().underlined(),
Span::from(")")
]))
);
}
}