Skip to content

Commit

Permalink
handle footnotes that contain footnotes or links
Browse files Browse the repository at this point in the history
Links can't contain other links or footnotes -- but footnotes can
contain either. Add tests and logic to handle it.

Resolves #98.

I found #188 (footnotes that cycle back to themselves) while writing
tests for this, but that's a special case, so I'm going to handle it
separately.
  • Loading branch information
yshavit authored Aug 26, 2024
1 parent 666adfa commit 09e5689
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 19 deletions.
100 changes: 88 additions & 12 deletions src/fmt_md_inlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MdElem>) {
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<MdElem>) {
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(&section.title);
self.find_references_in_footnote_elems(&section.body);
}
MdElem::Paragraph(para) => {
self.find_references_in_footnote_inlines(&para.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<I>(&mut self, text: I)
where
I: IntoIterator<Item = &'md Inline>,
{
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
}
}
}
Expand Down Expand Up @@ -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?
}
}

Expand Down
17 changes: 10 additions & 7 deletions tests/integ_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ impl<const N: usize> Case<N> {
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::<serde_json::Value>(&actual_out).unwrap(),
serde_json::from_str::<serde_json::Value>(self.expect_output).unwrap()
);
let (actual_out, expect_out) = if self.expect_output_json {
let actual_obj = serde_json::from_str::<serde_json::Value>(&actual_out).unwrap();
let expect_obj = serde_json::from_str::<serde_json::Value>(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);
}
}
Expand Down
104 changes: 104 additions & 0 deletions tests/md_cases/footnotes_in_footnotes.toml
Original file line number Diff line number Diff line change
@@ -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]
'''

0 comments on commit 09e5689

Please sign in to comment.