mdforge 0.1.0

Define, validate, and render typed Markdown extensions for LLM-generated content.
Documentation
# mdforge 説明書

`mdforge` は、LLM やユーザーが書いた拡張 Markdown を、アプリケーション側で安全に扱うための Rust ライブラリです。

普通の Markdown に独自のブロックやインライン記法を足したいとき、先に「許可する構文」と「引数の型」を定義しておくことで、入力をパース・検証し、最終的にアプリで扱いやすい DOM 風の中間表現 (`VNode`) に変換できます。

## 1. 何ができるか

`mdforge` が提供する主な機能は次のとおりです。

- 拡張ブロック記法 `:::name key=value ... :::` の定義
- 拡張インライン記法 `{name key=value ...}` の定義
- `Int` / `String` / `StaticEnum` / `DynamicEnum` による引数検証
- `parse -> validate -> eval -> render_dom` の処理パイプライン
- `Diagnostic` による構造化されたエラー情報の取得
- LLM に渡すための仕様文字列 `signature()` の生成

たとえば、LLM に「カード UI 用の Markdown を出して」と依頼したい場合でも、アプリ側では `card` ブロックや `badge` インラインだけを許可し、未定義の構文や型違いの値を検出できます。

## 2. インストール

`Cargo.toml` に依存関係を追加します。

```toml
[dependencies]
mdforge = "0.1.0"
```

ローカル開発中のリポジトリから使う場合は、パス依存にします。

```toml
[dependencies]
mdforge = { path = "../mdforge" }
```

## 3. 基本的な使い方

### 3.1 Forge を定義する

まず、アプリで許可する拡張 Markdown の仕様を `Forge::builder()` で定義します。

```rust
use mdforge::{ArgType, Forge};

let forge = Forge::builder()
    .block("card")
    .arg("title", ArgType::String.required())
    .arg("kind", ArgType::StaticEnum(&["info", "warn"]).required())
    .arg("ref", ArgType::DynamicEnum("items").required())
    .body_markdown()
    .register()
    .inline("badge")
    .arg("level", ArgType::Int.required())
    .register()
    .build();
```

この例では、次の構文を許可しています。

- `card` ブロック
- `card``title` 文字列引数
- `card``kind` 固定列挙引数
- `card``ref` 動的列挙引数
- `badge` インライン
- `badge``level` 整数引数

### 3.2 入力 Markdown を処理する

```rust
use std::collections::{HashMap, HashSet};
use mdforge::{EvalContext, Forge};

let input = ":::card title=hello kind=info ref=item-1\nBody {badge level=2}\n:::\n";

let doc = forge.parse(input)?;
forge.validate(&doc)?;

let mut dynamic_values = HashMap::new();
dynamic_values.insert(
    "items".to_string(),
    HashSet::from(["item-1".to_string(), "item-2".to_string()]),
);

let ctx = EvalContext { dynamic_values };
let evaluated = forge.eval(&doc, &ctx)?;
```

処理の役割は次のとおりです。

- `parse`: 拡張ブロックを含む入力を `Document` に変換します。
- `validate`: 未定義ブロック、未定義インライン、必須引数、未知引数、型、固定列挙値を検証します。
- `eval`: `DynamicEnum` の値が、実行時に渡された候補に含まれるかを検証します。
- `render_dom`: アプリ側の `DomRenderer` を使って `VNode` に変換します。

### 3.3 DOM に変換する

`mdforge` は HTML を直接生成しません。代わりに、アプリ側で `DomRenderer` を実装し、ブロックやインラインをどのような `VNode` に変換するかを決めます。

```rust
use mdforge::{BlockNode, EvalContext, InlineExt, VElement, VNode};
use mdforge::forge::DomRenderer;

struct MyRenderer;

impl DomRenderer for MyRenderer {
    fn render_block(&self, block: &BlockNode, _ctx: &EvalContext, children: Vec<VNode>) -> VNode {
        VNode::Element(VElement {
            tag: format!("x-{}", block.name),
            attrs: vec![],
            children,
        })
    }

    fn render_inline(&self, inline: &InlineExt, _ctx: &EvalContext) -> VNode {
        VNode::Element(VElement {
            tag: format!("x-inline-{}", inline.name),
            attrs: vec![],
            children: vec![],
        })
    }
}

let nodes = forge.render_dom(&doc, &evaluated, &MyRenderer)?;
```

`VNode` は次の 2 種類です。

- `VNode::Text(String)`
- `VNode::Element(VElement { tag, attrs, children })`

この中間表現を、HTML、JSON、React コンポーネントなどへ変換するのはアプリ側の責務です。

### 3.4 HTML に変換する

HTML を直接得たい場合は `render_html` と `HtmlRenderer` を使えます。通常の Markdown 部分は `pulldown-cmark` で HTML 化され、拡張ブロックと拡張インラインだけをアプリ側の renderer が担当します。

```rust
use mdforge::{BlockNode, EvalContext, HtmlRenderer, InlineExt};

struct MyHtmlRenderer;

impl HtmlRenderer for MyHtmlRenderer {
    fn render_block(&self, block: &BlockNode, _ctx: &EvalContext, children_html: String) -> String {
        format!("<section class=\"{}\">{}</section>", block.name, children_html)
    }

    fn render_inline(&self, inline: &InlineExt, _ctx: &EvalContext) -> String {
        format!("<span class=\"{}\"></span>", inline.name)
    }
}

let html = forge.render_html(&doc, &evaluated, &MyHtmlRenderer)?;
```

この API は、通常 Markdown と独自 UI 構文を混ぜた Web プレビューや、LLM 生成結果を HTML カードへ変換する用途に向いています。

## 4. 拡張 Markdown の書き方

### 4.1 ブロック

ブロックは行頭の `:::` で開始し、単独の `:::` 行で閉じます。

```markdown
:::card title=hello kind=info ref=item-1
本文 {badge level=2}
:::
```

ブロック引数は `key=value` 形式です。現在の実装では、値に空白を含めるクォート構文は扱いません。

### 4.2 インライン

インラインは `{name key=value}` 形式で書きます。

```markdown
本文の途中に {badge level=2} を置けます。
```

インラインは Markdown テキスト内から検出され、`render_dom` 時に `DomRenderer::render_inline` へ渡されます。

### 4.3 引数型

引数には次の型を指定できます。

||| 検証タイミング |
| --- | --- | --- |
| `ArgType::Int` | `level=2` | `validate` |
| `ArgType::String` | `title=hello` | `validate` |
| `ArgType::StaticEnum(&["info", "warn"])` | `kind=info` | `validate` |
| `ArgType::DynamicEnum("items")` | `ref=item-1` | `eval` |

`required()` を付けると必須引数、`optional()` を付けると任意引数になります。

```rust
.arg("title", ArgType::String.required())
.arg("level", ArgType::Int.optional())
```

## 5. LLM に仕様を渡す

`signature()` を使うと、定義済みの構文をテキストとして出力できます。

```rust
let spec = forge.signature();
println!("{spec}");
```

出力例:

```text
Block: card
:::card title=<string> kind=<info|warn> ref=<dynamic:items>
Body: markdown

Inline: badge
{badge level=<int>}
```

LLM に生成を依頼するときは、この `signature()` の出力をプロンプトに含めると、許可した構文に寄せやすくなります。そのうえで、LLM の出力を必ず `parse`、`validate`、`eval` に通してから利用してください。

## 6. エラーの読み方

各処理は失敗すると `Vec<Diagnostic>` を返します。

`Diagnostic` には次の情報が入ります。

- `level`: `Error` または `Warning`
- `code`: エラー種別
- `message`: 人間向けの説明
- `span`: 入力中のおおよその位置
- `suggestion`: 修正候補がある場合の補足

主な `ErrorCode` は次のとおりです。

| コード | 意味 |
| --- | --- |
| `UnknownBlock` | 定義されていないブロックです。 |
| `UnknownInline` | 定義されていないインラインです。 |
| `MissingRequiredArg` | 必須引数がありません。 |
| `UnknownArg` | 定義されていない引数です。 |
| `InvalidType` | 引数の型が合っていません。 |
| `InvalidStaticEnumValue` | 固定列挙値の候補に含まれていません。 |
| `InvalidDynamicEnumValue` | 動的列挙値の候補に含まれていません。 |
| `BlockNotClosed` | ブロックの閉じ `:::` がありません。 |

エラーを UI に出す場合は、`message` と `suggestion` を組み合わせるとユーザーに伝えやすくなります。

## 7. 実用時のおすすめフロー

LLM 連携やユーザー入力を扱うアプリでは、次の順序で使うのがおすすめです。

1. アプリ起動時または機能単位で `Forge` を構築する。
2. `signature()` を LLM プロンプトに含める。
3. 生成または入力された Markdown を `parse` する。
4. `validate` で静的な構文と型を確認する。
5. DB や設定から `EvalContext` を作り、`eval` で動的な参照を確認する。
6. `render_dom` でアプリ固有の `VNode` に変換する。
7. `Diagnostic` が返った場合は、該当箇所と修正候補を表示して再入力を促す。

## 8. 現在の制約

現時点の実装では、次の点に注意してください。

- Markdown 全体を完全に解釈するライブラリではなく、拡張ブロックと拡張インラインを中心に扱います。
- 引数は空白区切りの `key=value` 形式です。引用符付き文字列やエスケープは未対応です。
- インラインのネストは想定していません。
- `body_markdown()` はシグネチャや仕様表現に使われますが、一般的な Markdown パーサ相当の詳細なイベント分解は行いません。
- HTML への直接レンダリングは提供せず、`VNode` から先の変換はアプリ側で実装します。

`mdforge` は、独自 Markdown を「自由に書かせる」のではなく、「先に決めた仕様の範囲で安全に書かせる」ための土台です。特に LLM 出力をアプリ UI や構造化データへ接続する場面で役立ちます。