use text_document::{PlainTextExportOptions, TextDocument};
fn doc_from(markdown: &str) -> TextDocument {
let doc = TextDocument::new();
doc.set_markdown(markdown)
.expect("import")
.wait()
.expect("import");
doc
}
#[test]
fn a_reference_with_no_definition_survives() {
let doc = doc_from("Text with a note[^solo] in it.\n");
let out = doc.to_markdown().expect("export");
assert!(
out.contains("[^solo]"),
"the reference must survive with no definition present, got {out:?}"
);
}
#[test]
fn a_reference_and_its_definition_both_survive() {
let doc = doc_from("Prose[^n1] here.\n\n[^n1]: The note body.\n");
let out = doc.to_markdown().expect("export");
assert!(out.contains("[^n1]"), "reference lost: {out:?}");
assert!(out.contains("[^n1]:"), "definition lost: {out:?}");
assert!(out.contains("The note body"), "note body lost: {out:?}");
}
#[test]
fn the_footnote_round_trip_is_a_fixpoint() {
for seed in [
"A note[^a] here.\n",
"Prose[^a] and more[^b].\n\n[^a]: First.\n\n[^b]: Second.\n",
"Before[^only] after.\n\n[^only]: Body.\n",
] {
let once = doc_from(seed).to_markdown().expect("export");
let twice = doc_from(&once).to_markdown().expect("re-export");
assert_eq!(once, twice, "not a fixpoint for {seed:?}");
}
}
#[test]
fn a_definition_before_its_reference_still_resolves() {
let doc = doc_from("[^n1]: The note body.\n\nProse[^n1] here.\n");
let out = doc.to_markdown().expect("export");
assert!(out.contains("[^n1]"), "reference lost: {out:?}");
assert!(out.contains("The note body"), "note body lost: {out:?}");
}
#[test]
fn a_multi_paragraph_definition_survives() {
let doc = doc_from("Prose[^n1] here.\n\n[^n1]: First paragraph.\n\n Second paragraph.\n");
let out = doc.to_markdown().expect("export");
assert!(
out.contains("First paragraph"),
"first paragraph of the note lost: {out:?}"
);
assert!(
out.contains("Second paragraph"),
"second paragraph of the note lost: {out:?}"
);
}
#[test]
fn a_reference_costs_exactly_one_character() {
let without = doc_from("ab cd\n");
let with = doc_from("ab[^n] cd\n");
assert_eq!(
with.character_count() - without.character_count(),
1,
"a reference must cost one character, no more and no less"
);
}
#[test]
fn the_addressable_view_counts_the_reference_too() {
let doc = doc_from("ab[^n] cd\n\n[^n]: A note.\n");
let addressable = doc
.to_plain_text_with(PlainTextExportOptions::addressable())
.expect("addressable");
assert_eq!(
addressable.chars().count(),
doc.character_count(),
"the addressable view and the document disagree about length: \
{addressable:?} vs {} chars",
doc.character_count()
);
}
#[test]
fn a_parsed_reference_is_superscript() {
use text_document::CharVerticalAlignment::SuperScript;
let doc = doc_from("Prose[^n1] here.\n");
let raised = doc
.flow()
.iter()
.filter_map(|e| match e {
text_document::FlowElement::Block(b) => Some(b),
_ => None,
})
.flat_map(|b| b.fragments())
.find_map(|f| match f {
text_document::FragmentContent::FootnoteReference { format, .. } => {
Some(format.vertical_alignment == Some(SuperScript))
}
_ => None,
});
assert_eq!(
raised,
Some(true),
"a Markdown-parsed reference must be superscript"
);
}
#[test]
fn references_are_reportable_by_position_and_label() {
let doc = doc_from("One[^a] two[^b] three.\n");
let refs = doc.footnote_references();
assert_eq!(
refs.iter().map(|(_, l)| l.as_str()).collect::<Vec<_>>(),
vec!["a", "b"],
"references must come back in reading order"
);
let (pos_a, _) = refs[0];
assert_eq!(doc.footnote_reference_at(pos_a).as_deref(), Some("a"));
assert_eq!(doc.footnote_reference_at(pos_a + 1), None);
}
#[test]
fn reference_positions_are_characters_not_bytes() {
let doc = doc_from("café—dash[^a]\n");
let refs = doc.footnote_references();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].0, 9, "the position must be in characters");
assert_eq!(doc.footnote_reference_at(9).as_deref(), Some("a"));
}
#[test]
fn a_note_body_is_not_also_rendered_as_prose() {
let doc = doc_from("Prose[^n] here.\n\n[^n]: UNIQUEBODYTEXT.\n");
let html = doc.to_html().expect("html");
assert_eq!(
html.matches("UNIQUEBODYTEXT").count(),
1,
"the note body was rendered more than once: {html}"
);
}
#[test]
fn html_renders_a_noteref_and_its_aside() {
let doc = doc_from("Prose[^n1] here.\n\n[^n1]: The note body.\n");
let html = doc.to_html().expect("html");
assert!(
html.contains(r#"role="doc-noteref""#),
"no noteref marker: {html}"
);
assert!(
html.contains(r#"epub:type="footnote""#) && html.contains(r#"role="doc-footnote""#),
"no footnote aside: {html}"
);
assert!(
html.contains("The note body"),
"the note's body never rendered: {html}"
);
}
#[test]
fn plain_text_lists_notes_only_in_the_presentation_view() {
let doc = doc_from("Prose[^n1] here.\n\n[^n1]: The note body.\n");
let presented = doc
.to_plain_text_with(PlainTextExportOptions::presentation())
.expect("presentation");
assert!(
presented.contains("1. The note body"),
"no endnote list: {presented:?}"
);
let addressable = doc
.to_plain_text_with(PlainTextExportOptions::addressable())
.expect("addressable");
assert_eq!(
addressable.chars().count(),
doc.character_count(),
"the addressable view stopped matching the document: {addressable:?}"
);
assert!(
!addressable.contains("1. The note body"),
"an endnote list leaked into the addressable view: {addressable:?}"
);
}