コンテンツにスキップ

はじめに

前提知識: MDI とは?コア概念 ―― IR・span・診断が何かを既に知っている前提です。Node.js 20 以降も必要です。

このページは MDI 2.0 を実装しています。規範的な人間向け仕様は SYNTAX.md です。以下のすべてのコマンドとコードはそのままコピーして実行できます ―― このリポジトリから実際に公開されているパッケージに対して動くもので、架空のものはありません。

novel.mdi を作成します。

---
mdi: "2.0"
title: 雪女
author: 小泉八雲
lang: ja
writing-mode: vertical
---
# 第一章
{雪女|ゆき.おんな}が現れたのは、第^12^話のことだった。
彼は[[em:決して]]忘れないと誓った。[[br]]
その日は大安[[warichu:六曜の一つで吉日とされる]]であった。

CLI をグローバルにインストールします。

Terminal window
npm install --global @illusions-lab/mdi-cli

実行します。

Terminal window
mdi build novel.mdi --to html
Written /path/to/novel.html

CLI 自身の使用方法メッセージそのままの、完全なコマンド形式です。

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 pdfnovel.pdf を、--to txt-rubynovel_ruby.txt を書き出します(CLI はテキストの各バリアントを <stem>_<variant>.txt と命名し、プレーンな txt にはサフィックスがありません)。
--config <path>省略可。ページサイズ、フォント、マージン、テキストの字下げを制御するエクスポート・プロファイル JSON ファイルへのパス。

すべての出力形式を試してみます。

Terminal window
mdi build novel.mdi --to html # novel.html
mdi build novel.mdi --to pdf # novel.pdf
mdi build novel.mdi --to epub -o dist/novel.epub # dist/novel.epub
mdi build novel.mdi --to docx # novel.docx
mdi 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 は拒否される
  • HTML、TXT/txt-ruby/narou/kakuyomu/aozora、EPUB、DOCX はすべて Rust コアが直接描画します(@illusions-lab/mdirenderHtmlrenderTextFormatrenderEpubrenderDocx)―― CLI はその間で何も再解析・再解釈しません。
  • PDF は同じ Rust 描画の HTML を、ローカルにインストールされた Chromium 系ブラウザに渡し、そのブラウザがページ分割と printToPDF の呼び出しを行います。Chromium は .mdi ソースを一切受け取らず、構文上の判断もしません。Chromium 系ブラウザが見つからない場合、コマンドは不足している依存関係を名指しするエラーで失敗します ―― 特定の実行ファイルを指す方法は レンダリングモデル を参照してください。
  • aozora は書き出し時に Shift_JIS でエンコードされます。青空文庫自身の投稿ツールが期待する形式に合わせたものです。他のテキストバリアントはすべて UTF-8 で書き出されます。

CLI はスタックトレースを表示しません。エラーは stderr へ1行書き出され、プロセスは終了コード 1 で終了します。

Terminal window
mdi build missing.mdi --to html
ENOENT: no such file or directory, open 'missing.mdi'
Terminal window
mdi build novel.mdi --to svg
Usage: mdi build <input.mdi> --to html|pdf|epub|docx|txt|txt-ruby|narou|kakuyomu|aozora|txt-all [--config export.json] [-o <output>]

認識できない --to の値(や、その他の不正な引数列)は、意味を推測しようとせず、上記の使用方法を表示して終了コード 1 を返します。

CLI に頼らずアプリケーションを構築する場合は、主要パッケージをインストールします。

Terminal window
npm install @illusions-lab/mdi
import { 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 を投げる

すべてのエクスポート関数(renderEpubrenderDocxrenderTextrenderTextFormatserializeMdi)の完全なシグネチャと例は Bindings: JavaScript / TypeScript を参照してください。

novel.mdi の先頭のフロントマターブロックは、同じ parse() 呼び出しの中で解析される通常の YAML です。

---
mdi: "2.0"
title: 雪女
author: 小泉八雲
lang: ja
writing-mode: vertical
---
  • mdi は文書が対象とする構文バージョンを宣言します。省略すると、パーサーは自身が対応する最新バージョンを前提とします。パーサーが対応するより新しいバージョンを宣言すると、mdi.version.unsupported という警告診断が出ます ―― 解析はベストエフォートで続行されます(診断参照)。
  • writing-mode: verticalrenderHtml のレイアウト方法を変えます(ルート要素に 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 アダプター にあります。