From 6a681f99d08ba81062976931c463126d3745c08b Mon Sep 17 00:00:00 2001 From: digitalMoksha Date: Tue, 2 May 2023 20:36:34 -0500 Subject: [PATCH 1/6] Use footnote name for reference id --- src/cm.rs | 2 +- src/html.rs | 31 ++++++++++++++++--------------- src/nodes.rs | 4 ++-- src/parser/inlines.rs | 2 +- src/parser/mod.rs | 11 ++++++----- src/tests/api.rs | 2 +- src/xml.rs | 2 +- 7 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/cm.rs b/src/cm.rs index 1e6f4ed2..518fe8a8 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -356,7 +356,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { NodeValue::TableRow(..) => self.format_table_row(entering), NodeValue::TableCell => self.format_table_cell(node, entering), NodeValue::FootnoteDefinition(_) => self.format_footnote_definition(entering), - NodeValue::FootnoteReference(ref r) => { + NodeValue::FootnoteReference(ref r, _) => { self.format_footnote_reference(r.as_bytes(), entering) } }; diff --git a/src/html.rs b/src/html.rs index ab776210..80173dfe 100644 --- a/src/html.rs +++ b/src/html.rs @@ -701,13 +701,14 @@ impl<'o> HtmlFormatter<'o> { self.render_sourcepos(node)?; self.output.write_all(b">")?; } else { - if matches!( - node.parent().unwrap().data.borrow().value, - NodeValue::FootnoteDefinition(..) - ) && node.next_sibling().is_none() - { - self.output.write_all(b" ")?; - self.put_footnote_backref()?; + match &node.parent().unwrap().data.borrow().value { + NodeValue::FootnoteDefinition(name) => { + if node.next_sibling().is_none() { + self.output.write_all(b" ")?; + self.put_footnote_backref(&name)?; + } + } + _ => {} } self.output.write_all(b"

\n")?; } @@ -931,7 +932,7 @@ impl<'o> HtmlFormatter<'o> { self.output.write_all(b"")?; } } - NodeValue::FootnoteDefinition(_) => { + NodeValue::FootnoteDefinition(ref name) => { if entering { if self.footnote_ix == 0 { self.output.write_all(b" HtmlFormatter<'o> { self.footnote_ix += 1; self.output.write_all(b"", self.footnote_ix)?; + writeln!(self.output, " id=\"fn-{}\">", name)?; } else { - if self.put_footnote_backref()? { + if self.put_footnote_backref(name)? { self.output.write_all(b"\n")?; } self.output.write_all(b"\n")?; } } - NodeValue::FootnoteReference(ref r) => { + NodeValue::FootnoteReference(ref r, ix) => { if entering { self.output.write_all(b"{}", - r, r, r + r, r, ix )?; } } @@ -993,7 +994,7 @@ impl<'o> HtmlFormatter<'o> { Ok(()) } - fn put_footnote_backref(&mut self) -> io::Result { + fn put_footnote_backref(&mut self, name: &str) -> io::Result { if self.written_footnote_ix >= self.footnote_ix { return Ok(false); } @@ -1001,8 +1002,8 @@ impl<'o> HtmlFormatter<'o> { self.written_footnote_ix = self.footnote_ix; write!( self.output, - "", - self.footnote_ix + "", + name, self.footnote_ix, self.footnote_ix )?; Ok(true) } diff --git a/src/nodes.rs b/src/nodes.rs index 6d4c6ceb..a72fa039 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -146,7 +146,7 @@ pub enum NodeValue { Image(NodeLink), /// **Inline**. A footnote reference; the `String` is the referent footnote's name. - FootnoteReference(String), + FootnoteReference(String, u32), #[cfg(feature = "shortcodes")] /// **Inline**. An Emoji character generated from a shortcode. Enable with feature "shortcodes". @@ -422,7 +422,7 @@ impl NodeValue { NodeValue::FrontMatter(_) => "frontmatter", NodeValue::TaskItem { .. } => "taskitem", NodeValue::Superscript => "superscript", - NodeValue::FootnoteReference(_) => "footnote_reference", + NodeValue::FootnoteReference(_, _) => "footnote_reference", #[cfg(feature = "shortcodes")] NodeValue::ShortCode(_) => "shortcode", } diff --git a/src/parser/inlines.rs b/src/parser/inlines.rs index a08832e7..9e7e9218 100644 --- a/src/parser/inlines.rs +++ b/src/parser/inlines.rs @@ -1229,7 +1229,7 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> { let text = text.unwrap(); if text.len() > 1 && text.as_bytes()[0] == b'^' { let inl = self.make_inline( - NodeValue::FootnoteReference(text[1..].to_string()), + NodeValue::FootnoteReference(text[1..].to_string(), 0), // Overridden immediately below. self.pos, self.pos, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d2a08f65..7f2fadc8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -600,7 +600,8 @@ pub struct Reference { struct FootnoteDefinition<'a> { ix: Option, node: &'a AstNode<'a>, -} + name: String, + } impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { fn new( @@ -1761,7 +1762,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { if let Some(ix) = f.ix { match f.node.data.borrow_mut().value { NodeValue::FootnoteDefinition(ref mut name) => { - *name = format!("{}", ix); + *name = format!("{}", f.name); } _ => unreachable!(), } @@ -1780,7 +1781,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { node.detach(); map.insert( strings::normalize_label(name), - FootnoteDefinition { ix: None, node }, + FootnoteDefinition { ix: None, node, name: strings::normalize_label(name) }, ); } _ => { @@ -1799,7 +1800,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { let mut ast = node.data.borrow_mut(); let mut replace = None; match ast.value { - NodeValue::FootnoteReference(ref mut name) => { + NodeValue::FootnoteReference(ref mut name, ref mut index) => { if let Some(ref mut footnote) = map.get_mut(name) { let ix = match footnote.ix { Some(ix) => ix, @@ -1809,7 +1810,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { *ixp } }; - *name = format!("{}", ix); + *index = ix; } else { replace = Some(name.clone()); } diff --git a/src/tests/api.rs b/src/tests/api.rs index b12f8a56..f9d4d338 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -207,7 +207,7 @@ fn exercise_full_api() { nodes::NodeValue::ShortCode(ne) => { let _: &str = ne.shortcode(); } - nodes::NodeValue::FootnoteReference(name) => { + nodes::NodeValue::FootnoteReference(name, _) => { let _: &String = name; } } diff --git a/src/xml.rs b/src/xml.rs index 53045cbc..7da0dc3b 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -233,7 +233,7 @@ impl<'o> XmlFormatter<'o> { self.escape(fd.as_bytes())?; self.output.write_all(b"\"")?; } - NodeValue::FootnoteReference(ref fr) => { + NodeValue::FootnoteReference(ref fr, _) => { self.output.write_all(b" label=\"")?; self.escape(fr.as_bytes())?; self.output.write_all(b"\"")?; From 358de5d86dcfff5f5ff3292d8d285cbed285df1f Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Thu, 4 May 2023 12:22:54 +1000 Subject: [PATCH 2/6] nodes: add NodeFootnoteReference --- src/cm.rs | 4 ++-- src/html.rs | 4 ++-- src/nodes.rs | 16 +++++++++++++--- src/parser/inlines.rs | 7 +++++-- src/parser/mod.rs | 18 +++++++++++------- src/tests/api.rs | 5 +++-- src/xml.rs | 4 ++-- 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/cm.rs b/src/cm.rs index 518fe8a8..67abc2a8 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -356,8 +356,8 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { NodeValue::TableRow(..) => self.format_table_row(entering), NodeValue::TableCell => self.format_table_cell(node, entering), NodeValue::FootnoteDefinition(_) => self.format_footnote_definition(entering), - NodeValue::FootnoteReference(ref r, _) => { - self.format_footnote_reference(r.as_bytes(), entering) + NodeValue::FootnoteReference(ref nfr) => { + self.format_footnote_reference(nfr.name.as_bytes(), entering) } }; true diff --git a/src/html.rs b/src/html.rs index 80173dfe..a40fd18f 100644 --- a/src/html.rs +++ b/src/html.rs @@ -951,13 +951,13 @@ impl<'o> HtmlFormatter<'o> { self.output.write_all(b"\n")?; } } - NodeValue::FootnoteReference(ref r, ix) => { + NodeValue::FootnoteReference(ref nfr) => { if entering { self.output.write_all(b"{}", - r, r, ix + nfr.name, nfr.name, nfr.ix, )?; } } diff --git a/src/nodes.rs b/src/nodes.rs index a72fa039..959eaf1c 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -145,8 +145,8 @@ pub enum NodeValue { /// **Inline**. An [image](https://github.github.com/gfm/#images). Image(NodeLink), - /// **Inline**. A footnote reference; the `String` is the referent footnote's name. - FootnoteReference(String, u32), + /// **Inline**. A footnote reference. + FootnoteReference(NodeFootnoteReference), #[cfg(feature = "shortcodes")] /// **Inline**. An Emoji character generated from a shortcode. Enable with feature "shortcodes". @@ -329,6 +329,16 @@ pub struct NodeHtmlBlock { pub literal: String, } +/// The metadata of a footnote reference. +#[derive(Debug, Default, Clone)] +pub struct NodeFootnoteReference { + /// The name of the footnote. + pub name: String, + + /// The index of the footnote in the document. + pub ix: u32, +} + impl NodeValue { /// Indicates whether this node is a block node or inline node. pub fn block(&self) -> bool { @@ -422,7 +432,7 @@ impl NodeValue { NodeValue::FrontMatter(_) => "frontmatter", NodeValue::TaskItem { .. } => "taskitem", NodeValue::Superscript => "superscript", - NodeValue::FootnoteReference(_, _) => "footnote_reference", + NodeValue::FootnoteReference(..) => "footnote_reference", #[cfg(feature = "shortcodes")] NodeValue::ShortCode(_) => "shortcode", } diff --git a/src/parser/inlines.rs b/src/parser/inlines.rs index 9e7e9218..0ea66beb 100644 --- a/src/parser/inlines.rs +++ b/src/parser/inlines.rs @@ -1,7 +1,7 @@ use crate::arena_tree::Node; use crate::ctype::{ispunct, isspace}; use crate::entity; -use crate::nodes::{Ast, AstNode, NodeCode, NodeLink, NodeValue, Sourcepos}; +use crate::nodes::{Ast, AstNode, NodeCode, NodeFootnoteReference, NodeLink, NodeValue, Sourcepos}; #[cfg(feature = "shortcodes")] use crate::parser::shortcodes::NodeShortCode; use crate::parser::{ @@ -1229,7 +1229,10 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> { let text = text.unwrap(); if text.len() > 1 && text.as_bytes()[0] == b'^' { let inl = self.make_inline( - NodeValue::FootnoteReference(text[1..].to_string(), 0), + NodeValue::FootnoteReference(NodeFootnoteReference { + name: text[1..].to_string(), + ix: 0, + }), // Overridden immediately below. self.pos, self.pos, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7f2fadc8..2bd54786 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -601,7 +601,7 @@ struct FootnoteDefinition<'a> { ix: Option, node: &'a AstNode<'a>, name: String, - } +} impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { fn new( @@ -1759,7 +1759,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { let mut v = map.into_values().collect::>(); v.sort_unstable_by(|a, b| a.ix.cmp(&b.ix)); for f in v { - if let Some(ix) = f.ix { + if f.ix.is_some() { match f.node.data.borrow_mut().value { NodeValue::FootnoteDefinition(ref mut name) => { *name = format!("{}", f.name); @@ -1781,7 +1781,11 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { node.detach(); map.insert( strings::normalize_label(name), - FootnoteDefinition { ix: None, node, name: strings::normalize_label(name) }, + FootnoteDefinition { + ix: None, + node, + name: strings::normalize_label(name), + }, ); } _ => { @@ -1800,8 +1804,8 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { let mut ast = node.data.borrow_mut(); let mut replace = None; match ast.value { - NodeValue::FootnoteReference(ref mut name, ref mut index) => { - if let Some(ref mut footnote) = map.get_mut(name) { + NodeValue::FootnoteReference(ref mut nfr) => { + if let Some(ref mut footnote) = map.get_mut(&nfr.name) { let ix = match footnote.ix { Some(ix) => ix, None => { @@ -1810,9 +1814,9 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { *ixp } }; - *index = ix; + nfr.ix = ix; } else { - replace = Some(name.clone()); + replace = Some(nfr.name.clone()); } } _ => { diff --git a/src/tests/api.rs b/src/tests/api.rs index f9d4d338..af3f01c5 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -207,8 +207,9 @@ fn exercise_full_api() { nodes::NodeValue::ShortCode(ne) => { let _: &str = ne.shortcode(); } - nodes::NodeValue::FootnoteReference(name, _) => { - let _: &String = name; + nodes::NodeValue::FootnoteReference(nfr) => { + let _: String = nfr.name; + let _: u32 = nfr.ix; } } } diff --git a/src/xml.rs b/src/xml.rs index 7da0dc3b..092a9457 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -233,9 +233,9 @@ impl<'o> XmlFormatter<'o> { self.escape(fd.as_bytes())?; self.output.write_all(b"\"")?; } - NodeValue::FootnoteReference(ref fr, _) => { + NodeValue::FootnoteReference(ref nfr) => { self.output.write_all(b" label=\"")?; - self.escape(fr.as_bytes())?; + self.escape(nfr.name.as_bytes())?; self.output.write_all(b"\"")?; } NodeValue::TaskItem(Some(_)) => { From 3501ab0c19cadac78f425f7a0e59463a13bacf94 Mon Sep 17 00:00:00 2001 From: digitalMoksha Date: Thu, 11 May 2023 16:03:37 -0500 Subject: [PATCH 3/6] Fix footnote tests --- src/cm.rs | 7 +++---- src/parser/mod.rs | 2 +- src/tests/footnotes.rs | 39 +++++++++++++++++++-------------------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/cm.rs b/src/cm.rs index 67abc2a8..1d5f2c97 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -355,7 +355,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { NodeValue::Table(..) => self.format_table(entering), NodeValue::TableRow(..) => self.format_table_row(entering), NodeValue::TableCell => self.format_table_cell(node, entering), - NodeValue::FootnoteDefinition(_) => self.format_footnote_definition(entering), + NodeValue::FootnoteDefinition(ref name) => self.format_footnote_definition(name, entering), NodeValue::FootnoteReference(ref nfr) => { self.format_footnote_reference(nfr.name.as_bytes(), entering) } @@ -738,11 +738,10 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { } } } - fn format_footnote_definition(&mut self, entering: bool) { + fn format_footnote_definition(&mut self, name: &String, entering: bool) { if entering { self.footnote_ix += 1; - let footnote_ix = self.footnote_ix; - writeln!(self, "[^{}]:", footnote_ix).unwrap(); + writeln!(self, "[^{}]:", name).unwrap(); write!(self.prefix, " ").unwrap(); } else { let new_len = self.prefix.len() - 4; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2bd54786..2f0a270b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -256,7 +256,7 @@ pub struct ComrakExtensionOptions { /// let mut options = ComrakOptions::default(); /// options.extension.footnotes = true; /// assert_eq!(markdown_to_html("Hi[^x].\n\n[^x]: A greeting.\n", &options), - /// "

Hi1.

\n
\n
    \n
  1. \n

    A greeting.

    \n
  2. \n
\n
\n"); + /// "

Hi1.

\n
\n
    \n
  1. \n

    A greeting.

    \n
  2. \n
\n
\n"); /// ``` pub footnotes: bool, diff --git a/src/tests/footnotes.rs b/src/tests/footnotes.rs index 0420363e..f23c52a0 100644 --- a/src/tests/footnotes.rs +++ b/src/tests/footnotes.rs @@ -26,26 +26,26 @@ fn footnotes() { concat!( "

Here is a[^nowhere] footnote reference,1 and another.2

\n", - "

This is another note.3

\n", + href=\"#fn-longnote\" id=\"fnref-longnote\" data-footnote-ref>2

\n", + "

This is another note.3

\n", "

This is regular content.

\n", "
\n", "
    \n", "
  1. \n", "

    Here is the footnote.

    \n", + class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩

    \n", "
  2. \n", - "
  3. \n", + "
  4. \n", "

    Here's one with multiple blocks.

    \n", "

    Subsequent paragraphs are indented.

    \n", "
    code\n",
                 "
    \n", - "\n", + "\n", "
  5. \n", - "
  6. \n", - "

    Hi.

    \n", + "
  7. \n", + "

    Hi.

    \n", "
  8. \n", "
\n", "
\n" @@ -59,12 +59,11 @@ fn footnote_does_not_eat_exclamation() { [extension.footnotes], concat!("Here's my footnote![^a]\n", "\n", "[^a]: Yep.\n"), concat!( - "

Here's my footnote!1

\n", + "

Here's my footnote!1

\n", "
\n", "
    \n", - "
  1. \n", - "

    Yep.

    \n", + "
  2. \n", + "

    Yep.

    \n", "
  3. \n", "
\n", "
\n" @@ -103,7 +102,7 @@ fn footnote_in_table() { "
\n", "
    \n", "
  1. \n", - "

    a footnote

    \n", + "

    a footnote

    \n", "
  2. \n", "
\n", "
\n", @@ -127,18 +126,18 @@ fn footnote_with_superscript() { concat!( "

Here is a footnote reference.1

\n", - "

Here is a longer footnote reference.2

\n", + "

Here is a longer footnote reference.2

\n", "

e = mc2.

\n", "
\n", "
    \n", "
  1. \n", "

    Here is the footnote.

    \n", + class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩

    \n", "
  2. \n", - "
  3. \n", - "

    Here is another footnote.

    \n", + "
  4. \n", + "

    Here is another footnote.

    \n", "
  5. \n", "
\n", "
\n" From 1ab6c30031755f17dfff0e278074db3fda7237d9 Mon Sep 17 00:00:00 2001 From: digitalMoksha Date: Fri, 12 May 2023 19:37:08 -0500 Subject: [PATCH 4/6] Output information when a footnote is referenced multiple times --- src/cm.rs | 4 +++- src/html.rs | 46 +++++++++++++++++++++++++++++------------- src/nodes.rs | 15 +++++++++++++- src/parser/inlines.rs | 1 + src/parser/mod.rs | 22 +++++++++++++------- src/tests/api.rs | 5 +++-- src/tests/footnotes.rs | 14 +++++++------ src/xml.rs | 2 +- 8 files changed, 77 insertions(+), 32 deletions(-) diff --git a/src/cm.rs b/src/cm.rs index 1d5f2c97..1e448fbc 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -355,7 +355,9 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { NodeValue::Table(..) => self.format_table(entering), NodeValue::TableRow(..) => self.format_table_row(entering), NodeValue::TableCell => self.format_table_cell(node, entering), - NodeValue::FootnoteDefinition(ref name) => self.format_footnote_definition(name, entering), + NodeValue::FootnoteDefinition(ref nfd) => { + self.format_footnote_definition(&nfd.name, entering) + }, NodeValue::FootnoteReference(ref nfr) => { self.format_footnote_reference(nfr.name.as_bytes(), entering) } diff --git a/src/html.rs b/src/html.rs index a40fd18f..2a86f70a 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,6 +1,6 @@ //! The HTML renderer for the CommonMark AST, as well as helper functions. use crate::ctype::isspace; -use crate::nodes::{AstNode, ListType, NodeCode, NodeValue, TableAlignment}; +use crate::nodes::{AstNode, ListType, NodeCode, NodeFootnoteDefinition, NodeValue, TableAlignment}; use crate::parser::{ComrakOptions, ComrakPlugins}; use crate::scanners; use once_cell::sync::Lazy; @@ -702,10 +702,10 @@ impl<'o> HtmlFormatter<'o> { self.output.write_all(b">")?; } else { match &node.parent().unwrap().data.borrow().value { - NodeValue::FootnoteDefinition(name) => { + NodeValue::FootnoteDefinition(nfd) => { if node.next_sibling().is_none() { self.output.write_all(b" ")?; - self.put_footnote_backref(&name)?; + self.put_footnote_backref(&nfd)?; } } _ => {} @@ -932,7 +932,7 @@ impl<'o> HtmlFormatter<'o> { self.output.write_all(b"")?; } } - NodeValue::FootnoteDefinition(ref name) => { + NodeValue::FootnoteDefinition(ref nfd) => { if entering { if self.footnote_ix == 0 { self.output.write_all(b" HtmlFormatter<'o> { self.footnote_ix += 1; self.output.write_all(b"", name)?; + writeln!(self.output, " id=\"fn-{}\">", nfd.name)?; } else { - if self.put_footnote_backref(name)? { + if self.put_footnote_backref(&nfd)? { self.output.write_all(b"\n")?; } self.output.write_all(b"\n")?; @@ -953,11 +953,17 @@ impl<'o> HtmlFormatter<'o> { } NodeValue::FootnoteReference(ref nfr) => { if entering { + let mut ref_id = format!("fnref-{}", nfr.name); + self.output.write_all(b" 1 { + ref_id = format!("{}-{}", ref_id, nfr.ref_num); + } write!( - self.output, " class=\"footnote-ref\">{}", - nfr.name, nfr.name, nfr.ix, + self.output, " class=\"footnote-ref\">{}", + nfr.name, ref_id, nfr.ix, )?; } } @@ -994,17 +1000,29 @@ impl<'o> HtmlFormatter<'o> { Ok(()) } - fn put_footnote_backref(&mut self, name: &str) -> io::Result { + fn put_footnote_backref(&mut self, nfd: &NodeFootnoteDefinition) -> io::Result { if self.written_footnote_ix >= self.footnote_ix { return Ok(false); } self.written_footnote_ix = self.footnote_ix; - write!( - self.output, - "", - name, self.footnote_ix, self.footnote_ix - )?; + + let mut ref_suffix = String::new(); + let mut superscript = String::new(); + + for ref_num in 1..=nfd.total_references { + if ref_num > 1 { + ref_suffix = format!("-{}", ref_num); + superscript = format!("{}", ref_num); + write!(self.output, " ")?; + } + + write!( + self.output, + "↩{}", + nfd.name, ref_suffix, self.footnote_ix, ref_suffix, self.footnote_ix, ref_suffix, superscript + )?; + } Ok(true) } } diff --git a/src/nodes.rs b/src/nodes.rs index 959eaf1c..6856ee1f 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -88,7 +88,7 @@ pub enum NodeValue { /// **Block**. A footnote definition. The `String` is the footnote's name. /// Contains other **blocks**. - FootnoteDefinition(String), + FootnoteDefinition(NodeFootnoteDefinition), /// **Block**. A [table](https://github.github.com/gfm/#tables-extension-) per the GFM spec. /// Contains table rows. @@ -329,12 +329,25 @@ pub struct NodeHtmlBlock { pub literal: String, } +/// The metadata of a footnote definition. +#[derive(Debug, Default, Clone)] +pub struct NodeFootnoteDefinition { + /// The name of the footnote. + pub name: String, + + /// Total number of references to this footnote + pub total_references: u32, +} + /// The metadata of a footnote reference. #[derive(Debug, Default, Clone)] pub struct NodeFootnoteReference { /// The name of the footnote. pub name: String, + /// The index of reference to the same footnote + pub ref_num: u32, + /// The index of the footnote in the document. pub ix: u32, } diff --git a/src/parser/inlines.rs b/src/parser/inlines.rs index 0ea66beb..f56f82c6 100644 --- a/src/parser/inlines.rs +++ b/src/parser/inlines.rs @@ -1231,6 +1231,7 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> { let inl = self.make_inline( NodeValue::FootnoteReference(NodeFootnoteReference { name: text[1..].to_string(), + ref_num: 0, ix: 0, }), // Overridden immediately below. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2f0a270b..96701bb7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8,7 +8,7 @@ use crate::adapters::SyntaxHighlighterAdapter; use crate::arena_tree::Node; use crate::ctype::{isdigit, isspace}; use crate::entity; -use crate::nodes::{self, Sourcepos}; +use crate::nodes::{self, NodeFootnoteDefinition, Sourcepos}; use crate::nodes::{ Ast, AstNode, ListDelimType, ListType, NodeCodeBlock, NodeDescriptionItem, NodeHeading, NodeHtmlBlock, NodeList, NodeValue, @@ -601,6 +601,7 @@ struct FootnoteDefinition<'a> { ix: Option, node: &'a AstNode<'a>, name: String, + total_references: u32, } impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { @@ -1081,7 +1082,10 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { self.advance_offset(line, offset, false); *container = self.add_child( container, - NodeValue::FootnoteDefinition(str::from_utf8(c).unwrap().to_string()), + NodeValue::FootnoteDefinition(NodeFootnoteDefinition { + name: str::from_utf8(c).unwrap().to_string(), + total_references: 0, + }), self.first_nonspace + 1, ); container.data.borrow_mut().internal_offset = matched; @@ -1761,8 +1765,9 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { for f in v { if f.ix.is_some() { match f.node.data.borrow_mut().value { - NodeValue::FootnoteDefinition(ref mut name) => { - *name = format!("{}", f.name); + NodeValue::FootnoteDefinition(ref mut nfd) => { + nfd.name = format!("{}", f.name); + nfd.total_references = f.total_references; } _ => unreachable!(), } @@ -1777,14 +1782,15 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { map: &mut HashMap>, ) { match node.data.borrow().value { - NodeValue::FootnoteDefinition(ref name) => { + NodeValue::FootnoteDefinition(ref nfd) => { node.detach(); map.insert( - strings::normalize_label(name), + strings::normalize_label(&nfd.name), FootnoteDefinition { ix: None, node, - name: strings::normalize_label(name), + name: strings::normalize_label(&nfd.name), + total_references: 0, }, ); } @@ -1814,6 +1820,8 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { *ixp } }; + footnote.total_references += 1; + nfr.ref_num = footnote.total_references; nfr.ix = ix; } else { replace = Some(nfr.name.clone()); diff --git a/src/tests/api.rs b/src/tests/api.rs index af3f01c5..2267db42 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -164,8 +164,9 @@ fn exercise_full_api() { let _: bool = nh.setext; } nodes::NodeValue::ThematicBreak => {} - nodes::NodeValue::FootnoteDefinition(name) => { - let _: &String = name; + nodes::NodeValue::FootnoteDefinition(nfd) => { + let _: &String = &nfd.name; + let _: u32 = nfd.total_references; } nodes::NodeValue::Table(aligns) => { let _: &Vec = aligns; diff --git a/src/tests/footnotes.rs b/src/tests/footnotes.rs index f23c52a0..c16fa530 100644 --- a/src/tests/footnotes.rs +++ b/src/tests/footnotes.rs @@ -7,7 +7,7 @@ fn footnotes() { concat!( "Here is a[^nowhere] footnote reference,[^1] and another.[^longnote]\n", "\n", - "This is another note.[^note]\n", + "This is another note.[^note] And footnote[^longnote] is referenced again.\n", "\n", "[^note]: Hi.\n", "\n", @@ -27,8 +27,9 @@ fn footnotes() { "

Here is a[^nowhere] footnote reference,1 and another.2

\n", - "

This is another note.3

\n", + "

This is another note.3 And footnote2 is referenced again.

\n", "

This is regular content.

\n", "
\n", "
    \n", @@ -41,7 +42,8 @@ fn footnotes() { "

    Subsequent paragraphs are indented.

    \n", "
    code\n",
                 "
    \n", - "\n", + " \ + 2\n", "\n", "
  1. \n", "

    Hi. \n", "\n", "\n", - "foot 1\n", + "foot 1\n", "note\n", "\n", "\n", @@ -102,7 +104,7 @@ fn footnote_in_table() { "

    \n", "
      \n", "
    1. \n", - "

      a footnote

      \n", + "

      a footnote 2

      \n", "
    2. \n", "
    \n", "
    \n", diff --git a/src/xml.rs b/src/xml.rs index 092a9457..35f80400 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -230,7 +230,7 @@ impl<'o> XmlFormatter<'o> { } NodeValue::FootnoteDefinition(ref fd) => { self.output.write_all(b" label=\"")?; - self.escape(fd.as_bytes())?; + self.escape(fd.name.as_bytes())?; self.output.write_all(b"\"")?; } NodeValue::FootnoteReference(ref nfr) => { From 03ff9c9fc29d00f0ad0508f4205aeb6afca50a90 Mon Sep 17 00:00:00 2001 From: digitalMoksha Date: Fri, 12 May 2023 19:55:37 -0500 Subject: [PATCH 5/6] Fix clippy and rustfmt --- src/cm.rs | 2 +- src/html.rs | 19 ++++++++++--------- src/parser/mod.rs | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/cm.rs b/src/cm.rs index 1e448fbc..f27c4641 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -357,7 +357,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { NodeValue::TableCell => self.format_table_cell(node, entering), NodeValue::FootnoteDefinition(ref nfd) => { self.format_footnote_definition(&nfd.name, entering) - }, + } NodeValue::FootnoteReference(ref nfr) => { self.format_footnote_reference(nfr.name.as_bytes(), entering) } diff --git a/src/html.rs b/src/html.rs index 2a86f70a..0aa732bf 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,6 +1,8 @@ //! The HTML renderer for the CommonMark AST, as well as helper functions. use crate::ctype::isspace; -use crate::nodes::{AstNode, ListType, NodeCode, NodeFootnoteDefinition, NodeValue, TableAlignment}; +use crate::nodes::{ + AstNode, ListType, NodeCode, NodeFootnoteDefinition, NodeValue, TableAlignment, +}; use crate::parser::{ComrakOptions, ComrakPlugins}; use crate::scanners; use once_cell::sync::Lazy; @@ -701,14 +703,13 @@ impl<'o> HtmlFormatter<'o> { self.render_sourcepos(node)?; self.output.write_all(b">")?; } else { - match &node.parent().unwrap().data.borrow().value { - NodeValue::FootnoteDefinition(nfd) => { - if node.next_sibling().is_none() { - self.output.write_all(b" ")?; - self.put_footnote_backref(&nfd)?; - } + if let NodeValue::FootnoteDefinition(nfd) = + &node.parent().unwrap().data.borrow().value + { + if node.next_sibling().is_none() { + self.output.write_all(b" ")?; + self.put_footnote_backref(nfd)?; } - _ => {} } self.output.write_all(b"

    \n")?; } @@ -945,7 +946,7 @@ impl<'o> HtmlFormatter<'o> { self.render_sourcepos(node)?; writeln!(self.output, " id=\"fn-{}\">", nfd.name)?; } else { - if self.put_footnote_backref(&nfd)? { + if self.put_footnote_backref(nfd)? { self.output.write_all(b"\n")?; } self.output.write_all(b"
  2. \n")?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 96701bb7..4b309dd8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1766,7 +1766,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> { if f.ix.is_some() { match f.node.data.borrow_mut().value { NodeValue::FootnoteDefinition(ref mut nfd) => { - nfd.name = format!("{}", f.name); + nfd.name = f.name.to_string(); nfd.total_references = f.total_references; } _ => unreachable!(), From bf2069e30a12d04b7f429b499a54757e109c4e3d Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Fri, 19 May 2023 11:52:05 +1000 Subject: [PATCH 6/6] cm: &String -> &str --- src/cm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cm.rs b/src/cm.rs index f27c4641..7a3c7332 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -740,7 +740,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { } } } - fn format_footnote_definition(&mut self, name: &String, entering: bool) { + fn format_footnote_definition(&mut self, name: &str, entering: bool) { if entering { self.footnote_ix += 1; writeln!(self, "[^{}]:", name).unwrap();