calibreのEPUBが嫌いすぎて、chatgptと対話しながらEPUB作成ソフトを作るなどをしていた。エラーはありつつEPUB作成まではできた・。
you:
node.jsでmarkdown texをEPUBに変換するコードを書くには
chatgpt :
Node.js で Markdown + TeX(LaTeX 数式)を EPUB に変換するなら、Markdown を HTML に変換し、数式を MathML に変換してから EPUB 3 を生成する構成がおすすめです。
電気工学の問題集のように、見出し・箇条書き・表・数式・SVG 図を含む教材を作る場合も、この構成なら拡張できます。
1. 変換の構成
Markdown ファイル(.md)
見出し・表・LaTeX 数式・SVG
Markdown → HTML
markdown-it + 数式プラグイン
LaTeX → MathML
MathJax(数式を EPUB 内で表示)
EPUB 3 の生成
章分割・目次・CSS・画像・数式
完成した .epub
重要なのは、EPUB 内で JavaScript の数式描画に依存しないことです。MathJax で MathML に変換して EPUB に格納すれば、対応するリーダーでは数式を直接表示できます。ただし、MathML の対応状況は EPUB リーダーによって異なります。
2. 必要なパッケージをインストールする
まずは、1つの Markdown ファイルを EPUB に変換する最小構成から作ります。
mkdir md-tex-epub
cd md-tex-epub
npm init -y
npm install markdown-it markdown-it-texmath katex epub-gen
各ライブラリの役割は次のとおりです。
| ライブラリ |
役割 |
markdown-it |
Markdown を HTML に変換 |
markdown-it-texmath |
$...$ や $$...$$ の数式を処理 |
katex |
数式を HTML・MathML に描画 |
epub-gen |
HTML から EPUB を生成 |
epub-gen は HTML を入力にして EPUB を生成できる Node.js 用ライブラリです。
image
GitHub
3. Markdown ファイルを用意する
sample.md を作成します。
# 電気工学の基礎
## 1. オームの法則
電圧 $V$、電流 $I$、抵抗 $R$ の間には、
次の関係が成り立つ。
$$
V = RI
$$
## 2. 電力
電力 $P$ は次の式で求められる。
$$
P = VI = RI^2 = \frac{V^2}{R}
$$
## 3. 比較表
| 物理量 | 記号 | 単位 |
|---|---|---|
| 電圧 | $V$ | V |
| 電流 | $I$ | A |
| 抵抗 | $R$ | Ω |
## 4. まとめ
オームの法則を利用すると、電圧・電流・抵抗を
相互に計算できる。
4. EPUB 変換コードを書く
convert.mjs を作成します。
import fs from "node:fs/promises";
import path from "node:path";
import MarkdownIt from "markdown-it";
import texmath from "markdown-it-texmath";
import katex from "katex";
import Epub from "epub-gen";
const inputFile = process.argv[2] ?? "sample.md";
const outputFile = process.argv[3] ?? "sample.epub";
async function main() {
const source = await fs.readFile(inputFile, "utf8");
const md = new MarkdownIt({
html: false,
breaks: false,
linkify: true,
});
// LaTeX 数式を MathML を含む HTML に変換
md.use(texmath, {
engine: katex,
delimiters: "dollars",
katexOptions: {
output: "htmlAndMathml",
throwOnError: false,
},
});
const html = md.render(source);
const epubCss = `
body {
font-family: serif;
line-height: 1.8;
margin: 5%;
}
h1, h2, h3 {
line-height: 1.4;
page-break-after: avoid;
}
p {
margin: 0.6em 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
th, td {
border: 1px solid #888;
padding: 0.35em;
}
img, svg {
max-width: 100%;
height: auto;
}
pre, code {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.katex-display {
overflow-x: auto;
overflow-y: hidden;
margin: 1em 0;
}
`;
const epub = new Epub(
{
title: path.basename(inputFile, path.extname(inputFile)),
author: "著者名",
lang: "ja",
tocTitle: "目次",
css: epubCss,
content: [
{
title: "本文",
data: `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:epub="http://www.idpf.org/2007/ops"
lang="ja" xml:lang="ja">
<head>
<meta charset="UTF-8"/>
<title>本文</title>
</head>
<body>
${html}
</body>
</html>`,
},
],
},
outputFile
);
await epub.promise;
console.log(`EPUBを生成しました: ${outputFile}`);
}
main().catch((error) => {
console.error("変換に失敗しました:", error);
process.exitCode = 1;
});
実行方法は次のとおりです。
node convert.mjs sample.md sample.epub
注意点: これは最小構成の実装例です。実際の EPUB リーダーでの数式表示は別途確認してください。KaTeX の HTML 表示には CSS やフォントの同梱も必要で、上のコードでは KaTeX のフォントファイルを EPUB に格納していません。また、数式の出力方式や EPUB リーダーによって表示結果が異なります。
image
GitHub
+1
5. 本格的な変換ツールにするなら
今後、問題集や技術書を継続して作成するなら、次の機能を追加すると使いやすくなります。
- 章分割:
# や ## ごとに XHTML ファイルを生成し、目次を作成する。
- 数式対応: インライン数式・独立数式・複数行数式・行列・場合分けを処理する。
- 図表: Markdown の表、SVG、画像、コードブロックを EPUB に格納する。
- 日本語対応: 縦書き・横書き、ルビ、禁則処理、改ページを CSS で設定する。
- 互換性: EPUBCheck で EPUB 3 の構造を検証し、Kindle などの実機でも表示を確認する。
なお、最初から多くの機能を自作する必要はありません。日本語組版と数式を重視するなら、既存の Markdown → EPUB ツールである Kappan も参考になります。MathML や数式の出力方式、Kindle との互換性について実装例があります。
image
GitHub
確認したいのは1点です。 作りたいのはどちらでしょうか?
用途
電気工学の問題集・技術書(数式、表、SVG 図を重視)
小説(青空文庫形式、ルビ、縦書きを重視)
一般的な Markdown ファイルの EPUB 変換ツール
本文のレイアウト
横書き EPUB
縦書き EPUB
横書き・縦書きを切り替えられる
この条件で実装案を作る
you:
Node.js の Markdown + TeX → EPUB 変換ツールを具体的に実装したい。用途: 電気工学の問題集・技術書。数式・表・SVG 図を重視。レイアウト: 横書き。実行可能な Node.js コード、必要な依存関係、EPUB 互換性を含めて提案して。
chatgpt:
この用途なら、単純に epub-gen で HTML を包むだけでなく、数式の出力形式を選択できる構造にしておくのが重要です。
特に Kindle では MathML だけに頼ると数式が表示されない場合があります。KaTeX の HTML+MathML 出力を利用する方法や、数式を SVG に変換する方法を用意しておくと、用途に応じて対応できます。
image
GitHub
+1
また、Node.js で実装する場合、EPUB 生成ライブラリの内部処理にすべてを任せるより、Markdown の解析と EPUB のパッケージングを分離した方が、後から章分割や SVG 処理を改善しやすくなります。
推奨する実装方針
- Markdown の見出しから章を分割し、EPUB の目次を生成する。
- 数式は KaTeX で描画し、MathML と HTML の両方を保持する。
- SVG は画像として参照するだけでなく、EPUB 内の画像ファイルとして格納する。
- 数式・表が横幅を超えた場合の CSS を用意する。
最後に EPUBCheck で構造を検証する。
image
W3C
1. プロジェクト構成
まずは Windows 上の Node.js で動作する、横書き技術書向けの変換ツールを作ります。
md-tex-epub/
├─ package.json
├─ convert.mjs
├─ book.json
├─ input/
│ ├─ 00-index.md
│ ├─ 01-electricity.md
│ ├─ 02-motors.md
│ └─ images/
│ └─ circuit.svg
└─ output/
└─ textbook.epub
この実装では、入力ファイルを章ごとに分けます。章単位で Markdown ファイルを管理すると、長い問題集でも修正しやすくなります。
2. ライブラリのインストール
Node.js 22 以降を推奨します。KaTeX の現行ドキュメントでも、Node.js 環境での利用方法が案内されています。
image
KaTeX
mkdir md-tex-epub
cd md-tex-epub
npm init -y
npm install markdown-it markdown-it-texmath katex epub-gen
package.json に ESM の設定を追加します。
{
"name": "md-tex-epub",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node convert.mjs"
},
"dependencies": {
"epub-gen": "^0.1.0",
"katex": "^0.16.0",
"markdown-it": "^14.0.0",
"markdown-it-texmath": "^1.0.0"
}
}
上記のバージョン指定は初期設定の例です。実際にインストールされたバージョンは package-lock.json で固定してください。既存の package.json を使う場合は、手動でバージョンを置き換えるより、先ほどの npm install で生成された依存関係を使うのが確実です。
3. 書籍の設定ファイル
book.json を作成します。
{
"title": "電気工学の基礎",
"author": "著者名",
"language": "ja",
"inputDir": "input",
"outputDir": "output",
"outputFile": "textbook.epub"
}
この設定により、書名や著者名をソースコードから分離できます。
4. 変換プログラム
次のコードは、章ごとの Markdown 読み込み、数式処理、画像パスの解決、EPUB 生成をまとめた初期実装です。
convert.mjs
import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import MarkdownIt from "markdown-it";
import texmath from "markdown-it-texmath";
import katex from "katex";
import Epub from "epub-gen";
const ROOT = process.cwd();
const config = JSON.parse(
await fs.readFile(path.join(ROOT, "book.json"), "utf8")
);
const inputDir = path.resolve(ROOT, config.inputDir);
const outputDir = path.resolve(ROOT, config.outputDir);
const outputFile = path.join(outputDir, config.outputFile);
await fs.mkdir(outputDir, { recursive: true });
const md = new MarkdownIt({
html: false,
linkify: true,
typographer: false,
});
md.use(texmath, {
engine: katex,
delimiters: ["dollars", "brackets"],
katexOptions: {
output: "htmlAndMathml",
throwOnError: false,
strict: "warn",
},
});
// Markdown 内の相対画像パスを file: URL に変換する。
// epub-gen がローカル画像を取り込めるようにする。
const defaultImageRender =
md.renderer.rules.image ??
((tokens, idx, options, env, self) =>
self.renderToken(tokens, idx, options));
md.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx];
const src = token.attrGet("src");
if (
src &&
!/^(?:https?:|data:|file:|#)/i.test(src)
) {
const sourceFile = env.sourceFile;
const imagePath = path.resolve(
path.dirname(sourceFile),
decodeURIComponent(src.split("#")[0].split("?")[0])
);
token.attrSet("src", pathToFileURL(imagePath).href);
}
return defaultImageRender(tokens, idx, options, env, self);
};
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
function getTitle(markdown, filename) {
const match = markdown.match(/^#\s+(.+)$/m);
return match
? match[1].trim()
: path.basename(filename, ".md");
}
function renderChapter(markdown, filename) {
const html = md.render(markdown, {
sourceFile: filename,
});
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
lang="ja" xml:lang="ja">
<head>
<meta charset="UTF-8"/>
<title>${escapeHtml(getTitle(markdown, filename))}</title>
</head>
<body>
${html}
</body>
</html>`;
}
const css = `
body {
font-family: serif;
line-height: 1.75;
margin: 4%;
overflow-wrap: break-word;
}
h1, h2, h3, h4 {
line-height: 1.4;
page-break-after: avoid;
break-after: avoid;
}
h1 {
font-size: 1.6em;
margin-top: 0;
}
h2 {
font-size: 1.3em;
margin-top: 1.8em;
}
p {
margin: 0.7em 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
font-size: 0.9em;
}
th, td {
border: 1px solid #888;
padding: 0.35em;
vertical-align: top;
}
th {
font-weight: bold;
}
img, svg {
max-width: 100%;
height: auto;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.katex-display {
margin: 1em 0;
overflow-x: auto;
overflow-y: hidden;
text-align: center;
}
blockquote {
margin: 1em 0;
padding-left: 1em;
border-left: 3px solid #888;
}
`;
const filenames = (await fs.readdir(inputDir))
.filter((name) => name.toLowerCase().endsWith(".md"))
.sort((a, b) => a.localeCompare(b, "en"));
if (filenames.length === 0) {
throw new Error(`Markdown ファイルがありません: ${inputDir}`);
}
const chapters = [];
for (const filename of filenames) {
const fullPath = path.join(inputDir, filename);
const markdown = await fs.readFile(fullPath, "utf8");
chapters.push({
title: getTitle(markdown, filename),
data: renderChapter(markdown, fullPath),
});
console.log(`読み込み: ${filename}`);
}
const epub = new Epub(
{
title: config.title,
author: config.author,
lang: config.language ?? "ja",
version: 3,
tocTitle: "目次",
appendChapterTitles: false,
css,
content: chapters,
},
outputFile
);
await epub.promise;
console.log(`EPUB を生成しました: ${outputFile}`);
実行します。
npm run build
これで output/textbook.epub が生成されます。
このコードは実装の土台です。実際の EPUB リーダーでの表示テストは必要で、特に数式フォントの同梱や SVG の取り込みについては次の手順で補強してください。
5. 数式・表・SVG の記法
数式
入力 Markdown では、一般的な LaTeX 記法を使います。
## 直流回路の電力
電力は電圧と電流の積である。
$$
P = VI
$$
オームの法則 $V=RI$ を代入すると、
$$
P = RI^2 = \frac{V^2}{R}
$$
複数行の式は次のように記述できる。
$$
\begin{aligned}
V &= RI \\
P &= VI \\
&= RI^2
\end{aligned}
$$
markdown-it-texmath は KaTeX を使い、インライン数式・独立数式・複数行の数式環境を処理できます。
image
GitHub
表
| 物理量 | 記号 | 単位 |
|---|---|---|
| 電圧 | $V$ | V |
| 電流 | $I$ | A |
| 抵抗 | $R$ | $\Omega$ |
| 電力 | $P$ | W |
Markdown の表は HTML のテーブルとして出力されます。複雑な表や列数の多い表は、画面の幅に収まらない場合があるため、EPUB リーダーで確認してください。
SVG 図
Markdown から SVG ファイルを参照します。
## 直流回路

circuit.svg は input/images/ に置きます。
SVG は EPUB 3 で使用できますが、リーダーごとに対応状況が異なります。SVG の取り込みが成功しても、図の内部に外部ファイルや外部フォントを参照する記述がある場合は、追加の処理が必要です。
6. EPUB 互換性で必ず対応したいこと
ここは重要です。上記のコードは EPUB を生成するところまでですが、生成できたことと、すべての端末で数式が正しく表示されることは別です。
| 項目 |
現状 |
本格運用での対応 |
| EPUB 3 |
生成対象 |
EPUBCheck で検証 |
| 数式 |
KaTeX の HTML+MathML |
CSS・フォントを同梱 |
| 表 |
HTML テーブル |
横幅と改ページを確認 |
| SVG |
画像参照 |
EPUB 内の格納・表示を確認 |
| 目次 |
章ファイル単位 |
必要なら節単位の目次も作る |
| Kindle |
未検証 |
数式の SVG 化などを検討 |
KaTeX は CSS とフォントファイルを利用して数式を表示するため、これらを EPUB に含める処理を追加してください。
image
KaTeX
また、Kindle 対応を優先する場合は、数式を MathML だけで出力するのではなく、HTML と CSS を併記する方式、または数式 SVG を埋め込む方式を検討します。Kappan の技術書向け実装にも、数式の出力方式とリーダー別の互換性に関する説明があります。
image
GitHub
最後に EPUBCheck で検証します。
java -jar epubcheck.jar output/textbook.epub
EPUBCheck は EPUB 2 / EPUB 3 の構造を検証する公式ツールです。
image
W3C
7. さらに本格的に作る場合の選択肢
日本語の技術書を長期的に制作するなら、既存の Kappan も比較対象になります。Markdown から EPUB 3.3 を生成することを目的にしており、横組み技術書の例に KaTeX 数式・表・図・相互参照が含まれています。
image
GitHub
自作するメリットは、電気工学の問題集に合わせて、問題番号・解答・解説・数式番号・図番号・参照リンクなどを自由に追加できることです。
私なら、まず上記のコードをベースにして、次の順番で拡張します。
- KaTeX の CSS とフォントを EPUB 内に格納する。
- 数式を HTML+MathML と SVG のどちらで出力するか選択できるようにする。
- Markdown の見出しから章・節の目次を生成する。
- SVG と表の表示テストを追加する。
- EPUBCheck をビルド手順に組み込む。
これなら、単なる Markdown 変換ではなく、電気工学の問題集を継続的に出版するための EPUB 生成ツールへ発展させられます。