@@ -4,158 +4,196 @@ const fs = require("fs").promises;
44const path = require ( 'path' ) ;
55
66const 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