use ferromark::{to_html_with_options, Options};
fn opts() -> Options {
Options {
footnotes: true,
..Options::default()
}
}
fn render(input: &str) -> String {
to_html_with_options(input, &opts())
}
#[test]
fn basic_footnote() {
let result = render("Text[^1].\n\n[^1]: Footnote content.");
assert!(result.contains("<sup><a href=\"#user-content-fn-1\""), "Missing footnote ref: {result}");
assert!(result.contains("id=\"user-content-fnref-1\""), "Missing fnref id: {result}");
assert!(result.contains("data-footnote-ref>1</a></sup>"), "Missing ref number: {result}");
assert!(result.contains("<section data-footnotes class=\"footnotes\">"), "Missing footnote section: {result}");
assert!(result.contains("<li id=\"user-content-fn-1\">"), "Missing footnote li: {result}");
assert!(result.contains("Footnote content."), "Missing footnote content: {result}");
assert!(result.contains("</ol>\n</section>\n"), "Missing section close: {result}");
}
#[test]
fn footnote_backref() {
let result = render("Text[^a].\n\n[^a]: Note.");
assert!(
result.contains("href=\"#user-content-fnref-a\" class=\"data-footnote-backref\""),
"Missing backref: {result}"
);
assert!(result.contains("\u{21a9}"), "Missing ↩ symbol: {result}");
}
#[test]
fn multiple_footnotes_numbered_by_appearance() {
let result = render("First[^b] second[^a].\n\n[^a]: Note A.\n\n[^b]: Note B.");
assert!(result.contains("data-footnote-ref>1</a></sup>"), "First ref not numbered 1: {result}");
assert!(result.contains("data-footnote-ref>2</a></sup>"), "Second ref not numbered 2: {result}");
let b_pos = result.find("fn-b").unwrap();
let a_pos = result.find("fn-a").unwrap();
let section_start = result.find("<section").unwrap();
let b_in_section = result[section_start..].find("fn-b").unwrap();
let a_in_section = result[section_start..].find("fn-a").unwrap();
assert!(b_in_section < a_in_section, "Footnotes not ordered by first appearance: b@{b_pos} a@{a_pos}");
}
#[test]
fn duplicate_reference_same_number() {
let result = render("First[^x] and again[^x].\n\n[^x]: Content X.");
let count = result.matches("data-footnote-ref>1</a></sup>").count();
assert_eq!(count, 2, "Expected 2 refs with number 1, got {count}: {result}");
}
#[test]
fn undefined_reference_literal() {
let result = render("Text[^undef].");
assert!(!result.contains("<sup>"), "Undefined ref should not create sup: {result}");
assert!(result.contains("[^undef]"), "Undefined ref should be literal: {result}");
}
#[test]
fn duplicate_definition_first_wins() {
let result = render("Ref[^d].\n\n[^d]: First def.\n\n[^d]: Second def.");
assert!(result.contains("First def."), "First def should win: {result}");
assert!(!result.contains("Second def."), "Second def should be discarded: {result}");
}
#[test]
fn footnotes_disabled_literal() {
let opts = Options {
footnotes: false,
..Options::default()
};
let result = to_html_with_options("Text[^1].\n\n[^1]: Content.", &opts);
assert!(!result.contains("<sup>"), "Should not render footnote when disabled: {result}");
assert!(!result.contains("<section"), "Should not render section when disabled: {result}");
}
#[test]
fn multi_paragraph_footnote() {
let input = "Text[^mp].\n\n[^mp]: First paragraph.\n\n Second paragraph.";
let result = render(input);
assert!(result.contains("First paragraph."), "Missing first para: {result}");
assert!(result.contains("Second paragraph."), "Missing second para: {result}");
}
#[test]
fn footnote_with_code_block() {
let input = "Text[^code].\n\n[^code]: Some text.\n\n code here";
let result = render(input);
assert!(result.contains("Some text."), "Missing text: {result}");
assert!(result.contains("code here"), "Missing code: {result}");
}
#[test]
fn label_with_alphanumeric() {
let result = render("Text[^abc123].\n\n[^abc123]: Content.");
assert!(result.contains("<sup>"), "Should resolve alphanumeric label: {result}");
}
#[test]
fn label_with_dash_underscore() {
let result = render("Text[^my-note_1].\n\n[^my-note_1]: Content.");
assert!(result.contains("<sup>"), "Should resolve dash/underscore label: {result}");
}
#[test]
fn empty_label_literal() {
let result = render("Text[^].");
assert!(!result.contains("<sup>"), "Empty label should not create ref: {result}");
assert!(result.contains("[^]"), "Empty label should be literal: {result}");
}
#[test]
fn footnote_def_not_in_paragraph() {
let result = render("Text[^1].\n\n[^1]: Note content.");
assert!(result.contains("<section"), "Should have footnote section: {result}");
}
#[test]
fn no_refs_no_section() {
let result = render("[^1]: Note content.\n\nJust a paragraph.");
assert!(!result.contains("<section"), "Should not have section when no refs used: {result}");
}
#[test]
fn case_insensitive_labels() {
let result = render("Text[^ABC].\n\n[^abc]: Content.");
assert!(result.contains("<sup>"), "Labels should be case-insensitive: {result}");
}