md_reader 0.2.2

A fast, native markdown reader and editor with live preview, syntax highlighting, and multi-file support.
# Plan: PDF Export of Preview Mode

## Context
Users want to export the current document's preview rendering to a PDF file. The app currently has no export functionality. The `ParsedDoc` AST (Block/Inline tree) is fully egui-independent, making a direct AST→PDF approach the cleanest path — no HTML intermediate, no external binaries, no headless browser.

The chosen crate is **`printpdf`** (the most widely used and maintained Rust PDF crate). It gives full control over pages, fonts, text placement, colors, and rectangles, at the cost of manual text-flow logic (line wrapping + pagination), which we implement ourselves.

---

## Files to Modify / Create
- `Cargo.toml` — add `printpdf`
- `src/pdf_export.rs` — new file: the entire PDF renderer
- `src/main.rs` — declare the new module
- `src/app.rs` — add "Export PDF" button to toolbar, wire up dialog + export call

---

## Step 1 — Add Dependency (`Cargo.toml`)

```toml
printpdf = "0.8"
```

---

## Step 2 — New module `src/pdf_export.rs`

### 2a. Public entry point

```rust
pub fn export_pdf(doc: &ParsedDoc, dest: &std::path::Path) -> Result<(), Box<dyn std::error::Error>>
```

Takes the parsed doc and a destination path (chosen via rfd save dialog), writes a `.pdf` file.

### 2b. Internal renderer struct

```rust
struct PdfRenderer {
    doc:          PdfDocumentReference,
    page_idx:     PdfPageIndex,
    layer:        PdfLayerReference,
    // Font handles
    font_regular: IndirectFontRef,
    font_bold:    IndirectFontRef,
    font_italic:  IndirectFontRef,
    font_mono:    IndirectFontRef,
    // Layout state
    x:       Mm,   // left margin (fixed)
    y:       Mm,   // current Y cursor (decreases top→bottom)
    page_w:  Mm,
    page_h:  Mm,
    margin:  Mm,
}
```

### 2c. Fonts

Use fonts already bundled under `assets/fonts/` (used by the egui preview). If a font file
is missing at runtime, fall back to printpdf's built-in Helvetica / Courier.

### 2d. Page flow logic

```rust
fn ensure_space(&mut self, needed: Mm) {
    if self.y - needed < self.margin {
        self.new_page();
    }
}

fn new_page(&mut self) {
    let (page, layer) = self.doc.add_page(self.page_w, self.page_h, "Layer 1");
    self.page_idx = page;
    self.layer    = self.doc.get_page(page).get_layer(layer);
    self.y        = self.page_h - self.margin;
}
```

### 2e. Block rendering

| Block type | PDF output |
|---|---|
| `Heading(level, inlines)` | font size 24/20/16/14 by level, bold, extra vertical space above |
| `Paragraph(inlines)` | word-wrapped at body size (11pt), standard line height |
| `CodeBlock(lang, code)` | light-gray filled rect, monospace font, preserve newlines |
| `BlockQuote(inlines)` | indented, italic, left gray bar via `add_line` |
| `List(ordered, items)` | "•" or "n." prefix, indented, recurse for nested items |
| `Table(headers, rows)` | grid with horizontal lines, columns distributed evenly |
| `Rule` | thin horizontal line across content width |

### 2f. Inline text rendering

Collect consecutive inlines into styled runs for word-wrapping:

| Inline | Style |
|---|---|
| `Text` | regular font |
| `Bold` | bold font |
| `Italic` | italic font |
| `BoldItalic` | bold-italic font |
| `Code` | monospace, small gray box behind text |
| `Link` | blue color, regular font (text only; no PDF hyperlink annotation in v1) |
| `Image` | load via `image` crate (already a dep), embed as XObject; skip with placeholder if path unresolvable |

Word-wrap: measure character widths per font/size, break lines at content width.

---

## Step 3 — Toolbar button (`app.rs`)

### 3a. New field on App struct

```rust
pending_export_pdf: bool,  // initialized to false
```

### 3b. Button in toolbar

Inside `TopBottomPanel::top("toolbar")`:

```rust
if ui.button("Export PDF")
    .on_hover_text("Export current document as PDF")
    .clicked()
{
    self.pending_export_pdf = true;
}
```

Only show the button when an active tab with a parsed doc exists.

### 3c. Handle export outside the UI closure

```rust
if self.pending_export_pdf {
    self.pending_export_pdf = false;
    if let Some(idx) = self.active_tab {
        if let Some(doc) = &self.tabs[idx].parsed_doc {
            let default_name = self.tabs[idx].path
                .file_stem().unwrap_or_default()
                .to_string_lossy().to_string() + ".pdf";
            if let Some(dest) = rfd::FileDialog::new()
                .set_file_name(&default_name)
                .add_filter("PDF", &["pdf"])
                .save_file()
            {
                if let Err(e) = crate::pdf_export::export_pdf(doc, &dest) {
                    eprintln!("PDF export failed: {e}");
                }
            }
        }
    }
}
```

`rfd` is already a dependency — no new crate needed for the dialog.

---

## Scope & Limitations (v1)
- No syntax highlight colors in code blocks (monospace on gray bg only)
- Tables: fixed-width columns, no cell text wrapping
- Links: blue text only, no clickable PDF annotations
- No page header/footer
- Images skipped with placeholder if path can't be resolved

All of the above can be improved in follow-up iterations.

---

## Verification
1. Open `test.md` in the app
2. Click "Export PDF" in the toolbar
3. Save dialog appears with `test.pdf` as default filename
4. Open exported PDF — check: headings larger/bold, code blocks gray bg, lists bulleted, rules visible, paragraph text wraps across pages
5. Test pagination with a long document
6. Test with a document containing images