0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
498 lines
22 KiB
JavaScript
498 lines
22 KiB
JavaScript
const fs = require('fs');
|
|
const { execSync } = require('child_process');
|
|
const path = require('path');
|
|
const puppeteer = require('puppeteer');
|
|
|
|
console.log('=== MD to PDF Converter Starting ===');
|
|
console.log('Node.js version:', process.version);
|
|
console.log('Working directory:', process.cwd());
|
|
console.log('Input path:', process.argv[2]);
|
|
console.log('Output path:', process.argv[3]);
|
|
|
|
// Arguments: node md_to_pdf.js <input_md_path> <output_pdf_path>
|
|
const inputPath = process.argv[2];
|
|
const outputPath = process.argv[3];
|
|
|
|
if (!inputPath || !outputPath) {
|
|
console.error('Usage: node md_to_pdf.js <input_md_path> <output_pdf_path>');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Processing Markdown: ${inputPath}`);
|
|
|
|
(async () => {
|
|
try {
|
|
console.log('Reading input file...');
|
|
let mdContent = fs.readFileSync(inputPath, 'utf8');
|
|
console.log(`File read successfully, length: ${mdContent.length} characters`);
|
|
|
|
// 1. Protect Math Blocks
|
|
const mathBlocks = [];
|
|
const placeholderPrefix = 'MATHBLOCK_PLACEHOLDER_';
|
|
|
|
mdContent = mdContent.replace(/\$\$([\s\S]*?)\$\$/g, (match, p1) => {
|
|
const id = mathBlocks.length;
|
|
mathBlocks.push(`$$${p1}$$`);
|
|
return `${placeholderPrefix}${id}`;
|
|
});
|
|
|
|
mdContent = mdContent.replace(/\$([^\$\n]+?)\$/g, (match, p1) => {
|
|
const id = mathBlocks.length;
|
|
mathBlocks.push(`$${p1}$`);
|
|
return `${placeholderPrefix}${id}`;
|
|
});
|
|
console.log(`Protected ${mathBlocks.length} math blocks`);
|
|
|
|
// 2. Convert to HTML using marked (CLI via npx or library?)
|
|
// Since we are in a container, we should use the library directly if possible,
|
|
// but the reference uses npx. To avoid npx/network dependency at runtime,
|
|
// we will require 'marked' from node_modules (assuming we verify it's installed).
|
|
const marked = require('marked');
|
|
console.log('Parsing markdown content...');
|
|
let finalHtml = marked.parse(mdContent);
|
|
console.log('Markdown parsed successfully');
|
|
|
|
// 3. Restore Math Blocks
|
|
mathBlocks.forEach((block, index) => {
|
|
finalHtml = finalHtml.replace(`${placeholderPrefix}${index}`, block);
|
|
});
|
|
|
|
// 4. Fix Mermaid syntax
|
|
finalHtml = finalHtml.replace(
|
|
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
|
(match, content) => {
|
|
content = content.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(/>/g, '>')
|
|
.replace(/</g, '<')
|
|
.replace(/&/g, '&');
|
|
return `<div class="mermaid">${content}</div>`;
|
|
}
|
|
);
|
|
|
|
// 5. Wrap in Template
|
|
const template = `
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Document</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-light.min.css">
|
|
|
|
<!-- Mermaid -->
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
|
|
<!-- MathJax -->
|
|
<script>
|
|
window.MathJax = {
|
|
tex: {
|
|
inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
|
|
displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
|
|
processEscapes: false
|
|
},
|
|
startup: {
|
|
pageReady: () => {
|
|
return MathJax.startup.defaultPageReady().then(() => {
|
|
const div = document.createElement('div');
|
|
div.id = 'mathjax-finished';
|
|
div.style.display = 'none';
|
|
document.body.appendChild(div);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
|
|
|
|
<style>
|
|
body {
|
|
box-sizing: border-box;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
.mermaid {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin: 20px 0;
|
|
}
|
|
table {
|
|
width: 100% !important;
|
|
display: table !important;
|
|
}
|
|
</style>
|
|
|
|
<!-- Embedded Mermaid Library -->
|
|
<script>
|
|
// This is a minimal stub to prevent errors when mermaid is referenced but not available
|
|
if (typeof window.mermaid === 'undefined') {
|
|
window.mermaid = {
|
|
initialize: function() {},
|
|
init: function() {},
|
|
render: function() {}
|
|
};
|
|
}
|
|
</script>
|
|
|
|
<!-- MathJax configuration and library -->
|
|
<script>
|
|
window.MathJax = {
|
|
tex: {
|
|
inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
|
|
displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
|
|
processEscapes: false
|
|
},
|
|
startup: {
|
|
pageReady: () => {
|
|
return MathJax.startup.defaultPageReady().then(() => {
|
|
const div = document.createElement('div');
|
|
div.id = 'mathjax-finished';
|
|
div.style.display = 'none';
|
|
document.body.appendChild(div);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
|
|
</head>
|
|
<body class="markdown-body">
|
|
${finalHtml}
|
|
<script>
|
|
// Initialize mermaid if it's available
|
|
if (typeof mermaid !== 'undefined') {
|
|
mermaid.initialize({ startOnLoad: true, theme: 'default', securityLevel: 'loose' });
|
|
} else {
|
|
console.log('Mermaid library not loaded, skipping initialization');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
|
|
console.log('Template prepared, starting PDF generation...');
|
|
|
|
// 6. Generate PDF with Puppeteer
|
|
console.log('Starting Puppeteer browser launch...');
|
|
const browser = await puppeteer.launch({
|
|
executablePath: '/usr/bin/chromium-browser', // Alpine location
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-background-timer-throttling',
|
|
'--disable-renderer-backgrounding',
|
|
'--disable-backgrounding-occluded-windows',
|
|
'--memory-pressure-off',
|
|
'--js-flags=--max-old-space-size=4096', // 增加内存限制
|
|
'--enable-features=NetworkService',
|
|
'--disable-features=VizDisplayCompositor',
|
|
'--disable-gpu',
|
|
'--disable-web-security',
|
|
'--disable-features=VizDisplayCompositor'
|
|
],
|
|
headless: 'new',
|
|
timeout: 120000 // Increased timeout for containerized environment
|
|
});
|
|
console.log('Browser launched successfully');
|
|
|
|
const page = await browser.newPage();
|
|
console.log('Page created successfully');
|
|
|
|
// ページのビューポートとユーザーエージェントを設定
|
|
await page.setViewport({ width: 1200, height: 800 });
|
|
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
|
|
console.log('Viewport and user agent set');
|
|
|
|
// さまざまなタイムアウトを設定 - 長時間の待機を避けるためにデフォルト値を低下
|
|
await page.setDefaultNavigationTimeout(30000); // 30秒
|
|
await page.setDefaultTimeout(30000); // 30秒
|
|
console.log('Timeouts configured');
|
|
|
|
// すべての外部リソースの読み込みをブロックするリクエストをインターセプト
|
|
await page.setRequestInterception(true);
|
|
page.on('request', (req) => {
|
|
// すべての外部リソース要求を完全にブロック(CDNリソースを含む)してネットワークタイムアウトを回避
|
|
const url = req.url();
|
|
|
|
if (url.startsWith('http') || url.startsWith('https') || url.startsWith('ftp')) {
|
|
// すべての外部リクエストに空白のレスポンスを返して、ネットワークタイムアウトエラーを回避
|
|
req.respond({
|
|
status: 200,
|
|
contentType: 'text/plain',
|
|
body: ''
|
|
}).catch(() => {});
|
|
} else {
|
|
// ローカルおよびdata URLリソースを許可
|
|
req.continue().catch(() => {});
|
|
}
|
|
});
|
|
console.log('Request interception configured to block all external resources');
|
|
|
|
// エラーイベントを監視
|
|
page.on('error', (error) => {
|
|
console.error('Page error:', error);
|
|
});
|
|
|
|
page.on('pageerror', (error) => {
|
|
console.error('Page error event:', error);
|
|
});
|
|
|
|
page.on('console', (msg) => {
|
|
console.log('Browser console:', msg.text());
|
|
});
|
|
|
|
console.log('Error listeners attached');
|
|
|
|
// 再試行メカニズム
|
|
let success = false;
|
|
let attempts = 0;
|
|
const maxAttempts = 3;
|
|
|
|
while (!success && attempts < maxAttempts) {
|
|
attempts++;
|
|
console.log(`Attempt ${attempts} of ${maxAttempts} for PDF generation...`);
|
|
console.log(`HTML template length: ${template.length} characters`);
|
|
|
|
try {
|
|
console.log('About to navigate to data URL...');
|
|
// 外部リソースを待たずに高速なナビゲーションオプションを使用
|
|
await page.goto(`data:text/html;charset=UTF-8,${encodeURIComponent(template)}`, {
|
|
waitUntil: 'domcontentloaded', // 等待DOM加载完成,但不等待资源
|
|
timeout: 30000 // Reduced timeout for faster failure
|
|
});
|
|
console.log('Page loaded successfully');
|
|
|
|
// 画像の読み込みを待機(タイムアウトあり、読み込み失敗の画像は素早くスキップ)
|
|
try {
|
|
console.log('Checking for images to load...');
|
|
await page.evaluate(async () => {
|
|
const images = Array.from(document.querySelectorAll('img'));
|
|
console.log(`Found ${images.length} images on the page`);
|
|
if (images.length > 0) {
|
|
// すべての画像の読み込みを待つのではなく、短時間だけ待って次に進む
|
|
await new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
console.log(`Continuing after attempting to load ${images.length} images`);
|
|
resolve();
|
|
}, 500); // 只等待500ms,不管图像是否加载完成
|
|
});
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.warn('Error checking images:', e.message);
|
|
}
|
|
|
|
// MathJaxのレンダリングを待機(タイムアウトあり)
|
|
console.log('Checking for MathJax...');
|
|
let mathjaxFinished = false;
|
|
let mermaidProcessed = false; // 移动变量声明到这里
|
|
try {
|
|
// ページに数式が含まれているか確認(MathJaxは通常、$...$または$$...$$形式の数式を処理します)
|
|
const hasMathContent = await page.evaluate(() => {
|
|
const html = document.documentElement.innerHTML;
|
|
// 数学記号のタグを確認
|
|
return html.includes('$') || html.includes('\\(') || html.includes('\\[') ||
|
|
html.includes('\\begin{') || html.includes('math-tex') ||
|
|
document.querySelectorAll('mjx-container').length > 0 ||
|
|
document.querySelectorAll('[class*="math"]').length > 0;
|
|
});
|
|
|
|
console.log(`Math content found: ${hasMathContent}`);
|
|
|
|
if (hasMathContent) {
|
|
console.log('Math content detected, waiting for MathJax...');
|
|
|
|
// 特定のセレクタを無限に待つのではなく、MathJaxの初期化に合理的な時間を待機
|
|
await new Promise(r => setTimeout(r, 1000)); // 短暂等待1秒
|
|
|
|
// MathJaxが存在するか再度確認
|
|
const mathjaxExists = await page.evaluate(() => typeof window.MathJax !== 'undefined');
|
|
|
|
if (mathjaxExists) {
|
|
// MathJaxが存在する場合、レンダリング完了を待機
|
|
await page.evaluate(async () => {
|
|
if (window.MathJax && window.MathJax.Hub) {
|
|
await window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub]);
|
|
} else if (window.MathJax && window.MathJax.typesetPromise) {
|
|
await window.MathJax.typesetPromise();
|
|
}
|
|
});
|
|
|
|
console.log('MathJax typesetting completed');
|
|
mathjaxFinished = true;
|
|
} else {
|
|
console.log('MathJax not found after content check');
|
|
}
|
|
} else {
|
|
console.log('No math content found, skipping MathJax wait');
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error checking MathJax:', e.message);
|
|
}
|
|
|
|
// MathJaxが完了していない場合、追加の時間を待機
|
|
if (!mathjaxFinished) {
|
|
console.log('Waiting 1 second before generating PDF...');
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
|
|
// Mermaidが完了していない場合、追加の時間を待機
|
|
if (!mermaidProcessed) {
|
|
console.log('Waiting 1 second before generating PDF...');
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
|
|
// 等待 Mermaid 图表渲染
|
|
console.log('Checking for Mermaid diagrams...');
|
|
try {
|
|
// ページにMermaidチャートコンテナがあるか確認
|
|
const mermaidElementsCount = await page.evaluate(() => document.querySelectorAll('.mermaid').length);
|
|
console.log(`Mermaid diagrams found: ${mermaidElementsCount > 0}`);
|
|
|
|
if (mermaidElementsCount > 0) {
|
|
console.log(`Processing ${mermaidElementsCount} Mermaid diagrams...`);
|
|
|
|
// Mermaidライブラリが存在するか確認し、初期化を試みる
|
|
const mermaidExists = await page.evaluate(() => typeof mermaid !== 'undefined');
|
|
|
|
if (mermaidExists) {
|
|
console.log('Mermaid library found, attempting to initialize...');
|
|
|
|
await page.evaluate(async () => {
|
|
// mermaidオブジェクトが存在するか確認
|
|
if (typeof mermaid !== 'undefined' && mermaid.init) {
|
|
try {
|
|
// Mermaidチャートの初期化を試みる
|
|
mermaid.init(undefined, '.mermaid');
|
|
} catch (e) {
|
|
console.log('Mermaid init error:', e.message);
|
|
}
|
|
} else {
|
|
console.log('Mermaid library not fully loaded, skipping initialization');
|
|
}
|
|
|
|
// レンダリング完了を待機(最大5秒)
|
|
const startTime = Date.now();
|
|
while (Date.now() - startTime < 5000) {
|
|
// 未完成のMermaidチャートがあるか確認
|
|
const incompleteCharts = document.querySelectorAll('.mermaid:not(.mermaid-loaded)');
|
|
if (incompleteCharts.length === 0) {
|
|
break;
|
|
}
|
|
// 等待一小段时间后重试
|
|
await new Promise(r => setTimeout(r, 100));
|
|
}
|
|
});
|
|
} else {
|
|
console.log('Mermaid library not found in document, skipping processing');
|
|
}
|
|
|
|
console.log('Mermaid diagrams processed');
|
|
mermaidProcessed = true;
|
|
} else {
|
|
console.log('No Mermaid diagrams found, skipping wait');
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error processing Mermaid:', e.message);
|
|
}
|
|
|
|
// 等待页面基本渲染完成(不等待所有外部资源)
|
|
console.log('Waiting for basic page content to be loaded...');
|
|
try {
|
|
// complete状態ではなくDOMContentLoadedイベントを待機
|
|
await page.waitForFunction(() => document.readyState !== 'loading', { timeout: 10000 }); // Reduced timeout
|
|
console.log('Page DOM loaded, readyState is not loading');
|
|
} catch (e) {
|
|
console.warn('DOM did not finish loading, continuing...', e.message);
|
|
}
|
|
|
|
// 确保所有异步操作完成后再生成PDF
|
|
console.log('Waiting 2 seconds before generating PDF...');
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
console.log('Generating PDF file...');
|
|
await page.pdf({
|
|
path: outputPath,
|
|
format: 'A4',
|
|
printBackground: true,
|
|
scale: 0.75, // Scale down to fit more content
|
|
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
|
|
timeout: 120000
|
|
});
|
|
console.log('PDF generated successfully');
|
|
|
|
success = true;
|
|
console.log(`PDF successfully generated at ${outputPath}`);
|
|
|
|
} catch (error) {
|
|
console.error(`Attempt ${attempts} failed:`, error.message);
|
|
console.error(`Error stack:`, error.stack);
|
|
|
|
// 致命的なエラーの場合は再試行不要
|
|
if (error.message.includes('Protocol error') ||
|
|
error.message.includes('Target closed') ||
|
|
error.message.includes('Browser closed') ||
|
|
error.message.includes('Connection closed') ||
|
|
error.message.includes('Navigation failed') ||
|
|
error.message.includes('net::ERR_CONNECTION_CLOSED')) {
|
|
console.error('Fatal browser error occurred, aborting retries');
|
|
throw error;
|
|
}
|
|
|
|
if (attempts >= maxAttempts) {
|
|
// すべての再試行が失敗した場合、最も簡略化された方法を試す
|
|
console.log('All attempts failed, trying most basic PDF generation...');
|
|
console.log('Creating a new page for basic method...');
|
|
|
|
// 重新创建页面以确保干净的状态
|
|
const basicPage = await browser.newPage();
|
|
await basicPage.setViewport({ width: 1200, height: 800 });
|
|
await basicPage.setDefaultNavigationTimeout(60000);
|
|
|
|
await basicPage.goto(`data:text/html;charset=UTF-8,${encodeURIComponent(template)}`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 120000 // Increased timeout for containerized environment
|
|
});
|
|
|
|
// 等待一段较短的时间
|
|
console.log('Waiting 2 seconds in basic method...');
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
try {
|
|
console.log('Generating PDF with basic method...');
|
|
await basicPage.pdf({
|
|
path: outputPath,
|
|
format: 'A4',
|
|
printBackground: true,
|
|
scale: 0.75,
|
|
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
|
|
timeout: 300000 // Increased timeout for containerized environment
|
|
});
|
|
success = true;
|
|
console.log(`PDF generated using basic method at ${outputPath}`);
|
|
await basicPage.close();
|
|
} catch (basicError) {
|
|
console.error('Basic PDF generation also failed:', basicError.message);
|
|
console.error('Basic error stack:', basicError.stack);
|
|
await basicPage.close();
|
|
throw basicError;
|
|
}
|
|
} else {
|
|
// 一定時間待機してから再試行(システムが復旧する時間を与える)
|
|
const delay = 10000 * attempts; // 逐次的に遅延時間を増加
|
|
console.log(`Waiting ${delay}ms before retry...`);
|
|
await new Promise(r => setTimeout(r, delay));
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Closing browser...');
|
|
await browser.close();
|
|
console.log('Browser closed');
|
|
console.log('=== MD to PDF Conversion Completed Successfully ===');
|
|
|
|
} catch (err) {
|
|
console.error('Error during conversion:', err);
|
|
console.error('Error stack:', err.stack);
|
|
process.exit(1);
|
|
}
|
|
})(); |