UNPKG

oimp

Version:

A CLI tool for generating OI problem and packages

280 lines (264 loc) 14.5 kB
/** * 题目编辑命令:支持题目信息、标签、checker类型、时间/内存限制等交互式编辑, * 并允许修改题目ID(即目录名),如有变更则自动重命名目录。 * 支持 checker 类型切换和模板文件补全,流程与 init.js 保持一致。 */ const path = require("path"); const fs = require("fs"); const inquirer = require("inquirer"); const yaml = require("js-yaml"); const chalk = require("chalk"); const { mkdirp } = require("mkdirp"); const { getTemplate, writeFileWithDir, writeYaml, updateChecklistStatus } = require("../utils"); module.exports = async function editCommand(problemName) { // 1. 读取原始目录和配置 const oldProblemDir = path.join(process.cwd(), problemName); const problemYamlPath = path.join(oldProblemDir, "problem.yaml"); const testDataDir = path.join(oldProblemDir, "testdata"); const configYamlPath = path.join(testDataDir, "config.yaml"); // 检查目录是否存在 if (!fs.existsSync(oldProblemDir)) { console.error(chalk.red(`题目目录 ${problemName} 不存在`)); process.exit(1); } // 读取现有配置 let currentConfig = { title: problemName, tag: ["入门"] }; let currentLimits = { time: "1000ms", memory: "256m" }; let currentChecker = ""; try { if (fs.existsSync(problemYamlPath)) { const loaded = yaml.load(fs.readFileSync(problemYamlPath, "utf-8")) || {}; currentConfig = { ...currentConfig, ...loaded }; } if (fs.existsSync(configYamlPath)) { currentLimits = { ...currentLimits, ...(yaml.load(fs.readFileSync(configYamlPath, "utf-8")) || {}) }; } // 检查 checker 类型 const checkerPath = path.join(oldProblemDir, "src", "checker.cpp"); if (fs.existsSync(checkerPath)) { // 简单猜测checker类型 const checkerContent = fs.readFileSync(checkerPath, "utf-8"); const checkerList = [ "ncmp.cpp","wcmp.cpp","fcmp.cpp","rcmp.cpp","rcmp4.cpp","rcmp6.cpp","rcmp9.cpp","rncmp.cpp","uncmp.cpp","yesno.cpp","acmp.cpp","caseicmp.cpp","casencmp.cpp","casewcmp.cpp","dcmp.cpp","hcmp.cpp","icmp.cpp","lcmp.cpp","nyesno.cpp","pointscmp.cpp","pointsinfo.cpp" ]; for (const c of checkerList) { if (checkerContent.includes(c.replace('.cpp',''))) { currentChecker = c; break; } } } } catch (e) { console.error(chalk.yellow("配置文件读取错误,将使用默认值:"), e.message); } // 提取当前标签中的自定义标签(不在预设标签列表中的标签) const presetTags = ["入门", "普及-", "普及/提高-", "普及+/提高", "提高+/省选-", "省选/NOI-", "NOI/NOI+/CTSC", "动态规划", "图论", "数据结构", "数学", "基础算法", "字符串", "其他"]; const currentCustomTags = currentConfig.tag.filter(tag => !presetTags.includes(tag)); const currentPresetTags = currentConfig.tag.filter(tag => presetTags.includes(tag)); // 2. 交互式编辑(题目ID、标题、标签、checker、限制等) const answers = await inquirer.prompt([ { type: "input", name: "newProblemName", message: "题目ID(可修改,修改后将重命名目录):", default: problemName, validate: (input) => input && input.trim() ? true : "题目ID不能为空" }, { type: "input", name: "title", message: "题目标题:", default: currentConfig.title, }, { type: "checkbox", name: "tags", message: "题目标签:", choices: [ { name: "入门", value: "入门" }, { name: "普及-", value: "普及-" }, { name: "普及/提高-", value: "普及/提高-" }, { name: "普及+/提高", value: "普及+/提高" }, { name: "提高+/省选-", value: "提高+/省选-" }, { name: "省选/NOI-", value: "省选/NOI-" }, { name: "NOI/NOI+/CTSC", value: "NOI/NOI+/CTSC" }, new inquirer.Separator(), { name: "动态规划", value: "动态规划" }, { name: "图论", value: "图论" }, { name: "数据结构", value: "数据结构" }, { name: "数学", value: "数学" }, { name: "基础算法", value: "基础算法" }, { name: "字符串", value: "字符串" }, { name: "其他", value: "其他" }, ], default: currentPresetTags, }, { type: "input", name: "othertag", message: "其他分类标签(用空格或,分割多个):", default: currentCustomTags.join(' '), }, { type: "input", name: "time", message: "时间限制 (如1000ms):", default: currentLimits.time, validate: (input) => /^\d+[sm]s?$/i.test(input) || "格式应为数字+ms/s (如1000ms或1s)", }, { type: "input", name: "memory", message: "内存限制 (如256m):", default: currentLimits.memory, validate: (input) => /^\d+[kmg]b?$/i.test(input) || "格式应为数字+k/m/g (如256m或1g)", }, ]); // 3. checker类型选择 const checkerType = await inquirer.prompt([ { type: "list", name: "selectedChecker", message: "请选择需要的Checker 类型:", choices: [ { name: "ncmp (标准比较器:逐字节对比用户输出和标准答案,但会忽略行末空格和文件末尾的多余换行)", value: "ncmp.cpp" }, { name: "wcmp (比较两个单词序列,按顺序逐个比较单词,若某对单词不同或序列长度不同,则判定为答案错误;否则判定为答案正确。)", value: "wcmp.cpp" }, { name: "fcmp (将文件按行作为字符串序列进行比较,若某行内容不同,则判定为答案错误;否则判定为答案正确。)", value: "fcmp.cpp" }, { name: "rcmp (比较两个双精度浮点数,允许最大绝对误差为 1.5E-6。若误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp.cpp" }, { name: "rcmp4 (比较两个双精度浮点数序列,允许最大绝对或相对误差为 1E-4。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp4.cpp" }, { name: "rcmp6 (比较两个双精度浮点数序列,允许最大绝对或相对误差为 1E-6。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp6.cpp" }, { name: "rcmp9 (比较两个双精度浮点数序列,允许最大绝对或相对误差为 1E-9。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp9.cpp" }, { name: "rncmp (比较两个双精度浮点数序列,允许最大绝对误差为 1.5E-5。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rncmp.cpp" }, { name: "uncmp (比较两个无序的有符号长整型序列,会先排序再比较,若序列长度或元素不同,则判定为错误。)", value: "uncmp.cpp" }, { name: 'yesno (检查输入是否为 "YES" 或 "NO" (大小写不敏感),若输入不符合要求或与答案不一致,则判定为错误。)', value: "yesno.cpp" }, { name: "acmp (比较两个双精度浮点数,允许最大绝对误差为 1.5E-6。若误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "acmp.cpp" }, { name: "caseicmp (带有测试用例支持的单 int64 检查器,用于比较两个有序的 int64 序列,若序列长度不同或对应元素不同,则判定为错误。)", value: "caseicmp.cpp" }, { name: 'casencmp (用于比较输出和答案的格式为 "Case X: <number> <number> ..." 的情况,按测试用例逐行比较长整型序列。)', value: "casencmp.cpp" }, { name: 'casewcmp (用于比较输出和答案的格式为 "Case X: <token> <token> ..." 的情况,按测试用例逐行比较字符串序列。)', value: "casewcmp.cpp" }, { name: "dcmp (比较两个双精度浮点数,允许最大绝对或相对误差为 1E-6。若误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "dcmp.cpp" }, { name: "hcmp (比较两个有符号的大整数,会先检查输入是否为有效的整数格式,若格式错误或数值不同,则判定为错误。)", value: "hcmp.cpp" }, { name: "icmp (比较两个有符号的整数,若两个整数不相等,则判定为答案错误;否则判定为答案正确。)", value: "icmp.cpp" }, { name: "lcmp (将文件按行拆分为单词序列进行比较,若某行的单词序列不同,则判定为答案错误;否则判定为答案正确。)", value: "lcmp.cpp" }, { name: "ncmp (比较两个有序的有符号长整型序列,会检查序列长度和对应元素是否相同,若不同则判定为错误。)", value: "ncmp.cpp" }, { name: 'nyesno (用于检查多个 "YES" 或 "NO" (大小写不敏感)的输入,会统计 "YES" 和 "NO" 的数量,若输入不符合要求或与答案不一致,则判定为错误。)', value: "nyesno.cpp" }, { name: "pointscmp (示例得分检查器,通过比较两个双精度浮点数的差值来给出得分。)", value: "pointscmp.cpp" }, { name: "pointsinfo (示例带有 points_info 的检查器,读取两个双精度浮点数,记录相关信息并退出。)", value: "pointsinfo.cpp" }, ], default: currentChecker || "wcmp.cpp", }, ]); // 4. 题目ID变更处理(如有变更则重命名目录) let newProblemDir = oldProblemDir; if (answers.newProblemName !== problemName) { newProblemDir = path.join(process.cwd(), answers.newProblemName); if (fs.existsSync(newProblemDir)) { console.error(chalk.red(`目标目录 ${answers.newProblemName} 已存在,无法重命名。`)); process.exit(1); } fs.renameSync(oldProblemDir, newProblemDir); console.log(chalk.green(`题目目录已重命名为: ${answers.newProblemName}`)); } // 5. 标签处理 let tags = answers.tags.filter((item) => item !== ""); if (answers.othertag) { answers.othertag.split(/[\s,,;]+/).forEach((el) => { if (el && !tags.includes(el)) tags.push(el); }); } // 6. 写入/更新 problem.yaml const updatedConfig = { title: answers.title, tag: tags, }; writeYaml(path.join(newProblemDir, "problem.yaml"), updatedConfig); // 7. 写入/更新 testdata/config.yaml const updatedLimits = { time: answers.time.replace(/\s/g, "").toLowerCase(), memory: answers.memory.replace(/\s/g, "").toLowerCase(), }; writeYaml(path.join(newProblemDir, "testdata", "config.yaml"), updatedLimits); // 8. 生成/更新 status.json const statusFileName = path.resolve(newProblemDir, "status.json"); let problemStatus = {}; if (fs.existsSync(statusFileName)) { problemStatus = JSON.parse(fs.readFileSync(statusFileName, 'utf-8')); } const standardKeys = [ ['problem', '题面'], ['std', '标准程序'], ['generator', '数据生成器'], ['validator', '数据验证器'], ['data', '测评数据'], ['check', '完整性检查'], ['package', '打包'] ]; for (const [k, desc] of standardKeys) { if (!problemStatus[k]) problemStatus[k] = { desc, status: 'pending' }; } try { fs.writeFileSync(statusFileName, JSON.stringify(problemStatus), "utf-8"); } catch (e) { console.error(chalk.red("写入题目状态文件失败:"), e.message); process.exit(1); } // 保存后统一推进 checklist 状态 // 9. 检查并补全模板文件(如 generator、validator、checker、testlib.h、solution/stdsol.md 等) const templates = { "src/std.cpp": "std.cpp", "problem_zh.md": "problem_zh.md", "src/generator.cpp": "generator.cpp", "src/validator.cpp": "validator.cpp", "src/testlib.h": "testlib.h", "solution/stdsol.md": "solution/stdsol.md", // checker.cpp 总是覆盖为新模板 "src/checker.cpp": `checkers/${checkerType.selectedChecker}`, }; for (const [filePath, templateName] of Object.entries(templates)) { const absPath = path.join(newProblemDir, filePath); // checker.cpp 总是覆盖,其他文件只补全缺失 if (filePath === "src/checker.cpp" || !fs.existsSync(absPath)) { writeFileWithDir(absPath, getTemplate(templateName)); console.log(chalk.green(`已补全/覆盖模板文件: ${filePath}`)); } } // 自动补全 sample 目录和样例文件 const sampleDir = path.join(newProblemDir, 'sample'); if (!fs.existsSync(sampleDir)) fs.mkdirSync(sampleDir); const sampleIn = path.join(sampleDir, 'sample01.in'); const sampleAns = path.join(sampleDir, 'sample01.ans'); if (!fs.existsSync(sampleIn)) fs.writeFileSync(sampleIn, ''); if (!fs.existsSync(sampleAns)) fs.writeFileSync(sampleAns, ''); console.log(chalk.green('已补全 sample 目录和样例文件')); // 10. 完成提示 console.log(chalk.green(`\n题目 "${answers.newProblemName}" 信息已更新!`)); console.log(chalk.blue(`目录结构: ${newProblemDir}`)); console.log(chalk.yellow("接下来您可以:")); console.log("1. 编辑 problem_zh.md 编写题面"); console.log("2. 编写 src/std.cpp 标准程序,可以在src中加入std开头的其他c++文件进行TLE、WA等的测评"); console.log("3. 编写 src/checker.cpp Special Judge程序"); console.log("4. 编写 src/generator.cpp 数据生成器"); console.log("5. 编写 src/validator.cpp 输入验证器"); console.log("6. 编写 solution/stdsol.md 题解"); console.log("7. 使用 oimp check 检查题目"); console.log("8. 使用 oimp package 打包题目,生成对应格式的zip压缩包"); console.log(chalk.cyan(`如需生成测评数据:oimp gendata ${answers.newProblemName},如需检查完整性:oimp check ${answers.newProblemName},如需打包:oimp package ${answers.newProblemName}`)); // 记录关键文件的 mtime const snapshotDir = path.join(newProblemDir, '.snapshots'); const mtimePath = path.join(snapshotDir, 'file-mtime.json'); const files = [ 'src/generator.cpp', 'src/validator.cpp', 'src/std.cpp', 'src/checker.cpp' ].map(f => path.join(newProblemDir, f)); let mtimeInfo = {}; for (const file of files) { if (fs.existsSync(file)) { mtimeInfo[path.basename(file)] = fs.statSync(file).mtimeMs; } } fs.writeFileSync(mtimePath, JSON.stringify(mtimeInfo, null, 2)); console.log(chalk.green('已更新 .snapshots/file-mtime.json')); };