pdfsmith 0.1.1

A customizable PDF generation library from Markdown or structured JSON input
Documentation
// examples/technical_doc.rs
//
// Technical documentation generated entirely from JSON with paraSequence.
// Uses header + footer, heading numbers (baked into JSON via paraSequence),
// lots of code blocks, tables, and technical content.  2+ pages.
//
// Run:  cargo run --example technical_doc

use pdfsmith::{FooterConfig, HeaderConfig, PageMargins, PdfBuilder};
use serde_json::json;

/// Helper: build the content array in small chunks to avoid serde_json
/// macro recursion limits on large arrays.
fn build_content() -> serde_json::Value {
    let mut blocks = Vec::new();

    // ─── Section 1 ──────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "1", "text": "pdfsmith API Reference" }),
        json!({ "type": "paragraph", "text": "Complete API documentation for the `pdfsmith` crate v0.1.0.  This document covers every public type, method, and configuration option." }),
    ]);

    // ─── 1.1 ────────────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "1.1", "text": "Installation" }),
        json!({ "type": "paragraph", "text": "Add the crate to your `Cargo.toml`:" }),
        json!({ "type": "code", "language": "toml", "text": "[dependencies]\npdfsmith = \"0.1.0\"" }),
        json!({ "type": "paragraph", "text": "**Prerequisites:** Chromium or Google Chrome must be installed on the system.  On Ubuntu/Debian:" }),
        json!({ "type": "code", "language": "bash", "text": "sudo apt update && sudo apt install -y chromium-browser" }),
    ]);

    // ─── 1.2 ────────────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "1.2", "text": "Quick Start" }),
        json!({ "type": "code", "language": "rust", "text": "use pdfsmith::PdfBuilder;\n\nfn main() {\n    let pdf = PdfBuilder::new()\n        .title(\"My Document\")\n        .heading_numbers(true)\n        .from_markdown(\"# Hello\\n\\n## World\")\n        .expect(\"Failed\");\n\n    std::fs::write(\"output.pdf\", &pdf).unwrap();\n}" }),
    ]);

    // ─── Section 2 ──────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "2", "text": "PdfBuilder" }),
        json!({ "type": "paragraph", "text": "The main entry point.  All methods are chainable.  Call one of the generation methods (`from_markdown`, `from_json`, `from_html`) last." }),
    ]);

    // ─── 2.1 ────────────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "2.1", "text": "Constructor Methods" }),
        json!({ "type": "table",
          "headers": ["Method", "Description"],
          "rows": [
              ["`PdfBuilder::new()`", "Create with default config (A4, 0.75\" margins, no header/footer)"],
              ["`PdfBuilder::with_config(cfg)`", "Create from an existing `PdfConfig` struct"]
          ]
        }),
    ]);

    // ─── 2.2 ────────────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "2.2", "text": "Configuration Setters" }),
        json!({ "type": "table",
          "headers": ["Method", "Type", "Default", "Description"],
          "rows": [
              ["`title()`",                "impl Into<String>", "\"\"",          "Document title"],
              ["`custom_css()`",           "impl Into<String>", "None",         "Replace default CSS entirely"],
              ["`extra_css()`",            "impl Into<String>", "None",         "Append CSS after default"],
              ["`paper_size()`",           "PaperSize",         "A4",           "Paper dimensions"],
              ["`margins()`",              "PageMargins",       "0.75\" all",   "Page margins in inches"],
              ["`landscape()`",            "bool",              "false",        "Landscape orientation"],
              ["`header()`",               "HeaderConfig",      "empty",        "Page header"],
              ["`footer()`",               "FooterConfig",      "empty",        "Page footer"],
              ["`display_header_footer()`","bool",              "false",        "Show header/footer"],
              ["`heading_numbers()`",      "bool",              "false",        "CSS counter numbering"],
              ["`print_background()`",     "bool",              "true",         "Print background colors"],
              ["`markdown_options()`",     "MarkdownOptions",   "all true",     "Markdown extension toggles"],
              ["`chrome_window_size()`",   "(u32, u32)",        "(1280, 900)",  "Chrome viewport"],
              ["`page_load_wait_secs()`",  "u64",               "5",            "Wait after page load"]
          ]
        }),
    ]);

    // ─── 2.3 ────────────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "2.3", "text": "Generation Methods" }),
        json!({ "type": "table",
          "headers": ["Method", "Input", "Description"],
          "rows": [
              ["`from_markdown(md)`",      "&str",              "Markdown string → PDF"],
              ["`from_markdown_file(path)`","impl AsRef<Path>",  "Read .md file → PDF"],
              ["`from_json(json)`",        "&serde_json::Value", "JSON blocks → Markdown → PDF"],
              ["`from_json_file(path)`",   "impl AsRef<Path>",  "Read .json file → PDF"],
              ["`from_html(html)`",        "&str",              "Raw HTML → PDF (no CSS injection)"]
          ]
        }),
    ]);

    // ─── Section 3 ──────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "3", "text": "Configuration Types" }),
        json!({ "type": "heading", "paraSequence": "3.1", "text": "PaperSize" }),
        json!({ "type": "code", "language": "rust", "text": "pub enum PaperSize {\n    A4,                              // 8.27 × 11.69 in\n    Letter,                          // 8.5  × 11    in\n    Legal,                           // 8.5  × 14    in\n    Custom { width: f64, height: f64 }, // any size in inches\n}" }),
    ]);

    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "3.2", "text": "PageMargins" }),
        json!({ "type": "code", "language": "rust", "text": "pub struct PageMargins {\n    pub top: f64,     // inches (default 0.75)\n    pub bottom: f64,  // inches (default 0.75)\n    pub left: f64,    // inches (default 0.75)\n    pub right: f64,   // inches (default 0.75)\n}" }),
    ]);

    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "3.3", "text": "HeaderConfig / FooterConfig" }),
        json!({ "type": "paragraph", "text": "Both share the same structure.  Set `custom_html` for full control, or use `left` / `center` / `right` text fields:" }),
        json!({ "type": "code", "language": "rust", "text": "pub struct HeaderConfig {\n    pub custom_html: Option<String>,  // full HTML — overrides everything\n    pub left: Option<String>,\n    pub center: Option<String>,\n    pub right: Option<String>,\n    pub font_size: Option<String>,    // e.g. \"9px\"\n    pub color: Option<String>,        // e.g. \"#333\"\n}" }),
        json!({ "type": "paragraph", "text": "Chrome placeholders available in header/footer text:" }),
        json!({ "type": "table",
          "headers": ["Placeholder", "Renders As"],
          "rows": [
              ["<span class=\"pageNumber\">",  "Current page number"],
              ["<span class=\"totalPages\">",  "Total page count"],
              ["<span class=\"title\">",       "Document title"],
              ["<span class=\"date\">",        "Current date"]
          ]
        }),
    ]);

    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "3.4", "text": "MarkdownOptions" }),
        json!({ "type": "code", "language": "rust", "text": "pub struct MarkdownOptions {\n    pub unsafe_html: bool,        // pass-through <html> tags\n    pub tables: bool,             // | col | col |\n    pub footnotes: bool,          // [^1]: text\n    pub description_lists: bool,  // term\\n: definition\n    pub strikethrough: bool,      // ~~text~~\n    pub tasklist: bool,           // - [x] item\n    pub autolink: bool,           // https://auto.link\n    pub superscript: bool,        // 2^10\n}  // All default to true" }),
    ]);

    // ─── Section 4 ──────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "4", "text": "JSON Content Blocks" }),
        json!({ "type": "paragraph", "text": "When using `from_json()`, the input is an array of typed content blocks.  Each block has a `\"type\"` field and type-specific data." }),
    ]);

    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "4.1", "text": "Block Type Reference" }),
        json!({ "type": "table",
          "headers": ["Type", "Key Fields", "Description"],
          "rows": [
              ["heading",    "paraSequence, text, level (opt)", "Section heading"],
              ["paragraph",  "text",                            "Body text (Markdown)"],
              ["code",       "language, text",                  "Fenced code block"],
              ["list",       "ordered, items",                  "Bullet or numbered list"],
              ["quote",      "text",                            "Blockquote"],
              ["table",      "headers, rows",                   "Data table"],
              ["image",      "src, alt",                        "Image from URL/path"],
              ["divider",    "—",                               "Horizontal rule"],
              ["html",       "text",                            "Raw HTML pass-through"]
          ]
        }),
    ]);

    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "4.2", "text": "paraSequence" }),
        json!({ "type": "paragraph", "text": "The `paraSequence` field defines the hierarchical section number.  The heading level (h1–h6) is **automatically derived** from the depth:" }),
        json!({ "type": "table",
          "headers": ["paraSequence", "Depth", "Level", "Example"],
          "rows": [
              ["\"1\"",       "1", "h1", "# 1 Introduction"],
              ["\"1.1\"",     "2", "h2", "## 1.1 Background"],
              ["\"1.1.1\"",   "3", "h3", "### 1.1.1 Details"],
              ["\"A\"",       "1", "h1", "# A Appendix"],
              ["\"A.1\"",     "2", "h2", "## A.1 First section"]
          ]
        }),
        json!({ "type": "paragraph", "text": "You can override the derived level with an explicit `\"level\"` field if needed." }),
    ]);

    // ─── Section 5 ──────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "5", "text": "Error Handling" }),
        json!({ "type": "code", "language": "rust", "text": "pub enum MdocError {\n    Chrome(String),\n    Io(std::io::Error),\n    Json(String),\n    ImageDownload { url: String, reason: String },\n    Other(String),\n}" }),
        json!({ "type": "paragraph", "text": "All generation methods return `pdfsmith::Result<Vec<u8>>`." }),
        json!({ "type": "code", "language": "rust", "text": "match PdfBuilder::new().from_markdown(\"# Test\") {\n    Ok(pdf)                       => std::fs::write(\"out.pdf\", pdf).unwrap(),\n    Err(MdocError::Chrome(msg))   => eprintln!(\"Chrome: {msg}\"),\n    Err(MdocError::Io(err))       => eprintln!(\"IO: {err}\"),\n    Err(e)                        => eprintln!(\"Error: {e}\"),\n}" }),
    ]);

    // ─── Section 6 ──────────────────────────────────────────────
    blocks.extend(vec![
        json!({ "type": "heading", "paraSequence": "6", "text": "CSS Customisation" }),
        json!({ "type": "heading", "paraSequence": "6.1", "text": "custom_css vs extra_css" }),
        json!({ "type": "table",
          "headers": ["Method", "Behaviour", "Use When"],
          "rows": [
              ["`custom_css()`", "Replaces entire default stylesheet", "Full control"],
              ["`extra_css()`",  "Appended after default stylesheet", "Small tweaks"]
          ]
        }),
        json!({ "type": "heading", "paraSequence": "6.2", "text": "heading_numbers(true)" }),
        json!({ "type": "paragraph", "text": "Injects CSS counter rules for automatic hierarchical heading numbers.  Works with **all** input types: Markdown, JSON, and HTML." }),
        json!({ "type": "divider" }),
        json!({ "type": "paragraph", "text": "*End of API Reference — pdfsmith v0.1.0*" }),
    ]);

    serde_json::Value::Array(blocks)
}

fn main() {
    env_logger::init();

    let json = build_content();

    let pdf = PdfBuilder::new()
        .title("pdfsmith API Reference")
        .margins(PageMargins {
            top: 0.9,
            bottom: 0.9,
            left: 0.7,
            right: 0.7,
        })
        .display_header_footer(true)
        .header(HeaderConfig {
            left: Some("pdfsmith v0.1.0".into()),
            right: Some("API Reference".into()),
            font_size: Some("8px".into()),
            color: Some("#555".into()),
            ..Default::default()
        })
        .footer(FooterConfig {
            center: Some(
                r#"— <span class="pageNumber"></span> —"#.into(),
            ),
            font_size: Some("8px".into()),
            color: Some("#999".into()),
            ..Default::default()
        })
        .extra_css("h1 { color: #1a5276; } h2 { color: #2874a6; margin-left: 12px; } h3 { color: #1e8449; margin-left: 24px; }")
        .from_json(&json)
        .expect("Failed to generate PDF");

    std::fs::write("examples/output_pdfs/technical_doc.pdf", &pdf).expect("Failed to write PDF");
    println!("PDF saved to examples/output_pdfs/technical_doc.pdf ({} bytes)", pdf.len());
}