UNPKG

oicontest

Version:

OI Contest Management Tool

192 lines (180 loc) 8.4 kB
import { Command } from 'commander'; import { loadConfig } from '../lib/config'; import chalk from 'chalk'; import fs from 'fs-extra'; import path from 'path'; import archiver from 'archiver'; // @ts-ignore import { JSDOM } from 'jsdom'; export const exportCommand = new Command('export') .description('Export student.zip and teacher.zip for exam use') .action(async () => { try { const contestDir = process.cwd(); const config = await loadConfig(contestDir); const htmlDir = path.join(contestDir, 'html'); const pdfDir = path.join(contestDir, 'pdf'); const additionalDir = path.join(contestDir, 'problem'); const studentZipPath = path.join(contestDir, 'student.zip'); const teacherZipPath = path.join(contestDir, 'teacher.zip'); const tempStudentDir = path.join(contestDir, '.export_student_tmp'); const tempTeacherDir = path.join(contestDir, '.export_teacher_tmp'); // 清理临时目录 await fs.remove(tempStudentDir); await fs.remove(tempTeacherDir); await fs.ensureDir(tempStudentDir); await fs.ensureDir(tempTeacherDir); // 1. 检查 PDF/HTML 文件 let pdfFound = false; let htmlFound = false; const htmlFiles: string[] = []; const pdfFiles: string[] = []; if (await fs.pathExists(htmlDir)) { for (const file of await fs.readdir(htmlDir)) { if (file.endsWith('.html')) { htmlFound = true; htmlFiles.push(path.join(htmlDir, file)); } } } // 只查找 contest 根目录下的 pdf for (const file of await fs.readdir(contestDir)) { if (file.endsWith('.pdf')) { pdfFound = true; pdfFiles.push(path.join(contestDir, file)); } } if (!htmlFound) { console.log(chalk.red('No HTML files found in html/ directory. Please run oicontest genhtml first.')); return; } if (!pdfFound) { console.log(chalk.red('No PDF files found in contest directory.')); console.log(chalk.yellow('请用浏览器打开 html/ 下的 HTML 文件,打印为 PDF 并放在 contest 目录下。')); return; } // 2. 复制 HTML 文件并处理图片/链接路径和 CSS const htmlTargetDir = path.join(tempStudentDir); const assetsTargetDir = path.join(tempStudentDir, 'assets'); await fs.ensureDir(htmlTargetDir); await fs.ensureDir(assetsTargetDir); // 收集dist/templates/html/static目录下所有文件,复制到assets const cssSourceDir = path.resolve(__dirname, '../../templates/html/static'); const cssFiles: string[] = []; if (await fs.pathExists(cssSourceDir)) { for (const file of await fs.readdir(cssSourceDir)) { cssFiles.push(file); await fs.copy(path.join(cssSourceDir, file), path.join(assetsTargetDir, file)); } } for (const htmlFile of htmlFiles) { const htmlContent = await fs.readFile(htmlFile, 'utf-8'); const dom = new JSDOM(htmlContent); const document = dom.window.document; // 处理图片 document.querySelectorAll('img').forEach((img: any) => { const src = img.getAttribute('src'); if (src && src.startsWith('/')) { // 绝对路径,转换为相对 additional 路径 const match = src.match(/problem\/(.+?)\/additional_file\/(.+)$/); if (match) { const pid = match[1]; const fname = match[2]; img.setAttribute('src', `./additional/${pid}/${fname}`); } } }); // 处理 a 标签 document.querySelectorAll('a').forEach((a: any) => { const href = a.getAttribute('href'); if (href && href.startsWith('/')) { const match = href.match(/problem\/(.+?)\/additional_file\/(.+)$/); if (match) { const pid = match[1]; const fname = match[2]; a.setAttribute('href', `./additional/${pid}/${fname}`); } } }); // 处理css引用 document.querySelectorAll('link[rel="stylesheet"]').forEach((link: any) => { const href = link.getAttribute('href'); if (href && (href.endsWith('.css') || href.includes('.css?'))) { const cssName = href.split('/').pop().split('?')[0]; if (cssFiles.includes(cssName)) { link.setAttribute('href', `./assets/${cssName}`); } } }); // 插入复制按钮CSS和JS const copyBtnCss = `<style>\n.copy-btn { position: absolute; right: 8px; top: 8px; padding: 2px 8px; font-size: 12px; cursor: pointer; background: #eee; border: 1px solid #ccc; border-radius: 4px; z-index: 10; }\n.copy-btn:active { background: #ddd; }\n@media print { .copy-btn { display: none !important; } }\n.pre-block { position: relative; }\n</style>`; const copyBtnJs = `<script>\ndocument.querySelectorAll('pre > code').forEach(function(codeBlock) {\n var pre = codeBlock.parentElement;\n pre.classList.add('pre-block');\n var btn = document.createElement('button');\n btn.innerText = '复制';\n btn.className = 'copy-btn';\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.innerText);\n btn.innerText = '已复制!';\n setTimeout(() => btn.innerText = '复制', 1000);\n };\n pre.appendChild(btn);\n});\n</script>`; // 插入到head和body末尾 if (document.head) { document.head.insertAdjacentHTML('beforeend', copyBtnCss); } if (document.body) { document.body.insertAdjacentHTML('beforeend', copyBtnJs); } await fs.writeFile(path.join(htmlTargetDir, path.basename(htmlFile)), dom.serialize(), 'utf-8'); } // 3. 复制 PDF 文件 const pdfTargetDir = path.join(tempStudentDir); await fs.ensureDir(pdfTargetDir); for (const pdfFile of pdfFiles) { await fs.copy(pdfFile, path.join(pdfTargetDir, path.basename(pdfFile))); } // 4. 复制 additional_file const additionalTargetDir = path.join(tempStudentDir, 'additional'); await fs.ensureDir(additionalTargetDir); for (const p of config.problems) { const addSrc = path.join(contestDir, 'problem', p.id, 'additional_file'); const addDst = path.join(additionalTargetDir, p.id); if (await fs.pathExists(addSrc)) { await fs.copy(addSrc, addDst); } } // 5. 打包 student.zip await zipDirectory(tempStudentDir, studentZipPath); console.log(chalk.green(`student.zip generated at ${studentZipPath}`)); // 6. 复制 student.zip 内容到 teacher 临时目录 await fs.copy(tempStudentDir, tempTeacherDir); // 7. 复制 solution 和 std.cpp const solutionDir = path.join(tempTeacherDir, 'solution'); await fs.ensureDir(solutionDir); for (const p of config.problems) { const solSrc = path.join(contestDir, 'problem', p.id, 'solution'); const stdSrc = path.join(contestDir, 'problem', p.id, 'src', 'std.cpp'); const solDst = path.join(solutionDir, p.id); const stdDst = path.join(solutionDir, p.id); if (await fs.pathExists(solSrc)) { await fs.copy(solSrc, solDst); } if (await fs.pathExists(stdSrc)) { await fs.ensureDir(stdDst); await fs.copy(stdSrc, path.join(stdDst, 'std.cpp')); } } // 8. 打包 teacher.zip await zipDirectory(tempTeacherDir, teacherZipPath); console.log(chalk.green(`teacher.zip generated at ${teacherZipPath}`)); // 9. 清理临时目录 await fs.remove(tempStudentDir); await fs.remove(tempTeacherDir); console.log(chalk.green('Export complete!')); } catch (err: any) { console.error(chalk.red(`Export failed: ${err.message}`)); process.exit(1); } }); async function zipDirectory(source: string, out: string): Promise<void> { return new Promise((resolve, reject) => { const archive = archiver('zip', { zlib: { level: 9 } }); const stream = fs.createWriteStream(out); archive.directory(source, false); archive.on('error', err => reject(err)); stream.on('close', () => resolve()); archive.pipe(stream); archive.finalize(); }); }