oicontest
Version:
OI Contest Management Tool
199 lines (198 loc) • 11 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.exportCommand = void 0;
const commander_1 = require("commander");
const config_1 = require("../lib/config");
const chalk_1 = __importDefault(require("chalk"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const archiver_1 = __importDefault(require("archiver"));
// @ts-ignore
const jsdom_1 = require("jsdom");
exports.exportCommand = new commander_1.Command('export')
.description('Export student.zip and teacher.zip for exam use')
.action(() => __awaiter(void 0, void 0, void 0, function* () {
try {
const contestDir = process.cwd();
const config = yield (0, config_1.loadConfig)(contestDir);
const htmlDir = path_1.default.join(contestDir, 'html');
const pdfDir = path_1.default.join(contestDir, 'pdf');
const additionalDir = path_1.default.join(contestDir, 'problem');
const studentZipPath = path_1.default.join(contestDir, 'student.zip');
const teacherZipPath = path_1.default.join(contestDir, 'teacher.zip');
const tempStudentDir = path_1.default.join(contestDir, '.export_student_tmp');
const tempTeacherDir = path_1.default.join(contestDir, '.export_teacher_tmp');
// 清理临时目录
yield fs_extra_1.default.remove(tempStudentDir);
yield fs_extra_1.default.remove(tempTeacherDir);
yield fs_extra_1.default.ensureDir(tempStudentDir);
yield fs_extra_1.default.ensureDir(tempTeacherDir);
// 1. 检查 PDF/HTML 文件
let pdfFound = false;
let htmlFound = false;
const htmlFiles = [];
const pdfFiles = [];
if (yield fs_extra_1.default.pathExists(htmlDir)) {
for (const file of yield fs_extra_1.default.readdir(htmlDir)) {
if (file.endsWith('.html')) {
htmlFound = true;
htmlFiles.push(path_1.default.join(htmlDir, file));
}
}
}
// 只查找 contest 根目录下的 pdf
for (const file of yield fs_extra_1.default.readdir(contestDir)) {
if (file.endsWith('.pdf')) {
pdfFound = true;
pdfFiles.push(path_1.default.join(contestDir, file));
}
}
if (!htmlFound) {
console.log(chalk_1.default.red('No HTML files found in html/ directory. Please run oicontest genhtml first.'));
return;
}
if (!pdfFound) {
console.log(chalk_1.default.red('No PDF files found in contest directory.'));
console.log(chalk_1.default.yellow('请用浏览器打开 html/ 下的 HTML 文件,打印为 PDF 并放在 contest 目录下。'));
return;
}
// 2. 复制 HTML 文件并处理图片/链接路径和 CSS
const htmlTargetDir = path_1.default.join(tempStudentDir);
const assetsTargetDir = path_1.default.join(tempStudentDir, 'assets');
yield fs_extra_1.default.ensureDir(htmlTargetDir);
yield fs_extra_1.default.ensureDir(assetsTargetDir);
// 收集dist/templates/html/static目录下所有文件,复制到assets
const cssSourceDir = path_1.default.resolve(__dirname, '../../templates/html/static');
const cssFiles = [];
if (yield fs_extra_1.default.pathExists(cssSourceDir)) {
for (const file of yield fs_extra_1.default.readdir(cssSourceDir)) {
cssFiles.push(file);
yield fs_extra_1.default.copy(path_1.default.join(cssSourceDir, file), path_1.default.join(assetsTargetDir, file));
}
}
for (const htmlFile of htmlFiles) {
const htmlContent = yield fs_extra_1.default.readFile(htmlFile, 'utf-8');
const dom = new jsdom_1.JSDOM(htmlContent);
const document = dom.window.document;
// 处理图片
document.querySelectorAll('img').forEach((img) => {
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) => {
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) => {
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);
}
yield fs_extra_1.default.writeFile(path_1.default.join(htmlTargetDir, path_1.default.basename(htmlFile)), dom.serialize(), 'utf-8');
}
// 3. 复制 PDF 文件
const pdfTargetDir = path_1.default.join(tempStudentDir);
yield fs_extra_1.default.ensureDir(pdfTargetDir);
for (const pdfFile of pdfFiles) {
yield fs_extra_1.default.copy(pdfFile, path_1.default.join(pdfTargetDir, path_1.default.basename(pdfFile)));
}
// 4. 复制 additional_file
const additionalTargetDir = path_1.default.join(tempStudentDir, 'additional');
yield fs_extra_1.default.ensureDir(additionalTargetDir);
for (const p of config.problems) {
const addSrc = path_1.default.join(contestDir, 'problem', p.id, 'additional_file');
const addDst = path_1.default.join(additionalTargetDir, p.id);
if (yield fs_extra_1.default.pathExists(addSrc)) {
yield fs_extra_1.default.copy(addSrc, addDst);
}
}
// 5. 打包 student.zip
yield zipDirectory(tempStudentDir, studentZipPath);
console.log(chalk_1.default.green(`student.zip generated at ${studentZipPath}`));
// 6. 复制 student.zip 内容到 teacher 临时目录
yield fs_extra_1.default.copy(tempStudentDir, tempTeacherDir);
// 7. 复制 solution 和 std.cpp
const solutionDir = path_1.default.join(tempTeacherDir, 'solution');
yield fs_extra_1.default.ensureDir(solutionDir);
for (const p of config.problems) {
const solSrc = path_1.default.join(contestDir, 'problem', p.id, 'solution');
const stdSrc = path_1.default.join(contestDir, 'problem', p.id, 'src', 'std.cpp');
const solDst = path_1.default.join(solutionDir, p.id);
const stdDst = path_1.default.join(solutionDir, p.id);
if (yield fs_extra_1.default.pathExists(solSrc)) {
yield fs_extra_1.default.copy(solSrc, solDst);
}
if (yield fs_extra_1.default.pathExists(stdSrc)) {
yield fs_extra_1.default.ensureDir(stdDst);
yield fs_extra_1.default.copy(stdSrc, path_1.default.join(stdDst, 'std.cpp'));
}
}
// 8. 打包 teacher.zip
yield zipDirectory(tempTeacherDir, teacherZipPath);
console.log(chalk_1.default.green(`teacher.zip generated at ${teacherZipPath}`));
// 9. 清理临时目录
yield fs_extra_1.default.remove(tempStudentDir);
yield fs_extra_1.default.remove(tempTeacherDir);
console.log(chalk_1.default.green('Export complete!'));
}
catch (err) {
console.error(chalk_1.default.red(`Export failed: ${err.message}`));
process.exit(1);
}
}));
function zipDirectory(source, out) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
const stream = fs_extra_1.default.createWriteStream(out);
archive.directory(source, false);
archive.on('error', err => reject(err));
stream.on('close', () => resolve());
archive.pipe(stream);
archive.finalize();
});
});
}