citum-engine 0.79.0

Citum citation and bibliography processor
Documentation
/*
SPDX-License-Identifier: MIT OR Apache-2.0
SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
*/

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::indexing_slicing,
    clippy::todo,
    clippy::unimplemented,
    clippy::unreachable,
    clippy::get_unwrap,
    reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
    use crate::render::component::{ProcTemplateComponent, render_component_with_format};
    use crate::render::djot::Djot;
    use crate::render::format::{OutputFormat, QuoteMarks};
    use crate::render::html::Html;
    use crate::render::latex::Latex;
    use crate::render::plain::PlainText;
    use crate::render::typst::Typst;
    use citum_schema::{tc_contributor, tc_title, tc_variable};

    #[test]
    fn test_heading_numbered_vs_unnumbered() {
        // Group/bibliography headings must not participate in LaTeX section
        // numbering, while document body headings keep numbered commands.
        let latex = Latex;
        assert_eq!(
            latex.heading(2, "Sources".to_string()),
            "\\subsection{Sources}\n\n"
        );
        assert_eq!(
            latex.unnumbered_heading(2, "Sources".to_string()),
            "\\subsection*{Sources}\n\n"
        );
        // Formats without heading numbering share both variants.
        assert_eq!(
            Html.unnumbered_heading(2, "Sources".to_string()),
            Html.heading(2, "Sources".to_string())
        );
        assert_eq!(
            Typst.unnumbered_heading(2, "Sources".to_string()),
            Typst.heading(2, "Sources".to_string())
        );
    }

    #[test]
    fn test_html_title() {
        let component = ProcTemplateComponent {
            template_component: tc_title!(Primary, emph = true),
            value: "My Title".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Html>(&component);
        assert_eq!(
            result,
            r#"<span class="citum-title"><em>My Title</em></span>"#
        );
    }

    #[test]
    fn test_html_contributor() {
        let component = ProcTemplateComponent {
            template_component: tc_contributor!(Author, Long, small_caps = true),
            value: "Smith".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Html>(&component);
        assert_eq!(
            result,
            r#"<span class="citum-author"><span style="font-variant:small-caps">Smith</span></span>"#
        );
    }

    #[test]
    fn test_html_semantic_attributes_include_template_index() {
        let component = ProcTemplateComponent {
            template_component: tc_title!(Primary, emph = true),
            template_index: Some(2),
            value: "My Title".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Html>(&component);
        assert_eq!(
            result,
            r#"<span class="citum-title" data-index="2"><em>My Title</em></span>"#
        );
    }

    #[test]
    fn test_djot_title() {
        let component = ProcTemplateComponent {
            template_component: tc_title!(Primary, emph = true),
            value: "My Title".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Djot>(&component);
        assert_eq!(result, "[_My Title_]{.citum-title}");
    }

    #[test]
    fn test_unicode_formats_alternate_quote_depth() {
        let plain = PlainText;
        let html = Html;
        let djot = Djot;
        let typst = Typst;
        let marks = QuoteMarks::default();

        assert_eq!(
            plain.quote_with_depth("outer".to_string(), 0, &marks),
            "“outer”"
        );
        assert_eq!(
            plain.quote_with_depth("inner".to_string(), 1, &marks),
            "‘inner’"
        );
        assert_eq!(
            html.quote_with_depth("inner".to_string(), 1, &marks),
            "‘inner’"
        );
        assert_eq!(
            djot.quote_with_depth("inner".to_string(), 1, &marks),
            "‘inner’"
        );
        assert_eq!(
            typst.quote_with_depth("inner".to_string(), 1, &marks),
            "‘inner’"
        );
    }

    #[test]
    fn test_latex_quote_depth_uses_default_unicode_delimiters() {
        // LaTeX no longer hardcodes backtick quotes; with no resolved locale it
        // falls back to the same Unicode default as every other backend.
        let latex = Latex;
        let marks = QuoteMarks::default();

        assert_eq!(
            latex.quote_with_depth("outer".to_string(), 0, &marks),
            "“outer”"
        );
        assert_eq!(
            latex.quote_with_depth("inner".to_string(), 1, &marks),
            "‘inner’"
        );
    }

    #[test]
    fn test_latex_quote_depth_uses_locale_marks() {
        let latex = Latex;
        let marks = QuoteMarks {
            open: "«".to_string(),
            close: "»".to_string(),
            open_inner: "".to_string(),
            close_inner: "".to_string(),
            punctuation_realization: None,
        };

        assert_eq!(
            latex.quote_with_depth("outer".to_string(), 0, &marks),
            "«outer»"
        );
        assert_eq!(
            latex.quote_with_depth("inner".to_string(), 1, &marks),
            "‹inner›"
        );
    }

    #[test]
    fn test_djot_contributor() {
        let component = ProcTemplateComponent {
            template_component: tc_contributor!(Author, Long, small_caps = true),
            value: "Smith".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Djot>(&component);
        assert_eq!(result, "[[Smith]{.small-caps}]{.citum-author}");
    }

    #[test]
    fn test_html_link() {
        let component = ProcTemplateComponent {
            template_component: tc_variable!(Url),
            value: "https://example.com".to_string(),
            url: Some("https://example.com".to_string()),
            ..Default::default()
        };

        let result = render_component_with_format::<Html>(&component);
        assert_eq!(
            result,
            r#"<span class="citum-url"><a href="https://example.com">https://example.com</a></span>"#
        );
    }

    #[test]
    fn test_html_link_percent_encodes_href_breakout_chars() {
        let component = ProcTemplateComponent {
            template_component: tc_variable!(Url),
            value: "label".to_string(),
            url: Some(r#"" onmouseover="alert(1)"#.to_string()),
            ..Default::default()
        };

        let result = render_component_with_format::<Html>(&component);
        assert_eq!(
            result,
            r#"<span class="citum-url"><a href="%22%20onmouseover=%22alert(1)">label</a></span>"#
        );
    }

    #[test]
    fn test_html_title_link_doi() {
        use citum_schema::{
            options::{LinkAnchor, LinkTarget, LinksConfig},
            template::{TemplateTitle, TitleType},
        };
        let component = ProcTemplateComponent {
            template_component: citum_schema::template::TemplateComponent::Title(TemplateTitle {
                title: TitleType::Primary,
                links: Some(LinksConfig {
                    target: Some(LinkTarget::Doi),
                    anchor: Some(LinkAnchor::Title),
                    ..Default::default()
                }),
                ..Default::default()
            }),
            value: "My Title".to_string(),
            url: Some("https://doi.org/10.1001/test".to_string()),
            ..Default::default()
        };

        let result = render_component_with_format::<Html>(&component);
        assert_eq!(
            result,
            r#"<span class="citum-title"><a href="https://doi.org/10.1001/test">My Title</a></span>"#
        );
    }

    #[test]
    fn test_html_title_escapes_active_characters() {
        let component = ProcTemplateComponent {
            template_component: tc_title!(Primary),
            value: "Design & use of <T> arenas".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Html>(&component);
        assert_eq!(
            result,
            r#"<span class="citum-title">Design &amp; use of &lt;T&gt; arenas</span>"#
        );
    }

    #[test]
    fn test_html_citation_escapes_data_ref_ids() {
        let fmt = Html;
        let result = fmt.citation(
            vec![r#""><script>alert(1)</script>"#.to_string()],
            "content".to_string(),
        );

        assert_eq!(
            result,
            r#"<span class="citum-citation" data-ref="&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;">content</span>"#
        );
    }

    #[test]
    fn test_typst_title() {
        let component = ProcTemplateComponent {
            template_component: tc_title!(Primary, emph = true),
            value: "My Title".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Typst>(&component);
        assert_eq!(result, "#emph[My Title]");
    }

    #[test]
    fn test_typst_strong_composes_when_nested() {
        let fmt = Typst;
        let inner = fmt.strong(fmt.text("Note"));
        let outer = fmt.strong(inner);

        assert_eq!(outer, "#strong[#strong[Note]]");
    }

    #[test]
    fn test_typst_contributor_small_caps() {
        let component = ProcTemplateComponent {
            template_component: tc_contributor!(Author, Long, small_caps = true),
            template_index: Some(4),
            value: "Smith".to_string(),
            ..Default::default()
        };

        let result = render_component_with_format::<Typst>(&component);
        assert_eq!(result, "#smallcaps[Smith]");
    }

    #[test]
    fn test_typst_link() {
        let component = ProcTemplateComponent {
            template_component: tc_variable!(Url),
            value: "https://example.com".to_string(),
            url: Some("https://example.com".to_string()),
            ..Default::default()
        };

        let result = render_component_with_format::<Typst>(&component);
        assert_eq!(
            result,
            r#"#link("https://example.com")[https://example.com]"#
        );
    }

    #[test]
    fn test_typst_title_link_doi() {
        use citum_schema::{
            options::{LinkAnchor, LinkTarget, LinksConfig},
            template::{TemplateTitle, TitleType},
        };

        let component = ProcTemplateComponent {
            template_component: citum_schema::template::TemplateComponent::Title(TemplateTitle {
                title: TitleType::Primary,
                links: Some(LinksConfig {
                    target: Some(LinkTarget::Doi),
                    anchor: Some(LinkAnchor::Title),
                    ..Default::default()
                }),
                ..Default::default()
            }),
            value: "My Title".to_string(),
            url: Some("https://doi.org/10.1001/test".to_string()),
            ..Default::default()
        };

        let result = render_component_with_format::<Typst>(&component);
        assert_eq!(result, r#"#link("https://doi.org/10.1001/test")[My Title]"#);
    }

    #[test]
    fn test_latex_link_escapes_href_target() {
        let component = ProcTemplateComponent {
            template_component: tc_variable!(Url),
            value: "10.5555/example".to_string(),
            url: Some("https://doi.org/10.5555/exa%2Fmple#frag".to_string()),
            ..Default::default()
        };

        let result = render_component_with_format::<Latex>(&component);
        assert_eq!(
            result,
            r"\href{https://doi.org/10.5555/exa\%2Fmple\#frag}{10.5555/example}"
        );
    }
}