139 lines
4.2 KiB
Rust
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>"##
|
|
);
|
|
}
|
|
}
|