/**
* md-to-pdf.js
* ------------------------------------------------------------
* Markdownファイル(見出し/表/太字/リンク/箇条書き/水平線程度の書式)を
* 同名の .pdf として書き出します。外部npmパッケージ非依存(プロジェクトに
* package.jsonが無いことに合わせ、Node標準モジュールのみで完結させています)。
* PDF化にはOS標準のMicrosoft Edge(無ければGoogle Chrome)のヘッドレス印刷機能を使用します。
*
* 使い方:
* node md-to-pdf.js ./docs/site-475384_overview.md
* node md-to-pdf.js ./docs/*.md (シェルのグロブ展開に依存。複数ファイル可)
*
* 出力:
* 入力と同じフォルダに {同名}.pdf を生成(中間HTMLは生成後に削除)
*
* 注意:
* フォルダ名に # を含むパス(例: #GitHub)は file:// URL上でフラグメント区切りと
* 誤認識されるため、url.pathToFileURL() で必ずエンコードしてからブラウザに渡すこと。
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const os = require("os");
const url = require("url");
const { execFileSync } = require("child_process");
const CANDIDATE_BROWSERS = [
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
];
function findBrowser() {
for (const p of CANDIDATE_BROWSERS) {
if (fs.existsSync(p)) return p;
}
console.error("[エラー] Microsoft EdgeまたはGoogle Chromeが見つかりませんでした。");
console.error("インストール済みのブラウザパスをCANDIDATE_BROWSERSに追加してください。");
process.exit(1);
}
// --- 最小限のMarkdown→HTML変換(見出し/表/太字/リンク/箇条書き/水平線/段落のみ対応) ---
function escapeHtml(s) {
return s
.replace(/&/g, "&")
.replace(//g, ">");
}
function renderInline(text) {
let t = escapeHtml(text);
t = t.replace(/\*\*(.+?)\*\*/g, "$1");
t = t.replace(/`([^`]+)`/g, "$1");
t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
return t;
}
function isTableSeparator(line) {
return /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line) && line.includes("-");
}
function splitTableRow(line) {
let cells = line.trim();
if (cells.startsWith("|")) cells = cells.slice(1);
if (cells.endsWith("|")) cells = cells.slice(0, -1);
return cells.split("|").map((c) => c.trim());
}
function markdownToHtml(mdText) {
const lines = mdText.split(/\r?\n/);
const html = [];
let i = 0;
let inList = false;
function closeList() {
if (inList) {
html.push("");
inList = false;
}
}
while (i < lines.length) {
const line = lines[i];
if (/^\s*$/.test(line)) {
closeList();
i++;
continue;
}
const heading = line.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
closeList();
const level = heading[1].length;
html.push(`
| ${renderInline(c)} | `).join("") + "
|---|
| ${renderInline(c)} | `).join("") + "
${renderInline(line)}
`); i++; } closeList(); return html.join("\n"); } const CSS = ` @page { size: A4; margin: 18mm 16mm; } body { font-family: "Yu Gothic", "Meiryo", "Hiragino Sans", sans-serif; font-size: 10.5pt; line-height: 1.7; color: #1a1a1a; } h1 { font-size: 18pt; border-bottom: 3px solid #2a5599; padding-bottom: 6px; margin-top: 0; } h2 { font-size: 14pt; border-bottom: 1px solid #ccc; padding-bottom: 4px; margin-top: 28px; color: #2a5599; } h3 { font-size: 12pt; margin-top: 20px; } table { border-collapse: collapse; width: 100%; margin: 10px 0 16px; font-size: 9.5pt; } th, td { border: 1px solid #999; padding: 5px 8px; text-align: left; vertical-align: top; } th { background-color: #2a5599; color: #fff; } tr:nth-child(even) td { background-color: #f4f7fb; } ul, ol { margin: 6px 0; padding-left: 22px; } li { margin: 3px 0; } p { margin: 8px 0; } code { background: #eee; padding: 1px 4px; border-radius: 3px; } hr { border: none; border-top: 1px solid #ccc; margin: 20px 0; } a { color: #2a5599; } `; function convertOne(mdPath, browser) { const srcPath = path.resolve(mdPath); if (!fs.existsSync(srcPath)) { console.error(`[エラー] ファイルが見つかりません: ${srcPath}`); return false; } const mdText = fs.readFileSync(srcPath, "utf-8"); const titleMatch = mdText.match(/^#\s+(.+)$/m); const title = titleMatch ? titleMatch[1] : path.basename(srcPath); const bodyHtml = markdownToHtml(mdText); const htmlDoc = `