commit
e8f2002c69
8 changed files with 323 additions and 372 deletions
4
Makefile
4
Makefile
|
@ -29,7 +29,7 @@ check:
|
||||||
suite:
|
suite:
|
||||||
git submodule update --init modules/djot.js
|
git submodule update --init modules/djot.js
|
||||||
for f in $$(find modules/djot.js/test -name '*.test' | xargs basename -a); do \
|
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
|
done
|
||||||
(cd tests/suite && make)
|
(cd tests/suite && make)
|
||||||
cargo test --features suite suite::
|
cargo test --features suite suite::
|
||||||
|
@ -102,7 +102,7 @@ afl_tmin:
|
||||||
clean:
|
clean:
|
||||||
cargo clean
|
cargo clean
|
||||||
git submodule deinit -f --all
|
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)
|
(cd tests/suite && make clean)
|
||||||
rm -f tests/bench/*.dj
|
rm -f tests/bench/*.dj
|
||||||
(cd tests/bench && make clean)
|
(cd tests/bench && make clean)
|
||||||
|
|
24
bench/input/block_footnotes.dj
Normal file
24
bench/input/block_footnotes.dj
Normal file
|
@ -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.
|
238
src/html.rs
238
src/html.rs
|
@ -5,61 +5,93 @@ use crate::Container;
|
||||||
use crate::Event;
|
use crate::Event;
|
||||||
use crate::LinkType;
|
use crate::LinkType;
|
||||||
use crate::ListKind;
|
use crate::ListKind;
|
||||||
|
use crate::Map;
|
||||||
use crate::OrderedListNumbering::*;
|
use crate::OrderedListNumbering::*;
|
||||||
use crate::Render;
|
use crate::Render;
|
||||||
use crate::SpanLinkType;
|
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<Item = Event<'s>>,
|
||||||
|
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<Event<'s>>,
|
||||||
|
I: Iterator<Item = E>,
|
||||||
|
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 {
|
enum Raw {
|
||||||
None,
|
None,
|
||||||
Html,
|
Html,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Renderer {
|
impl Default for Raw {
|
||||||
raw: Raw,
|
|
||||||
img_alt_text: usize,
|
|
||||||
list_tightness: Vec<bool>,
|
|
||||||
encountered_footnote: bool,
|
|
||||||
footnote_number: Option<std::num::NonZeroUsize>,
|
|
||||||
first_line: bool,
|
|
||||||
close_para: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Renderer {
|
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self::None
|
||||||
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 {
|
#[derive(Default)]
|
||||||
fn render_event<'s, W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result
|
struct Writer<'s> {
|
||||||
|
raw: Raw,
|
||||||
|
img_alt_text: usize,
|
||||||
|
list_tightness: Vec<bool>,
|
||||||
|
not_first_line: bool,
|
||||||
|
ignore: bool,
|
||||||
|
footnotes: Footnotes<'s>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Writer<'s> {
|
||||||
|
fn render_event<W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result
|
||||||
where
|
where
|
||||||
W: std::fmt::Write,
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let close_para = self.close_para;
|
if matches!(&e, Event::Start(Container::LinkDefinition { .. }, ..)) {
|
||||||
if close_para {
|
self.ignore = true;
|
||||||
self.close_para = false;
|
return Ok(());
|
||||||
if !matches!(&e, Event::End(Container::Footnote { .. })) {
|
}
|
||||||
// no need to add href before para close
|
|
||||||
out.write_str("</p>")?;
|
if matches!(&e, Event::End(Container::LinkDefinition { .. })) {
|
||||||
}
|
self.ignore = false;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ignore {
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match e {
|
match e {
|
||||||
Event::Start(c, attrs) => {
|
Event::Start(c, attrs) => {
|
||||||
if c.is_block() && !self.first_line {
|
if c.is_block() && self.not_first_line {
|
||||||
out.write_char('\n')?;
|
out.write_char('\n')?;
|
||||||
}
|
}
|
||||||
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
|
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
|
||||||
|
@ -95,16 +127,7 @@ impl Render for Renderer {
|
||||||
}
|
}
|
||||||
Container::DescriptionList => out.write_str("<dl")?,
|
Container::DescriptionList => out.write_str("<dl")?,
|
||||||
Container::DescriptionDetails => out.write_str("<dd")?,
|
Container::DescriptionDetails => out.write_str("<dd")?,
|
||||||
Container::Footnote { number, .. } => {
|
Container::Footnote { .. } => unreachable!(),
|
||||||
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("<section role=\"doc-endnotes\">\n<hr>\n<ol>\n")?;
|
|
||||||
}
|
|
||||||
write!(out, "<li id=\"fn{}\">", number)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Container::Table => out.write_str("<table")?,
|
Container::Table => out.write_str("<table")?,
|
||||||
Container::TableRow { .. } => out.write_str("<tr")?,
|
Container::TableRow { .. } => out.write_str("<tr")?,
|
||||||
Container::Section { .. } => out.write_str("<section")?,
|
Container::Section { .. } => out.write_str("<section")?,
|
||||||
|
@ -158,6 +181,7 @@ impl Render for Renderer {
|
||||||
Container::Strong => out.write_str("<strong")?,
|
Container::Strong => out.write_str("<strong")?,
|
||||||
Container::Emphasis => out.write_str("<em")?,
|
Container::Emphasis => out.write_str("<em")?,
|
||||||
Container::Mark => out.write_str("<mark")?,
|
Container::Mark => out.write_str("<mark")?,
|
||||||
|
Container::LinkDefinition { .. } => return Ok(()),
|
||||||
}
|
}
|
||||||
|
|
||||||
for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") {
|
for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") {
|
||||||
|
@ -263,7 +287,7 @@ impl Render for Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::End(c) => {
|
Event::End(c) => {
|
||||||
if c.is_block_container() && !matches!(c, Container::Footnote { .. }) {
|
if c.is_block_container() {
|
||||||
out.write_char('\n')?;
|
out.write_char('\n')?;
|
||||||
}
|
}
|
||||||
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
|
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
|
||||||
|
@ -287,19 +311,7 @@ impl Render for Renderer {
|
||||||
}
|
}
|
||||||
Container::DescriptionList => out.write_str("</dl>")?,
|
Container::DescriptionList => out.write_str("</dl>")?,
|
||||||
Container::DescriptionDetails => out.write_str("</dd>")?,
|
Container::DescriptionDetails => out.write_str("</dd>")?,
|
||||||
Container::Footnote { number, .. } => {
|
Container::Footnote { .. } => unreachable!(),
|
||||||
if !close_para {
|
|
||||||
// create a new paragraph
|
|
||||||
out.write_str("\n<p>")?;
|
|
||||||
}
|
|
||||||
write!(
|
|
||||||
out,
|
|
||||||
r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a></p>"##,
|
|
||||||
number,
|
|
||||||
)?;
|
|
||||||
out.write_str("\n</li>")?;
|
|
||||||
self.footnote_number = None;
|
|
||||||
}
|
|
||||||
Container::Table => out.write_str("</table>")?,
|
Container::Table => out.write_str("</table>")?,
|
||||||
Container::TableRow { .. } => out.write_str("</tr>")?,
|
Container::TableRow { .. } => out.write_str("</tr>")?,
|
||||||
Container::Section { .. } => out.write_str("</section>")?,
|
Container::Section { .. } => out.write_str("</section>")?,
|
||||||
|
@ -308,10 +320,8 @@ impl Render for Renderer {
|
||||||
if matches!(self.list_tightness.last(), Some(true)) {
|
if matches!(self.list_tightness.last(), Some(true)) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if self.footnote_number.is_none() {
|
if !self.footnotes.in_epilogue() {
|
||||||
out.write_str("</p>")?;
|
out.write_str("</p>")?;
|
||||||
} else {
|
|
||||||
self.close_para = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
||||||
|
@ -350,6 +360,7 @@ impl Render for Renderer {
|
||||||
Container::Strong => out.write_str("</strong>")?,
|
Container::Strong => out.write_str("</strong>")?,
|
||||||
Container::Emphasis => out.write_str("</em>")?,
|
Container::Emphasis => out.write_str("</em>")?,
|
||||||
Container::Mark => out.write_str("</mark>")?,
|
Container::Mark => out.write_str("</mark>")?,
|
||||||
|
Container::LinkDefinition { .. } => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Str(s) => match self.raw {
|
Event::Str(s) => match self.raw {
|
||||||
|
@ -358,7 +369,8 @@ impl Render for Renderer {
|
||||||
Raw::Html => out.write_str(s)?,
|
Raw::Html => out.write_str(s)?,
|
||||||
Raw::Other => {}
|
Raw::Other => {}
|
||||||
},
|
},
|
||||||
Event::FootnoteReference(_tag, number) => {
|
Event::FootnoteReference(label) => {
|
||||||
|
let number = self.footnotes.reference(label);
|
||||||
if self.img_alt_text == 0 {
|
if self.img_alt_text == 0 {
|
||||||
write!(
|
write!(
|
||||||
out,
|
out,
|
||||||
|
@ -378,7 +390,7 @@ impl Render for Renderer {
|
||||||
Event::NonBreakingSpace => out.write_str(" ")?,
|
Event::NonBreakingSpace => out.write_str(" ")?,
|
||||||
Event::Hardbreak => out.write_str("<br>\n")?,
|
Event::Hardbreak => out.write_str("<br>\n")?,
|
||||||
Event::Softbreak => out.write_char('\n')?,
|
Event::Softbreak => out.write_char('\n')?,
|
||||||
Event::Escape | Event::Blankline => unreachable!("filtered out"),
|
Event::Escape | Event::Blankline => {}
|
||||||
Event::ThematicBreak(attrs) => {
|
Event::ThematicBreak(attrs) => {
|
||||||
out.write_str("\n<hr")?;
|
out.write_str("\n<hr")?;
|
||||||
for (a, v) in attrs.iter() {
|
for (a, v) in attrs.iter() {
|
||||||
|
@ -389,7 +401,7 @@ impl Render for Renderer {
|
||||||
out.write_str(">")?;
|
out.write_str(">")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.first_line = false;
|
self.not_first_line = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -398,9 +410,41 @@ impl Render for Renderer {
|
||||||
where
|
where
|
||||||
W: std::fmt::Write,
|
W: std::fmt::Write,
|
||||||
{
|
{
|
||||||
if self.encountered_footnote {
|
if self.footnotes.reference_encountered() {
|
||||||
|
out.write_str("\n<section role=\"doc-endnotes\">\n<hr>\n<ol>")?;
|
||||||
|
|
||||||
|
while let Some((number, events)) = self.footnotes.next() {
|
||||||
|
write!(out, "\n<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("\n<p>")?;
|
||||||
|
}
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a></p>"##,
|
||||||
|
number,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
out.write_str("\n</li>")?;
|
||||||
|
}
|
||||||
|
|
||||||
out.write_str("\n</ol>\n</section>")?;
|
out.write_str("\n</ol>\n</section>")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.write_char('\n')?;
|
out.write_char('\n')?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -445,3 +489,73 @@ where
|
||||||
}
|
}
|
||||||
out.write_str(s)
|
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<Event<'s>>)>,
|
||||||
|
/// Footnote references in the order they were first encountered.
|
||||||
|
references: Vec<&'s str>,
|
||||||
|
/// Events for each footnote.
|
||||||
|
events: Map<&'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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
259
src/lib.rs
259
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`]
|
/// If ownership of the [`Event`]s cannot be given to the renderer, use [`Render::push_borrowed`]
|
||||||
/// or [`Render::write_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
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// Push to a [`String`] (implements [`std::fmt::Write`]):
|
/// Push to a [`String`] (implements [`std::fmt::Write`]):
|
||||||
|
@ -90,7 +85,7 @@ type CowStr<'s> = std::borrow::Cow<'s, str>;
|
||||||
/// # use jotdown::Render;
|
/// # use jotdown::Render;
|
||||||
/// # let events = std::iter::empty();
|
/// # let events = std::iter::empty();
|
||||||
/// let mut output = String::new();
|
/// let mut output = String::new();
|
||||||
/// let mut renderer = jotdown::html::Renderer::default();
|
/// let renderer = jotdown::html::Renderer::default();
|
||||||
/// renderer.push(events, &mut output);
|
/// renderer.push(events, &mut output);
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -103,54 +98,22 @@ type CowStr<'s> = std::borrow::Cow<'s, str>;
|
||||||
/// # use jotdown::Render;
|
/// # use jotdown::Render;
|
||||||
/// # let events = std::iter::empty();
|
/// # let events = std::iter::empty();
|
||||||
/// let mut out = std::io::BufWriter::new(std::io::stdout());
|
/// 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();
|
/// renderer.write(events, &mut out).unwrap();
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait Render {
|
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<W>(&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<W>(&mut self, _out: W) -> std::fmt::Result
|
|
||||||
where
|
|
||||||
W: std::fmt::Write,
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push owned [`Event`]s to a unicode-accepting buffer or stream.
|
/// 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
|
where
|
||||||
I: Iterator<Item = Event<'s>>,
|
I: Iterator<Item = Event<'s>>,
|
||||||
W: fmt::Write,
|
W: fmt::Write;
|
||||||
{
|
|
||||||
self.render_prologue(&mut out)?;
|
|
||||||
events.try_for_each(|e| self.render_event(&e, &mut out))?;
|
|
||||||
self.render_epilogue(&mut out)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write owned [`Event`]s to a byte sink, encoded as UTF-8.
|
/// 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.
|
/// NOTE: This performs many small writes, so IO writes should be buffered with e.g.
|
||||||
/// [`std::io::BufWriter`].
|
/// [`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
|
where
|
||||||
I: Iterator<Item = Event<'s>>,
|
I: Iterator<Item = Event<'s>>,
|
||||||
W: io::Write,
|
W: io::Write,
|
||||||
|
@ -177,26 +140,21 @@ pub trait Render {
|
||||||
/// # use jotdown::Render;
|
/// # use jotdown::Render;
|
||||||
/// # let events: &[jotdown::Event] = &[];
|
/// # let events: &[jotdown::Event] = &[];
|
||||||
/// let mut output = String::new();
|
/// 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);
|
/// 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
|
where
|
||||||
E: AsRef<Event<'s>>,
|
E: AsRef<Event<'s>>,
|
||||||
I: Iterator<Item = E>,
|
I: Iterator<Item = E>,
|
||||||
W: fmt::Write,
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write borrowed [`Event`]s to a byte sink, encoded as UTF-8.
|
/// 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.
|
/// NOTE: This performs many small writes, so IO writes should be buffered with e.g.
|
||||||
/// [`std::io::BufWriter`].
|
/// [`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
|
where
|
||||||
E: AsRef<Event<'s>>,
|
E: AsRef<Event<'s>>,
|
||||||
I: Iterator<Item = E>,
|
I: Iterator<Item = E>,
|
||||||
|
@ -251,7 +209,7 @@ pub enum Event<'s> {
|
||||||
/// A string object, text only.
|
/// A string object, text only.
|
||||||
Str(CowStr<'s>),
|
Str(CowStr<'s>),
|
||||||
/// A footnote reference.
|
/// A footnote reference.
|
||||||
FootnoteReference(&'s str, usize),
|
FootnoteReference(&'s str),
|
||||||
/// A symbol, by default rendered literally but may be treated specially.
|
/// A symbol, by default rendered literally but may be treated specially.
|
||||||
Symbol(CowStr<'s>),
|
Symbol(CowStr<'s>),
|
||||||
/// Left single quotation mark.
|
/// Left single quotation mark.
|
||||||
|
@ -304,7 +262,7 @@ pub enum Container<'s> {
|
||||||
/// Details describing a term within a description list.
|
/// Details describing a term within a description list.
|
||||||
DescriptionDetails,
|
DescriptionDetails,
|
||||||
/// A footnote definition.
|
/// A footnote definition.
|
||||||
Footnote { tag: &'s str, number: usize },
|
Footnote { label: &'s str },
|
||||||
/// A table element.
|
/// A table element.
|
||||||
Table,
|
Table,
|
||||||
/// A row element of a table.
|
/// A row element of a table.
|
||||||
|
@ -327,6 +285,8 @@ pub enum Container<'s> {
|
||||||
Caption,
|
Caption,
|
||||||
/// A term within a description list.
|
/// A term within a description list.
|
||||||
DescriptionTerm,
|
DescriptionTerm,
|
||||||
|
/// A link definition.
|
||||||
|
LinkDefinition { label: &'s str },
|
||||||
/// A block with raw markup for a specific output format.
|
/// A block with raw markup for a specific output format.
|
||||||
RawBlock { format: &'s str },
|
RawBlock { format: &'s str },
|
||||||
/// A block with code in a specific language.
|
/// A block with code in a specific language.
|
||||||
|
@ -381,6 +341,7 @@ impl<'s> Container<'s> {
|
||||||
| Self::TableCell { .. }
|
| Self::TableCell { .. }
|
||||||
| Self::Caption
|
| Self::Caption
|
||||||
| Self::DescriptionTerm
|
| Self::DescriptionTerm
|
||||||
|
| Self::LinkDefinition { .. }
|
||||||
| Self::RawBlock { .. }
|
| Self::RawBlock { .. }
|
||||||
| Self::CodeBlock { .. } => true,
|
| Self::CodeBlock { .. } => true,
|
||||||
Self::Span
|
Self::Span
|
||||||
|
@ -419,6 +380,7 @@ impl<'s> Container<'s> {
|
||||||
| Self::TableCell { .. }
|
| Self::TableCell { .. }
|
||||||
| Self::Caption
|
| Self::Caption
|
||||||
| Self::DescriptionTerm
|
| Self::DescriptionTerm
|
||||||
|
| Self::LinkDefinition { .. }
|
||||||
| Self::RawBlock { .. }
|
| Self::RawBlock { .. }
|
||||||
| Self::CodeBlock { .. }
|
| Self::CodeBlock { .. }
|
||||||
| Self::Span
|
| Self::Span
|
||||||
|
@ -607,15 +569,6 @@ pub struct Parser<'s> {
|
||||||
/// Currently within a verbatim code block.
|
/// Currently within a verbatim code block.
|
||||||
verbatim: bool,
|
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: inline::Parser<'s>,
|
inline_parser: inline::Parser<'s>,
|
||||||
}
|
}
|
||||||
|
@ -793,10 +746,6 @@ impl<'s> Parser<'s> {
|
||||||
block_attributes: Attributes::new(),
|
block_attributes: Attributes::new(),
|
||||||
table_head_row: false,
|
table_head_row: false,
|
||||||
verbatim: false,
|
verbatim: false,
|
||||||
footnote_references: Vec::new(),
|
|
||||||
footnotes: Map::new(),
|
|
||||||
footnote_index: 0,
|
|
||||||
footnote_active: false,
|
|
||||||
inline_parser,
|
inline_parser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -885,19 +834,7 @@ impl<'s> Parser<'s> {
|
||||||
}
|
}
|
||||||
inline::EventKind::Atom(a) => match a {
|
inline::EventKind::Atom(a) => match a {
|
||||||
inline::Atom::FootnoteReference => {
|
inline::Atom::FootnoteReference => {
|
||||||
let tag = inline.span.of(self.src);
|
Event::FootnoteReference(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)
|
|
||||||
}
|
}
|
||||||
inline::Atom::Symbol => Event::Symbol(inline.span.of(self.src).into()),
|
inline::Atom::Symbol => Event::Symbol(inline.span.of(self.src).into()),
|
||||||
inline::Atom::Quote { ty, left } => match (ty, left) {
|
inline::Atom::Quote { ty, left } => match (ty, left) {
|
||||||
|
@ -941,14 +878,6 @@ impl<'s> Parser<'s> {
|
||||||
let cont = match c {
|
let cont = match c {
|
||||||
block::Node::Leaf(l) => {
|
block::Node::Leaf(l) => {
|
||||||
self.inline_parser.reset();
|
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 {
|
match l {
|
||||||
block::Leaf::Paragraph => Container::Paragraph,
|
block::Leaf::Paragraph => Container::Paragraph,
|
||||||
block::Leaf::Heading { has_section } => Container::Heading {
|
block::Leaf::Heading { has_section } => Container::Heading {
|
||||||
|
@ -977,7 +906,9 @@ impl<'s> Parser<'s> {
|
||||||
head: self.table_head_row,
|
head: self.table_head_row,
|
||||||
},
|
},
|
||||||
block::Leaf::Caption => Container::Caption,
|
block::Leaf::Caption => Container::Caption,
|
||||||
block::Leaf::LinkDefinition => unreachable!(),
|
block::Leaf::LinkDefinition => {
|
||||||
|
Container::LinkDefinition { label: content }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
block::Node::Container(c) => match c {
|
block::Node::Container(c) => match c {
|
||||||
|
@ -985,12 +916,7 @@ impl<'s> Parser<'s> {
|
||||||
block::Container::Div { .. } => Container::Div {
|
block::Container::Div { .. } => Container::Div {
|
||||||
class: (!ev.span.is_empty()).then(|| content),
|
class: (!ev.span.is_empty()).then(|| content),
|
||||||
},
|
},
|
||||||
block::Container::Footnote => {
|
block::Container::Footnote => Container::Footnote { label: content },
|
||||||
debug_assert!(enter);
|
|
||||||
self.footnotes.insert(content, self.tree.take_branch());
|
|
||||||
self.block_attributes = Attributes::new();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
block::Container::List(block::ListKind { ty, tight }) => {
|
block::Container::List(block::ListKind { ty, tight }) => {
|
||||||
if matches!(ty, block::ListType::Description) {
|
if matches!(ty, block::ListType::Description) {
|
||||||
Container::DescriptionList
|
Container::DescriptionList
|
||||||
|
@ -1057,43 +983,13 @@ impl<'s> Parser<'s> {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn footnote(&mut self) -> Option<Event<'s>> {
|
|
||||||
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> {
|
impl<'s> Iterator for Parser<'s> {
|
||||||
type Item = Event<'s>;
|
type Item = Event<'s>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
self.inline()
|
self.inline().or_else(|| self.block())
|
||||||
.or_else(|| self.block())
|
|
||||||
.or_else(|| self.footnote())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1418,6 +1314,9 @@ mod test {
|
||||||
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
|
Start(LinkDefinition { label: "tag" }, Attributes::new()),
|
||||||
|
Str("url".into()),
|
||||||
|
End(LinkDefinition { label: "tag" }),
|
||||||
);
|
);
|
||||||
test_parse!(
|
test_parse!(
|
||||||
concat!(
|
concat!(
|
||||||
|
@ -1434,6 +1333,9 @@ mod test {
|
||||||
End(Image("url".into(), SpanLinkType::Reference)),
|
End(Image("url".into(), SpanLinkType::Reference)),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
|
Start(LinkDefinition { label: "tag" }, Attributes::new()),
|
||||||
|
Str("url".into()),
|
||||||
|
End(LinkDefinition { label: "tag" }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1483,6 +1385,9 @@ mod test {
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
End(Blockquote),
|
End(Blockquote),
|
||||||
Blankline,
|
Blankline,
|
||||||
|
Start(LinkDefinition { label: "a b" }, Attributes::new()),
|
||||||
|
Str("url".into()),
|
||||||
|
End(LinkDefinition { label: "a b" }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1504,6 +1409,11 @@ mod test {
|
||||||
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
|
Start(LinkDefinition { label: "tag" }, Attributes::new()),
|
||||||
|
Str("u".into()),
|
||||||
|
Softbreak,
|
||||||
|
Str("rl".into()),
|
||||||
|
End(LinkDefinition { label: "tag" }),
|
||||||
);
|
);
|
||||||
test_parse!(
|
test_parse!(
|
||||||
concat!(
|
concat!(
|
||||||
|
@ -1521,6 +1431,9 @@ mod test {
|
||||||
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
|
Start(LinkDefinition { label: "tag" }, Attributes::new()),
|
||||||
|
Str("url".into()),
|
||||||
|
End(LinkDefinition { label: "tag" }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1543,6 +1456,12 @@ mod test {
|
||||||
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
|
Start(
|
||||||
|
LinkDefinition { label: "tag" },
|
||||||
|
[("a", "b")].into_iter().collect()
|
||||||
|
),
|
||||||
|
Str("url".into()),
|
||||||
|
End(LinkDefinition { label: "tag" }),
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
Str("para".into()),
|
Str("para".into()),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
|
@ -1584,43 +1503,10 @@ mod test {
|
||||||
test_parse!(
|
test_parse!(
|
||||||
"[^a][^b][^c]",
|
"[^a][^b][^c]",
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
FootnoteReference("a", 1),
|
FootnoteReference("a"),
|
||||||
FootnoteReference("b", 2),
|
FootnoteReference("b"),
|
||||||
FootnoteReference("c", 3),
|
FootnoteReference("c"),
|
||||||
End(Paragraph),
|
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
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1629,23 +1515,14 @@ mod test {
|
||||||
test_parse!(
|
test_parse!(
|
||||||
"[^a]\n\n[^a]: a\n",
|
"[^a]\n\n[^a]: a\n",
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
FootnoteReference("a", 1),
|
FootnoteReference("a"),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
Start(
|
Start(Footnote { label: "a" }, Attributes::new()),
|
||||||
Footnote {
|
|
||||||
tag: "a",
|
|
||||||
number: 1
|
|
||||||
},
|
|
||||||
Attributes::new()
|
|
||||||
),
|
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
Str("a".into()),
|
Str("a".into()),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
End(Footnote {
|
End(Footnote { label: "a" }),
|
||||||
tag: "a",
|
|
||||||
number: 1
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1660,16 +1537,10 @@ mod test {
|
||||||
" def", //
|
" def", //
|
||||||
),
|
),
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
FootnoteReference("a", 1),
|
FootnoteReference("a"),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
Start(
|
Start(Footnote { label: "a" }, Attributes::new()),
|
||||||
Footnote {
|
|
||||||
tag: "a",
|
|
||||||
number: 1
|
|
||||||
},
|
|
||||||
Attributes::new()
|
|
||||||
),
|
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
Str("abc".into()),
|
Str("abc".into()),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
|
@ -1677,10 +1548,7 @@ mod test {
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
Str("def".into()),
|
Str("def".into()),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
End(Footnote {
|
End(Footnote { label: "a" }),
|
||||||
tag: "a",
|
|
||||||
number: 1
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1694,26 +1562,17 @@ mod test {
|
||||||
"para\n", //
|
"para\n", //
|
||||||
),
|
),
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
FootnoteReference("a", 1),
|
FootnoteReference("a"),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
Blankline,
|
Blankline,
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Footnote { label: "a" }, Attributes::new()),
|
||||||
Str("para".into()),
|
|
||||||
End(Paragraph),
|
|
||||||
Start(
|
|
||||||
Footnote {
|
|
||||||
tag: "a",
|
|
||||||
number: 1
|
|
||||||
},
|
|
||||||
Attributes::new()
|
|
||||||
),
|
|
||||||
Start(Paragraph, Attributes::new()),
|
Start(Paragraph, Attributes::new()),
|
||||||
Str("note".into()),
|
Str("note".into()),
|
||||||
End(Paragraph),
|
End(Paragraph),
|
||||||
End(Footnote {
|
End(Footnote { label: "a" }),
|
||||||
tag: "a",
|
Start(Paragraph, Attributes::new()),
|
||||||
number: 1
|
Str("para".into()),
|
||||||
}),
|
End(Paragraph),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ fn run() -> Result<(), std::io::Error> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let parser = jotdown::Parser::new(&content);
|
let parser = jotdown::Parser::new(&content);
|
||||||
let mut renderer = jotdown::html::Renderer::default();
|
let renderer = jotdown::html::Renderer::default();
|
||||||
|
|
||||||
match app.output {
|
match app.output {
|
||||||
Some(path) => renderer.write(parser, File::create(path)?)?,
|
Some(path) => renderer.write(parser, File::create(path)?)?,
|
||||||
|
|
104
src/tree.rs
104
src/tree.rs
|
@ -36,14 +36,6 @@ pub struct Tree<C: 'static, A: 'static> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Clone, A: Clone> Tree<C, A> {
|
impl<C: Clone, A: Clone> Tree<C, A> {
|
||||||
pub fn empty() -> Self {
|
|
||||||
Self {
|
|
||||||
nodes: vec![].into_boxed_slice().into(),
|
|
||||||
branch: Vec::new(),
|
|
||||||
head: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count number of direct children nodes.
|
/// Count number of direct children nodes.
|
||||||
pub fn count_children(&self) -> usize {
|
pub fn count_children(&self) -> usize {
|
||||||
let mut head = self.head;
|
let mut head = self.head;
|
||||||
|
@ -56,22 +48,6 @@ impl<C: Clone, A: Clone> Tree<C, A> {
|
||||||
count
|
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
|
/// Retrieve all inlines until the end of the current container. Panics if any upcoming node is
|
||||||
/// not an inline node.
|
/// not an inline node.
|
||||||
pub fn take_inlines(&mut self) -> impl Iterator<Item = Span> + '_ {
|
pub fn take_inlines(&mut self) -> impl Iterator<Item = Span> + '_ {
|
||||||
|
@ -410,9 +386,6 @@ impl<C: std::fmt::Debug + Clone, A: std::fmt::Debug + Clone> std::fmt::Debug for
|
||||||
mod test {
|
mod test {
|
||||||
use crate::Span;
|
use crate::Span;
|
||||||
|
|
||||||
use super::Event;
|
|
||||||
use super::EventKind;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fmt() {
|
fn fmt() {
|
||||||
let mut tree = super::Builder::new();
|
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::<Vec<_>>(),
|
|
||||||
&[
|
|
||||||
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::<Vec<_>>(),
|
|
||||||
&[
|
|
||||||
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::<Vec<_>>(),
|
|
||||||
&[
|
|
||||||
Event {
|
|
||||||
kind: EventKind::Enter(3),
|
|
||||||
span: sp
|
|
||||||
},
|
|
||||||
Event {
|
|
||||||
kind: EventKind::Atom(31),
|
|
||||||
span: sp
|
|
||||||
},
|
|
||||||
Event {
|
|
||||||
kind: EventKind::Exit(3),
|
|
||||||
span: sp
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
TEST=$(shell find . -name '*.test' | sort)
|
TEST=$(shell find . -name '*.test' | sort)
|
||||||
TEST_RS=${TEST:.test=.rs}
|
TEST_RS=${TEST:.test=.rs}
|
||||||
|
|
||||||
BLACKLIST += filters # lua filters not implemented
|
BLACKLIST += djot_js_filters # lua filters not implemented
|
||||||
BLACKLIST += symb # uses ast
|
BLACKLIST += djot_js_symb # uses ast
|
||||||
BLACKLIST += sourcepos # not parsable
|
BLACKLIST += djot_js_sourcepos # not parsable
|
||||||
|
|
||||||
.PHONY: suite
|
.PHONY: suite
|
||||||
suite: mod.rs
|
suite: mod.rs
|
||||||
|
|
58
tests/suite/footnotes.test
Normal file
58
tests/suite/footnotes.test
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
Footnote references may appear within a footnote.
|
||||||
|
|
||||||
|
```
|
||||||
|
[^a]
|
||||||
|
|
||||||
|
[^a]: a[^b][^c]
|
||||||
|
[^b]: b
|
||||||
|
.
|
||||||
|
<p><a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
|
||||||
|
<section role="doc-endnotes">
|
||||||
|
<hr>
|
||||||
|
<ol>
|
||||||
|
<li id="fn1">
|
||||||
|
<p>a<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a><a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a><a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
|
||||||
|
</li>
|
||||||
|
<li id="fn2">
|
||||||
|
<p>b<a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
|
||||||
|
</li>
|
||||||
|
<li id="fn3">
|
||||||
|
<p><a href="#fnref3" role="doc-backlink">↩︎︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
Footnote references in unreferenced footnotes are ignored.
|
||||||
|
|
||||||
|
```
|
||||||
|
para
|
||||||
|
|
||||||
|
[^a]: a[^b][^c]
|
||||||
|
[^b]: b
|
||||||
|
.
|
||||||
|
<p>para</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Footnotes may appear within footnotes.
|
||||||
|
|
||||||
|
```
|
||||||
|
[^b]
|
||||||
|
[^a]
|
||||||
|
|
||||||
|
[^a]: [^b]: inner
|
||||||
|
.
|
||||||
|
<p><a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>
|
||||||
|
<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a></p>
|
||||||
|
<section role="doc-endnotes">
|
||||||
|
<hr>
|
||||||
|
<ol>
|
||||||
|
<li id="fn1">
|
||||||
|
<p>inner<a href="#fnref1" role="doc-backlink">↩︎︎</a></p>
|
||||||
|
</li>
|
||||||
|
<li id="fn2">
|
||||||
|
<p><a href="#fnref2" role="doc-backlink">↩︎︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
```
|
Loading…
Reference in a new issue