704 lines
19 KiB
Rust
704 lines
19 KiB
Rust
//! An HTML renderer that takes an iterator of [`Event`]s and emits HTML.
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use anyhow::anyhow;
|
||
use jotdown::Alignment;
|
||
use jotdown::Container;
|
||
use jotdown::Event;
|
||
use jotdown::LinkType;
|
||
use jotdown::ListKind;
|
||
use jotdown::OrderedListNumbering::*;
|
||
use jotdown::SpanLinkType;
|
||
|
||
use crate::template;
|
||
use crate::template::TemplateRenderer;
|
||
|
||
// {{{ Renderer
|
||
/// Render djot content as HTML.
|
||
pub fn render_html<'s>(
|
||
mut events: impl Iterator<Item = Event<'s>>,
|
||
mut out: impl std::fmt::Write,
|
||
) -> anyhow::Result<()> {
|
||
let mut w = Writer::new();
|
||
events.try_for_each(|e| w.render_event(&e, &mut out))?;
|
||
w.render_epilogue(&mut out)?;
|
||
|
||
Ok(())
|
||
}
|
||
// }}}
|
||
|
||
pub struct Writer<'s> {
|
||
list_tightness: Vec<bool>,
|
||
states: Vec<State<'s>>,
|
||
footnotes: Footnotes<'s>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
enum State<'s> {
|
||
TextOnly,
|
||
Ignore,
|
||
Raw,
|
||
Math(bool),
|
||
Aside(TemplateRenderer<'s>),
|
||
}
|
||
|
||
impl<'s> Writer<'s> {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
list_tightness: Vec::new(),
|
||
states: Vec::new(),
|
||
footnotes: Footnotes::default(),
|
||
}
|
||
}
|
||
|
||
#[allow(clippy::single_match)]
|
||
pub fn render_event(
|
||
&mut self,
|
||
e: &Event<'s>,
|
||
mut out: impl std::fmt::Write,
|
||
) -> anyhow::Result<()> {
|
||
// {{{ Handle footnotes
|
||
if let Event::Start(Container::Footnote { label }, ..) = e {
|
||
self.footnotes.start(label, Vec::new());
|
||
return Ok(());
|
||
} else if let Some(events) = self.footnotes.current() {
|
||
if matches!(e, Event::End(Container::Footnote { .. })) {
|
||
self.footnotes.end();
|
||
} else {
|
||
events.push(e.clone());
|
||
}
|
||
return Ok(());
|
||
}
|
||
// }}}
|
||
// {{{ Handle blocks which trigger the `Ignore` state.
|
||
match e {
|
||
Event::Start(Container::LinkDefinition { .. }, ..) => {
|
||
self.states.push(State::Ignore);
|
||
return Ok(());
|
||
}
|
||
Event::End(Container::LinkDefinition { .. }) => {
|
||
assert!(matches!(self.states.last(), Some(State::Ignore)));
|
||
self.states.pop();
|
||
}
|
||
|
||
Event::Start(Container::RawBlock { format } | Container::RawInline { format }, ..) => {
|
||
if format == &"html" {
|
||
self.states.push(State::Raw);
|
||
} else {
|
||
self.states.push(State::Ignore);
|
||
};
|
||
|
||
return Ok(());
|
||
}
|
||
Event::End(Container::RawBlock { format } | Container::RawInline { format }) => {
|
||
if format == &"html" {
|
||
assert!(matches!(self.states.last(), Some(State::Raw)));
|
||
} else {
|
||
assert!(matches!(self.states.last(), Some(State::Ignore)));
|
||
};
|
||
|
||
self.states.pop();
|
||
}
|
||
|
||
_ => {}
|
||
}
|
||
// }}}
|
||
|
||
if matches!(self.states.last(), Some(State::Ignore)) {
|
||
return Ok(());
|
||
}
|
||
|
||
match e {
|
||
// {{{ Container start
|
||
Event::Start(c, attrs) => {
|
||
if matches!(self.states.last(), Some(&State::TextOnly)) {
|
||
return Ok(());
|
||
}
|
||
|
||
match &c {
|
||
Container::RawBlock { .. } => {}
|
||
Container::RawInline { .. } => unreachable!(),
|
||
Container::Footnote { .. } => unreachable!(),
|
||
// {{{ List
|
||
Container::List { kind, tight } => {
|
||
self.list_tightness.push(*tight);
|
||
match kind {
|
||
ListKind::Unordered(..) | ListKind::Task(..) => out.write_str("<ul")?,
|
||
ListKind::Ordered {
|
||
numbering, start, ..
|
||
} => {
|
||
out.write_str("<ol")?;
|
||
if *start > 1 {
|
||
write!(out, r#" start="{}""#, start)?;
|
||
}
|
||
|
||
if let Some(ty) = match numbering {
|
||
Decimal => None,
|
||
AlphaLower => Some('a'),
|
||
AlphaUpper => Some('A'),
|
||
RomanLower => Some('i'),
|
||
RomanUpper => Some('I'),
|
||
} {
|
||
write!(out, r#" type="{}""#, ty)?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Link
|
||
Container::Link(dst, ty) => {
|
||
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
|
||
out.write_str("<a")?;
|
||
} else {
|
||
out.write_str(r#"<a href=""#)?;
|
||
if matches!(ty, LinkType::Email) {
|
||
out.write_str("mailto:")?;
|
||
}
|
||
write_attr(dst, &mut out)?;
|
||
out.write_char('"')?;
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Paragraph
|
||
Container::Paragraph => {
|
||
if self.list_tightness.last() == Some(&true) {
|
||
return Ok(());
|
||
}
|
||
|
||
out.write_str("<p")?;
|
||
}
|
||
// }}}
|
||
Container::Blockquote => out.write_str("<blockquote")?,
|
||
Container::ListItem { .. } => out.write_str("<li")?,
|
||
Container::TaskListItem { .. } => out.write_str("<li")?,
|
||
Container::DescriptionList => out.write_str("<dl")?,
|
||
Container::DescriptionDetails => out.write_str("<dd")?,
|
||
Container::Table => out.write_str("<table")?,
|
||
Container::TableRow { .. } => out.write_str("<tr")?,
|
||
Container::Section { .. } => out.write_str("<section")?,
|
||
Container::Div {
|
||
class: class @ ("aside" | "long-aside" | "char-aside"),
|
||
} => {
|
||
if *class == "aside" {
|
||
self.list_tightness.push(true);
|
||
}
|
||
|
||
let template = if *class == "aside" {
|
||
template!("templates/aside.html")?
|
||
} else if *class == "char-aside" {
|
||
template!("templates/character-aside.html")?
|
||
} else {
|
||
template!("templates/long-aside.html")?
|
||
};
|
||
|
||
let mut renderer = TemplateRenderer::new(template);
|
||
|
||
while let Some(label) = renderer.current(&mut out)? {
|
||
if label == "character" {
|
||
let character = attrs.get_value("character").ok_or_else(|| {
|
||
anyhow!("Cannot find `character` attribute on `aside` element")
|
||
})?;
|
||
|
||
character
|
||
.parts()
|
||
.try_for_each(|part| write_attr(part, &mut out))?;
|
||
renderer.next(&mut out)?;
|
||
} else if label == "title" {
|
||
let title = attrs.get_value("title").ok_or_else(|| {
|
||
anyhow!("Cannot find `title` attribute on `aside` element")
|
||
})?;
|
||
|
||
title
|
||
.parts()
|
||
.try_for_each(|part| write_attr(part, &mut out))?;
|
||
renderer.next(&mut out)?;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
self.states.push(State::Aside(renderer));
|
||
}
|
||
Container::Div { .. } => out.write_str("<div")?,
|
||
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
|
||
Container::TableCell { head: false, .. } => out.write_str("<td")?,
|
||
Container::TableCell { head: true, .. } => out.write_str("<th")?,
|
||
Container::Caption => out.write_str("<caption")?,
|
||
Container::Image(..) => out.write_str("<img")?,
|
||
Container::DescriptionTerm => out.write_str("<dt")?,
|
||
Container::CodeBlock { .. } => out.write_str("<pre")?,
|
||
Container::Span | Container::Math { .. } => out.write_str("<span")?,
|
||
Container::Verbatim => out.write_str("<code")?,
|
||
Container::Subscript => out.write_str("<sub")?,
|
||
Container::Superscript => out.write_str("<sup")?,
|
||
Container::Insert => out.write_str("<ins")?,
|
||
Container::Delete => out.write_str("<del")?,
|
||
Container::Strong => out.write_str("<strong")?,
|
||
Container::Emphasis => out.write_str("<em")?,
|
||
Container::Mark => out.write_str("<mark")?,
|
||
Container::LinkDefinition { .. } => return Ok(()),
|
||
}
|
||
|
||
let mut write_attribs = true;
|
||
if matches!(
|
||
c,
|
||
Container::Div {
|
||
class: "aside" | "long-aside" | "char-aside"
|
||
}
|
||
) {
|
||
write_attribs = false;
|
||
}
|
||
|
||
if write_attribs {
|
||
// {{{ Write attributes
|
||
let mut id_written = false;
|
||
let mut class_written = false;
|
||
|
||
if write_attribs {
|
||
for (a, v) in attrs.unique_pairs() {
|
||
let is_class = a == "class";
|
||
let is_id = a == "id";
|
||
if (!is_id || !id_written) && (!is_class || !class_written) {
|
||
write!(out, r#" {}=""#, a)?;
|
||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||
out.write_char('"')?;
|
||
|
||
id_written |= is_id;
|
||
class_written |= is_class;
|
||
};
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Write default ids/classes
|
||
if let Container::Heading {
|
||
id,
|
||
has_section: false,
|
||
..
|
||
}
|
||
| Container::Section { id } = &c
|
||
{
|
||
if !id_written {
|
||
out.write_str(r#" id=""#)?;
|
||
write_attr(id, &mut out)?;
|
||
out.write_char('"')?;
|
||
}
|
||
// TODO: do I not want this to add onto the provided class?
|
||
} else if (matches!(c, Container::Div { class } if !class.is_empty())
|
||
|| matches!(
|
||
c,
|
||
Container::Math { .. }
|
||
| Container::List {
|
||
kind: ListKind::Task(..),
|
||
..
|
||
} | Container::TaskListItem { .. }
|
||
)) && !class_written
|
||
{
|
||
out.write_str(r#" class=""#)?;
|
||
write_class(c, false, &mut out)?;
|
||
out.write_char('"')?;
|
||
}
|
||
// }}}
|
||
|
||
match c {
|
||
// {{{ Write css for aligning table cell text
|
||
Container::TableCell { alignment, .. }
|
||
if !matches!(alignment, Alignment::Unspecified) =>
|
||
{
|
||
let a = match alignment {
|
||
Alignment::Unspecified => unreachable!(),
|
||
Alignment::Left => "left",
|
||
Alignment::Center => "center",
|
||
Alignment::Right => "right",
|
||
};
|
||
write!(out, r#" style="text-align: {};">"#, a)?;
|
||
}
|
||
// }}}
|
||
// {{{ Write language for codeblock
|
||
Container::CodeBlock { language } => {
|
||
if language.is_empty() {
|
||
out.write_str("><code>")?;
|
||
} else {
|
||
out.write_str(r#"><code class="language-"#)?;
|
||
write_attr(language, &mut out)?;
|
||
out.write_str(r#"">"#)?;
|
||
}
|
||
}
|
||
// }}}
|
||
Container::Image(..) => out.write_str(r#" alt=""#)?,
|
||
Container::Math { display } => {
|
||
out.write_str(r#">"#)?;
|
||
self.states.push(State::Math(*display));
|
||
}
|
||
_ => out.write_char('>')?,
|
||
}
|
||
}
|
||
|
||
match &c {
|
||
Container::Heading { id, .. } => {
|
||
out.write_str(r##"<a href="#"##)?;
|
||
write_attr(id, &mut out)?;
|
||
out.write_str(r#"">◇</a> "#)?;
|
||
}
|
||
Container::Image(..) => {
|
||
self.states.push(State::TextOnly);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Container end
|
||
Event::End(c) => {
|
||
match &c {
|
||
Container::Image(..) => {
|
||
assert!(matches!(self.states.last(), Some(State::TextOnly)));
|
||
self.states.pop();
|
||
}
|
||
_ => {}
|
||
}
|
||
|
||
if matches!(self.states.last(), Some(State::TextOnly)) {
|
||
return Ok(());
|
||
}
|
||
|
||
match c {
|
||
Container::RawBlock { .. } => {}
|
||
Container::RawInline { .. } => unreachable!(),
|
||
Container::Footnote { .. } => unreachable!(),
|
||
// {{{ List
|
||
Container::List { kind, .. } => {
|
||
self.list_tightness.pop();
|
||
match kind {
|
||
ListKind::Unordered(..) | ListKind::Task(..) => {
|
||
out.write_str("</ul>")?
|
||
}
|
||
ListKind::Ordered { .. } => out.write_str("</ol>")?,
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Paragraph
|
||
Container::Paragraph => {
|
||
if matches!(self.list_tightness.last(), Some(true)) {
|
||
return Ok(());
|
||
}
|
||
|
||
if !self.footnotes.in_epilogue() {
|
||
out.write_str("</p>")?;
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Image
|
||
Container::Image(src, ..) => {
|
||
if !src.is_empty() {
|
||
out.write_str(r#"" src=""#)?;
|
||
write_attr(src, &mut out)?;
|
||
}
|
||
|
||
out.write_str(r#"">"#)?;
|
||
}
|
||
// }}}
|
||
// {{{ Math
|
||
Container::Math { .. } => {
|
||
assert!(matches!(self.states.last(), Some(State::Math(_))));
|
||
self.states.pop();
|
||
out.write_str(r#"</span>"#)?;
|
||
}
|
||
// }}}
|
||
Container::Blockquote => out.write_str("</blockquote>")?,
|
||
Container::ListItem { .. } => out.write_str("</li>")?,
|
||
Container::TaskListItem { .. } => out.write_str("</li>")?,
|
||
Container::DescriptionList => out.write_str("</dl>")?,
|
||
Container::DescriptionDetails => out.write_str("</dd>")?,
|
||
Container::Table => out.write_str("</table>")?,
|
||
Container::TableRow { .. } => out.write_str("</tr>")?,
|
||
Container::Section { .. } => out.write_str("</section>")?,
|
||
Container::Div {
|
||
class: class @ ("aside" | "long-aside" | "char-aside"),
|
||
} => {
|
||
if *class == "aside" {
|
||
self.list_tightness.pop();
|
||
}
|
||
|
||
let state = self.states.pop().unwrap();
|
||
let State::Aside(mut renderer) = state else {
|
||
panic!("Finished `aside` element without being in the `Aside` state.")
|
||
};
|
||
|
||
assert_eq!(renderer.current(&mut out)?, Some("content"));
|
||
renderer.finish(&mut out)?;
|
||
}
|
||
Container::Div { .. } => out.write_str("</div>")?,
|
||
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
||
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
|
||
Container::TableCell { head: true, .. } => out.write_str("</th>")?,
|
||
Container::Caption => out.write_str("</caption>")?,
|
||
Container::DescriptionTerm => out.write_str("</dt>")?,
|
||
Container::CodeBlock { .. } => out.write_str("</code></pre>")?,
|
||
Container::Span => out.write_str("</span>")?,
|
||
Container::Link(..) => out.write_str("</a>")?,
|
||
Container::Verbatim => out.write_str("</code>")?,
|
||
Container::Subscript => out.write_str("</sub>")?,
|
||
Container::Superscript => out.write_str("</sup>")?,
|
||
Container::Insert => out.write_str("</ins>")?,
|
||
Container::Delete => out.write_str("</del>")?,
|
||
Container::Strong => out.write_str("</strong>")?,
|
||
Container::Emphasis => out.write_str("</em>")?,
|
||
Container::Mark => out.write_str("</mark>")?,
|
||
Container::LinkDefinition { .. } => unreachable!(),
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Raw string
|
||
Event::Str(s) => match self.states.last() {
|
||
Some(State::TextOnly) => write_attr(s, &mut out)?,
|
||
Some(State::Raw) => out.write_str(s)?,
|
||
Some(State::Math(display)) => {
|
||
let config = pulldown_latex::RenderConfig {
|
||
display_mode: {
|
||
use pulldown_latex::config::DisplayMode::*;
|
||
if *display {
|
||
Block
|
||
} else {
|
||
Inline
|
||
}
|
||
},
|
||
annotation: None,
|
||
error_color: (178, 34, 34),
|
||
xml: true,
|
||
math_style: pulldown_latex::config::MathStyle::TeX,
|
||
};
|
||
|
||
let mut mathml = String::new();
|
||
let storage = pulldown_latex::Storage::new();
|
||
let parser = pulldown_latex::Parser::new(s, &storage);
|
||
pulldown_latex::push_mathml(&mut mathml, parser, config).unwrap();
|
||
out.write_str(&mathml)?;
|
||
}
|
||
_ => write_text(s, &mut out)?,
|
||
},
|
||
// }}}
|
||
// {{{ Footnote reference
|
||
Event::FootnoteReference(label) => {
|
||
let number = self.footnotes.reference(label);
|
||
if !matches!(self.states.last(), Some(State::TextOnly)) {
|
||
write!(
|
||
out,
|
||
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
|
||
number, number, number
|
||
)?;
|
||
}
|
||
}
|
||
// }}}
|
||
// {{{ Symbol
|
||
Event::Symbol(sym) => write!(out, ":{}:", sym)?,
|
||
Event::LeftSingleQuote => out.write_str("‘")?,
|
||
Event::RightSingleQuote => out.write_str("’")?,
|
||
Event::LeftDoubleQuote => out.write_str("“")?,
|
||
Event::RightDoubleQuote => out.write_str("”")?,
|
||
Event::Ellipsis => out.write_str("…")?,
|
||
Event::EnDash => out.write_str("–")?,
|
||
Event::EmDash => out.write_str("—")?,
|
||
Event::NonBreakingSpace => out.write_str(" ")?,
|
||
Event::Hardbreak => out.write_str("<br>")?,
|
||
Event::Softbreak => out.write_char('\n')?,
|
||
// }}}
|
||
// {{{ Thematic break
|
||
Event::ThematicBreak(attrs) => {
|
||
out.write_str("<hr")?;
|
||
for (a, v) in attrs.unique_pairs() {
|
||
write!(out, r#" {}=""#, a)?;
|
||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||
out.write_char('"')?;
|
||
}
|
||
out.write_str(">")?;
|
||
}
|
||
// }}}
|
||
Event::Escape | Event::Blankline | Event::Attributes(..) => {}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// {{{ Render epilogue
|
||
fn render_epilogue(&mut self, mut out: impl std::fmt::Write) -> anyhow::Result<()> {
|
||
if self.footnotes.reference_encountered() {
|
||
out.write_str("<section role=\"doc-endnotes\">")?;
|
||
out.write_str("<hr>")?;
|
||
out.write_str("<ol>")?;
|
||
|
||
while let Some((number, events)) = self.footnotes.next() {
|
||
write!(out, "<li id=\"fn{}\">", number)?;
|
||
|
||
let mut unclosed_para = false;
|
||
for e in events.iter().flatten() {
|
||
if matches!(&e, Event::Blankline | Event::Escape) {
|
||
continue;
|
||
}
|
||
if unclosed_para {
|
||
// not a footnote, so no need to add href before para close
|
||
out.write_str("</p>")?;
|
||
}
|
||
self.render_event(e, &mut out)?;
|
||
unclosed_para = matches!(e, Event::End(Container::Paragraph { .. }))
|
||
&& !matches!(self.list_tightness.last(), Some(true));
|
||
}
|
||
if !unclosed_para {
|
||
// create a new paragraph
|
||
out.write_str("<p>")?;
|
||
}
|
||
write!(
|
||
out,
|
||
"<a href=\"#fnref{}\" role=\"doc-backlink\">\u{21A9}\u{FE0E}</a></p>",
|
||
number,
|
||
)?;
|
||
|
||
out.write_str("</li>")?;
|
||
}
|
||
|
||
out.write_str("</ol>")?;
|
||
out.write_str("</section>")?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
// }}}
|
||
}
|
||
|
||
// {{{ Writing helpers
|
||
fn write_class<W>(c: &Container, mut first_written: bool, out: &mut W) -> std::fmt::Result
|
||
where
|
||
W: std::fmt::Write,
|
||
{
|
||
if let Some(cls) = match c {
|
||
Container::List {
|
||
kind: ListKind::Task(..),
|
||
..
|
||
} => Some("task-list"),
|
||
Container::TaskListItem { checked: false } => Some("unchecked"),
|
||
Container::TaskListItem { checked: true } => Some("checked"),
|
||
Container::Math { display: false } => Some("math inline"),
|
||
Container::Math { display: true } => Some("math display"),
|
||
_ => None,
|
||
} {
|
||
first_written = true;
|
||
out.write_str(cls)?;
|
||
}
|
||
if let Container::Div { class } = c {
|
||
if !class.is_empty() {
|
||
if first_written {
|
||
out.write_char(' ')?;
|
||
}
|
||
out.write_str(class)?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[inline]
|
||
fn write_text(s: &str, out: impl std::fmt::Write) -> std::fmt::Result {
|
||
write_escape(s, false, out)
|
||
}
|
||
|
||
#[inline]
|
||
fn write_attr(s: &str, out: impl std::fmt::Write) -> std::fmt::Result {
|
||
write_escape(s, true, out)
|
||
}
|
||
|
||
fn write_escape(
|
||
mut s: &str,
|
||
escape_quotes: bool,
|
||
mut out: impl std::fmt::Write,
|
||
) -> std::fmt::Result {
|
||
let mut ent = "";
|
||
while let Some(i) = s.find(|c| {
|
||
match c {
|
||
'<' => Some("<"),
|
||
'>' => Some(">"),
|
||
'&' => Some("&"),
|
||
'"' if escape_quotes => Some("""),
|
||
_ => None,
|
||
}
|
||
.map_or(false, |s| {
|
||
ent = s;
|
||
true
|
||
})
|
||
}) {
|
||
out.write_str(&s[..i])?;
|
||
out.write_str(ent)?;
|
||
s = &s[i + 1..];
|
||
}
|
||
out.write_str(s)
|
||
}
|
||
// }}}
|
||
// {{{ Footnotes
|
||
/// Helper to aggregate footnotes for rendering at the end of the document. It will cache footnote
|
||
/// events until they should be emitted at the end.
|
||
///
|
||
/// When footnotes should be rendered, they can be pulled with the [`Footnotes::next`] function in
|
||
/// the order they were first referenced.
|
||
#[derive(Default)]
|
||
struct Footnotes<'s> {
|
||
/// Stack of current open footnotes, with label and staging buffer.
|
||
open: Vec<(&'s str, Vec<Event<'s>>)>,
|
||
/// Footnote references in the order they were first encountered.
|
||
references: Vec<&'s str>,
|
||
/// Events for each footnote.
|
||
events: HashMap<&'s str, Vec<Event<'s>>>,
|
||
/// Number of last footnote that was emitted.
|
||
number: usize,
|
||
}
|
||
|
||
impl<'s> Footnotes<'s> {
|
||
/// Returns `true` if any reference has been encountered.
|
||
fn reference_encountered(&self) -> bool {
|
||
!self.references.is_empty()
|
||
}
|
||
|
||
/// Returns `true` if within the epilogue, i.e. if any footnotes have been pulled.
|
||
fn in_epilogue(&self) -> bool {
|
||
self.number > 0
|
||
}
|
||
|
||
/// Add a footnote reference.
|
||
fn reference(&mut self, label: &'s str) -> usize {
|
||
self.references
|
||
.iter()
|
||
.position(|t| *t == label)
|
||
.map_or_else(
|
||
|| {
|
||
self.references.push(label);
|
||
self.references.len()
|
||
},
|
||
|i| i + 1,
|
||
)
|
||
}
|
||
|
||
/// Start aggregating a footnote.
|
||
fn start(&mut self, label: &'s str, events: Vec<Event<'s>>) {
|
||
self.open.push((label, events));
|
||
}
|
||
|
||
/// Obtain the current (most recently started) footnote.
|
||
fn current(&mut self) -> Option<&mut Vec<Event<'s>>> {
|
||
self.open.last_mut().map(|(_, e)| e)
|
||
}
|
||
|
||
/// End the current (most recently started) footnote.
|
||
fn end(&mut self) {
|
||
let (label, stage) = self.open.pop().unwrap();
|
||
self.events.insert(label, stage);
|
||
}
|
||
}
|
||
|
||
impl<'s> Iterator for Footnotes<'s> {
|
||
type Item = (usize, Option<Vec<Event<'s>>>);
|
||
|
||
fn next(&mut self) -> Option<Self::Item> {
|
||
self.references.get(self.number).map(|label| {
|
||
self.number += 1;
|
||
(self.number, self.events.remove(label))
|
||
})
|
||
}
|
||
}
|
||
// }}}
|