Skip to content

Commit

Permalink
separate footnote bodies into a doc-scoped context
Browse files Browse the repository at this point in the history
Rather than having the reference's definition (ie, body) directly into
the Inline element, separate it out into a new MdContext that gets
returned alongside the nodes. This forces us to add indirection when
looking up footnotes, which lets us handle cycles.

Resolves #188
  • Loading branch information
yshavit authored Sep 23, 2024
1 parent 09e5689 commit 9751a98
Show file tree
Hide file tree
Showing 10 changed files with 486 additions and 255 deletions.
204 changes: 110 additions & 94 deletions src/fmt_md.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,16 @@ pub enum ReferencePlacement {
Doc,
}

pub fn write_md<'md, I, W>(options: &'md MdOptions, out: &mut Output<W>, nodes: I)
pub fn write_md<'md, I, W>(options: &'md MdOptions, out: &mut Output<W>, ctx: &'md MdContext, nodes: I)
where
I: Iterator<Item = MdElemRef<'md>>,
W: SimpleWrite,
{
let mut writer_state = MdWriterState {
ctx,
opts: options,
prev_was_thematic_break: false,
inlines_writer: &mut MdInlinesWriter::new(options.inline_options),
inlines_writer: &mut MdInlinesWriter::new(ctx, options.inline_options),
};
let nodes_count = writer_state.write_md(out, nodes, true);

Expand All @@ -79,6 +80,7 @@ where
}

struct MdWriterState<'s, 'md> {
ctx: &'md MdContext,
opts: &'md MdOptions,
prev_was_thematic_break: bool,
inlines_writer: &'s mut MdInlinesWriter<'md>,
Expand Down Expand Up @@ -430,7 +432,7 @@ impl<'s, 'md> MdWriterState<'s, 'md> {

if matches!(which, DefinitionsToWrite::Links | DefinitionsToWrite::Both) {
let mut defs_to_write: Vec<_> = self.inlines_writer.drain_pending_links();
defs_to_write.sort_by_key(|(k, _)| k.get_sort_string());
defs_to_write.sort_by_key(|(k, _)| k.get_sort_string(self.ctx));

for (link_ref, link_def) in defs_to_write {
out.write_char('[');
Expand Down Expand Up @@ -667,7 +669,6 @@ pub mod tests {
}

mod block_quote {

use super::*;

#[test]
Expand Down Expand Up @@ -1790,14 +1791,15 @@ pub mod tests {

#[test]
fn single_line() {
check_render(
vec![
MdElem::Inline(Inline::Footnote(Footnote {
label: "a".to_string(),
text: md_elems!["Hello, world."],
})),
m_node!(MdElem::ThematicBreak),
],
check_render_with_ctx(
&MdOptions::default_for_tests(),
(
MdContext::empty().with("a", md_elems!["Hello, world."]),
vec![
MdElem::Inline(Inline::Footnote("a".into())),
m_node!(MdElem::ThematicBreak),
],
),
indoc! {r#"
[^a]
Expand All @@ -1809,14 +1811,15 @@ pub mod tests {

#[test]
fn two_lines() {
check_render(
vec![
MdElem::Inline(Inline::Footnote(Footnote {
label: "a".to_string(),
text: md_elems!["Hello,\nworld."],
})),
m_node!(MdElem::ThematicBreak),
],
check_render_with_ctx(
&MdOptions::default_for_tests(),
(
MdContext::empty().with("a", md_elems!["Hello,\nworld."]),
vec![
MdElem::Inline(Inline::Footnote("a".into())),
m_node!(MdElem::ThematicBreak),
],
),
indoc! {r#"
[^a]
Expand All @@ -1830,9 +1833,10 @@ pub mod tests {
/// see [crate::footnote_transform::test] for more extensive tests TODO need to add those tests!
#[test]
fn footnote_transform_smoke_test() {
check_render_refs_with(
let (ctx, graf) = footnote_a_in_paragraph();
check_render_refs_with_ctx(
&MdOptions::new_with(|mdo| mdo.inline_options.renumber_footnotes = true),
vec![MdElemRef::Paragraph(&footnote_a_in_paragraph())],
(ctx, vec![MdElemRef::Paragraph(&graf)]),
indoc! {r#"
[^1]
Expand All @@ -1842,51 +1846,55 @@ pub mod tests {

#[test]
fn footnote_no_transform_smoke_test() {
check_render_refs_with(
let (ctx, graf) = footnote_a_in_paragraph();
check_render_refs_with_ctx(
&MdOptions::new_with(|mdo| mdo.inline_options.renumber_footnotes = false),
vec![MdElemRef::Paragraph(&footnote_a_in_paragraph())],
(ctx, vec![MdElemRef::Paragraph(&graf)]),
indoc! {r#"
[^a]
[^a]: the footnote text"#},
);
}

fn footnote_a_in_paragraph() -> Paragraph {
Paragraph {
body: vec![Inline::Footnote(Footnote {
label: "a".to_string(),
text: md_elems!("the footnote text"),
})],
}
fn footnote_a_in_paragraph() -> (MdContext, Paragraph) {
(
MdContext::empty().with("a", md_elems!("the footnote text")),
Paragraph {
body: vec![Inline::Footnote("a".into())],
},
)
}
}

mod annotation_and_footnote_layouts {
use super::*;
use crate::md_elems;
use indoc::indoc;

#[test]
fn link_and_footnote() {
check_render(
md_elems![Paragraph {
body: vec![
mdq_inline!("Hello, "),
m_node!(Inline::Link {
text: vec![mdq_inline!("world"),],
link_definition: LinkDefinition {
url: "https://example.com".to_string(),
title: None,
reference: LinkReference::Full("1".to_string()),
}
}),
mdq_inline!("! This is interesting"),
Inline::Footnote(Footnote {
label: "a".to_string(),
text: md_elems!["this is my note"],
}),
mdq_inline!("."),
],
}],
check_render_with_ctx(
&MdOptions::default_for_tests(),
(
MdContext::empty().with("a", md_elems!["this is my note"]),
md_elems![Paragraph {
body: vec![
mdq_inline!("Hello, "),
m_node!(Inline::Link {
text: vec![mdq_inline!("world"),],
link_definition: LinkDefinition {
url: "https://example.com".to_string(),
title: None,
reference: LinkReference::Full("1".to_string()),
}
}),
mdq_inline!("! This is interesting"),
Inline::Footnote("a".into()),
mdq_inline!("."),
],
}],
),
indoc! {r#"
Hello, [world][1]! This is interesting[^a].
Expand All @@ -1897,7 +1905,7 @@ pub mod tests {

#[test]
fn both_in_sections() {
check_render_with(
check_render_with_ctx(
&MdOptions::new_with(|mdo| {
mdo.link_reference_placement = ReferencePlacement::Section;
mdo.footnote_reference_placement = ReferencePlacement::Section;
Expand All @@ -1921,7 +1929,7 @@ pub mod tests {

#[test]
fn only_link_in_section() {
check_render_with(
check_render_with_ctx(
&MdOptions::new_with(|mdo| {
mdo.link_reference_placement = ReferencePlacement::Section;
mdo.footnote_reference_placement = ReferencePlacement::Doc;
Expand Down Expand Up @@ -1972,7 +1980,7 @@ pub mod tests {

#[test]
fn only_footnote_in_section() {
check_render_with(
check_render_with_ctx(
&MdOptions::new_with(|mdo| {
mdo.link_reference_placement = ReferencePlacement::Doc;
mdo.footnote_reference_placement = ReferencePlacement::Section;
Expand All @@ -1999,7 +2007,7 @@ pub mod tests {

#[test]
fn both_bottom_of_doc() {
check_render_with(
check_render_with_ctx(
&MdOptions::new_with(|mdo| {
mdo.link_reference_placement = ReferencePlacement::Doc;
mdo.footnote_reference_placement = ReferencePlacement::Doc;
Expand All @@ -2025,40 +2033,39 @@ pub mod tests {

#[test]
fn ordering() {
check_render_with(
check_render_with_ctx(
&MdOptions::new_with(|mdo| {
mdo.link_reference_placement = ReferencePlacement::Doc;
mdo.footnote_reference_placement = ReferencePlacement::Doc;
}),
// Define them in the opposite order that we'd expect them
md_elems![Paragraph {
body: vec![
Inline::Footnote(Footnote {
label: "d".to_string(),
text: md_elems!["footnote 1"]
}),
Inline::Footnote(Footnote {
label: "c".to_string(),
text: md_elems!["footnote 2"]
}),
m_node!(Inline::Link {
text: vec![mdq_inline!("b-text")],
link_definition: LinkDefinition {
url: "https://example.com/b".to_string(),
title: None,
reference: LinkReference::Full("b".to_string()),
},
}),
m_node!(Inline::Link {
text: vec![mdq_inline!("a-text")],
link_definition: LinkDefinition {
url: "https://example.com/a".to_string(),
title: None,
reference: LinkReference::Full("a".to_string()),
},
}),
]
}],
(
MdContext::empty()
.with("d", md_elems!["footnote 1"])
.with("c", md_elems!["footnote 2"]),
// Define them in the opposite order that we'd expect them
md_elems![Paragraph {
body: vec![
Inline::Footnote("d".into()),
Inline::Footnote("c".into()),
m_node!(Inline::Link {
text: vec![mdq_inline!("b-text")],
link_definition: LinkDefinition {
url: "https://example.com/b".to_string(),
title: None,
reference: LinkReference::Full("b".to_string()),
},
}),
m_node!(Inline::Link {
text: vec![mdq_inline!("a-text")],
link_definition: LinkDefinition {
url: "https://example.com/a".to_string(),
title: None,
reference: LinkReference::Full("a".to_string()),
},
}),
]
}],
),
indoc! {r#"
[^d][^c][b-text][b][a-text][a]
Expand All @@ -2069,8 +2076,8 @@ pub mod tests {
);
}

fn link_and_footnote_markdown() -> Vec<MdElem> {
md_elems![
fn link_and_footnote_markdown() -> (MdContext, Vec<MdElem>) {
let elems = md_elems![
Section {
depth: 1,
title: vec![mdq_inline!("First section")],
Expand All @@ -2085,10 +2092,7 @@ pub mod tests {
},
}),
mdq_inline!(" and then a thought"),
Inline::Footnote(Footnote {
label: "a".to_string(),
text: md_elems!["the footnote"],
}),
Inline::Footnote("a".into()),
mdq_inline!("."),
],
}],
Expand All @@ -2098,7 +2102,9 @@ pub mod tests {
title: vec![mdq_inline!("Second section")],
body: md_elems!["Second section contents."],
},
]
];
let ctx = MdContext::empty().with("a", md_elems!["the footnote"]);
(ctx, elems)
}
}

Expand Down Expand Up @@ -2154,22 +2160,32 @@ pub mod tests {
}

fn check_render_with(options: &MdOptions, nodes: Vec<MdElem>, expect: &str) {
check_render_with_ctx(options, (MdContext::empty(), nodes), expect)
}

fn check_render_with_ctx(options: &MdOptions, inputs: (MdContext, Vec<MdElem>), expect: &str) {
let (ctx, nodes) = inputs;
let mut wrapped = Vec::with_capacity(nodes.len());
for node in &nodes {
wrapped.push(node.into());
}
check_render_refs_with(options, wrapped, expect)
check_render_refs_with_ctx(options, (ctx, wrapped), expect)
}

fn check_render_refs(nodes: Vec<MdElemRef>, expect: &str) {
check_render_refs_with(&MdOptions::default_for_tests(), nodes, expect)
}

fn check_render_refs_with(options: &MdOptions, nodes: Vec<MdElemRef>, expect: &str) {
check_render_refs_with_ctx(options, (MdContext::empty(), nodes), expect)
}

fn check_render_refs_with_ctx(options: &MdOptions, inputs: (MdContext, Vec<MdElemRef>), expect: &str) {
let (ctx, nodes) = inputs;
nodes.iter().for_each(|n| VARIANTS_CHECKER.see(n));

let mut out = Output::new(String::default());
write_md(options, &mut out, nodes.into_iter());
write_md(options, &mut out, &ctx, nodes.into_iter());
let actual = out.take_underlying().unwrap();
assert_eq!(&actual, expect);
}
Expand Down
Loading

0 comments on commit 9751a98

Please sign in to comment.