ken_nogi/Pleasanter/.claude/js/md-to-docx.js
Kenichiro NOGI ce58cb4be4 初回コミット: dev配下(NodeSrv/Pleasanter等)をGitea管理下に統合
GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。
NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは
別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter
インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:37:06 +09:00

140 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* md-to-docx.js
* ------------------------------------------------------------
* Markdownファイル見出し/表/太字/リンク/箇条書き/水平線程度の書式)を
* 同名の .docx として書き出します。Markdown→HTML変換はmd-to-pdf.jsの
* markdownToHtml()を再利用外部npmパッケージ非依存
* docx化にはOS標準のMicrosoft WordCOM自動化を使用します。
* ClaudePleasanter直下に置く共通実装。全プロジェクトの薄いラッパーから require される。
* 入出力パスは引数で受け取るのみのためbaseDir/layoutに依存しない。
*
* 使い方:
* node md-to-docx.js ./docs/site-475384_overview.md
* node md-to-docx.js ./docs/*.md (シェルのグロブ展開に依存。複数ファイル可)
*
* 出力:
* 入力と同じフォルダに {同名}.docx を生成中間HTML・PowerShellスクリプトは生成後に削除
* ------------------------------------------------------------
*/
const fs = require("fs");
const path = require("path");
const os = require("os");
const { execFileSync } = require("child_process");
const { markdownToHtml } = require("./md-to-pdf.js");
const CSS = `
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; }
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 escapeHtml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// PowerShellのシングルクォート文字列リテラル用エスケープ'を''にする)
function psSingleQuote(s) {
return `'${String(s).replace(/'/g, "''")}'`;
}
function convertOne(mdPath) {
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 = `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>${escapeHtml(title)}</title>
<style>${CSS}</style>
</head>
<body>
${bodyHtml}
</body>
</html>`;
const tmpId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const tmpHtmlPath = path.join(os.tmpdir(), `md-to-docx-${tmpId}.html`);
const tmpPs1Path = path.join(os.tmpdir(), `md-to-docx-${tmpId}.ps1`);
// SaveAs2はパスに # を含むと内部でURLのフラグメント区切りと誤認識し失敗するため、
// 一旦 # を含まないTempフォルダへ保存してから最終的な保存先へ移動する。
const tmpDocxPath = path.join(os.tmpdir(), `md-to-docx-${tmpId}.docx`);
const docxPath = srcPath.replace(/\.md$/i, ".docx");
fs.writeFileSync(tmpHtmlPath, htmlDoc, "utf-8");
// wdFormatXMLDocument = 12 (.docx)
const ps1 = `
$ErrorActionPreference = "Stop"
$word = New-Object -ComObject Word.Application
$word.Visible = $false
try {
$doc = $word.Documents.Open(${psSingleQuote(tmpHtmlPath)})
$doc.SaveAs2(${psSingleQuote(tmpDocxPath)}, 12)
$doc.Close()
} finally {
$word.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($word) | Out-Null
}
`;
fs.writeFileSync(tmpPs1Path, ps1, "utf-8");
try {
execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpPs1Path], {
stdio: "pipe",
});
fs.copyFileSync(tmpDocxPath, docxPath);
console.log(`[OK] docx生成: ${docxPath}`);
return true;
} catch (err) {
console.error(`[エラー] docx生成に失敗しました: ${srcPath}`);
console.error(err.stderr ? err.stderr.toString() : err.message);
return false;
} finally {
fs.unlinkSync(tmpHtmlPath);
fs.unlinkSync(tmpPs1Path);
if (fs.existsSync(tmpDocxPath)) fs.unlinkSync(tmpDocxPath);
}
}
if (require.main === module) {
const targets = process.argv.slice(2);
if (targets.length === 0) {
console.error("[エラー] 変換対象のMarkdownファイルを引数で指定してください。");
console.error("例: node md-to-docx.js ./docs/site-475384_overview.md");
process.exit(1);
}
let ok = true;
for (const target of targets) {
ok = convertOne(target) && ok;
}
if (!ok) process.exit(1);
}
module.exports = { convertOne };