diff --git a/src/html.rs b/src/html.rs index f5da61c..32f6bc0 100644 --- a/src/html.rs +++ b/src/html.rs @@ -5,6 +5,7 @@ use crate::Container; use crate::Event; use crate::LinkType; use crate::ListKind; +use crate::Map; use crate::OrderedListNumbering::*; use crate::Render; use crate::SpanLinkType; @@ -48,23 +49,29 @@ impl Default for Raw { } #[derive(Default)] -struct Writer { +struct Writer<'s> { raw: Raw, img_alt_text: usize, list_tightness: Vec, - encountered_footnote: bool, - footnote_number: Option, not_first_line: bool, - close_para: bool, ignore: bool, + footnotes: Footnotes<'s>, } -impl Writer { - fn render_event<'s, W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result +impl<'s> Writer<'s> { + fn render_event(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result where W: std::fmt::Write, { - if matches!(&e, Event::Blankline | Event::Escape) { + 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(()); } @@ -82,15 +89,6 @@ impl Writer { return Ok(()); } - let close_para = self.close_para; - if close_para { - self.close_para = false; - if !matches!(&e, Event::End(Container::Footnote { .. })) { - // no need to add href before para close - out.write_str("

")?; - } - } - match e { Event::Start(c, attrs) => { if c.is_block() && self.not_first_line { @@ -129,16 +127,7 @@ impl Writer { } Container::DescriptionList => out.write_str(" out.write_str(" { - debug_assert!(self.footnote_number.is_none()); - self.footnote_number = Some((*number).try_into().unwrap()); - if !self.encountered_footnote { - self.encountered_footnote = true; - out.write_str("
\n
\n
    \n")?; - } - write!(out, "
  1. ", number)?; - return Ok(()); - } + Container::Footnote { .. } => unreachable!(), Container::Table => out.write_str(" out.write_str(" out.write_str(" { - if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { + if c.is_block_container() { out.write_char('\n')?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { @@ -322,19 +311,7 @@ impl Writer { } Container::DescriptionList => out.write_str("")?, Container::DescriptionDetails => out.write_str("")?, - Container::Footnote { number, .. } => { - if !close_para { - // create a new paragraph - out.write_str("\n

    ")?; - } - write!( - out, - r##"↩︎︎

    "##, - number, - )?; - out.write_str("\n
  2. ")?; - self.footnote_number = None; - } + Container::Footnote { .. } => unreachable!(), Container::Table => out.write_str("")?, Container::TableRow { .. } => out.write_str("")?, Container::Section { .. } => out.write_str("
")?, @@ -343,10 +320,8 @@ impl Writer { if matches!(self.list_tightness.last(), Some(true)) { return Ok(()); } - if self.footnote_number.is_none() { + if !self.footnotes.in_epilogue() { out.write_str("

")?; - } else { - self.close_para = true; } } Container::Heading { level, .. } => write!(out, "", level)?, @@ -394,7 +369,8 @@ impl Writer { Raw::Html => out.write_str(s)?, Raw::Other => {} }, - Event::FootnoteReference(_tag, number) => { + Event::FootnoteReference(label) => { + let number = self.footnotes.reference(label); if self.img_alt_text == 0 { write!( out, @@ -414,7 +390,7 @@ impl Writer { Event::NonBreakingSpace => out.write_str(" ")?, Event::Hardbreak => out.write_str("
\n")?, Event::Softbreak => out.write_char('\n')?, - Event::Escape | Event::Blankline => unreachable!("filtered out"), + Event::Escape | Event::Blankline => {} Event::ThematicBreak(attrs) => { out.write_str("\n\n
\n
    ")?; + + while let Some((number, events)) = self.footnotes.next() { + write!(out, "\n
  1. ", 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("

    ")?; + } + 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("\n

    ")?; + } + write!( + out, + r##"↩︎︎

    "##, + number, + )?; + + out.write_str("\n
  2. ")?; + } + out.write_str("\n
\n")?; } + out.write_char('\n')?; Ok(()) @@ -481,3 +489,73 @@ where } out.write_str(s) } + +/// 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>)>, + /// Footnote references in the order they were first encountered. + references: Vec<&'s str>, + /// Events for each footnote. + events: Map<&'s str, Vec>>, + /// 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>) { + self.open.push((label, events)); + } + + /// Obtain the current (most recently started) footnote. + fn current(&mut self) -> Option<&mut Vec>> { + 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>>); + + fn next(&mut self) -> Option { + self.references.get(self.number).map(|label| { + self.number += 1; + (self.number, self.events.remove(label)) + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 73ede1b..9ce7812 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -209,7 +209,7 @@ pub enum Event<'s> { /// A string object, text only. Str(CowStr<'s>), /// A footnote reference. - FootnoteReference(&'s str, usize), + FootnoteReference(&'s str), /// A symbol, by default rendered literally but may be treated specially. Symbol(CowStr<'s>), /// Left single quotation mark. @@ -262,7 +262,7 @@ pub enum Container<'s> { /// Details describing a term within a description list. DescriptionDetails, /// A footnote definition. - Footnote { tag: &'s str, number: usize }, + Footnote { label: &'s str }, /// A table element. Table, /// A row element of a table. @@ -569,15 +569,6 @@ pub struct Parser<'s> { /// Currently within a verbatim code block. verbatim: bool, - /// Footnote references in the order they were encountered, without duplicates. - footnote_references: Vec<&'s str>, - /// Cache of footnotes to emit at the end. - footnotes: Map<&'s str, block::Tree>, - /// Next or current footnote being parsed and emitted. - footnote_index: usize, - /// Currently within a footnote. - footnote_active: bool, - /// Inline parser. inline_parser: inline::Parser<'s>, } @@ -755,10 +746,6 @@ impl<'s> Parser<'s> { block_attributes: Attributes::new(), table_head_row: false, verbatim: false, - footnote_references: Vec::new(), - footnotes: Map::new(), - footnote_index: 0, - footnote_active: false, inline_parser, } } @@ -847,19 +834,7 @@ impl<'s> Parser<'s> { } inline::EventKind::Atom(a) => match a { inline::Atom::FootnoteReference => { - let tag = inline.span.of(self.src); - let number = self - .footnote_references - .iter() - .position(|t| *t == tag) - .map_or_else( - || { - self.footnote_references.push(tag); - self.footnote_references.len() - }, - |i| i + 1, - ); - Event::FootnoteReference(inline.span.of(self.src), number) + Event::FootnoteReference(inline.span.of(self.src)) } inline::Atom::Symbol => Event::Symbol(inline.span.of(self.src).into()), inline::Atom::Quote { ty, left } => match (ty, left) { @@ -941,12 +916,7 @@ impl<'s> Parser<'s> { block::Container::Div { .. } => Container::Div { class: (!ev.span.is_empty()).then(|| content), }, - block::Container::Footnote => { - debug_assert!(enter); - self.footnotes.insert(content, self.tree.take_branch()); - self.block_attributes = Attributes::new(); - continue; - } + block::Container::Footnote => Container::Footnote { label: content }, block::Container::List(block::ListKind { ty, tight }) => { if matches!(ty, block::ListType::Description) { Container::DescriptionList @@ -1013,43 +983,13 @@ impl<'s> Parser<'s> { } None } - - fn footnote(&mut self) -> Option> { - if self.footnote_active { - let tag = self.footnote_references.get(self.footnote_index).unwrap(); - self.footnote_index += 1; - self.footnote_active = false; - Some(Event::End(Container::Footnote { - tag, - number: self.footnote_index, - })) - } else if let Some(tag) = self.footnote_references.get(self.footnote_index) { - self.tree = self - .footnotes - .remove(tag) - .unwrap_or_else(block::Tree::empty); - self.footnote_active = true; - - Some(Event::Start( - Container::Footnote { - tag, - number: self.footnote_index + 1, - }, - Attributes::new(), - )) - } else { - None - } - } } impl<'s> Iterator for Parser<'s> { type Item = Event<'s>; fn next(&mut self) -> Option { - self.inline() - .or_else(|| self.block()) - .or_else(|| self.footnote()) + self.inline().or_else(|| self.block()) } } @@ -1563,43 +1503,10 @@ mod test { test_parse!( "[^a][^b][^c]", Start(Paragraph, Attributes::new()), - FootnoteReference("a", 1), - FootnoteReference("b", 2), - FootnoteReference("c", 3), + FootnoteReference("a"), + FootnoteReference("b"), + FootnoteReference("c"), End(Paragraph), - Start( - Footnote { - tag: "a", - number: 1 - }, - Attributes::new() - ), - End(Footnote { - tag: "a", - number: 1 - }), - Start( - Footnote { - tag: "b", - number: 2 - }, - Attributes::new() - ), - End(Footnote { - tag: "b", - number: 2 - }), - Start( - Footnote { - tag: "c", - number: 3 - }, - Attributes::new() - ), - End(Footnote { - tag: "c", - number: 3 - }), ); } @@ -1608,23 +1515,14 @@ mod test { test_parse!( "[^a]\n\n[^a]: a\n", Start(Paragraph, Attributes::new()), - FootnoteReference("a", 1), + FootnoteReference("a"), End(Paragraph), Blankline, - Start( - Footnote { - tag: "a", - number: 1 - }, - Attributes::new() - ), + Start(Footnote { label: "a" }, Attributes::new()), Start(Paragraph, Attributes::new()), Str("a".into()), End(Paragraph), - End(Footnote { - tag: "a", - number: 1 - }), + End(Footnote { label: "a" }), ); } @@ -1639,16 +1537,10 @@ mod test { " def", // ), Start(Paragraph, Attributes::new()), - FootnoteReference("a", 1), + FootnoteReference("a"), End(Paragraph), Blankline, - Start( - Footnote { - tag: "a", - number: 1 - }, - Attributes::new() - ), + Start(Footnote { label: "a" }, Attributes::new()), Start(Paragraph, Attributes::new()), Str("abc".into()), End(Paragraph), @@ -1656,10 +1548,7 @@ mod test { Start(Paragraph, Attributes::new()), Str("def".into()), End(Paragraph), - End(Footnote { - tag: "a", - number: 1 - }), + End(Footnote { label: "a" }), ); } @@ -1673,26 +1562,17 @@ mod test { "para\n", // ), Start(Paragraph, Attributes::new()), - FootnoteReference("a", 1), + FootnoteReference("a"), End(Paragraph), Blankline, - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), - Start( - Footnote { - tag: "a", - number: 1 - }, - Attributes::new() - ), + Start(Footnote { label: "a" }, Attributes::new()), Start(Paragraph, Attributes::new()), Str("note".into()), End(Paragraph), - End(Footnote { - tag: "a", - number: 1 - }), + End(Footnote { label: "a" }), + Start(Paragraph, Attributes::new()), + Str("para".into()), + End(Paragraph), ); } diff --git a/src/tree.rs b/src/tree.rs index 4992a75..0101370 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -36,14 +36,6 @@ pub struct Tree { } impl Tree { - pub fn empty() -> Self { - Self { - nodes: vec![].into_boxed_slice().into(), - branch: Vec::new(), - head: None, - } - } - /// Count number of direct children nodes. pub fn count_children(&self) -> usize { let mut head = self.head; @@ -56,22 +48,6 @@ impl Tree { count } - /// Split off the remaining part of the current branch. The returned [`Tree`] will continue on - /// the branch, this [`Tree`] will skip over the current branch. - pub fn take_branch(&mut self) -> Self { - let head = self.head.take(); - self.head = self.branch.pop(); - if let Some(h) = self.head { - let n = &self.nodes[h.index()]; - self.head = n.next; - } - Self { - nodes: self.nodes.clone(), - branch: Vec::new(), - head, - } - } - /// Retrieve all inlines until the end of the current container. Panics if any upcoming node is /// not an inline node. pub fn take_inlines(&mut self) -> impl Iterator + '_ { @@ -410,9 +386,6 @@ impl std::fmt::Debug for mod test { use crate::Span; - use super::Event; - use super::EventKind; - #[test] fn fmt() { let mut tree = super::Builder::new(); @@ -451,81 +424,4 @@ mod test { ) ); } - - #[test] - fn branch_take_branch() { - let mut b = super::Builder::new(); - let sp = Span::new(0, 0); - b.enter(1, sp); - b.atom(11, sp); - b.exit(); - b.enter(2, sp); - b.enter(21, sp); - b.atom(211, sp); - b.exit(); - b.exit(); - b.enter(3, sp); - b.atom(31, sp); - b.exit(); - let mut tree = b.finish(); - - assert_eq!( - (&mut tree).take(3).collect::>(), - &[ - Event { - kind: EventKind::Enter(1), - span: sp - }, - Event { - kind: EventKind::Atom(11), - span: sp - }, - Event { - kind: EventKind::Exit(1), - span: sp - }, - ] - ); - assert_eq!( - tree.next(), - Some(Event { - kind: EventKind::Enter(2), - span: sp - }) - ); - assert_eq!( - tree.take_branch().collect::>(), - &[ - Event { - kind: EventKind::Enter(21), - span: sp - }, - Event { - kind: EventKind::Atom(211), - span: sp - }, - Event { - kind: EventKind::Exit(21), - span: sp - }, - ] - ); - assert_eq!( - tree.collect::>(), - &[ - Event { - kind: EventKind::Enter(3), - span: sp - }, - Event { - kind: EventKind::Atom(31), - span: sp - }, - Event { - kind: EventKind::Exit(3), - span: sp - }, - ] - ); - } }