# 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
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 や構造化データへ接続する場面で役立ちます。