import fs from "fs/promises";
import os from "os";
import path from "path";
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..");
const docsDir = path.join(rootDir, "docs");
const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const currentDate = "2026-04-03";
const documents = [
{
source: path.join(docsDir, "system-operation-manual.md"),
output: path.join(docsDir, "system-operation-manual.pdf"),
docCode: "UNIS-CRM-UM-2026",
subtitle: "正式交付版",
audience: "业务用户 / 管理人员",
},
{
source: path.join(docsDir, "system-construction.md"),
output: path.join(docsDir, "system-construction.pdf"),
docCode: "UNIS-CRM-BUILD-2026",
subtitle: "正式交付版",
audience: "项目组 / 运维 / 交付人员",
},
];
function escapeHtml(value) {
return value
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function inlineMarkdownToHtml(text, baseDir) {
let html = escapeHtml(text);
html = html.replace(/`([^`]+)`/g, (_, code) => `${escapeHtml(code)}`);
html = html.replace(/\*\*([^*]+)\*\*/g, "$1");
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => {
const resolved = resolveHref(href, baseDir);
return `${escapeHtml(label)}`;
});
return html;
}
function resolveHref(href, baseDir) {
if (/^(https?:|file:|data:|#)/i.test(href)) {
return href;
}
const absolutePath = path.resolve(baseDir, href);
return `file://${absolutePath}`;
}
function slugify(text, fallbackIndex) {
const normalized = text
.toLowerCase()
.replace(/[`~!@#$%^&*()+=[\]{};:'"\\|,.<>/?,。;:()【】《》?、\s]+/g, "-")
.replace(/^-+|-+$/g, "");
return normalized || `section-${fallbackIndex}`;
}
function extractTitle(markdown) {
const match = markdown.match(/^#\s+(.+)$/m);
return match ? match[1].trim() : "文档";
}
function parseBlocks(markdown, baseDir) {
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
const blocks = [];
let index = 0;
while (index < lines.length) {
const line = lines[index];
const trimmed = line.trim();
if (!trimmed) {
index += 1;
continue;
}
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
blocks.push({
type: "heading",
level: headingMatch[1].length,
text: headingMatch[2].trim(),
});
index += 1;
continue;
}
const codeMatch = trimmed.match(/^```(\w+)?$/);
if (codeMatch) {
const language = codeMatch[1] || "";
index += 1;
const content = [];
while (index < lines.length && !lines[index].trim().startsWith("```")) {
content.push(lines[index]);
index += 1;
}
if (index < lines.length) {
index += 1;
}
blocks.push({
type: "code",
language,
content: content.join("\n"),
});
continue;
}
const imageMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
if (imageMatch) {
blocks.push({
type: "image",
alt: imageMatch[1].trim(),
src: resolveHref(imageMatch[2].trim(), baseDir),
});
index += 1;
continue;
}
if (
trimmed.includes("|") &&
index + 1 < lines.length &&
/^\|?[\s:-]+(\|[\s:-]+)+\|?$/.test(lines[index + 1].trim())
) {
const tableLines = [trimmed];
index += 2;
while (index < lines.length && lines[index].trim().includes("|")) {
tableLines.push(lines[index].trim());
index += 1;
}
blocks.push({
type: "table",
rows: tableLines.map((row) =>
row
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim()),
),
});
continue;
}
const listMatch = trimmed.match(/^([-*]|\d+\.)\s+(.+)$/);
if (listMatch) {
const ordered = /\d+\./.test(listMatch[1]);
const items = [];
while (index < lines.length) {
const itemMatch = lines[index].trim().match(/^([-*]|\d+\.)\s+(.+)$/);
if (!itemMatch) {
break;
}
items.push(itemMatch[2].trim());
index += 1;
}
blocks.push({
type: "list",
ordered,
items,
});
continue;
}
const paragraphLines = [trimmed];
index += 1;
while (index < lines.length) {
const next = lines[index].trim();
if (
!next ||
/^#{1,6}\s+/.test(next) ||
/^```/.test(next) ||
/^!\[/.test(next) ||
/^([-*]|\d+\.)\s+/.test(next) ||
(
next.includes("|") &&
index + 1 < lines.length &&
/^\|?[\s:-]+(\|[\s:-]+)+\|?$/.test(lines[index + 1].trim())
)
) {
break;
}
paragraphLines.push(next);
index += 1;
}
blocks.push({
type: "paragraph",
text: paragraphLines.join(" "),
});
}
return blocks;
}
function buildContentAndToc(blocks, baseDir) {
const toc = [];
const html = [];
let headingIndex = 0;
let skippedTitle = false;
for (const block of blocks) {
if (block.type === "heading") {
headingIndex += 1;
if (block.level === 1 && !skippedTitle) {
skippedTitle = true;
continue;
}
const id = slugify(block.text, headingIndex);
if (block.level <= 3) {
toc.push({ level: block.level, text: block.text, id });
}
html.push(`
${inlineMarkdownToHtml(block.text, baseDir)}
`); continue; } if (block.type === "list") { const tag = block.ordered ? "ol" : "ul"; const items = block.items .map((item) => `${escapeHtml(block.content)}${escapeHtml(block.content)}`,
);
}
continue;
}
if (block.type === "table") {
const [headerRow, ...bodyRows] = block.rows;
const thead = `紫光汇智客户关系管理平台
${escapeHtml(options.subtitle)}