v6/src/generator/markdown/heading_anchors.rs

139 lines
4.2 KiB
Rust

use std::iter::empty;
use crate::generator::util::one_more::OneMore;
use crate::generator::util::slugify::slugify_iter;
use pulldown_cmark::{html, CowStr, Event, HeadingLevel, Tag};
use State::*;
pub struct HeadingAnchors<'a, I: Iterator<Item = Event<'a>>> {
iter: I,
state: State,
}
pub fn new<'a, I: Iterator<Item = Event<'a>>>(iter: I) -> HeadingAnchors<'a, I> {
HeadingAnchors {
iter,
state: TakeEvent,
}
}
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for HeadingAnchors<'a, I> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self.state {
TakeEvent => match self.iter.next() {
Some(e) => Some(self.handle_upstream_event(e)),
None => {
self.state = Done;
None
}
},
Done => None,
}
}
}
impl<'a, I: Iterator<Item = Event<'a>>> HeadingAnchors<'a, I> {
fn handle_upstream_event(&mut self, e: Event<'a>) -> Event<'a> {
match e {
Event::Start(Tag::Heading(level, _, _)) => {
let inside_heading = self.accumulate_heading_contents();
let slug = slugify_events(&inside_heading);
let mut inner = String::new();
html::push_html(&mut inner, inside_heading.into_iter());
let hash_marks = level_hash_marks(level);
let heading_html = format!(
"\
<{} id=\"{}\">\
<a href=\"#{}\" class=\"header-anchor\" aria-hidden=\"true\" role=\"presentation\">{}</a> \
{}\
</{}>",
level, slug, slug, hash_marks, inner, level
);
Event::Html(CowStr::Boxed(heading_html.into_boxed_str()))
}
_ => e,
}
}
fn accumulate_heading_contents(&mut self) -> Vec<Event<'a>> {
let mut events = vec![];
while let Some(e) = self.iter.next() {
if let Event::End(Tag::Heading(_, _, _)) = e {
break;
} else {
events.push(e);
}
}
events
}
}
fn slugify_events<'a>(events: &[Event<'a>]) -> String {
let chars = events
.iter()
.flat_map(|e| -> Box<dyn Iterator<Item = char>> {
match e {
Event::Text(s) | Event::Code(s) => Box::new(OneMore::new(s.chars(), '-')),
_ => Box::new(empty()),
}
});
slugify_iter(chars)
}
fn level_hash_marks(level: HeadingLevel) -> &'static str {
match level {
HeadingLevel::H1 => "#",
HeadingLevel::H2 => "##",
HeadingLevel::H3 => "###",
HeadingLevel::H4 => "####",
HeadingLevel::H5 => "#####",
HeadingLevel::H6 => "######",
}
}
enum State {
TakeEvent,
Done,
}
#[cfg(test)]
mod tests {
use pulldown_cmark::{html, CowStr, Event, Parser};
fn render(s: &str) -> String {
let mut out = String::new();
let parser = Parser::new(s);
let heading_anchors = super::new(parser);
html::push_html(&mut out, heading_anchors);
out
}
#[test]
fn test_slugify_events() {
let slugified = super::slugify_events(&[
Event::Text(CowStr::Borrowed("foo")),
Event::Code(CowStr::Borrowed("bar")),
Event::Text(CowStr::Borrowed("baz")),
]);
assert_eq!(slugified, "foo-bar-baz");
}
#[test]
fn test_heading_ids() {
assert_eq!(
render("# Test"),
r##"<h1 id="test"><a href="#test" class="header-anchor" aria-hidden="true" role="presentation">#</a> Test</h1>"##
);
assert_eq!(
render("# `Test`"),
r##"<h1 id="test"><a href="#test" class="header-anchor" aria-hidden="true" role="presentation">#</a> <code>Test</code></h1>"##
);
assert_eq!(
render("# [Test](https://example.com)"),
r##"<h1 id="test"><a href="#test" class="header-anchor" aria-hidden="true" role="presentation">#</a> <a href="https://example.com">Test</a></h1>"##
);
}
}