guanghulab/scripts/bridge/generate-pdf.js
2026-05-10 13:12:44 +08:00

164 lines
4.8 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.

// scripts/bridge/generate-pdf.js
// 🌉 桥接·Markdown → PDF 生成
//
// 读取 data/broadcasts/pdf/ 下的 Markdown 文件,
// 使用 md-to-pdf 转换为 PDF套用光湖广播模板
//
// 环境变量:
// MANIFEST_FILE fetch-broadcast.js 生成的清单文件路径
'use strict';
const fs = require('fs');
const path = require('path');
const OUTPUT_DIR = path.join('data', 'broadcasts', 'pdf');
// ══════════════════════════════════════════════════════════
// PDF 样式模板
// ══════════════════════════════════════════════════════════
const PDF_CSS = `
body {
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
line-height: 1.8;
color: #333;
padding: 40px;
}
h1 {
font-size: 24px;
border-bottom: 2px solid #1a73e8;
padding-bottom: 10px;
margin-bottom: 20px;
}
h2 { font-size: 20px; margin-top: 30px; }
h3 { font-size: 16px; margin-top: 20px; }
blockquote {
border-left: 4px solid #1a73e8;
padding: 10px 20px;
margin: 15px 0;
background: #f8f9fa;
color: #555;
}
code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
}
pre code {
display: block;
padding: 15px;
overflow-x: auto;
}
hr {
border: none;
border-top: 1px solid #ddd;
margin: 20px 0;
}
@page {
margin: 20mm;
@top-center { content: "光湖广播 · guanghulab.com"; font-size: 10px; color: #999; }
@bottom-center { content: counter(page) " / " counter(pages); font-size: 10px; color: #999; }
}
`;
// ══════════════════════════════════════════════════════════
// 主逻辑
// ══════════════════════════════════════════════════════════
async function main() {
const manifestFile = process.env.MANIFEST_FILE ||
path.join(OUTPUT_DIR, 'manifest.json');
if (!fs.existsSync(manifestFile)) {
console.log('📭 无 manifest.json跳过 PDF 生成');
process.exit(0);
}
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
if (manifest.length === 0) {
console.log('📭 清单为空,跳过 PDF 生成');
process.exit(0);
}
// 动态加载 md-to-pdf在 workflow 中安装)
let mdToPdf;
try {
mdToPdf = require('md-to-pdf').mdToPdf;
} catch (e) {
console.error('❌ md-to-pdf 未安装,请先运行: npm install md-to-pdf');
console.log('⚠️ 回退到纯文本 PDF 模式(将 Markdown 原文保留)');
// 回退:仅保留 .md 文件,标记为待处理
for (const item of manifest) {
if (fs.existsSync(item.file)) {
const pdfPath = item.file.replace(/\.md$/, '.pdf.pending');
fs.copyFileSync(item.file, pdfPath);
console.log(` 📝 ${pdfPath} (待转换)`);
}
}
return;
}
console.log(`📄 开始生成 ${manifest.length} 份 PDF…`);
let ok = 0, failed = 0;
const pdfFiles = [];
for (const item of manifest) {
if (!fs.existsSync(item.file)) {
console.log(` ⚠️ 文件不存在: ${item.file}`);
failed++;
continue;
}
try {
const pdfPath = item.file.replace(/\.md$/, '.pdf');
const result = await mdToPdf(
{ path: path.resolve(item.file) },
{
stylesheet: [],
css: PDF_CSS,
pdf_options: {
format: 'A4',
margin: { top: '25mm', bottom: '25mm', left: '20mm', right: '20mm' },
printBackground: true,
},
launch_options: { args: ['--no-sandbox', '--disable-setuid-sandbox'] },
}
);
if (result && result.content) {
fs.writeFileSync(pdfPath, result.content);
pdfFiles.push({ ...item, pdf: pdfPath });
console.log(`${pdfPath}`);
ok++;
} else {
console.error(` ❌ PDF 内容为空: ${item.file}`);
failed++;
}
} catch (e) {
console.error(` ❌ PDF 生成失败: ${item.file}: ${e.message}`);
failed++;
}
}
console.log(`\n✅ PDF 生成完成 · 成功 ${ok} 份 · 失败 ${failed}`);
// 更新 manifest 包含 PDF 路径
const pdfManifest = path.join(OUTPUT_DIR, 'pdf-manifest.json');
fs.writeFileSync(pdfManifest, JSON.stringify(pdfFiles, null, 2));
console.log(`📁 PDF 清单: ${pdfManifest}`);
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT,
`pdf_count=${ok}\npdf_manifest=${pdfManifest}\n`
);
}
}
main().catch(e => { console.error(e); process.exit(1); });