#![cfg(feature = "cite")]
use std::fs;
use mini_docs::{Builder, CiteProcessor, CiteStyle, DocError};
const BIB: &str = r#"
@article{smith2020,
author = {Jane Smith},
title = {On Small Things},
journal = {Journal of Minimal Systems},
year = {2020}
}
"#;
const TEMPLATE: &str = "{{ page.content | safe }}";
fn build_page(markdown: &str, bib: &str) -> Result<String, DocError> {
build_page_styled(markdown, bib, CiteStyle::default())
}
fn build_page_styled(markdown: &str, bib: &str, style: CiteStyle) -> Result<String, DocError> {
let input = tempfile::tempdir().expect("create input tempdir");
let templates = tempfile::tempdir().expect("create templates tempdir");
let output = tempfile::tempdir().expect("create output tempdir");
let bib_dir = tempfile::tempdir().expect("create bib tempdir");
fs::write(bib_dir.path().join("refs.bib"), bib).expect("write refs.bib");
fs::write(input.path().join("page.md"), markdown).expect("write page.md");
fs::write(templates.path().join("page.html"), TEMPLATE).expect("write page.html");
let processor = CiteProcessor::new(bib_dir.path())
.expect("bibliography should load")
.with_style(style);
Builder::new(input.path())
.templates(templates.path())
.output(output.path())
.default_template("page.html")
.processor(processor)
.build()?;
Ok(fs::read_to_string(output.path().join("page.html")).expect("read output file"))
}
#[test]
fn citation_becomes_a_footnote_reference_and_footnote_in_rendered_html() {
let html =
build_page("Small things matter [@smith2020].\n", BIB).expect("build should succeed");
assert!(
html.contains(r#"id="fnref-1""#),
"inline footnote reference missing from rendered HTML: {html}"
);
assert!(
html.contains(r##"href="#fn-1""##),
"reference does not link to its footnote: {html}"
);
assert!(
html.contains(r#"id="fn-1""#),
"footnote list item missing from rendered HTML: {html}"
);
assert!(
html.contains("Jane Smith. On Small Things. Journal of Minimal Systems. 2020."),
"formatted citation missing from rendered HTML: {html}"
);
assert!(
html.contains(r##"href="#fnref-1""##),
"footnote back-link missing from rendered HTML: {html}"
);
}
#[test]
#[cfg(feature = "sanitize")]
fn footnote_markup_survives_sanitization_intact() {
let html = build_page("Cited [@smith2020].\n", BIB).expect("build should succeed");
for fragment in [
"<sup",
r#"id="fnref-1""#,
r##"href="#fn-1""##,
"<hr",
"<ol",
"<li",
r#"id="fn-1""#,
r##"href="#fnref-1""##,
"↩",
] {
assert!(
html.contains(fragment),
"sanitizer stripped `{fragment}` from the output: {html}"
);
}
}
#[test]
fn footnote_block_is_its_own_block_not_glued_to_the_last_paragraph() {
let html = build_page("Cited [@smith2020].\n", BIB).expect("build should succeed");
let hr = html.find("<hr").expect("footnote block should be present");
let paragraph_end = html[..hr]
.rfind("</p>")
.expect("body paragraph should be closed before the footnote block");
assert!(
paragraph_end < hr,
"footnote block was absorbed into the preceding paragraph: {html}"
);
}
#[test]
fn page_without_citations_renders_unchanged_and_gains_no_footnotes() {
let html =
build_page("# Title\n\nJust prose, no citations.\n", BIB).expect("build should succeed");
assert!(html.contains("Just prose, no citations."));
assert!(!html.contains("<hr"), "unexpected footnote block: {html}");
assert!(!html.contains("fn-1"), "unexpected footnote: {html}");
}
#[test]
fn citation_in_a_code_block_is_left_literal() {
let html = build_page(
"Write it like this:\n\n```markdown\nSee [@smith2020].\n```\n",
BIB,
)
.expect("build should succeed");
assert!(
html.contains("[@smith2020]"),
"citation syntax in a code sample should render literally: {html}"
);
assert!(
!html.contains("<hr"),
"code sample produced a footnote: {html}"
);
}
#[test]
fn undefined_citation_key_fails_the_build_with_a_cite_prefixed_error() {
let result = build_page("Cite [@nosuchkey].\n", BIB);
match result {
Err(DocError::Extension(msg)) => {
assert!(
msg.starts_with("cite:"),
"extension errors must be attributed to the extension: {msg}"
);
assert!(
msg.contains("nosuchkey"),
"error should name the undefined key: {msg}"
);
}
other => panic!("expected DocError::Extension, got {other:?}"),
}
}
#[test]
fn malformed_bibliography_fails_at_construction_not_at_build() {
let bib_dir = tempfile::tempdir().expect("create bib tempdir");
fs::write(
bib_dir.path().join("broken.bib"),
"@article{oops, title = {Unterminated}",
)
.expect("write broken.bib");
let error = CiteProcessor::new(bib_dir.path())
.err()
.expect("malformed bibliography should not load");
assert!(
matches!(error, mini_cite::CiteError::Parse { .. }),
"expected a parse error, got: {error}"
);
}
#[test]
fn duplicate_citation_key_across_bib_files_fails_at_construction() {
let bib_dir = tempfile::tempdir().expect("create bib tempdir");
fs::write(bib_dir.path().join("a.bib"), "@article{dup, title = {A}}").expect("write a.bib");
fs::write(bib_dir.path().join("b.bib"), "@article{dup, title = {B}}").expect("write b.bib");
let error = CiteProcessor::new(bib_dir.path())
.err()
.expect("duplicate key should not load");
assert!(
matches!(error, mini_cite::CiteError::DuplicateKey { .. }),
"expected a duplicate-key error, got: {error}"
);
}
#[test]
fn custom_classes_reach_the_rendered_page() {
let style = CiteStyle::default()
.reference_class("c-ref")
.separator_class("c-sep")
.list_class("c-list")
.item_class("c-item")
.backlink_class("c-back");
let html =
build_page_styled("Cited [@smith2020].\n", BIB, style).expect("build should succeed");
for fragment in [
r#"class="c-ref""#,
r#"class="c-sep""#,
r#"class="c-list""#,
r#"class="c-item""#,
r#"class="c-back""#,
] {
assert!(
html.contains(fragment),
"`{fragment}` did not survive to the rendered page: {html}"
);
}
}
#[test]
fn default_classes_reach_the_rendered_page() {
let html = build_page("Cited [@smith2020].\n", BIB).expect("build should succeed");
assert!(html.contains(r#"class="footnote-ref""#), "{html}");
assert!(html.contains(r#"class="footnotes""#), "{html}");
assert!(html.contains(r#"class="footnote-back""#), "{html}");
}
#[test]
fn a_custom_backlink_label_reaches_the_rendered_page() {
let style = CiteStyle::default().backlink_label("back");
let html =
build_page_styled("Cited [@smith2020].\n", BIB, style).expect("build should succeed");
assert!(
html.contains(">back</a>"),
"custom back-link label missing: {html}"
);
assert!(!html.contains('↩'), "default glyph survived: {html}");
}
#[test]
fn processor_reports_the_bibliography_it_loaded() {
let bib_dir = tempfile::tempdir().expect("create bib tempdir");
fs::write(bib_dir.path().join("refs.bib"), BIB).expect("write refs.bib");
let processor = CiteProcessor::new(bib_dir.path()).expect("bibliography should load");
assert_eq!(processor.len(), 1);
assert!(!processor.is_empty());
assert_eq!(processor.bib_dir(), bib_dir.path());
}