3 Commits

Author SHA1 Message Date
phyang
ea963d131e Update manual_lint.js:重构校验脚本为模块化结构,提升可读性与可维护性;使用 path.resolve 统一管理 glob 路径,路径处理更安全。 (#1587)
Some checks failed
Continuous Deployment / build-readme-file (push) Has been cancelled
Continuous Deployment / build-docker-image (push) Has been cancelled
* Update manual_lint.js

* Update manual_lint.js

---------

Co-authored-by: Anduin Xue <anduin@aiursoft.com>
2025-05-13 13:49:14 +00:00
github-actions[bot]
01299778d7 [ci skip] Automatic file changes/fix 2025-05-13 13:48:24 +00:00
phyang
cf70399e54 Create 3 vegetable_dishes (#1588)
* Create 清蒸南瓜.md

* Create 凉拌金针菇.md

* Create 清炒花菜.md
2025-05-13 13:47:53 +00:00
7 changed files with 321 additions and 141 deletions

320
.github/manual_lint.js vendored
View File

@@ -4,158 +4,196 @@ const fs = require("fs").promises;
const path = require('path');
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
// glob 模式,定位菜谱 Markdown 文件和所有文件
const DISHES_GLOB = path.resolve(__dirname, '../../dishes/**/*.md');
const ALL_FILES_GLOB = path.resolve(__dirname, '../../dishes/**/*');
async function checkFileSize(filePath) {
try {
const stats = await fs.stat(filePath);
return stats.size;
} catch (error) {
console.error(`Error checking file size for ${filePath}: ${error.message}`);
return 0;
}
// 工具函数:获取文件状态,包括大小
async function getFileStats(filePath) {
try {
const stats = await fs.stat(filePath);
return stats;
} catch (err) {
console.error(`检查文件状态时出错: ${filePath} -> ${err.message}`);
return null;
}
}
// 工具函数:读取文件内容并按行返回
async function readLines(filePath) {
const content = await fs.readFile(filePath, 'utf8');
return content.split('\n').map(line => line.trim());
}
// 校验函数集合
const validators = [
async (filePath, lines, errors) => {
const filenameWithoutExt = path.parse(filePath).name; // .name 是不带扩展名的文件名
if (filenameWithoutExt.includes(' ')) {
errors.push(`文件 ${filePath} 不符合仓库的规范!文件名不能包含空格! (当前文件名: ${filenameWithoutExt})`);
}
},
async (filePath, lines, errors) => {
const filenameWithoutExt = path.parse(filePath).name;
const expectedMainTitle = `# ${filenameWithoutExt}的做法`;
const titles = lines.filter(l => l.startsWith('#'));
if (!titles.length || titles[0] !== expectedMainTitle) {
errors.push(`文件 ${filePath} 不符合仓库的规范!它的大标题应该是: "${expectedMainTitle}"! 而它现在是 "${titles[0] || '未找到主标题'}"!`);
return;
}
const sections = lines.filter(l => l.startsWith('## '));
const requiredSections = ['## 必备原料和工具', '## 计算', '## 操作', '## 附加内容'];
if (sections.length !== requiredSections.length) {
errors.push(`文件 ${filePath} 不符合仓库的规范!它并不是四个二级标题的格式 (应为 ${requiredSections.length} 个,实际 ${sections.length} 个)。请从示例菜模板中创建菜谱!请不要破坏模板的格式!`);
return;
}
requiredSections.forEach((sec, idx) => {
if (sections[idx] !== sec) {
let titleName = "";
if (idx === 0) titleName = "第一个";
else if (idx === 1) titleName = "第二个";
else if (idx === 2) titleName = "第三个";
else if (idx === 3) titleName = "第四个";
errors.push(`文件 ${filePath} 不符合仓库的规范!${titleName}标题不是 ${sec}! (当前为: "${sections[idx] || '未找到'}")`);
}
});
// 检查烹饪难度
const mainTitleIndex = titles.length > 0 ? lines.indexOf(titles[0]) : -1;
const firstSecondTitleIndex = sections.length > 0 ? lines.indexOf(sections[0]) : -1;
if (mainTitleIndex >= 0 && firstSecondTitleIndex >= 0 && mainTitleIndex < firstSecondTitleIndex) {
const contentBetweenTitles = lines.slice(mainTitleIndex + 1, firstSecondTitleIndex);
let hasDifficultyLine = false;
const difficultyPatternGeneral = /^预估烹饪难度:(★*)$/;
const difficultyPatternStrict = /^预估烹饪难度:★{1,5}$/;
for (const line of contentBetweenTitles) {
if (difficultyPatternGeneral.test(line)) {
hasDifficultyLine = true;
if (!difficultyPatternStrict.test(line)) {
const starMatch = line.match(/★/g);
const starCount = starMatch ? starMatch.length : 0;
errors.push(`文件 ${filePath} 不符合仓库的规范烹饪难度的星星数量必须在1-5颗之间(当前为 ${starCount} 颗)`);
}
break;
}
}
if (!hasDifficultyLine) {
errors.push(`文件 ${filePath} 不符合仓库的规范!在大标题和第一个二级标题之间必须包含"预估烹饪难度:★★"格式的难度评级星星数量必须在1-5颗之间`);
}
} else if (mainTitleIndex === -1 || firstSecondTitleIndex === -1) {
errors.push(`文件 ${filePath} 结构错误,无法定位烹饪难度区域。`);
}
},
async (filePath, lines, errors) => {
const count = keyword => lines.filter(l => l.includes(keyword)).length;
if (count('勺') > count('勺子') + count('炒勺') + count('漏勺') + count('吧勺')) {
errors.push(`文件 ${filePath} 不符合仓库的规范!勺 不是一个精准的单位!`);
}
if (count(' 杯') > count('杯子')) {
errors.push(`文件 ${filePath} 不符合仓库的规范!杯 不是一个精准的单位!`);
}
['适量', '少许'].forEach(w => {
if (count(w) > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!${w} 不是一个精准的描述!请给出克 g 或毫升 ml。`);
}
});
if (count('min') > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范min 这个词汇有多重含义。建议改成中文"分钟"。`);
}
if (count('左右') > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!左右 不是一个能够明确定量的标准! 如果是在描述一个模糊物体的特征,请使用 '大约'。例如大约1kg`);
}
['你', '我'].forEach(pronoun => {
if (count(pronoun) > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!请不要出现人称代词。`);
}
});
},
async (filePath, lines, errors) => {
const hasPortion = lines.some(l => l.includes('份数'));
const hasTotal = lines.some(l => l.includes('总量'));
const hasTemplateLine = lines.some(l => l.includes('每次制作前需要确定计划做几份。一份正好够'));
if (hasPortion && (!hasTotal || !hasTemplateLine)) {
errors.push(`文件 ${filePath} 不符合仓库的规范!它使用份数作为基础,这种情况下一般是一次制作,制作多份的情况。请标明:总量 并写明 '每次制作前需要确定计划做几份。一份正好够 几 个人食用。'。`);
}
if (lines.some(l => l.includes('每人') || l.includes('人数'))) {
errors.push(`文件 ${filePath} 不符合仓库的规范!请基于每道菜\\每份为基准。不要基于人数。人数是一个可能会导致在应用中发生问题的单位。如果需要面向大量的人食用,请标明一个人需要几份。`);
}
},
async (filePath, lines, errors) => {
const footer = '如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。';
if (!lines.includes(footer)) {
errors.push(`文件 ${filePath} 不符合仓库的规范! 它没有包含必需的附加内容!,需要在最后一行添加模板中的【${footer}`);
}
}
];
async function main() {
var errors = [];
var directories = await glob(__dirname + '../../dishes/**/*.md');
const errors = [];
// 获取所有文件和 Markdown 文件路径
const allPaths = await glob(ALL_FILES_GLOB);
const mdPaths = await glob(DISHES_GLOB);
// Check all files in dishes directory for size
var allFiles = await glob(__dirname + '../../dishes/**/*');
// Check each file size
for (var filePath of allFiles) {
const fileSize = await checkFileSize(filePath);
if (fileSize > MAX_FILE_SIZE) {
errors.push(`文件 ${filePath} 超过了1MB大小限制 (${(fileSize/1048576).toFixed(2)}MB)! 请压缩图片或分割文件。`);
}
// 检查文件大小和扩展名
for (const p of allPaths) {
const stats = await getFileStats(p);
if (!stats) { // 如果获取状态失败,跳过后续检查
errors.push(`无法获取文件状态: ${p},跳过此文件的检查。`);
continue;
}
// Check for files without extensions
for (var filePath of allFiles) {
const stats = await fs.stat(filePath);
// Only check files (not directories)
if (stats.isFile()) {
const extension = path.extname(filePath);
if (extension === '') {
errors.push(`文件 ${filePath} 不符合仓库的规范!文件必须有扩展名!`);
}
}
if (stats.size > MAX_FILE_SIZE) {
errors.push(`文件 ${p} 超过了1MB大小限制 (${(stats.size/1048576).toFixed(2)}MB)! 请压缩图片或分割文件。`);
}
for (var filePath of directories) {
var data = await fs.readFile(filePath, 'utf8');
var filename = path.parse(filePath).base.replace(".md","");
if (filename.includes(' ')) {
errors.push(`文件 ${filePath} 不符合仓库的规范!文件名不能包含空格!`);
}
dataLines = data.split('\n').map(t => t.trim());
titles = dataLines.filter(t => t.startsWith('#'));
secondTitles = titles.filter(t => t.startsWith('## '));
if (dataLines.filter(line => line.includes('勺')).length >
dataLines.filter(line => line.includes('勺子')).length +
dataLines.filter(line => line.includes('炒勺')).length +
dataLines.filter(line => line.includes('漏勺')).length +
dataLines.filter(line => line.includes('吧勺')).length) {
errors.push(`文件 ${filePath} 不符合仓库的规范!勺 不是一个精准的单位!`);
}
if (dataLines.filter(line => line.includes(' 杯')).length >
dataLines.filter(line => line.includes('杯子')).length) {
errors.push(`文件 ${filePath} 不符合仓库的规范!杯 不是一个精准的单位!`);
}
if (dataLines.filter(line => line.includes('适量')).length > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!适量 不是一个精准的描述!请给出克 g 或毫升 ml。`);
}
if (dataLines.filter(line => line.includes('每人')).length + dataLines.filter(line => line.includes('人数')).length > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!请基于每道菜\\每份为基准。不要基于人数。人数是一个可能会导致在应用中发生问题的单位。如果需要面向大量的人食用,请标明一个人需要几份。`);
}
if (
dataLines.filter(line => line.includes('份数')).length > 0 &&
(
dataLines.filter(line => line.includes('总量')).length == 0 ||
dataLines.filter(line => line.includes('每次制作前需要确定计划做几份。一份正好够')).length == 0
)
) {
errors.push(`文件 ${filePath} 不符合仓库的规范!它使用份数作为基础,这种情况下一般是一次制作,制作多份的情况。请标明:总量 并写明 '每次制作前需要确定计划做几份。一份正好够 几 个人食用。'。`);
}
if (dataLines.filter(line => line.includes('min')).length > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范min 这个词汇有多重含义。建议改成中文"分钟"。`);
}
if (dataLines.filter(line => line.includes('左右')).length > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!左右 不是一个能够明确定量的标准! 如果是在描述一个模糊物体的特征,请使用 '大约'。例如大约1kg`);
}
if (dataLines.filter(line => line.includes('少许')).length > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!少许 不是一个精准的描述!请给出克 g 或毫升 ml。`);
}
if (dataLines.filter(line => line.includes('你')).length + dataLines.filter(line => line.includes('我')).length > 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范!请不要出现人称代词。`);
}
if (titles[0].trim() != "# " + filename + "的做法") {
errors.push(`文件 ${filePath} 不符合仓库的规范!它的大标题应该是: ${"# " + filename + "的做法"}! 而它现在是 ${titles[0].trim()}!`);
continue;
}
// 检查烹饪难度
const mainTitleIndex = dataLines.indexOf(titles[0].trim());
const firstSecondTitleIndex = dataLines.indexOf(secondTitles[0].trim());
if (mainTitleIndex >= 0 && firstSecondTitleIndex >= 0) {
// 检查大标题和第一个二级标题之间是否有预估烹饪难度
let hasDifficulty = false;
const difficultyPattern = /^预估烹饪难度:★{1,5}$/;
for (let i = mainTitleIndex + 1; i < firstSecondTitleIndex; i++) {
if (difficultyPattern.test(dataLines[i])) {
hasDifficulty = true;
// 检查星星数量是否在1-5之间
const starCount = (dataLines[i].match(/★/g) || []).length;
if (starCount < 1 || starCount > 5) {
errors.push(`文件 ${filePath} 不符合仓库的规范烹饪难度的星星数量必须在1-5颗之间`);
}
break;
}
}
if (!hasDifficulty) {
errors.push(`文件 ${filePath} 不符合仓库的规范!在大标题和第一个二级标题之间必须包含"预估烹饪难度:★★"格式的难度评级星星数量必须在1-5颗之间`);
}
}
if (secondTitles.length != 4) {
errors.push(`文件 ${filePath} 不符合仓库的规范!它并不是四个标题的格式。请从示例菜模板中创建菜谱!请不要破坏模板的格式!`);
continue;
}
if (secondTitles[0].trim() != "## 必备原料和工具") {
errors.push(`文件 ${filePath} 不符合仓库的规范!第一个标题不是 必备原料和工具!`);
}
if (secondTitles[1].trim() != "## 计算") {
errors.push(`文件 ${filePath} 不符合仓库的规范!第二个标题不是 计算!`);
}
if (secondTitles[2].trim() != "## 操作") {
errors.push(`文件 ${filePath} 不符合仓库的规范!第三个标题不是 操作`);
}
if (secondTitles[3].trim() != "## 附加内容") {
errors.push(`文件 ${filePath} 不符合仓库的规范!第四个标题不是 附加内容`);
}
var mustHave = '如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。';
var mustHaveIndex = dataLines.indexOf(mustHave);
if (mustHaveIndex < 0) {
errors.push(`文件 ${filePath} 不符合仓库的规范! 它没有包含必需的附加内容!,需要在最后一行添加模板中的【如果您遵循本指南的制作流程而发现有……】`);
}
// 检查扩展名
if (stats.isFile()) {
const ext = path.extname(p);
if (!ext) {
errors.push(`文件 ${p} 不符合仓库的规范!文件必须有扩展名!`);
}
}
if (errors.length > 0) {
for (var error of errors) {
console.error(error + "\n");
}
}
var message = `Found ${errors.length} errors! Please fix!`;
throw new Error(message);
// 对 Markdown 文件逐项校验内容
for (const p of mdPaths) {
const lines = await readLines(p);
for (const validate of validators) {
await validate(p, lines, errors);
}
}
// 输出错误并退出
if (errors.length) {
errors.forEach(e => console.error(e + "\n"));
const message = `Found ${errors.length} errors! Please fix!`;
throw new Error(message);
} else {
console.log("所有检查已通过!没有发现错误。");
}
}
main();
main().catch(err => {
console.error("\n" + err.message);
process.exit(1);
});

View File

@@ -89,11 +89,14 @@ docker run -d -p 5000:5000 ghcr.io/anduin2017/how-to-cook:latest
- [雷椒皮蛋](dishes/vegetable_dish/雷椒皮蛋.md)
- [凉拌豆腐](dishes/vegetable_dish/凉拌豆腐.md)
- [凉拌黄瓜](dishes/vegetable_dish/凉拌黄瓜.md)
- [凉拌金针菇](dishes/vegetable_dish/凉拌金针菇.md)
- [凉拌木耳](dishes/vegetable_dish/凉拌木耳/凉拌木耳.md)
- [凉拌莴笋](dishes/vegetable_dish/凉拌莴笋/凉拌莴笋.md)
- [凉拌油麦菜](dishes/vegetable_dish/凉拌油麦菜.md)
- [蒲烧茄子](dishes/vegetable_dish/蒲烧茄子.md)
- [芹菜拌茶树菇](dishes/vegetable_dish/芹菜拌茶树菇/芹菜拌茶树菇.md)
- [清炒花菜](dishes/vegetable_dish/清炒花菜.md)
- [清蒸南瓜](dishes/vegetable_dish/清蒸南瓜.md)
- [陕北熬豆角](dishes/vegetable_dish/陕北熬豆角.md)
- [上汤娃娃菜](dishes/vegetable_dish/上汤娃娃菜/上汤娃娃菜.md)
- [手撕包菜](dishes/vegetable_dish/手撕包菜/手撕包菜.md)

View File

@@ -0,0 +1,55 @@
# 凉拌金针菇的做法
凉拌金针菇是一道简单快捷的开胃凉菜。口感脆嫩爽滑,富含膳食纤维和多种维生素。制作过程无需复杂的烹饪技巧,非常适合新手和忙碌时快速准备。一般初学者只需要 10 分钟即可完成。
预估烹饪难度:★★
## 必备原料和工具
- 金针菇
- 小葱
- 大蒜
- 生抽
-
- 白糖(可选)
- 香油(可选)
- 辣椒油(可选)
## 计算
每次制作前需要确定计划做几份。一份正好够 1 个人吃。
每份:
- 金针菇 150 g (约 1 小包)
- 小葱 5 g
- 大蒜 2 瓣
- 生抽 15 ml
- 醋 10 ml
- 白糖 3 g可选
- 香油 5 ml可选
- 辣椒油 5 ml可选
## 操作
- 将 金针菇 根部切除,用清水冲洗干净,备用。
- 将 小葱 洗净,切成葱花,备用。
- 将 大蒜 去皮,切成蒜末,备用。
- 锅中加入 1000 ml 饮用水,大火烧开。
- 放入 金针菇,煮 **1-2 分钟**,至金针菇变软。
- 将 煮好的 金针菇 捞出,沥干水分,放入一个较大的碗中,备用。
- 在另一个干净的小碗中,加入 15 ml 生抽10 ml 醋3 g 白糖可选5 ml 香油(可选)。
- 加入切好的 大蒜末。
- 搅拌均匀,使 白糖 充分溶解,酱汁混合均匀。
- 将制作好的酱汁均匀淋在 金针菇 上。
- 撒上切好的 小葱花。
- 根据个人喜好,淋上 5 ml 辣椒油(可选)。
- 用 筷子 轻轻拌匀,即可食用。
## 附加内容
- 金针菇焯水时间不宜过长,以免影响口感。
- 酱汁的比例可以根据个人口味喜好进行调整。
- 如果不喜欢吃辣,可以省略辣椒油。
如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。

View File

@@ -0,0 +1,44 @@
# 清炒花菜的做法
清炒花菜是一道常见的家常素菜。富含维生素 C 和膳食纤维,口感脆嫩。做法简单,是一道快速上手的炒菜。一般初学者只需要 15 分钟即可完成。
预估烹饪难度:★★
## 必备原料和工具
- 花菜
- 大蒜
-
## 计算
每次制作前需要确定计划做几份。一份正好够 2 个人食用(作为配菜)。
每份:
- 花菜 约 300 g (约 1/2 中等大小的花菜)
- 大蒜 2-3 瓣
- 盐 3 g
- 食用油 15 ml
- 饮用水 50 ml (用于炒制过程)
## 操作
- 将 花菜 洗净,用刀或手掰成小朵,粗茎部分可以切片,备用。
- 将 大蒜 去皮,切成蒜片,备用。
- 锅中加入 1000 ml 饮用水,大火烧开。
- 放入 花菜 朵,煮 **2-3 分钟**,至花菜颜色变浅,口感稍微软化。
- 将 煮好的 花菜 捞出,沥干水分,备用。
- 热锅,加入 15 ml 食用油,大火烧热。
- 放入 蒜片,快速煸炒出香味。
- 放入 焯好水的 花菜 朵,转中大火,快速翻炒约 **2 分钟**,使花菜均匀受热。
- 加入 3 g 盐,继续翻炒均匀。
- 沿锅边淋入 50 ml 饮用水,盖上锅盖,焖 **1 分钟**,帮助花菜完全熟透入味。
- 开盖,快速翻炒均匀,即可出锅。
## 附加内容
- 提前将花菜焯水可以缩短炒制时间,并使花菜更容易熟透。
- 炒制时间可以根据个人喜欢的花菜软硬程度调整。喜欢脆一些可以缩短时间。
如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。

View File

@@ -0,0 +1,37 @@
# 清蒸南瓜的做法
清蒸南瓜是一道制作极其简单的家常甜点或主食。它最大程度地保留了南瓜本身的天然甜味和营养,口感软糯。是健康饮食的不错选择。一般初学者只需要 15-20 分钟即可完成(主要为蒸的时间)。
预估烹饪难度:★
## 必备原料和工具
- 南瓜
- 蒸锅
## 计算
每次制作前需要确定计划做几份。一份正好够 1-2 个人吃。
每份:
- 南瓜 300 g
- 饮用水 1000 ml (用于蒸锅)
## 操作
- 将 南瓜 外皮洗净,去除瓜瓤和籽。
- 将 南瓜 切成厚度大约 2 cm 的片,备用。
- 在 蒸锅 的锅中加入 1000 ml 饮用水。
- 将切好的 南瓜 片均匀摆放在盘中。
- 待蒸锅中的水烧开后,将装有 南瓜 的盘子放入蒸锅中。
- 盖上锅盖,保持大火蒸 **15-20 分钟**,直至南瓜变软,可以用筷子轻松穿透。
- 关火,小心取出盘子。
## 附加内容
- 南瓜的品种不同,甜度和口感会有差异。老南瓜通常更甜更面。
- 南瓜皮含有营养,如果喜欢也可以不去皮蒸,但需要彻底洗净。
- 蒸的时间取决于南瓜块的大小和厚度,以及南瓜的品种。用筷子测试是判断是否蒸熟的好方法。
如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。

View File

@@ -25,4 +25,5 @@
* [麻油拌面](./../dishes/staple/麻油拌面.md)
* [凉拌油麦菜](./../dishes/vegetable_dish/凉拌油麦菜.md)
* [凉拌黄瓜](./../dishes/vegetable_dish/凉拌黄瓜.md)
* [清蒸南瓜](./../dishes/vegetable_dish/清蒸南瓜.md)
* [炒滑蛋](./../dishes/vegetable_dish/炒滑蛋/炒滑蛋.md)

View File

@@ -55,9 +55,11 @@
* [凉拌木耳](./../dishes/vegetable_dish/凉拌木耳/凉拌木耳.md)
* [凉拌莴笋](./../dishes/vegetable_dish/凉拌莴笋/凉拌莴笋.md)
* [凉拌豆腐](./../dishes/vegetable_dish/凉拌豆腐.md)
* [凉拌金针菇](./../dishes/vegetable_dish/凉拌金针菇.md)
* [松仁玉米](./../dishes/vegetable_dish/松仁玉米.md)
* [水油焖蔬菜](./../dishes/vegetable_dish/水油焖蔬菜.md)
* [洋葱炒鸡蛋](./../dishes/vegetable_dish/洋葱炒鸡蛋/洋葱炒鸡蛋.md)
* [清炒花菜](./../dishes/vegetable_dish/清炒花菜.md)
* [炒青菜](./../dishes/vegetable_dish/炒青菜.md)
* [白灼菜心](./../dishes/vegetable_dish/白灼菜心/白灼菜心.md)
* [糖拌西红柿](./../dishes/vegetable_dish/糖拌西红柿/糖拌西红柿.md)