ferromark 0.7.0

Ultra-high-performance Markdown to HTML compiler
Documentation
use ferromark::{InlineEvent, InlineParser, Options, to_html_with_options};

fn options() -> Options {
    Options {
        inline_footnotes: true,
        ..Options::default()
    }
}

fn render(input: &str) -> String {
    to_html_with_options(input, &options())
}

#[test]
fn inline_footnotes_are_disabled_by_default() {
    assert_eq!(
        to_html_with_options("Text.^[A note.]", &Options::default()),
        "<p>Text.^[A note.]</p>\n"
    );
}

#[test]
fn basic_inline_footnote_renders_at_document_end() {
    assert_eq!(
        render("Text.^[A note.]"),
        "<p>Text.<sup><a href=\"#user-content-inline-fn-1\" id=\"user-content-inline-fnref-1\" data-footnote-ref>1</a></sup></p>\n<section data-footnotes class=\"footnotes\">\n<ol>\n<li id=\"user-content-inline-fn-1\">\n<p>A note. <a href=\"#user-content-inline-fnref-1\" class=\"data-footnote-backref\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n</ol>\n</section>\n"
    );
}

#[test]
fn inline_note_content_supports_inline_markdown_and_balanced_brackets() {
    let html = render("Text.^[A *formatted* [link](https://example.com).]");

    assert!(html.contains("<em>formatted</em>"), "{html}");
    assert!(
        html.contains("<a href=\"https://example.com\">link</a>"),
        "{html}"
    );
    assert_eq!(html.matches("data-footnote-ref>").count(), 1);
}

#[test]
fn inline_and_reference_footnotes_share_first_appearance_numbering() {
    let input = "\
Inline.^[First.]

Reference.[^ref]

[^ref]: Second.";
    let html = to_html_with_options(
        input,
        &Options {
            footnotes: true,
            inline_footnotes: true,
            ..Options::default()
        },
    );

    assert!(html.contains("data-footnote-ref>1</a></sup>"), "{html}");
    assert!(html.contains("data-footnote-ref>2</a></sup>"), "{html}");
    let inline_pos = html.find("id=\"user-content-inline-fn-1\"").unwrap();
    let reference_pos = html.find("id=\"user-content-fn-ref\"").unwrap();
    assert!(inline_pos < reference_pos, "{html}");
}

#[test]
fn inline_notes_take_precedence_over_superscript() {
    let html = to_html_with_options(
        "2^10^ and a note.^[Content.]",
        &Options {
            superscript: true,
            inline_footnotes: true,
            ..Options::default()
        },
    );

    assert!(html.contains("2<sup>10</sup>"), "{html}");
    assert_eq!(html.matches("data-footnote-ref>").count(), 1);
}

#[test]
fn whitespace_only_notes_do_not_capture_later_superscript_delimiters() {
    let html = to_html_with_options(
        "some text^[  ]^word^",
        &Options {
            superscript: true,
            inline_footnotes: true,
            ..Options::default()
        },
    );

    assert_eq!(html, "<p>some text^[  ]<sup>word</sup></p>\n");
    assert!(!html.contains("data-footnote-ref>"), "{html}");
}

#[test]
fn escaped_empty_unclosed_and_code_syntax_stays_literal() {
    let html = render(r"\^[escaped] ^[] ^[   ] ^[unclosed `^[code]`");

    assert!(!html.contains("data-footnote-ref>"), "{html}");
    assert!(html.contains("^[escaped]"), "{html}");
    assert!(html.contains("^[]"), "{html}");
    assert!(html.contains("^[unclosed"), "{html}");
    assert!(html.contains("<code>^[code]</code>"), "{html}");
}

#[test]
fn multiline_inline_notes_remain_single_paragraph_notes() {
    let html = render("Text.^[First line\nsecond line.]");

    assert!(html.contains("<p>First line\nsecond line."), "{html}");
    assert_eq!(html.matches("<li id=\"user-content-inline-fn-").count(), 1);
}

#[test]
fn parser_exposes_the_note_content_range() {
    let input = b"Text.^[A *note*.]";
    let mut parser = InlineParser::new();
    let mut events = Vec::new();
    parser.parse_with_options(
        input,
        None,
        true,
        true,
        false,
        false,
        false,
        false,
        false,
        true,
        None,
        &mut events,
    );

    let range = events
        .iter()
        .find_map(|event| match event {
            InlineEvent::InlineFootnote(range) => Some(*range),
            _ => None,
        })
        .expect("inline footnote event");
    assert_eq!(range.slice(input), b"A *note*.");
}

#[test]
fn malformed_candidate_runs_remain_bounded_and_literal() {
    let input = "^[".repeat(10_000);
    let html = render(&input);

    assert!(!html.contains("data-footnote-ref>"), "{html}");
    assert!(html.starts_with("<p>^[^[^["));
}