use std::sync::LazyLock;
use std::vec;
use ansi_to_tui::IntoText;
use itertools::{Itertools, Position};
use pulldown_cmark::{
BlockQuoteKind, CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag, TagEnd,
};
use ratatui::style::Style;
use ratatui::text::{Line, Span, Text};
use syntect::{
easy::HighlightLines,
highlighting::ThemeSet,
parsing::SyntaxSet,
util::{LinesWithEndings, as_24_bit_terminal_escaped},
};
use tracing::{debug, instrument, warn};
use crate::core::library::settings::theme::Theme;
use crate::ui::tools::styles;
pub fn from_str(input: &str, theme: Option<Theme>) -> Text<'_> {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
options.insert(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
options.insert(Options::ENABLE_SUPERSCRIPT);
options.insert(Options::ENABLE_SUBSCRIPT);
let parser = Parser::new_ext(input, options);
let mut writer = TextWriter::new(parser, theme);
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> {
iter: I,
text: Text<'a>,
inline_styles: Vec<Style>,
line_prefixes: Vec<Span<'a>>,
line_styles: Vec<Style>,
code_highlighter: Option<HighlightLines<'a>>,
list_indices: Vec<Option<u64>>,
link: Option<CowStr<'a>>,
image: Option<CowStr<'a>>,
heading_meta: Option<HeadingMeta<'a>>,
in_metadata_block: bool,
needs_newline: bool,
theme: Option<Theme>,
}
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
impl<'a, I> TextWriter<'a, I>
where
I: Iterator<Item = Event<'a>>,
{
fn new(iter: I, theme: Option<Theme>) -> Self {
Self {
iter,
text: Text::default(),
inline_styles: vec![],
line_styles: vec![],
line_prefixes: vec![],
list_indices: vec![],
needs_newline: false,
code_highlighter: None,
link: None,
image: None,
heading_meta: None,
in_metadata_block: false,
theme,
}
}
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(_html) => warn!("Html not yet supported"),
Event::InlineHtml(_html) => 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 {
link_type,
dest_url,
title,
..
} => self.push_image(link_type, dest_url, title),
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 => self.pop_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().style(styles::p(self.theme.as_ref())));
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 style = match level {
HeadingLevel::H1 => styles::h1(self.theme.as_ref()),
HeadingLevel::H2 => styles::h2(self.theme.as_ref()),
HeadingLevel::H3 => styles::h3(self.theme.as_ref()),
HeadingLevel::H4 => styles::h4(self.theme.as_ref()),
HeadingLevel::H5 => styles::h5(self.theme.as_ref()),
HeadingLevel::H6 => styles::h6(self.theme.as_ref()),
};
let content = format!("{} ", "#".repeat(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()
&& let Some(suffix) = meta.to_suffix()
{
self.push_span(Span::styled(suffix, Style::new().dim()));
}
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(styles::blockquote(self.theme.as_ref()));
}
fn end_blockquote(&mut self) {
self.line_prefixes.pop();
self.line_styles.pop();
self.needs_newline = true;
}
fn text(&mut self, text: CowStr<'a>) {
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, styles::code(self.theme.as_ref()));
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(styles::metadata(self.theme.as_ref()));
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);
self.inline_styles
.push(styles::list_item(self.theme.as_ref()));
}
fn end_list(&mut self) {
self.list_indices.pop();
self.inline_styles.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) + "\u{00A0}\u{00A0}• "),
Some(index) => {
*index += 1;
Span::from(format!("\u{00A0}\u{00A0}{:width$}. ", *index - 1))
}
};
self.push_span(span.style(styles::list_item(self.theme.as_ref())));
}
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 - 4); content.push_str("• [");
content.push(marker);
content.push_str("] ");
return;
}
if content.ends_with(". ") {
let len = content.len();
content.truncate(len - 1);
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_line(Line::default().style(styles::p(self.theme.as_ref())));
}
}
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 => "",
};
self.line_styles.push(styles::code(self.theme.as_ref()));
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;
self.line_styles.pop();
self.clear_code_highlighter();
}
#[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);
}
}
#[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, styles::link(self.theme.as_ref())));
self.push_span(")".into());
}
}
#[instrument(level = "trace", skip(self))]
fn push_image(
&mut self,
_link_type: pulldown_cmark::LinkType,
dest_url: CowStr<'a>,
_title: CowStr<'a>,
) {
self.image = Some(dest_url);
let text = "[Image: ";
self.push_line(Line::styled(text, styles::p(self.theme.as_ref())));
}
#[instrument(level = "trace", skip(self))]
fn pop_image(&mut self) {
if let Some(image_link) = self.image.take() {
self.push_span(" -> ".into());
self.push_span(Span::styled(image_link, styles::link(self.theme.as_ref())));
self.push_span("]".into());
}
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use pretty_assertions::assert_eq;
use ratatui::style::{Color, Stylize};
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("", None), Text::default());
}
#[rstest]
fn paragraph_single(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("Hello, world!", None),
Text::from(Line::from("Hello, world!").style(styles::p(None)))
);
}
#[rstest]
fn paragraph_soft_break(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
Hello
World
"},
None
),
Text::from_iter([
Line::from("Hello").style(styles::p(None)),
Line::from("World").style(styles::p(None)),
])
);
}
#[rstest]
fn paragraph_multiple(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
Paragraph 1
Paragraph 2
"},
None
),
Text::from_iter([
Line::from("Paragraph 1").style(styles::p(None)),
Line::default(),
Line::from("Paragraph 2").style(styles::p(None))
])
);
}
#[rstest]
fn rule(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
Paragraph 1
---
Paragraph 2
"},
None
),
Text::from_iter([
Line::from("Paragraph 1").style(styles::p(None)),
Line::default(),
Line::from("---"),
Line::default(),
Line::from("Paragraph 2").style(styles::p(None))
])
);
}
#[rstest]
fn headings(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
"},
None
),
Text::from_iter([
Line::from_iter(["# ", "Heading 1"]).style(styles::h1(None)),
Line::default(),
Line::from_iter(["## ", "Heading 2"]).style(styles::h2(None)),
Line::default(),
Line::from_iter(["### ", "Heading 3"]).style(styles::h3(None)),
Line::default(),
Line::from_iter(["#### ", "Heading 4"]).style(styles::h4(None)),
Line::default(),
Line::from_iter(["##### ", "Heading 5"]).style(styles::h5(None)),
Line::default(),
Line::from_iter(["###### ", "Heading 6"]).style(styles::h6(None)),
])
);
}
#[rstest]
fn blockquote_after_paragraph(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
Hello, world!
> Blockquote
"},
None
),
Text::from_iter([
Line::from("Hello, world!").style(styles::blockquote(None)),
Line::default(),
Line::from_iter([">", " ", "Blockquote"]).style(styles::blockquote(None)),
])
);
}
#[rstest]
fn blockquote_single(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("> Blockquote", None),
Text::from(Line::from_iter([">", " ", "Blockquote"]).style(styles::blockquote(None)))
);
}
#[rstest]
fn blockquote_soft_break(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
> Blockquote 1
> Blockquote 2
"},
None
),
Text::from_iter([
Line::from_iter([">", " ", "Blockquote 1"]).style(styles::blockquote(None)),
Line::from_iter([">", " ", "Blockquote 2"]).style(styles::blockquote(None)),
])
);
}
#[rstest]
fn blockquote_multiple(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
> Blockquote 1
>
> Blockquote 2
"},
None
),
Text::from_iter([
Line::from_iter([">", " ", "Blockquote 1"]).style(styles::blockquote(None)),
Line::from_iter([">", " "]).style(styles::blockquote(None)),
Line::from_iter([">", " ", "Blockquote 2"]).style(styles::blockquote(None)),
])
);
}
#[rstest]
fn blockquote_multiple_with_break(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
> Blockquote 1
> Blockquote 2
"},
None
),
Text::from_iter([
Line::from_iter([">", " ", "Blockquote 1"]).style(styles::blockquote(None)),
Line::default(),
Line::from_iter([">", " ", "Blockquote 2"]).style(styles::blockquote(None)),
])
);
}
#[rstest]
fn blockquote_nested(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
> Blockquote 1
>> Nested Blockquote
"},
None
),
Text::from_iter([
Line::from_iter([">", " ", "Blockquote 1"]).style(styles::blockquote(None)),
Line::from_iter([">", " "]).style(styles::blockquote(None)),
Line::from_iter([">", ">", " ", "Nested Blockquote"])
.style(styles::blockquote(None)),
])
);
}
#[rstest]
fn list_single(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
- List item 1
"},
None
),
Text::from(Line::from_iter([
Span::from("\u{a0}\u{a0}• ").style(styles::list_item(None)),
Span::from("List item 1").style(styles::list_item(None))
]))
);
}
#[rstest]
fn list_multiple(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
- List item 1
- List item 2
"},
None
),
Text::from_iter([
Line::from_iter([
Span::from("\u{a0}\u{a0}• ").style(styles::list_item(None)),
Span::from("List item 1").style(styles::list_item(None))
]),
Line::from_iter([
Span::from("\u{a0}\u{a0}• ").style(styles::list_item(None)),
Span::from("List item 2").style(styles::list_item(None))
]),
])
);
}
#[rstest]
fn list_ordered(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
1. List item 1
2. List item 2
"},
None
),
Text::from_iter([
Line::from_iter([
Span::from("\u{a0}\u{a0}1. ").style(styles::list_item(None)),
Span::from("List item 1").style(styles::list_item(None))
]),
Line::from_iter([
Span::from("\u{a0}\u{a0}2. ").style(styles::list_item(None)),
Span::from("List item 2").style(styles::list_item(None))
]),
])
);
}
#[rstest]
fn list_nested(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
- List item 1
- Nested list item 1
"},
None
),
Text::from_iter([
Line::from_iter([
Span::from("\u{a0}\u{a0}• ").style(styles::list_item(None)),
Span::from("List item 1").style(styles::list_item(None))
]),
Line::from_iter([
Span::from(" \u{a0}\u{a0}• ").style(styles::list_item(None)),
Span::from("Nested list item 1").style(styles::list_item(None))
]),
])
);
}
#[rstest]
fn list_task_items(_with_tracing: DefaultGuard) {
let result = from_str(
indoc! {"
- [ ] Incomplete
- [x] Complete
"},
None,
);
assert_eq!(result.lines.len(), 2);
}
#[rstest]
fn strong(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("**Strong**", None),
Text::from(Line::from("Strong".bold()).style(styles::p(None)))
);
}
#[rstest]
fn emphasis(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("*Emphasis*", None),
Text::from(Line::from("Emphasis".italic()).style(styles::p(None)))
);
}
#[rstest]
fn strikethrough(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("~~Strikethrough~~", None),
Text::from(Line::from("Strikethrough".crossed_out()).style(styles::p(None)))
);
}
#[rstest]
fn strong_emphasis(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("**Strong *emphasis***", None),
Text::from(
Line::from_iter(["Strong ".bold(), "emphasis".bold().italic()])
.style(styles::p(None))
)
);
}
#[rstest]
fn superscript(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("H ^2^ O", None),
Text::from(
Line::from_iter([
Span::from("H "),
Span::styled("2", Style::new().dim().italic()),
Span::from(" O"),
])
.fg(Color::Rgb(255, 255, 255))
)
);
}
#[rstest]
fn subscript(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("H ~2~ O", None),
Text::from(
Line::from_iter([
Span::from("H "),
Span::styled("2", Style::new().dim().italic()),
Span::from(" O"),
])
.fg(Color::Rgb(255, 255, 255))
)
);
}
#[rstest]
fn metadata_block(_with_tracing: DefaultGuard) {
assert_eq!(
from_str(
indoc! {"
---
title: Demo
---
Body
"},
None
),
Text::from_iter([
Line::from("---").style(Style::new().fg(Color::Rgb(255, 255, 255))),
Line::from("title: Demo").style(Style::new().fg(Color::Rgb(255, 255, 255))),
Line::from("---").style(Style::new().fg(Color::Rgb(255, 255, 255))),
Line::default(),
Line::from("Body").fg(Color::Rgb(255, 255, 255)),
])
);
}
#[rstest]
fn link(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("[Link](https://example.com)", None),
Text::from(
Line::from_iter([
Span::from("Link"),
Span::from(" ("),
Span::from("https://example.com")
.style(styles::p(None))
.underlined(),
Span::from(")")
])
.style(styles::p(None))
)
);
}
#[rstest]
fn image(_with_tracing: DefaultGuard) {
assert_eq!(
from_str("", None),
Text::from_iter([
Line::default().style(styles::p(None)),
Line::from_iter([
Span::from("[Image: "),
Span::from("TestImage"),
Span::from(" -> "),
Span::from("/test.html").style(styles::p(None)).underlined(),
Span::from("]"),
])
.style(styles::p(None)),
])
);
}
}