From 91ae737be3d19507448ade303cf1fb1219cd12a1 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 8 Apr 2023 18:17:31 +0200 Subject: [PATCH 1/8] suite: prepend djot_js tests avoid name conflicts when adding own tests --- Makefile | 2 +- tests/suite/Makefile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 51516ae..c89bf0a 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ check: suite: git submodule update --init modules/djot.js for f in $$(find modules/djot.js/test -name '*.test' | xargs basename -a); do \ - ln -fs ../../modules/djot.js/test/$$f tests/suite/$$f; \ + ln -fs ../../modules/djot.js/test/$$f tests/suite/djot_js_$$f; \ done (cd tests/suite && make) cargo test --features suite suite:: diff --git a/tests/suite/Makefile b/tests/suite/Makefile index ed00f7e..e52b030 100644 --- a/tests/suite/Makefile +++ b/tests/suite/Makefile @@ -5,9 +5,9 @@ TEST=$(shell find . -name '*.test' | sort) TEST_RS=${TEST:.test=.rs} -BLACKLIST += filters # lua filters not implemented -BLACKLIST += symb # uses ast -BLACKLIST += sourcepos # not parsable +BLACKLIST += djot_js_filters # lua filters not implemented +BLACKLIST += djot_js_symb # uses ast +BLACKLIST += djot_js_sourcepos # not parsable .PHONY: suite suite: mod.rs From 00d653e12d79b21939283bec0fdf3b446283cd76 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 8 Apr 2023 18:20:48 +0200 Subject: [PATCH 2/8] make: only rm symlink tests avoid deleting own tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c89bf0a..3dcb494 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ afl_tmin: clean: cargo clean git submodule deinit -f --all - rm -f tests/suite/*.test + find tests -type l -path 'tests/suite/*.test' -print0 | xargs -0 rm -f (cd tests/suite && make clean) rm -f tests/bench/*.dj (cd tests/bench && make clean) From ebe7a913bf1677ce0ec1dc61a04e5417bc3e6eb7 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 8 Apr 2023 23:55:36 +0200 Subject: [PATCH 3/8] suite: add footnotes unit tests complementary to ones borrowed from djot.js suite --- tests/suite/footnotes.test | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/suite/footnotes.test diff --git a/tests/suite/footnotes.test b/tests/suite/footnotes.test new file mode 100644 index 0000000..3132f9e --- /dev/null +++ b/tests/suite/footnotes.test @@ -0,0 +1,58 @@ +Footnote references may appear within a footnote. + +``` +[^a] + +[^a]: a[^b][^c] +[^b]: b +. +

1

+
+
+
    +
  1. +

    a23↩︎︎

    +
  2. +
  3. +

    b↩︎︎

    +
  4. +
  5. +

    ↩︎︎

    +
  6. +
+
+``` + +Footnote references in unreferenced footnotes are ignored. + +``` +para + +[^a]: a[^b][^c] +[^b]: b +. +

para

+``` + +Footnotes may appear within footnotes. + +``` +[^b] +[^a] + +[^a]: [^b]: inner +. +

1 +2

+
+
+
    +
  1. +

    inner↩︎︎

    +
  2. +
  3. +

    ↩︎︎

    +
  4. +
+
+``` From 1b90ba9423fad23130b4fce0a5d02839e93d432e Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 10 Apr 2023 11:41:37 +0200 Subject: [PATCH 4/8] bench: add block_footnotes benchmark current benchmarks do not contain a single footnote --- bench/input/block_footnotes.dj | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 bench/input/block_footnotes.dj diff --git a/bench/input/block_footnotes.dj b/bench/input/block_footnotes.dj new file mode 100644 index 0000000..842aa35 --- /dev/null +++ b/bench/input/block_footnotes.dj @@ -0,0 +1,24 @@ +[^abc]: footnotes may appear before + +Some[^a] paragraph[^b] with[^c] a[^d] lot[^e] of[^f] footnotes[^g]. + +[^a]: A typical footnote may have a single paragraph. +[^b]: A typical footnote may have a single paragraph. +[^c]: A typical footnote may have a single paragraph. +[^d]: A typical footnote may have a single paragraph. +[^e]: A typical footnote may have a single paragraph. +[^f]: A typical footnote may have a single paragraph. +[^g]: Footnotes may also be + + - long and, + - contain multiple block elements. + + such as + + > blockquotes. + +Footnote [^labels may be long but not multi line] + +[^labels may be long but not multi line]: longer than the footnote.. + +the reference[^abc] to it. From 17b166867f51a4be80a886caf10cba1776db7372 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Tue, 11 Apr 2023 20:02:08 +0200 Subject: [PATCH 5/8] html: derive Renderer::default setting defaults manually mostly causes rebase conflicts --- src/html.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/html.rs b/src/html.rs index 2dfd22a..be4b57f 100644 --- a/src/html.rs +++ b/src/html.rs @@ -15,30 +15,23 @@ enum Raw { Other, } +impl Default for Raw { + fn default() -> Self { + Self::None + } +} + +#[derive(Default)] pub struct Renderer { raw: Raw, img_alt_text: usize, list_tightness: Vec, encountered_footnote: bool, footnote_number: Option, - first_line: bool, + not_first_line: bool, close_para: bool, } -impl Default for Renderer { - fn default() -> Self { - Self { - raw: Raw::None, - img_alt_text: 0, - list_tightness: Vec::new(), - encountered_footnote: false, - footnote_number: None, - first_line: true, - close_para: false, - } - } -} - impl Render for Renderer { fn render_event<'s, W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result where @@ -59,7 +52,7 @@ impl Render for Renderer { match e { Event::Start(c, attrs) => { - if c.is_block() && !self.first_line { + if c.is_block() && self.not_first_line { out.write_char('\n')?; } if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { @@ -389,7 +382,7 @@ impl Render for Renderer { out.write_str(">")?; } } - self.first_line = false; + self.not_first_line = true; Ok(()) } From 8e48021f7a0e97414cdb8c54ca7287fdf85aceab Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 7 Apr 2023 15:33:45 +0200 Subject: [PATCH 6/8] lib: emit LinkDefinition event resolves #14 --- src/html.rs | 17 +++++++++++++++++ src/lib.rs | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/html.rs b/src/html.rs index be4b57f..07e896d 100644 --- a/src/html.rs +++ b/src/html.rs @@ -30,6 +30,7 @@ pub struct Renderer { footnote_number: Option, not_first_line: bool, close_para: bool, + ignore: bool, } impl Render for Renderer { @@ -41,6 +42,20 @@ impl Render for Renderer { return Ok(()); } + if matches!(&e, Event::Start(Container::LinkDefinition { .. }, ..)) { + self.ignore = true; + return Ok(()); + } + + if matches!(&e, Event::End(Container::LinkDefinition { .. })) { + self.ignore = false; + return Ok(()); + } + + if self.ignore { + return Ok(()); + } + let close_para = self.close_para; if close_para { self.close_para = false; @@ -151,6 +166,7 @@ impl Render for Renderer { Container::Strong => out.write_str(" out.write_str(" out.write_str(" return Ok(()), } for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") { @@ -343,6 +359,7 @@ impl Render for Renderer { Container::Strong => out.write_str("")?, Container::Emphasis => out.write_str("")?, Container::Mark => out.write_str("")?, + Container::LinkDefinition { .. } => unreachable!(), } } Event::Str(s) => match self.raw { diff --git a/src/lib.rs b/src/lib.rs index 05e1228..dd0a126 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -327,6 +327,8 @@ pub enum Container<'s> { Caption, /// A term within a description list. DescriptionTerm, + /// A link definition. + LinkDefinition { label: &'s str }, /// A block with raw markup for a specific output format. RawBlock { format: &'s str }, /// A block with code in a specific language. @@ -381,6 +383,7 @@ impl<'s> Container<'s> { | Self::TableCell { .. } | Self::Caption | Self::DescriptionTerm + | Self::LinkDefinition { .. } | Self::RawBlock { .. } | Self::CodeBlock { .. } => true, Self::Span @@ -419,6 +422,7 @@ impl<'s> Container<'s> { | Self::TableCell { .. } | Self::Caption | Self::DescriptionTerm + | Self::LinkDefinition { .. } | Self::RawBlock { .. } | Self::CodeBlock { .. } | Self::Span @@ -941,14 +945,6 @@ impl<'s> Parser<'s> { let cont = match c { block::Node::Leaf(l) => { self.inline_parser.reset(); - if matches!(l, block::Leaf::LinkDefinition) { - // ignore link definitions - if enter { - self.tree.take_inlines().last(); - } - self.block_attributes = Attributes::new(); - continue; - } match l { block::Leaf::Paragraph => Container::Paragraph, block::Leaf::Heading { has_section } => Container::Heading { @@ -977,7 +973,9 @@ impl<'s> Parser<'s> { head: self.table_head_row, }, block::Leaf::Caption => Container::Caption, - block::Leaf::LinkDefinition => unreachable!(), + block::Leaf::LinkDefinition => { + Container::LinkDefinition { label: content } + } } } block::Node::Container(c) => match c { @@ -1418,6 +1416,9 @@ mod test { End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), End(Paragraph), Blankline, + Start(LinkDefinition { label: "tag" }, Attributes::new()), + Str("url".into()), + End(LinkDefinition { label: "tag" }), ); test_parse!( concat!( @@ -1434,6 +1435,9 @@ mod test { End(Image("url".into(), SpanLinkType::Reference)), End(Paragraph), Blankline, + Start(LinkDefinition { label: "tag" }, Attributes::new()), + Str("url".into()), + End(LinkDefinition { label: "tag" }), ); } @@ -1483,6 +1487,9 @@ mod test { End(Paragraph), End(Blockquote), Blankline, + Start(LinkDefinition { label: "a b" }, Attributes::new()), + Str("url".into()), + End(LinkDefinition { label: "a b" }), ); } @@ -1504,6 +1511,11 @@ mod test { End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), End(Paragraph), Blankline, + Start(LinkDefinition { label: "tag" }, Attributes::new()), + Str("u".into()), + Softbreak, + Str("rl".into()), + End(LinkDefinition { label: "tag" }), ); test_parse!( concat!( @@ -1521,6 +1533,9 @@ mod test { End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), End(Paragraph), Blankline, + Start(LinkDefinition { label: "tag" }, Attributes::new()), + Str("url".into()), + End(LinkDefinition { label: "tag" }), ); } @@ -1543,6 +1558,12 @@ mod test { End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), End(Paragraph), Blankline, + Start( + LinkDefinition { label: "tag" }, + [("a", "b")].into_iter().collect() + ), + Str("url".into()), + End(LinkDefinition { label: "tag" }), Start(Paragraph, Attributes::new()), Str("para".into()), End(Paragraph), From c4ecd0c67779e38eeb98e83156bcca0472fc42ca Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Fri, 7 Apr 2023 23:17:17 +0200 Subject: [PATCH 7/8] Revert "lib: add Render::render_{event, prologue, epilogue}" This reverts commit e8503e28fd775e47543d621ed7eaddcff1c8f056. This imposed too many limitations on the renderer implementation. E.g. making it impossible to store `Event<'s>`'s in the renderer struct. Revert back to having the renderer struct separate from the implementor of the Render trait. The implementor may instead create a renderer struct without any restrictions. --- src/html.rs | 30 +++++++++++++++++++++++++-- src/lib.rs | 60 ++++++++--------------------------------------------- src/main.rs | 2 +- 3 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/html.rs b/src/html.rs index 07e896d..f5da61c 100644 --- a/src/html.rs +++ b/src/html.rs @@ -9,6 +9,32 @@ use crate::OrderedListNumbering::*; use crate::Render; use crate::SpanLinkType; +#[derive(Default)] +pub struct Renderer {} + +impl Render for Renderer { + fn push<'s, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result + where + I: Iterator>, + W: std::fmt::Write, + { + let mut w = Writer::default(); + events.try_for_each(|e| w.render_event(&e, &mut out))?; + w.render_epilogue(&mut out) + } + + fn push_borrowed<'s, E, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result + where + E: AsRef>, + I: Iterator, + W: std::fmt::Write, + { + let mut w = Writer::default(); + events.try_for_each(|e| w.render_event(e.as_ref(), &mut out))?; + w.render_epilogue(&mut out) + } +} + enum Raw { None, Html, @@ -22,7 +48,7 @@ impl Default for Raw { } #[derive(Default)] -pub struct Renderer { +struct Writer { raw: Raw, img_alt_text: usize, list_tightness: Vec, @@ -33,7 +59,7 @@ pub struct Renderer { ignore: bool, } -impl Render for Renderer { +impl Writer { fn render_event<'s, W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result where W: std::fmt::Write, diff --git a/src/lib.rs b/src/lib.rs index dd0a126..73ede1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,11 +75,6 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// If ownership of the [`Event`]s cannot be given to the renderer, use [`Render::push_borrowed`] /// or [`Render::write_borrowed`]. /// -/// An implementor needs to at least implement the [`Render::render_event`] function that renders a -/// single event to the output. If anything needs to be rendered at the beginning or end of the -/// output, the [`Render::render_prologue`] and [`Render::render_epilogue`] can be implemented as -/// well. -/// /// # Examples /// /// Push to a [`String`] (implements [`std::fmt::Write`]): @@ -90,7 +85,7 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// # use jotdown::Render; /// # let events = std::iter::empty(); /// let mut output = String::new(); -/// let mut renderer = jotdown::html::Renderer::default(); +/// let renderer = jotdown::html::Renderer::default(); /// renderer.push(events, &mut output); /// # } /// ``` @@ -103,54 +98,22 @@ type CowStr<'s> = std::borrow::Cow<'s, str>; /// # use jotdown::Render; /// # let events = std::iter::empty(); /// let mut out = std::io::BufWriter::new(std::io::stdout()); -/// let mut renderer = jotdown::html::Renderer::default(); +/// let renderer = jotdown::html::Renderer::default(); /// renderer.write(events, &mut out).unwrap(); /// # } /// ``` pub trait Render { - /// Render a single event. - fn render_event<'s, W>(&mut self, e: &Event<'s>, out: W) -> std::fmt::Result - where - W: std::fmt::Write; - - /// Render something before any events have been provided. - /// - /// This does nothing by default, but an implementation may choose to prepend data at the - /// beginning of the output if needed. - fn render_prologue(&mut self, _out: W) -> std::fmt::Result - where - W: std::fmt::Write, - { - Ok(()) - } - - /// Render something after all events have been provided. - /// - /// This does nothing by default, but an implementation may choose to append extra data at the - /// end of the output if needed. - fn render_epilogue(&mut self, _out: W) -> std::fmt::Result - where - W: std::fmt::Write, - { - Ok(()) - } - /// Push owned [`Event`]s to a unicode-accepting buffer or stream. - fn push<'s, I, W>(&mut self, mut events: I, mut out: W) -> fmt::Result + fn push<'s, I, W>(&self, events: I, out: W) -> fmt::Result where I: Iterator>, - W: fmt::Write, - { - self.render_prologue(&mut out)?; - events.try_for_each(|e| self.render_event(&e, &mut out))?; - self.render_epilogue(&mut out) - } + W: fmt::Write; /// Write owned [`Event`]s to a byte sink, encoded as UTF-8. /// /// NOTE: This performs many small writes, so IO writes should be buffered with e.g. /// [`std::io::BufWriter`]. - fn write<'s, I, W>(&mut self, events: I, out: W) -> io::Result<()> + fn write<'s, I, W>(&self, events: I, out: W) -> io::Result<()> where I: Iterator>, W: io::Write, @@ -177,26 +140,21 @@ pub trait Render { /// # use jotdown::Render; /// # let events: &[jotdown::Event] = &[]; /// let mut output = String::new(); - /// let mut renderer = jotdown::html::Renderer::default(); + /// let renderer = jotdown::html::Renderer::default(); /// renderer.push_borrowed(events.iter(), &mut output); /// # } /// ``` - fn push_borrowed<'s, E, I, W>(&mut self, mut events: I, mut out: W) -> fmt::Result + fn push_borrowed<'s, E, I, W>(&self, events: I, out: W) -> fmt::Result where E: AsRef>, I: Iterator, - W: fmt::Write, - { - self.render_prologue(&mut out)?; - events.try_for_each(|e| self.render_event(e.as_ref(), &mut out))?; - self.render_epilogue(&mut out) - } + W: fmt::Write; /// Write borrowed [`Event`]s to a byte sink, encoded as UTF-8. /// /// NOTE: This performs many small writes, so IO writes should be buffered with e.g. /// [`std::io::BufWriter`]. - fn write_borrowed<'s, E, I, W>(&mut self, events: I, out: W) -> io::Result<()> + fn write_borrowed<'s, E, I, W>(&self, events: I, out: W) -> io::Result<()> where E: AsRef>, I: Iterator, diff --git a/src/main.rs b/src/main.rs index e73c081..d207dbe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,7 @@ fn run() -> Result<(), std::io::Error> { }; let parser = jotdown::Parser::new(&content); - let mut renderer = jotdown::html::Renderer::default(); + let renderer = jotdown::html::Renderer::default(); match app.output { Some(path) => renderer.write(parser, File::create(path)?)?, From 99f4691e521a55e0df9d07f796db0152f958be37 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 8 Apr 2023 11:52:58 +0200 Subject: [PATCH 8/8] lib: emit footnotes as they are encountered Previously, footnotes and their children events were skipped (stored in block tree) and inline parsed at the end. Now, they are emitted by the parser immediately and the responsibility to aggregate them has been moved to the renderer. resolves #31 --- src/html.rs | 170 ++++++++++++++++++++++++++++++++++++++-------------- src/lib.rs | 160 +++++++------------------------------------------ src/tree.rs | 104 -------------------------------- 3 files changed, 144 insertions(+), 290 deletions(-) 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 - }, - ] - ); - } }