はじめに
前提知識: MDI とは? と コア概念 ―― IR・span・診断が何かを既に知っている前提です。Node.js 20 以降も必要です。
このページは MDI 2.0 を実装しています。規範的な人間向け仕様は SYNTAX.md です。以下のすべてのコマンドとコードはそのままコピーして実行できます ―― このリポジトリから実際に公開されているパッケージに対して動くもので、架空のものはありません。
1. .mdi ファイルを書く
Section titled “1. .mdi ファイルを書く”novel.mdi を作成します。
---mdi: "2.0"title: 雪女author: 小泉八雲lang: jawriting-mode: vertical---
# 第一章
{雪女|ゆき.おんな}が現れたのは、第^12^話のことだった。彼は[[em:決して]]忘れないと誓った。[[br]]その日は大安[[warichu:六曜の一つで吉日とされる]]であった。2. CLI で変換する
Section titled “2. CLI で変換する”CLI をグローバルにインストールします。
npm install --global @illusions-lab/mdi-cli実行します。
mdi build novel.mdi --to htmlWritten /path/to/novel.htmlCLI 自身の使用方法メッセージそのままの、完全なコマンド形式です。
mdi build <input.mdi> --to html|pdf|epub|docx|txt|txt-ruby|narou|kakuyomu|aozora|txt-all [--config export.json] [-o <output>]| フラグ | 意味 |
|---|---|
--to <format> | 必須。上記のいずれかの形式。 |
-o <path> | 省略可。出力パス。指定しない場合、入力ファイルのそばに形式の拡張子で保存されます ―― novel.mdi --to pdf は novel.pdf を、--to txt-ruby は novel_ruby.txt を書き出します(CLI はテキストの各バリアントを <stem>_<variant>.txt と命名し、プレーンな txt にはサフィックスがありません)。 |
--config <path> | 省略可。ページサイズ、フォント、マージン、テキストの字下げを制御するエクスポート・プロファイル JSON ファイルへのパス。 |
すべての出力形式を試してみます。
mdi build novel.mdi --to html # novel.htmlmdi build novel.mdi --to pdf # novel.pdfmdi build novel.mdi --to epub -o dist/novel.epub # dist/novel.epubmdi build novel.mdi --to docx # novel.docxmdi build novel.mdi --to txt # novel.txt ―― ルビは破棄mdi build novel.mdi --to txt-ruby # novel_ruby.txt ―― ルビは {base|reading} として保持mdi build novel.mdi --to narou # novel_narou.txt ―― 小説家になろうの記法mdi build novel.mdi --to kakuyomu # novel_kakuyomu.txt ―― カクヨムの記法mdi build novel.mdi --to aozora # novel_aozora.txt ―― 青空文庫の記法、Shift_JIS でエンコードmdi build novel.mdi --to txt-all # 6 種類のテキストをすべて書き出す。-o は拒否される各形式で実際に何が起きるか
Section titled “各形式で実際に何が起きるか”- HTML、TXT/
txt-ruby/narou/kakuyomu/aozora、EPUB、DOCX はすべて Rust コアが直接描画します(@illusions-lab/mdiのrenderHtml、renderTextFormat、renderEpub、renderDocx)―― CLI はその間で何も再解析・再解釈しません。 - PDF は同じ Rust 描画の HTML を、ローカルにインストールされた Chromium 系ブラウザに渡し、そのブラウザがページ分割と
printToPDFの呼び出しを行います。Chromium は.mdiソースを一切受け取らず、構文上の判断もしません。Chromium 系ブラウザが見つからない場合、コマンドは不足している依存関係を名指しするエラーで失敗します ―― 特定の実行ファイルを指す方法は レンダリングモデル を参照してください。 aozoraは書き出し時に Shift_JIS でエンコードされます。青空文庫自身の投稿ツールが期待する形式に合わせたものです。他のテキストバリアントはすべて UTF-8 で書き出されます。
何か問題が起きたとき
Section titled “何か問題が起きたとき”CLI はスタックトレースを表示しません。エラーは stderr へ1行書き出され、プロセスは終了コード 1 で終了します。
mdi build missing.mdi --to htmlENOENT: no such file or directory, open 'missing.mdi'mdi build novel.mdi --to svgUsage: mdi build <input.mdi> --to html|pdf|epub|docx|txt|txt-ruby|narou|kakuyomu|aozora|txt-all [--config export.json] [-o <output>]認識できない --to の値(や、その他の不正な引数列)は、意味を推測しようとせず、上記の使用方法を表示して終了コード 1 を返します。
3. JavaScript から解析・描画する
Section titled “3. JavaScript から解析・描画する”CLI に頼らずアプリケーションを構築する場合は、主要パッケージをインストールします。
npm install @illusions-lab/mdiimport { readFile } from "node:fs/promises";import { parse, renderHtml } from "@illusions-lab/mdi";
const source = await readFile("novel.mdi", "utf8");
const { document, diagnostics, syntaxVersion, irVersion } = parse(source);console.log(syntaxVersion, irVersion); // "2.0" "1.0"console.log(diagnostics); // このファイルには [] ―― 警告すべきものはないconsole.log(document.frontmatter.entries);// [{ key: "mdi", value: "2.0" }, { key: "title", value: "雪女" }, ...]
const html = renderHtml(source);parse はホスト側の Markdown parser を一切必要としません ―― CommonMark、GFM、フロントマター、MDI はすべて1回の parse 呼び出しの中で決定されます。通常の不正な構文は利用可能な文書と(大抵は空の)診断を返します。try/catch はプログラミングエラー、例えば文字列でない値を渡した場合のためにとっておきます。
parse(42); // TypeError: source must be a string を投げるすべてのエクスポート関数(renderEpub、renderDocx、renderText、renderTextFormat、serializeMdi)の完全なシグネチャと例は Bindings: JavaScript / TypeScript を参照してください。
4. フロントマターの解説
Section titled “4. フロントマターの解説”novel.mdi の先頭のフロントマターブロックは、同じ parse() 呼び出しの中で解析される通常の YAML です。
---mdi: "2.0"title: 雪女author: 小泉八雲lang: jawriting-mode: vertical---mdiは文書が対象とする構文バージョンを宣言します。省略すると、パーサーは自身が対応する最新バージョンを前提とします。パーサーが対応するより新しいバージョンを宣言すると、mdi.version.unsupportedという警告診断が出ます ―― 解析はベストエフォートで続行されます(診断参照)。writing-mode: verticalはrenderHtmlのレイアウト方法を変えます(ルート要素にwriting-mode: vertical-rl)。これが、縦中横や傍点がそもそも存在する理由です ―― どちらも縦書き特有の組版デバイスであり、横書きでも自然に劣化して動作します。- キーの順序と未知のキーは
document.frontmatter.entriesに保持されます。レンダラーは認識しないキーがあってもエラーにせず無視します。
5. 任意: unified/remark パイプラインへの組み込み
Section titled “5. 任意: unified/remark パイプラインへの組み込み”すでに mdast ノードを期待する unified パイプライン(Astro、静的サイトジェネレーター、remark ベースの lint ツールなど)がある場合以外は、この節は読み飛ばしてかまいません。@illusions-lab/mdi-remark はアダプターであり第二のパーサーではありません ―― 同じ Rust の parse() を呼び出し、その結果を mdast へ整形し直すだけです。
import { unified } from "unified";import remarkMdi from "@illusions-lab/mdi-remark";import remarkStringify from "remark-stringify";
const processor = unified().use(remarkMdi).use(remarkStringify);const tree = processor.parse(await readFile("novel.mdi", "utf8"));何が正しくラウンドトリップし、何がしないかを含む詳細は Remark / mdast アダプター にあります。
次のステップ
Section titled “次のステップ”- 完全構文リファレンス ―― 上の
novel.mdiで使ったすべての構文を1つずつ解説します。 - Rust 主導アーキテクチャ ―― 「文法は1つ、実装も1つ」の背後にある所有権規則。
- エクスポート・プロファイル ――
--configでページサイズ、フォント、マージンを制御します。