#![cfg(feature = "markdown")]
#![cfg_attr(feature = "doc-cfg", doc(cfg(feature = "markdown")))]
use std::borrow::Cow;
use crate::theme::{Effect, Style};
use crate::utils::markup::{StyledIndexedSpan, StyledString};
use crate::utils::span::IndexedCow;
use pulldown_cmark::{self, CowStr, Event, Tag};
use unicode_width::UnicodeWidthStr;
pub fn parse<S>(input: S) -> StyledString
where
S: Into<String>,
{
let input = input.into();
let spans = parse_spans(&input);
StyledString::with_spans(input, spans)
}
pub struct Parser<'a> {
first: bool,
stack: Vec<Style>,
input: &'a str,
parser: pulldown_cmark::Parser<'a, 'a>,
}
impl<'a> Parser<'a> {
pub fn new(input: &'a str) -> Self {
Parser {
input,
first: true,
parser: pulldown_cmark::Parser::new(input),
stack: Vec::new(),
}
}
fn literal<S>(&self, text: S) -> StyledIndexedSpan
where
S: Into<String>,
{
StyledIndexedSpan::simple_owned(text.into(), Style::merge(&self.stack))
}
}
fn heading(level: usize) -> &'static str {
&"##########"[..level]
}
impl<'a> Iterator for Parser<'a> {
type Item = StyledIndexedSpan;
fn next(&mut self) -> Option<Self::Item> {
loop {
let next = match self.parser.next() {
None => return None,
Some(event) => event,
};
match next {
Event::Start(tag) => match tag {
Tag::Emphasis => {
self.stack.push(Style::from(Effect::Italic))
}
Tag::Heading(level, ..) => {
return Some(
self.literal(format!(
"{} ",
heading(level as usize)
)),
)
}
Tag::BlockQuote => return Some(self.literal("> ")),
Tag::Link(_, _, _) => return Some(self.literal("[")),
Tag::CodeBlock(_) => return Some(self.literal("```")),
Tag::Strong => self.stack.push(Style::from(Effect::Bold)),
Tag::Paragraph if !self.first => {
return Some(self.literal("\n\n"))
}
_ => (),
},
Event::End(tag) => match tag {
Tag::Paragraph if self.first => self.first = false,
Tag::Heading(..) => return Some(self.literal("\n\n")),
Tag::Link(_, link, _) => {
return Some(self.literal(format!("]({link})")))
}
Tag::CodeBlock(_) => return Some(self.literal("```")),
Tag::Emphasis | Tag::Strong => {
self.stack.pop().unwrap();
}
_ => (),
},
Event::Rule => return Some(self.literal("---")),
Event::SoftBreak => return Some(self.literal("\n")),
Event::HardBreak => return Some(self.literal("\n")),
Event::FootnoteReference(text)
| Event::Html(text)
| Event::Text(text)
| Event::Code(text) => {
let text = match text {
CowStr::Boxed(text) => Cow::Owned(text.into()),
CowStr::Borrowed(text) => Cow::Borrowed(text),
CowStr::Inlined(text) => Cow::Owned(text.to_string()),
};
let width = text.width();
return Some(StyledIndexedSpan {
content: IndexedCow::from_cow(text, self.input),
attr: Style::merge(&self.stack),
width,
});
}
Event::TaskListMarker(checked) => {
let mark = if checked { "[x]" } else { "[ ]" };
return Some(self.literal(mark));
}
}
}
}
}
pub fn parse_spans(input: &str) -> Vec<StyledIndexedSpan> {
Parser::new(input).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::span::Span;
#[test]
fn test_parse() {
let input = r"
Attention
====
I *really* love __Cursive__!";
let spans = parse_spans(input);
let spans: Vec<_> =
spans.iter().map(|span| span.resolve(input)).collect();
assert_eq!(
&spans[..],
&[
Span {
content: "# ",
width: 2,
attr: &Style::none(),
},
Span {
content: "Attention",
width: 9,
attr: &Style::none(),
},
Span {
content: "\n\n",
width: 0,
attr: &Style::none(),
},
Span {
content: "I ",
width: 2,
attr: &Style::none(),
},
Span {
content: "really",
width: 6,
attr: &Style::from(Effect::Italic),
},
Span {
content: " love ",
width: 6,
attr: &Style::none(),
},
Span {
content: "Cursive",
width: 7,
attr: &Style::from(Effect::Bold),
},
Span {
content: "!",
width: 1,
attr: &Style::none(),
}
]
);
}
}