diff --git a/src/fmt_md_inlines.rs b/src/fmt_md_inlines.rs index 2cf5aeb..99aa19b 100644 --- a/src/fmt_md_inlines.rs +++ b/src/fmt_md_inlines.rs @@ -170,8 +170,80 @@ impl<'md> MdInlinesWriter<'md> { out.write_str("[^"); self.footnote_transformer.write(out, label); out.write_char(']'); - if self.seen_footnotes.insert(label) { - self.pending_references.footnotes.insert(label, text); + self.add_footnote(label, text); + } + } + } + + fn add_footnote(&mut self, label: &'md String, text: &'md Vec) { + if self.seen_footnotes.insert(label) { + self.pending_references.footnotes.insert(label, text); + self.find_references_in_footnote_elems(text); + } + } + + /// Searches the footnote's text to find any link references and additional footnotes. + /// Otherwise, by the time we see them it'll be too late to add them to their respective + /// collections. + fn find_references_in_footnote_elems(&mut self, text: &'md Vec) { + for elem in text { + match elem { + MdElem::BlockQuote(block) => { + self.find_references_in_footnote_elems(&block.body); + } + MdElem::List(list) => { + for li in &list.items { + self.find_references_in_footnote_elems(&li.item); + } + } + MdElem::Section(section) => { + self.find_references_in_footnote_inlines(§ion.title); + self.find_references_in_footnote_elems(§ion.body); + } + MdElem::Paragraph(para) => { + self.find_references_in_footnote_inlines(¶.body); + } + MdElem::Table(table) => { + for row in &table.rows { + for cell in row { + self.find_references_in_footnote_inlines(cell); + } + } + } + MdElem::Inline(inline) => { + self.find_references_in_footnote_inlines([inline]); // TODO do I need the array? + } + MdElem::CodeBlock(_) | MdElem::Html(_) | MdElem::ThematicBreak => { + // nothing + } + } + } + } + + fn find_references_in_footnote_inlines(&mut self, text: I) + where + I: IntoIterator, + { + for inline in text.into_iter() { + match inline { + Inline::Footnote(footnote) => { + self.add_footnote(&footnote.label, &footnote.text); + } + Inline::Formatting(item) => { + self.find_references_in_footnote_inlines(&item.children); + } + Inline::Link(link) => { + let link_label = match &link.link_definition.reference { + LinkReference::Inline => None, + LinkReference::Full(reference) => Some(LinkLabel::Text(Cow::Borrowed(reference))), + LinkReference::Collapsed | LinkReference::Shortcut => Some(LinkLabel::Inline(&link.text)), + }; + if let Some(label) = link_label { + self.add_link_reference(label, &link.link_definition); + } + } + Inline::Image(_) | Inline::Text(_) => { + // nothing } } } @@ -242,16 +314,20 @@ impl<'md> MdInlinesWriter<'md> { }; if let Some(reference_label) = reference_to_add { - if self.seen_links.insert(reference_label.clone()) { - self.pending_references.links.insert( - reference_label, - UrlAndTitle { - url: &link.url, - title: &link.title, - }, - ); - // else warn? - } + self.add_link_reference(reference_label, link); + } + } + + fn add_link_reference(&mut self, reference_label: LinkLabel<'md>, link: &'md LinkDefinition) { + if self.seen_links.insert(reference_label.clone()) { + self.pending_references.links.insert( + reference_label, + UrlAndTitle { + url: &link.url, + title: &link.title, + }, + ); + // else warn? } } diff --git a/tests/integ_test.rs b/tests/integ_test.rs index 145d797..055902d 100644 --- a/tests/integ_test.rs +++ b/tests/integ_test.rs @@ -14,14 +14,17 @@ impl Case { let all_cli_args = ["cmd"].iter().chain(&self.cli_args); let cli = mdq::cli::Cli::try_parse_from(all_cli_args).unwrap(); let (actual_success, actual_out) = mdq::run_in_memory(&cli, self.md); - if self.expect_output_json { - assert_eq!( - serde_json::from_str::(&actual_out).unwrap(), - serde_json::from_str::(self.expect_output).unwrap() - ); + let (actual_out, expect_out) = if self.expect_output_json { + let actual_obj = serde_json::from_str::(&actual_out).unwrap(); + let expect_obj = serde_json::from_str::(self.expect_output).unwrap(); + ( + serde_json::to_string_pretty(&actual_obj).unwrap(), + serde_json::to_string_pretty(&expect_obj).unwrap(), + ) } else { - assert_eq!(actual_out, self.expect_output); - } + (actual_out, self.expect_output.to_string()) + }; + assert_eq!(actual_out, expect_out); assert_eq!(actual_success, self.expect_success); } } diff --git a/tests/md_cases/footnotes_in_footnotes.toml b/tests/md_cases/footnotes_in_footnotes.toml new file mode 100644 index 0000000..69002d4 --- /dev/null +++ b/tests/md_cases/footnotes_in_footnotes.toml @@ -0,0 +1,104 @@ +[given] +md = ''' +- AAA: (footnotes in links don't work: see https://gist.github.com/yshavit/6af0a784e338dc32e66717aa6f495ffe ) +- BBB: footnote contains footnote[^2] +- CCC: footnote contains link[^3] + +[^1]: the link's footnote text +[^2]: this footnote contains[^a] a footnote +[^3]: this footnote contains a [link][3a] +[^a]: this is the footnote's footnote + +[3a]: https://example.com/3a +''' + +[chained] +needed = false + + +[expect."just footnote contains footnote"] +cli_args = ['- BBB'] +output = ''' +- BBB: footnote contains footnote[^1] + +[^1]: this footnote contains[^2] a footnote +[^2]: this is the footnote's footnote +''' + + +[expect."just footnote contains footnote: json"] +cli_args = ['- BBB | P: *', '--output', 'json'] +output_json = true +output = ''' +{ + "items": [ + { + "paragraph": "BBB: footnote contains footnote[^1]" + } + ], + "footnotes": { + "1": [ + { + "paragraph": "this footnote contains[^2] a footnote" + } + ], + "2": [ + { + "paragraph": "this is the footnote's footnote" + } + ] + } +} +''' + + +[expect."just footnote contains link"] +cli_args = ['- CCC'] +output = ''' +- CCC: footnote contains link[^1] + +[3a]: https://example.com/3a +[^1]: this footnote contains a [link][3a] +''' + + +[expect."just footnote contains link: json"] +cli_args = ['- CCC | P: *', '--output', 'json'] +output_json = true +output = ''' +{ + "items": [ + { + "paragraph": "CCC: footnote contains link[^1]" + } + ], + "footnotes": { + "1": [ + { + "paragraph": "this footnote contains a [link][3a]" + } + ] + }, + "links": { + "3a": { + "url": "https://example.com/3a" + } + } +} +''' + + +[expect."cyclic reference does't cause infinite loop"] +ignore = '#188' +# When ready to add this back in, add the following to the input markdown: +# +# - DDD: footnote contains cycle[^cycle] +# +# [^cycle]: this footnote references itself[^cycle] +# +cli_args = ['- DDD | P: *'] +output = ''' +- DDD: footnote contains cycle[^cycle] + +[^cycle]: this footnote references itself[^cycle] +'''