glyphweaveforge 0.1.2

Convert Markdown into PDF through an explicit Rust pipeline with minimal and Typst backends.
Documentation
![GlyphWeaveForge logo](logo.png)

# GlyphWeaveForge

GlyphWeaveForge converts Markdown into PDF through a small Rust pipeline with explicit boundaries:

`api -> pipeline -> core -> adapters`

The crate ships a lightweight built-in renderer by default and can optionally use Typst without changing the public builder API.

## What the crate provides today

- A fluent `Forge` builder for Markdown-to-PDF conversion.
- Three input modes: UTF-8 text, UTF-8 bytes, and filesystem paths (`fs` feature).
- Three output modes: in-memory bytes, explicit file path, and output directory (`fs` feature).
- Built-in page sizes (`A4`, `Letter`, `Legal`, custom millimeter sizes).
- Built-in layout modes (`Paged`, `SinglePage`).
- Built-in themes with optional JSON overrides.
- Resource resolution through filesystem lookup (`fs`) or caller-provided resolvers.
- Renderer extension points through public `RenderBackend` and `ResourceResolver` traits.
- Two backend selections behind a stable builder API: `Minimal` by default and `Typst` when the feature is enabled.

## Installation

```toml
[dependencies]
glyphweaveforge = "0.1.2"
```

Enable optional features when you need them:

```toml
[dependencies]
glyphweaveforge = { version = "0.1.2", features = ["renderer-typst"] }
```

## Main builder flow

```rust
use glyphweaveforge::Forge;

let pdf = Forge::new()
    .from_text("# Hello\n\nWorld")
    .to_memory()
    .convert()
    .expect("conversion should succeed");

let bytes = pdf.bytes.expect("memory output should contain PDF bytes");
assert!(bytes.starts_with(b"%PDF"));
```

`Forge::new()` starts with these defaults:

- source: not set
- output: not set
- backend: `RenderBackendSelection::Minimal`
- page size: `PageSize::A4`
- layout mode: `LayoutMode::Paged`
- theme: `BuiltInTheme::Professional`

## Supported inputs and outputs

Always available:

- `from_text`
- `from_bytes`
- `to_memory`
- injected resource resolvers through `with_resource_resolver` or `with_resource_adapter`
- explicit backend selection through `with_backend`
- custom renderer injection through `with_renderer`
- full option replacement through `with_options`

Available with the default `fs` feature:

- `from_path`
- `to_file`
- `to_directory`
- `with_output_file_name` for directory outputs
- filesystem-based resource lookup for local assets relative to the markdown file

### Output behavior

- `to_memory()` returns `PdfOutput { bytes: Some(...), written_path: None }`.
- `to_file(...)` writes the PDF and returns `PdfOutput { bytes: None, written_path: Some(...) }`.
- `to_directory(...)` creates the directory if needed and derives the file name from the source name unless `with_output_file_name(...)` overrides it.

When deriving a directory output name, the crate appends `.pdf` if it is missing.

## Page sizes, layout modes, and themes

### Page sizes

- `PageSize::A4`
- `PageSize::Letter`
- `PageSize::Legal`
- `PageSize::Custom { width_mm, height_mm }`

Custom page sizes must be strictly positive.

### Layout modes

- `LayoutMode::Paged`: paginates rendered lines across pages.
- `LayoutMode::SinglePage`: keeps the rendered content in a single page payload.

### Built-in themes

- `BuiltInTheme::Invoice`
- `BuiltInTheme::ScientificArticle`
- `BuiltInTheme::Professional` (default)
- `BuiltInTheme::Engineering`
- `BuiltInTheme::Informational`

`with_theme(...)` selects a built-in preset. `with_theme_config(...)` lets you combine an optional built-in theme with JSON overrides such as:

- `name`
- `body_font_size_pt`
- `code_font_size_pt`
- `heading_scale`
- `margin_mm`

Example:

```rust
use glyphweaveforge::{BuiltInTheme, Forge, LayoutMode, PageSize, ThemeConfig};
use serde_json::json;

let pdf = Forge::new()
    .from_text("# Report\n\nBody")
    .to_memory()
    .with_page_size(PageSize::Letter)
    .with_layout_mode(LayoutMode::SinglePage)
    .with_theme_config(ThemeConfig {
        built_in: Some(BuiltInTheme::Engineering),
        custom_theme_json: Some(json!({
            "name": "custom-engineering",
            "margin_mm": 14.0
        })),
    })
    .convert()
    .expect("conversion should succeed");

assert!(pdf.bytes.expect("bytes should exist").starts_with(b"%PDF"));
```

## Backend selection

The default build uses the minimal built-in renderer. To opt into the Typst backend, enable `renderer-typst` and select it explicitly:

```rust
# #[cfg(feature = "renderer-typst")]
# {
use glyphweaveforge::{Forge, RenderBackendSelection};

let pdf = Forge::new()
    .from_text("# Hello from Typst")
    .to_memory()
    .with_backend(RenderBackendSelection::Typst)
    .convert()
    .expect("typst backend should succeed");

assert!(pdf.bytes.expect("bytes should exist").starts_with(b"%PDF"));
# }
```

Backends exposed by the public API:

- `RenderBackendSelection::Minimal`: always available in the default feature set.
- `RenderBackendSelection::Typst`: available when `renderer-typst` is enabled.

The `typst` Cargo feature is a compatibility alias for `renderer-typst`.

If you need complete control, `with_renderer(...)` accepts any type implementing the public `RenderBackend` trait and overrides the built-in backend selection.

## Feature matrix

- `fs` (default): enables path-based source/output helpers and filesystem resource loading.
- `renderer-minimal` (default): keeps the built-in lightweight renderer enabled and addressable in tests/documentation.
- `renderer-typst`: enables the Typst-backed renderer.
- `typst`: compatibility alias for `renderer-typst`.
- `mermaid`: currently does **not** add real Mermaid rendering; fenced `mermaid` blocks remain visible as explicit unsupported fallbacks.
- `math`: currently does **not** add real math layout; fenced `math` blocks remain visible as explicit unsupported fallbacks.

## Current Markdown behavior

Supported today:

- headings
- paragraphs
- emphasis/strong text
- inline code
- links
- unordered and ordered lists
- block quotes
- thematic breaks
- fenced code blocks
- images, including injected resolvers and memory-backed assets

Resource handling behavior:

- For path-based markdown sources with the `fs` feature, local assets are resolved relative to the markdown file directory.
- For text/bytes sources, you can inject assets with `with_resource_resolver(...)` or `with_resource_adapter(...)`.
- Missing assets remain visible in the output as explicit fallback text.

Unsupported advanced Markdown stays visible instead of silently disappearing. Examples include:

- tables
- footnotes
- Mermaid diagrams
- math fences

These paths render deterministic fallback labels with the original content preserved in the output.

## Extending the crate

### Custom resource resolver

```rust
use std::io;

use glyphweaveforge::Forge;

let pdf = Forge::new()
    .from_text("![Logo](logo.png)")
    .to_memory()
    .with_resource_resolver(|href| {
        if href == "logo.png" {
            Ok(vec![1, 2, 3])
        } else {
            Err(io::Error::new(io::ErrorKind::NotFound, "missing"))
        }
    })
    .convert()
    .expect("conversion should succeed");

assert!(pdf.bytes.expect("bytes should exist").starts_with(b"%PDF"));
```

### Custom renderer

```rust
use glyphweaveforge::{Document, Forge, RenderBackend, RenderRequest, Result};

struct StubRenderer;

impl RenderBackend for StubRenderer {
    fn render(&self, _document: &Document, _request: &RenderRequest) -> Result<Vec<u8>> {
        Ok(b"stub-pdf".to_vec())
    }
}

let pdf = Forge::new()
    .from_text("hello")
    .to_memory()
    .with_renderer(StubRenderer)
    .convert()
    .expect("custom renderer should be used");

assert_eq!(pdf.bytes, Some(b"stub-pdf".to_vec()));
```

The public extension traits are:

- `RenderBackend`
- `ResourceResolver`

The rendering request/asset helper types exposed for adapters are:

- `RenderRequest`
- `ResolvedAsset`
- `ResourceStatus`

## Limitations

- This crate does **not** claim real Mermaid diagram rendering.
- This crate does **not** claim real TeX/LaTeX or advanced math typesetting.
- Advanced Markdown such as tables and footnotes is exposed through visible fallback text, not full layout support.
- The minimal renderer writes a compact PDF text stream; it is designed for deterministic output and testability rather than rich page design.
- `renderer-typst` is optional; docs.rs for the default documentation build does not require Typst support to use the crate.

## Public error and result types

- `Result<T>` is an alias for `std::result::Result<T, ForgeError>`.
- `ForgeError::MissingSource` and `ForgeError::MissingOutput` protect incomplete builder usage.
- `ForgeError::InputRead` and `ForgeError::InvalidUtf8` cover source loading.
- `ForgeError::Resource` covers resolver failures.
- `ForgeError::InvalidConfiguration` covers invalid runtime options such as non-positive custom page sizes.
- `ForgeError::Render`, `ForgeError::TypstCompile`, `ForgeError::TypstExport`, and `ForgeError::TypstAsset` cover backend/rendering failures.
- `ForgeError::OutputWrite`, `ForgeError::OutputDirectory`, and `ForgeError::InvalidOutputFileName` cover output persistence.

## Architecture note

Release work should preserve the internal boundary:

`api -> pipeline -> core -> adapters`