/** * md-to-pdf.js * ------------------------------------------------------------ * Markdownファイル(見出し/表/太字/リンク/箇条書き/水平線程度の書式)を * 同名の .pdf として書き出します。外部npmパッケージ非依存(プロジェクトに * package.jsonが無いことに合わせ、Node標準モジュールのみで完結させています)。 * PDF化にはOS標準のMicrosoft Edge(無ければGoogle Chrome)のヘッドレス印刷機能を使用します。 * ClaudePleasanter直下に置く共通実装。全プロジェクトの薄いラッパーから require される。 * 入出力パスは引数で受け取るのみのためbaseDir/layoutに依存しない。 * * 使い方: * 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(heading[2])}`); i++; continue; } if (/^\s*---+\s*$/.test(line)) { closeList(); html.push("
"); i++; continue; } // テーブル: 次行がセパレータ行であること if (line.includes("|") && i + 1 < lines.length && isTableSeparator(lines[i + 1])) { closeList(); const headerCells = splitTableRow(line); html.push("" + headerCells.map((c) => ``).join("") + ""); i += 2; while (i < lines.length && lines[i].includes("|") && !/^\s*$/.test(lines[i])) { const rowCells = splitTableRow(lines[i]); html.push("" + rowCells.map((c) => ``).join("") + ""); i++; } html.push("
${renderInline(c)}
${renderInline(c)}
"); continue; } const listItem = line.match(/^\s*-\s+(.*)$/); if (listItem) { if (!inList) { html.push("