markdown2pdf 1.0.0

Create PDF with Markdown files (a md to pdf transpiler)
Documentation
//! Schema, theme preset, merge, and error-handling tests for the
//! configuration overhaul. End-to-end byte-level renderer regressions
//! live in `tests/render_fonts.rs`.

use markdown2pdf::config::{ConfigSource, load_config_strict};
use markdown2pdf::styling::{
    Color, DocumentConfig, FontStyleVariant, FontWeight, PageSize, ResolveError, ResolvedStyle,
    Sides, TextAlignment, available_theme_names, load_theme_preset, merge_documents, resolve,
};

#[test]
fn bundled_themes_all_load_cleanly() {
    for name in available_theme_names() {
        let cfg = load_theme_preset(name)
            .unwrap_or_else(|e| panic!("preset `{}` failed to load: {}", name, e));
        let _resolved: ResolvedStyle = resolve(cfg, None)
            .unwrap_or_else(|e| panic!("preset `{}` failed to resolve: {}", name, e));
    }
}

#[test]
fn default_theme_round_trips_into_resolved_style() {
    let style = ResolvedStyle::default();
    assert_eq!(style.paragraph.font_size_pt, 8.0);
    assert_eq!(style.headings[0].font_size_pt, 14.0);
    assert_eq!(style.headings[5].font_size_pt, 8.0);
    assert_eq!(style.page.size, PageSize::A4);
    assert!(matches!(style.page.size, PageSize::A4));
}

#[test]
fn color_deserializes_from_hex_struct_and_array() {
    let hex = r##"[paragraph]
        text_color = "#FF8800""##;
    let s = load_config_strict(ConfigSource::Embedded(hex), None).unwrap();
    assert_eq!(s.paragraph.text_color, Color::rgb(0xFF, 0x88, 0x00));

    let short = r##"[paragraph]
        text_color = "#F80""##;
    let s = load_config_strict(ConfigSource::Embedded(short), None).unwrap();
    assert_eq!(s.paragraph.text_color, Color::rgb(0xFF, 0x88, 0x00));

    let strukt = r#"[paragraph]
        text_color = { r = 10, g = 20, b = 30 }"#;
    let s = load_config_strict(ConfigSource::Embedded(strukt), None).unwrap();
    assert_eq!(s.paragraph.text_color, Color::rgb(10, 20, 30));

    let array = r#"[paragraph]
        text_color = [10, 20, 30]"#;
    let s = load_config_strict(ConfigSource::Embedded(array), None).unwrap();
    assert_eq!(s.paragraph.text_color, Color::rgb(10, 20, 30));
}

#[test]
fn sides_accepts_scalar_pair_quad_and_struct() {
    let scalar = r#"[paragraph]
        padding = 4.0"#;
    let s = load_config_strict(ConfigSource::Embedded(scalar), None).unwrap();
    assert_eq!(s.paragraph.padding, Sides { top: 4.0, right: 4.0, bottom: 4.0, left: 4.0 });

    let pair = r#"[paragraph]
        padding = [2.0, 6.0]"#;
    let s = load_config_strict(ConfigSource::Embedded(pair), None).unwrap();
    assert_eq!(s.paragraph.padding, Sides { top: 2.0, right: 6.0, bottom: 2.0, left: 6.0 });

    let quad = r#"[paragraph]
        padding = [1.0, 2.0, 3.0, 4.0]"#;
    let s = load_config_strict(ConfigSource::Embedded(quad), None).unwrap();
    assert_eq!(s.paragraph.padding, Sides { top: 1.0, right: 2.0, bottom: 3.0, left: 4.0 });

    let strukt = r#"[paragraph]
        padding = { top = 1.0, right = 2.0, bottom = 3.0, left = 4.0 }"#;
    let s = load_config_strict(ConfigSource::Embedded(strukt), None).unwrap();
    assert_eq!(s.paragraph.padding, Sides { top: 1.0, right: 2.0, bottom: 3.0, left: 4.0 });
}

#[test]
fn page_size_accepts_named_and_custom() {
    let named = r#"[page]
        size = "Letter""#;
    let s = load_config_strict(ConfigSource::Embedded(named), None).unwrap();
    assert_eq!(s.page.size, PageSize::Letter);

    let custom = r#"[page]
        size = { width_mm = 100.0, height_mm = 150.0 }"#;
    let s = load_config_strict(ConfigSource::Embedded(custom), None).unwrap();
    assert_eq!(s.page.size, PageSize::Custom { width_mm: 100.0, height_mm: 150.0 });
}

#[test]
fn font_weight_accepts_string_and_numeric() {
    let bold = r#"[paragraph]
        font_weight = "bold""#;
    let s = load_config_strict(ConfigSource::Embedded(bold), None).unwrap();
    assert_eq!(s.paragraph.font_weight, FontWeight::Bold);

    let numeric = r#"[paragraph]
        font_weight = 700"#;
    let s = load_config_strict(ConfigSource::Embedded(numeric), None).unwrap();
    assert_eq!(s.paragraph.font_weight, FontWeight::Numeric(700));
    assert!(s.paragraph.is_bold(), "700 must count as bold");

    let normal = r#"[paragraph]
        font_weight = 400"#;
    let s = load_config_strict(ConfigSource::Embedded(normal), None).unwrap();
    assert!(!s.paragraph.is_bold(), "400 must not count as bold");
}

#[test]
fn inherits_resolves_recursively() {
    // `github` inherits from `default`; check that fields not set in
    // github.toml (e.g. h6) come through from default.toml.
    let cfg = load_theme_preset("github").unwrap();
    let resolved = resolve(cfg, None).unwrap();
    // GitHub theme sets h6 to 10.5pt; default would have set 8.0pt.
    // Confirms inheritance respected the override.
    assert_eq!(resolved.headings[5].font_size_pt, 10.5);
}

#[test]
fn user_config_overrides_preset_field_by_field() {
    let user = r#"theme = "github"
        [paragraph]
        font_size_pt = 13.0"#;
    let s = load_config_strict(ConfigSource::Embedded(user), None).unwrap();
    // The github preset sets paragraph 10pt; user overrides to 13.
    assert_eq!(s.paragraph.font_size_pt, 13.0);
    // But unrelated github fields stick: h1 still 22pt.
    assert_eq!(s.headings[0].font_size_pt, 22.0);
}

#[test]
fn theme_override_arg_beats_user_theme_field() {
    let user = r#"theme = "default""#;
    let s = load_config_strict(ConfigSource::Embedded(user), Some("compact")).unwrap();
    // Compact theme: paragraph 9pt, not default's 8pt.
    assert_eq!(s.paragraph.font_size_pt, 9.0);
}

#[test]
fn defaults_block_cascades_into_unset_block_fields() {
    let user = r#"
        [defaults]
        font_family = "Times"
        [paragraph]
        font_size_pt = 12.0
    "#;
    let s = load_config_strict(ConfigSource::Embedded(user), None).unwrap();
    assert_eq!(s.paragraph.font_family.as_deref(), Some("Times"));
    assert_eq!(s.paragraph.font_size_pt, 12.0);
}

#[test]
fn unknown_field_raises_typed_error() {
    let err = load_config_strict(
        ConfigSource::Embedded(
            r##"[paragraph]
            texcolor = "#000000""##,
        ),
        None,
    );
    assert!(matches!(err, Err(ResolveError::BadToml { .. })));
}

#[test]
fn unknown_theme_raises_typed_error() {
    let err = load_config_strict(ConfigSource::Default, Some("notarealtheme"));
    match err {
        Err(ResolveError::UnknownTheme { name, .. }) => assert_eq!(name, "notarealtheme"),
        other => panic!("expected UnknownTheme, got {:?}", other),
    }
}

#[test]
fn merge_documents_overlay_wins_on_some_for_block_fields() {
    let base: DocumentConfig = toml::from_str(
        r#"
        [paragraph]
        font_size_pt = 8.0
        font_weight = "normal"
    "#,
    )
    .unwrap();
    let overlay: DocumentConfig = toml::from_str(
        r#"
        [paragraph]
        font_size_pt = 12.0
    "#,
    )
    .unwrap();
    let merged = merge_documents(base, overlay);
    let p = merged.paragraph.unwrap();
    assert_eq!(p.font_size_pt, Some(12.0));
    // font_weight survives because overlay didn't touch it.
    assert_eq!(p.font_weight, Some(FontWeight::Normal));
}

#[test]
fn text_align_and_font_style_round_trip() {
    let cfg = r#"[paragraph]
        text_align = "justify"
        font_style = "italic""#;
    let s = load_config_strict(ConfigSource::Embedded(cfg), None).unwrap();
    assert_eq!(s.paragraph.text_align, TextAlignment::Justify);
    assert_eq!(s.paragraph.font_style, FontStyleVariant::Italic);
    assert!(s.paragraph.is_italic());
}

#[test]
fn print_effective_config_round_trip() {
    // Take the academic preset's resolved style, serialize to TOML,
    // and confirm it's still valid input for the parser. This
    // guarantees `--print-effective-config` produces consumable output.
    let resolved = load_config_strict(ConfigSource::Default, Some("academic")).unwrap();
    let toml_text = toml::to_string(&resolved).expect("serialize resolved style");
    // The serialized form is the *resolved* shape, not the input
    // schema — feeding it back as input would hit deny_unknown_fields.
    // What we assert here is that it serializes cleanly and contains
    // expected key markers.
    assert!(toml_text.contains("font_size_pt"));
    assert!(toml_text.contains("[page]"));
    assert!(toml_text.contains("[paragraph]"));
}