Skip to content

Commit ea963d1

Browse files
Update manual_lint.js:重构校验脚本为模块化结构,提升可读性与可维护性;使用 path.resolve 统一管理 glob 路径,路径处理更安全。 (#1587)
* Update manual_lint.js * Update manual_lint.js --------- Co-authored-by: Anduin Xue <anduin@aiursoft.com>
1 parent 0129977 commit ea963d1

File tree

1 file changed

+176
-138
lines changed

1 file changed

+176
-138
lines changed

.github/manual_lint.js

Lines changed: 176 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -4,158 +4,196 @@ const fs = require("fs").promises;
44
const path = require('path');
55

66
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
7+
// glob 模式,定位菜谱 Markdown 文件和所有文件
8+
const DISHES_GLOB = path.resolve(__dirname, '../../dishes/**/*.md');
9+
const ALL_FILES_GLOB = path.resolve(__dirname, '../../dishes/**/*');
710

8-
async function checkFileSize(filePath) {
9-
try {
10-
const stats = await fs.stat(filePath);
11-
return stats.size;
12-
} catch (error) {
13-
console.error(`Error checking file size for ${filePath}: ${error.message}`);
14-
return 0;
15-
}
11+
// 工具函数:获取文件状态,包括大小
12+
async function getFileStats(filePath) {
13+
try {
14+
const stats = await fs.stat(filePath);
15+
return stats;
16+
} catch (err) {
17+
console.error(`检查文件状态时出错: ${filePath} -> ${err.message}`);
18+
return null;
19+
}
1620
}
1721

18-
async function main() {
19-
var errors = [];
20-
var directories = await glob(__dirname + '../../dishes/**/*.md');
22+
// 工具函数:读取文件内容并按行返回
23+
async function readLines(filePath) {
24+
const content = await fs.readFile(filePath, 'utf8');
25+
return content.split('\n').map(line => line.trim());
26+
}
2127

22-
// Check all files in dishes directory for size
23-
var allFiles = await glob(__dirname + '../../dishes/**/*');
28+
// 校验函数集合
29+
const validators = [
30+
async (filePath, lines, errors) => {
31+
const filenameWithoutExt = path.parse(filePath).name; // .name 是不带扩展名的文件名
32+
if (filenameWithoutExt.includes(' ')) {
33+
errors.push(`文件 ${filePath} 不符合仓库的规范!文件名不能包含空格! (当前文件名: ${filenameWithoutExt})`);
34+
}
35+
},
2436

25-
// Check each file size
26-
for (var filePath of allFiles) {
27-
const fileSize = await checkFileSize(filePath);
28-
if (fileSize > MAX_FILE_SIZE) {
29-
errors.push(`文件 ${filePath} 超过了1MB大小限制 (${(fileSize/1048576).toFixed(2)}MB)! 请压缩图片或分割文件。`);
30-
}
37+
38+
async (filePath, lines, errors) => {
39+
const filenameWithoutExt = path.parse(filePath).name;
40+
const expectedMainTitle = `# ${filenameWithoutExt}的做法`;
41+
const titles = lines.filter(l => l.startsWith('#'));
42+
43+
if (!titles.length || titles[0] !== expectedMainTitle) {
44+
errors.push(`文件 ${filePath} 不符合仓库的规范!它的大标题应该是: "${expectedMainTitle}"! 而它现在是 "${titles[0] || '未找到主标题'}"!`);
45+
return;
3146
}
3247

33-
// Check for files without extensions
34-
for (var filePath of allFiles) {
35-
const stats = await fs.stat(filePath);
36-
// Only check files (not directories)
37-
if (stats.isFile()) {
38-
const extension = path.extname(filePath);
39-
if (extension === '') {
40-
errors.push(`文件 ${filePath} 不符合仓库的规范!文件必须有扩展名!`);
41-
}
42-
}
48+
const sections = lines.filter(l => l.startsWith('## '));
49+
const requiredSections = ['## 必备原料和工具', '## 计算', '## 操作', '## 附加内容'];
50+
51+
52+
if (sections.length !== requiredSections.length) {
53+
errors.push(`文件 ${filePath} 不符合仓库的规范!它并不是四个二级标题的格式 (应为 ${requiredSections.length} 个,实际 ${sections.length} 个)。请从示例菜模板中创建菜谱!请不要破坏模板的格式!`);
54+
return;
4355
}
4456

45-
for (var filePath of directories) {
46-
var data = await fs.readFile(filePath, 'utf8');
47-
var filename = path.parse(filePath).base.replace(".md","");
57+
requiredSections.forEach((sec, idx) => {
58+
if (sections[idx] !== sec) {
59+
let titleName = "";
60+
if (idx === 0) titleName = "第一个";
61+
else if (idx === 1) titleName = "第二个";
62+
else if (idx === 2) titleName = "第三个";
63+
else if (idx === 3) titleName = "第四个";
4864

49-
if (filename.includes(' ')) {
50-
errors.push(`文件 ${filePath} 不符合仓库的规范!文件名不能包含空格!`);
51-
}
52-
53-
dataLines = data.split('\n').map(t => t.trim());
54-
titles = dataLines.filter(t => t.startsWith('#'));
55-
secondTitles = titles.filter(t => t.startsWith('## '));
56-
57-
if (dataLines.filter(line => line.includes('勺')).length >
58-
dataLines.filter(line => line.includes('勺子')).length +
59-
dataLines.filter(line => line.includes('炒勺')).length +
60-
dataLines.filter(line => line.includes('漏勺')).length +
61-
dataLines.filter(line => line.includes('吧勺')).length) {
62-
errors.push(`文件 ${filePath} 不符合仓库的规范!勺 不是一个精准的单位!`);
63-
}
64-
if (dataLines.filter(line => line.includes(' 杯')).length >
65-
dataLines.filter(line => line.includes('杯子')).length) {
66-
errors.push(`文件 ${filePath} 不符合仓库的规范!杯 不是一个精准的单位!`);
67-
}
68-
if (dataLines.filter(line => line.includes('适量')).length > 0) {
69-
errors.push(`文件 ${filePath} 不符合仓库的规范!适量 不是一个精准的描述!请给出克 g 或毫升 ml。`);
70-
}
71-
if (dataLines.filter(line => line.includes('每人')).length + dataLines.filter(line => line.includes('人数')).length > 0) {
72-
errors.push(`文件 ${filePath} 不符合仓库的规范!请基于每道菜\\每份为基准。不要基于人数。人数是一个可能会导致在应用中发生问题的单位。如果需要面向大量的人食用,请标明一个人需要几份。`);
73-
}
74-
if (
75-
dataLines.filter(line => line.includes('份数')).length > 0 &&
76-
(
77-
dataLines.filter(line => line.includes('总量')).length == 0 ||
78-
dataLines.filter(line => line.includes('每次制作前需要确定计划做几份。一份正好够')).length == 0
79-
)
80-
) {
81-
errors.push(`文件 ${filePath} 不符合仓库的规范!它使用份数作为基础,这种情况下一般是一次制作,制作多份的情况。请标明:总量 并写明 '每次制作前需要确定计划做几份。一份正好够 几 个人食用。'。`);
82-
}
83-
if (dataLines.filter(line => line.includes('min')).length > 0) {
84-
errors.push(`文件 ${filePath} 不符合仓库的规范!min 这个词汇有多重含义。建议改成中文"分钟"。`);
85-
}
86-
if (dataLines.filter(line => line.includes('左右')).length > 0) {
87-
errors.push(`文件 ${filePath} 不符合仓库的规范!左右 不是一个能够明确定量的标准! 如果是在描述一个模糊物体的特征,请使用 '大约'。例如:鸡(大约1kg)`);
88-
}
89-
if (dataLines.filter(line => line.includes('少许')).length > 0) {
90-
errors.push(`文件 ${filePath} 不符合仓库的规范!少许 不是一个精准的描述!请给出克 g 或毫升 ml。`);
91-
}
92-
if (dataLines.filter(line => line.includes('你')).length + dataLines.filter(line => line.includes('我')).length > 0) {
93-
errors.push(`文件 ${filePath} 不符合仓库的规范!请不要出现人称代词。`);
94-
}
95-
if (titles[0].trim() != "# " + filename + "的做法") {
96-
errors.push(`文件 ${filePath} 不符合仓库的规范!它的大标题应该是: ${"# " + filename + "的做法"}! 而它现在是 ${titles[0].trim()}!`);
97-
continue;
98-
}
65+
errors.push(`文件 ${filePath} 不符合仓库的规范!${titleName}标题不是 ${sec}! (当前为: "${sections[idx] || '未找到'}")`);
66+
}
67+
});
9968

100-
// 检查烹饪难度
101-
const mainTitleIndex = dataLines.indexOf(titles[0].trim());
102-
const firstSecondTitleIndex = dataLines.indexOf(secondTitles[0].trim());
103-
104-
if (mainTitleIndex >= 0 && firstSecondTitleIndex >= 0) {
105-
// 检查大标题和第一个二级标题之间是否有预估烹饪难度
106-
let hasDifficulty = false;
107-
const difficultyPattern = /^{1,5}$/;
108-
109-
for (let i = mainTitleIndex + 1; i < firstSecondTitleIndex; i++) {
110-
if (difficultyPattern.test(dataLines[i])) {
111-
hasDifficulty = true;
112-
// 检查星星数量是否在1-5之间
113-
const starCount = (dataLines[i].match(//g) || []).length;
114-
if (starCount < 1 || starCount > 5) {
115-
errors.push(`文件 ${filePath} 不符合仓库的规范!烹饪难度的星星数量必须在1-5颗之间!`);
116-
}
117-
break;
118-
}
119-
}
120-
121-
if (!hasDifficulty) {
122-
errors.push(`文件 ${filePath} 不符合仓库的规范!在大标题和第一个二级标题之间必须包含"预估烹饪难度:★★"格式的难度评级,星星数量必须在1-5颗之间!`);
123-
}
124-
}
125-
126-
127-
if (secondTitles.length != 4) {
128-
errors.push(`文件 ${filePath} 不符合仓库的规范!它并不是四个标题的格式。请从示例菜模板中创建菜谱!请不要破坏模板的格式!`);
129-
continue;
130-
}
131-
if (secondTitles[0].trim() != "## 必备原料和工具") {
132-
errors.push(`文件 ${filePath} 不符合仓库的规范!第一个标题不是 必备原料和工具!`);
133-
}
134-
if (secondTitles[1].trim() != "## 计算") {
135-
errors.push(`文件 ${filePath} 不符合仓库的规范!第二个标题不是 计算!`);
136-
}
137-
if (secondTitles[2].trim() != "## 操作") {
138-
errors.push(`文件 ${filePath} 不符合仓库的规范!第三个标题不是 操作`);
139-
}
140-
if (secondTitles[3].trim() != "## 附加内容") {
141-
errors.push(`文件 ${filePath} 不符合仓库的规范!第四个标题不是 附加内容`);
142-
}
69+
// 检查烹饪难度
70+
const mainTitleIndex = titles.length > 0 ? lines.indexOf(titles[0]) : -1;
71+
const firstSecondTitleIndex = sections.length > 0 ? lines.indexOf(sections[0]) : -1;
14372

144-
var mustHave = '如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。';
145-
var mustHaveIndex = dataLines.indexOf(mustHave);
146-
if (mustHaveIndex < 0) {
147-
errors.push(`文件 ${filePath} 不符合仓库的规范! 它没有包含必需的附加内容!,需要在最后一行添加模板中的【如果您遵循本指南的制作流程而发现有……】`);
148-
}
73+
if (mainTitleIndex >= 0 && firstSecondTitleIndex >= 0 && mainTitleIndex < firstSecondTitleIndex) {
74+
const contentBetweenTitles = lines.slice(mainTitleIndex + 1, firstSecondTitleIndex);
75+
let hasDifficultyLine = false;
76+
const difficultyPatternGeneral = /^(*)$/;
77+
const difficultyPatternStrict = /^{1,5}$/;
78+
79+
for (const line of contentBetweenTitles) {
80+
if (difficultyPatternGeneral.test(line)) {
81+
hasDifficultyLine = true;
82+
if (!difficultyPatternStrict.test(line)) {
83+
const starMatch = line.match(//g);
84+
const starCount = starMatch ? starMatch.length : 0;
85+
errors.push(`文件 ${filePath} 不符合仓库的规范!烹饪难度的星星数量必须在1-5颗之间!(当前为 ${starCount} 颗)`);
86+
}
87+
break;
88+
}
89+
}
90+
if (!hasDifficultyLine) {
91+
errors.push(`文件 ${filePath} 不符合仓库的规范!在大标题和第一个二级标题之间必须包含"预估烹饪难度:★★"格式的难度评级,星星数量必须在1-5颗之间!`);
92+
}
93+
} else if (mainTitleIndex === -1 || firstSecondTitleIndex === -1) {
94+
errors.push(`文件 ${filePath} 结构错误,无法定位烹饪难度区域。`);
14995
}
150-
151-
if (errors.length > 0) {
152-
for (var error of errors) {
153-
console.error(error + "\n");
154-
}
96+
},
97+
98+
99+
async (filePath, lines, errors) => {
100+
const count = keyword => lines.filter(l => l.includes(keyword)).length;
101+
102+
if (count('勺') > count('勺子') + count('炒勺') + count('漏勺') + count('吧勺')) {
103+
errors.push(`文件 ${filePath} 不符合仓库的规范!勺 不是一个精准的单位!`);
104+
}
105+
if (count(' 杯') > count('杯子')) {
106+
errors.push(`文件 ${filePath} 不符合仓库的规范!杯 不是一个精准的单位!`);
107+
}
108+
['适量', '少许'].forEach(w => {
109+
if (count(w) > 0) {
110+
errors.push(`文件 ${filePath} 不符合仓库的规范!${w} 不是一个精准的描述!请给出克 g 或毫升 ml。`);
111+
}
112+
});
113+
if (count('min') > 0) {
114+
errors.push(`文件 ${filePath} 不符合仓库的规范!min 这个词汇有多重含义。建议改成中文"分钟"。`);
115+
}
116+
if (count('左右') > 0) {
117+
errors.push(`文件 ${filePath} 不符合仓库的规范!左右 不是一个能够明确定量的标准! 如果是在描述一个模糊物体的特征,请使用 '大约'。例如:鸡(大约1kg)`);
118+
}
119+
['你', '我'].forEach(pronoun => {
120+
if (count(pronoun) > 0) {
121+
errors.push(`文件 ${filePath} 不符合仓库的规范!请不要出现人称代词。`);
122+
}
123+
});
124+
},
125+
126+
127+
async (filePath, lines, errors) => {
128+
const hasPortion = lines.some(l => l.includes('份数'));
129+
const hasTotal = lines.some(l => l.includes('总量'));
130+
const hasTemplateLine = lines.some(l => l.includes('每次制作前需要确定计划做几份。一份正好够'));
131+
132+
if (hasPortion && (!hasTotal || !hasTemplateLine)) {
133+
errors.push(`文件 ${filePath} 不符合仓库的规范!它使用份数作为基础,这种情况下一般是一次制作,制作多份的情况。请标明:总量 并写明 '每次制作前需要确定计划做几份。一份正好够 几 个人食用。'。`);
134+
}
135+
if (lines.some(l => l.includes('每人') || l.includes('人数'))) {
136+
errors.push(`文件 ${filePath} 不符合仓库的规范!请基于每道菜\\每份为基准。不要基于人数。人数是一个可能会导致在应用中发生问题的单位。如果需要面向大量的人食用,请标明一个人需要几份。`);
137+
}
138+
},
155139

156-
var message = `Found ${errors.length} errors! Please fix!`;
157-
throw new Error(message);
140+
141+
async (filePath, lines, errors) => {
142+
const footer = '如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。';
143+
if (!lines.includes(footer)) {
144+
errors.push(`文件 ${filePath} 不符合仓库的规范! 它没有包含必需的附加内容!,需要在最后一行添加模板中的【${footer}】`);
158145
}
146+
}
147+
];
148+
149+
150+
async function main() {
151+
const errors = [];
152+
// 获取所有文件和 Markdown 文件路径
153+
const allPaths = await glob(ALL_FILES_GLOB);
154+
const mdPaths = await glob(DISHES_GLOB);
155+
156+
// 检查文件大小和扩展名
157+
for (const p of allPaths) {
158+
const stats = await getFileStats(p);
159+
if (!stats) { // 如果获取状态失败,跳过后续检查
160+
errors.push(`无法获取文件状态: ${p},跳过此文件的检查。`);
161+
continue;
162+
}
163+
164+
if (stats.size > MAX_FILE_SIZE) {
165+
errors.push(`文件 ${p} 超过了1MB大小限制 (${(stats.size/1048576).toFixed(2)}MB)! 请压缩图片或分割文件。`);
166+
}
167+
168+
// 检查扩展名
169+
if (stats.isFile()) {
170+
const ext = path.extname(p);
171+
if (!ext) {
172+
errors.push(`文件 ${p} 不符合仓库的规范!文件必须有扩展名!`);
173+
}
174+
}
175+
}
176+
177+
// 对 Markdown 文件逐项校验内容
178+
for (const p of mdPaths) {
179+
const lines = await readLines(p);
180+
for (const validate of validators) {
181+
await validate(p, lines, errors);
182+
}
183+
}
184+
185+
// 输出错误并退出
186+
if (errors.length) {
187+
errors.forEach(e => console.error(e + "\n"));
188+
const message = `Found ${errors.length} errors! Please fix!`;
189+
throw new Error(message);
190+
} else {
191+
console.log("所有检查已通过!没有发现错误。");
192+
}
159193
}
160194

161-
main();
195+
196+
main().catch(err => {
197+
console.error("\n" + err.message);
198+
process.exit(1);
199+
});

0 commit comments

Comments
 (0)