use pulldown_cmark::*;
use std::borrow::Cow;
use util;
use std::iter::Peekable;
#[derive(Debug, PartialEq)]
pub enum TocElement {
Html(String),
TocReference,
Heading(Heading, Vec<TocElement>),
}
#[derive(Debug, PartialEq)]
pub struct Heading {
pub level: i32,
pub contents: String,
pub slug: String,
}
pub fn parse_toc<'a, P: Iterator<Item = Event<'a>>>(parser: P) -> Vec<TocElement> {
parse_toc_at(&mut parser.peekable(), 0)
}
fn parse_toc_at<'a, P>(parser: &mut Peekable<P>, header_level: i32) -> Vec<TocElement>
where
P: Iterator<Item = Event<'a>>,
{
let mut toc = Vec::new();
let mut current = Vec::new();
loop {
if let Some(&Event::Start(Tag::Header(i))) = parser.peek() {
if i <= header_level {
break;
}
}
if let Some(e) = parser.next() {
match e {
Event::Start(Tag::Header(_)) => {
if current.len() > 0 {
toc.push(TocElement::Html(render_to_string(current.drain(..))));
}
}
Event::End(Tag::Header(i)) => {
toc.push(TocElement::Heading(
Heading::from_events(i, current.drain(..)),
parse_toc_at(parser, i),
));
}
Event::Text(ref t) if t.starts_with(":::") => {
let last = current.pop();
if let Some(event) = last {
if let Event::Start(Tag::CodeBlock(Cow::Borrowed(""))) = event {
current.push(Event::Start(Tag::CodeBlock(String::from(&t[3..]).into())));
} else {
current.push(event);
current.push(Event::Text(t.clone()));
}
}
}
Event::Text(ref t) if t == "[TOC]" => {
if let Some(&Event::Start(Tag::Paragraph)) = current.last() {
if current.len() > 0 {
toc.push(TocElement::Html(render_to_string(current.drain(..))));
}
toc.push(TocElement::TocReference);
} else {
current.push(Event::Text("[TOC]".into()))
}
}
e => current.push(e),
}
} else {
break;
}
}
if current.len() > 0 {
let rendered = render_to_string(current.into_iter());
toc.push(TocElement::Html(rendered));
}
toc
}
fn render_to_string<'a, I>(events: I) -> String
where
I: Iterator<Item = Event<'a>>,
{
let mut rendered = String::new();
html::push_html(&mut rendered, events.into_iter());
rendered
}
impl Heading {
pub fn from_events<'a, I>(level: i32, events: I) -> Self
where
I: Iterator<Item = Event<'a>>,
{
let mut slug = String::new();
let contents = render_to_string(events.inspect(|e| match e {
&Event::Text(ref t) => slug.push_str(&t),
_ => (),
}));
let slug = util::slugify(&slug);
Heading {
level: level,
contents: contents,
slug: slug,
}
}
}
#[cfg(test)]
mod test {
use super::*;
fn h(level: i32, contents: &str) -> Heading {
let slug = util::slugify(contents);
Heading {
level: level,
contents: contents.into(),
slug: slug,
}
}
#[test]
fn parse_wit_no_headings() {
let doc = "hello world";
let mut parser = Parser::new(doc);
let toc = parse_toc(&mut parser);
assert_eq!(vec![TocElement::Html("<p>hello world</p>\n".into())], toc);
}
#[test]
fn parse_with_single_heading() {
let doc = "# I am an H1";
let mut parser = Parser::new(doc);
let toc = parse_toc(&mut parser);
assert_eq!(
vec![
TocElement::Heading(h(1, "I am an H1"), Vec::new()),
],
toc
);
}
#[test]
fn parse_with_single_toc_reference() {
let doc = "[TOC]";
let mut parser = Parser::new(doc);
let toc = parse_toc(&mut parser);
assert_eq!(
vec![
TocElement::Html("<p>".into()),
TocElement::TocReference,
TocElement::Html("</p>\n".into()),
],
toc
);
}
#[test]
fn parse_with_nested_headings() {
let doc = r#"
# Heading 1.1
## Heading 2.1
### Heading 3.1
## Heading 2.2
# Heading 1.2
"#;
let mut parser = Parser::new(doc);
let toc = parse_toc(&mut parser);
assert_eq!(
vec![
TocElement::Heading(
h(1, "Heading 1.1"),
vec![
TocElement::Heading(
h(2, "Heading 2.1"),
vec![
TocElement::Heading(h(3, "Heading 3.1"), Vec::new()),
]
),
TocElement::Heading(h(2, "Heading 2.2"), Vec::new()),
]
),
TocElement::Heading(h(1, "Heading 1.2"), Vec::new()),
],
toc
)
}
}