@@ -12,141 +12,178 @@ const SITE_CONFIG = {
1212 title : 'Gametoolkit - 文档 RSS' ,
1313 description : 'Gametoolkit - 文档 RSS' ,
1414 language : 'zh-Hans' ,
15- maxItems : 15 , // RSS生成的最大文章个数
16- excludePaths : [ // 需要排除的路径(支持glob通配符、绝对/相对路径、处理后的路径)
15+ maxItems : 15 ,
16+ excludePaths : [
1717 'docs/01-index.md' ,
1818 ] ,
1919} ;
2020
21- // Docs 源文件目录(你的 Markdown 文档目录 )
22- const DOCS_SRC_DIR = path . join ( __dirname , '../docs ' ) ;
23- // RSS 输出路径
24- const OUTPUT_PATH = path . join ( __dirname , '../ build/docs/rss.xml' ) ;
21+ // 项目根目录(统一基准 )
22+ const PROJECT_ROOT = path . resolve ( __dirname , '..' ) ;
23+ const DOCS_SRC_DIR = path . join ( PROJECT_ROOT , 'docs' ) ;
24+ const OUTPUT_PATH = path . join ( PROJECT_ROOT , 'build/docs/rss.xml' ) ;
2525
2626/**
27- * 工具函数:将路径转换为POSIX风格的绝对路径(跨平台统一)
28- * @param {string } filePath 任意路径(相对/绝对、POSIX/Windows)
29- * @param {string } baseDir 基准目录(默认当前工作目录)
30- * @returns {string } POSIX风格的绝对路径(/分隔符)
27+ * 工具函数:转换为POSIX风格的绝对路径(跨平台统一)
3128 */
32- function toPosixAbsolutePath ( filePath , baseDir = process . cwd ( ) ) {
33- // 先解析为系统原生的绝对路径,再转换为POSIX风格并规范化
29+ function toPosixAbsolutePath ( filePath , baseDir = PROJECT_ROOT ) {
3430 const absolutePath = path . resolve ( baseDir , filePath ) ;
3531 return path . posix . normalize ( absolutePath . replace ( / \\ / g, '/' ) ) ;
3632}
3733
3834/**
39- * 工具函数:移除字符串开头的数字-前缀(如 01-、123-)
40- * @param {string } str 原始字符串(文件名或路径片段)
41- * @returns {string } 处理后的字符串
35+ * 工具函数:判断路径是否为文件(支持存在/不存在的路径,结合glob规则)
36+ * @param {string } filePath 路径(绝对/相对)
37+ * @returns {boolean } true=文件,false=文件夹/不存在/glob规则
38+ */
39+ function isFilePath ( filePath ) {
40+ // 排除glob通配符路径
41+ if ( glob . hasMagic ( filePath ) ) return false ;
42+ const absolutePath = toPosixAbsolutePath ( filePath ) ;
43+ try {
44+ // 存在的路径:判断是否为文件
45+ return fs . statSync ( absolutePath ) . isFile ( ) ;
46+ } catch ( err ) {
47+ // 不存在的路径:根据扩展名判断(如.md/.js为文件,无扩展名为文件夹)
48+ return path . extname ( absolutePath ) !== '' ;
49+ }
50+ }
51+
52+ /**
53+ * 工具函数:移除字符串开头的数字-前缀
4254 */
4355function removeLeadingNumberPrefix ( str ) {
44- // 正则匹配开头的一个或多个数字 + 连字符,替换为空
4556 return str . replace ( / ^ \d + - / , '' ) ;
4657}
4758
4859/**
49- * 工具函数:移除路径末尾的index(包括/index或index)
50- * @param {string } pathStr 原始路径
51- * @returns {string } 处理后的路径
60+ * 工具函数:移除路径末尾的index
5261 */
5362function removeTrailingIndex ( pathStr ) {
54- // 匹配末尾的 /index 或 index(支持带/和不带/的情况)
5563 return pathStr . replace ( / ( \/ | ^ ) i n d e x $ / , '' ) ;
5664}
5765
5866/**
59- * 工具函数:判断路径是否匹配排除规则(支持原始文件路径、处理后的路径)
60- * @param {string } pathToCheck 要检查的路径(可以是原始文件路径、处理后的相对路径)
61- * @param {string[] } excludePaths 排除的路径规则(支持glob通配符)
62- * @param {string } baseDir 基准目录(用于相对路径转换)
63- * @returns {boolean } true表示需要排除,false表示保留
67+ * 工具函数:获取文件对应的处理后简洁路径(与生成RSS条目时的路径一致)
68+ */
69+ function getProcessedCleanPath ( filePath ) {
70+ // 非文件路径直接返回空(避免非文件调用)
71+ if ( ! fs . existsSync ( filePath ) || ! fs . statSync ( filePath ) . isFile ( ) ) {
72+ return '' ;
73+ }
74+ const absoluteFilePath = toPosixAbsolutePath ( filePath ) ;
75+ const relativePath = path . relative ( DOCS_SRC_DIR , absoluteFilePath ) . replace ( / \\ / g, '/' ) ;
76+ let cleanPath = relativePath . replace ( / \. m d $ / , '' ) ;
77+ // 移除路径片段的数字前缀
78+ cleanPath = cleanPath . split ( '/' ) . map ( part => removeLeadingNumberPrefix ( part ) ) . join ( '/' ) ;
79+ // 移除末尾的index
80+ cleanPath = removeTrailingIndex ( cleanPath ) ;
81+ // 处理文件名与父文件夹同名的情况
82+ const originalFileName = path . basename ( absoluteFilePath , '.md' ) ;
83+ const fileName = removeLeadingNumberPrefix ( originalFileName ) ;
84+ const parentFolderName = removeLeadingNumberPrefix ( path . basename ( path . dirname ( absoluteFilePath ) ) ) ;
85+ const parentFolderRelativePath = path . relative ( DOCS_SRC_DIR , path . dirname ( absoluteFilePath ) )
86+ . replace ( / \\ / g, '/' )
87+ . split ( '/' )
88+ . map ( part => removeLeadingNumberPrefix ( part ) )
89+ . join ( '/' ) ;
90+ if ( fileName === parentFolderName ) {
91+ cleanPath = parentFolderRelativePath ? parentFolderRelativePath : fileName ;
92+ }
93+ // 拼接docs前缀(与生成RSS时的路径一致)
94+ return path . posix . join ( 'docs' , cleanPath ) . replace ( / \/ $ / , '' ) ;
95+ }
96+
97+ /**
98+ * 判断路径是否匹配排除规则(严格区分文件/文件夹规则)
6499 */
65- function isPathExcluded ( pathToCheck , excludePaths , baseDir = __dirname ) {
100+ function isPathExcluded ( pathToCheck , excludePaths , baseDir = PROJECT_ROOT ) {
66101 if ( ! excludePaths || excludePaths . length === 0 ) {
67102 return false ;
68103 }
69104
70- // 转换为POSIX风格的绝对路径(跨平台统一)
71105 const normalizedPath = toPosixAbsolutePath ( pathToCheck , baseDir ) ;
72- // 系统原生的基准目录(用于glob的cwd参数,确保IO操作正确)
73106 const systemBaseDir = path . resolve ( baseDir ) ;
107+ // 仅当pathToCheck是文件时,才生成处理后路径(避免文件夹路径的误匹配)
108+ const isCheckFile = fs . existsSync ( normalizedPath ) ? fs . statSync ( normalizedPath ) . isFile ( ) : isFilePath ( pathToCheck ) ;
109+ const processedCleanPath = isCheckFile ? getProcessedCleanPath ( normalizedPath ) : '' ;
74110
75- // 遍历排除规则,判断是否匹配
76111 for ( const excludePath of excludePaths ) {
77- // 转换排除规则为POSIX风格的绝对路径
78112 const normalizedExcludePath = toPosixAbsolutePath ( excludePath , baseDir ) ;
79-
113+ // 判断排除规则的类型:文件/文件夹
114+ const isExcludeFile = isFilePath ( excludePath ) ;
115+ // 排除规则对应的处理后路径(仅文件规则需要)
116+ const excludeProcessedCleanPath = isExcludeFile ? getProcessedCleanPath ( normalizedExcludePath ) : '' ;
117+
80118 // 情况1:排除规则是glob通配符
81119 if ( glob . hasMagic ( excludePath ) ) {
82- // glob使用系统原生路径作为cwd,匹配后转换为POSIX绝对路径
83- const matchedPaths = glob . sync ( excludePath , {
84- cwd : systemBaseDir , // 系统原生路径,确保Linux下glob能正确查找
120+ const matchedPaths = glob . sync ( excludePath , {
121+ cwd : systemBaseDir ,
85122 absolute : true ,
86- nodir : true // 只匹配文件(避免文件夹匹配)
87- } ) . map ( p => toPosixAbsolutePath ( p ) ) ; // 统一转换为POSIX风格路径
88-
89- // 检查当前路径是否在匹配结果中,或是否是匹配路径的父路径
90- if ( matchedPaths . includes ( normalizedPath ) ||
91- matchedPaths . some ( p => normalizedPath . startsWith ( p . replace ( / [ ^ / ] + $ / , '' ) + '/' ) ) ) {
123+ nodir : true // 只匹配文件,避免文件夹匹配
124+ } ) . map ( p => toPosixAbsolutePath ( p ) ) ;
125+ // glob规则仅精准匹配文件路径,不触发子路径
126+ if ( matchedPaths . includes ( normalizedPath ) ) {
92127 return true ;
93128 }
94- }
95- // 情况2:排除规则是普通路径(文件或文件夹 )
129+ }
130+ // 情况2:排除规则是普通路径(文件/文件夹 )
96131 else {
97- // 检查当前路径是否是排除文件、在排除文件夹下,或路径完全匹配
98- if ( normalizedPath === normalizedExcludePath ||
99- normalizedPath . startsWith ( normalizedExcludePath + '/' ) ) {
100- return true ;
132+ // 子逻辑1:文件规则 → 仅允许精准匹配(原始路径 或 处理后路径)
133+ if ( isExcludeFile ) {
134+ const isMatched = (
135+ normalizedPath === normalizedExcludePath || // 原始文件路径精准匹配
136+ ( processedCleanPath && processedCleanPath === excludeProcessedCleanPath ) // 处理后路径精准匹配
137+ ) ;
138+ if ( isMatched ) {
139+ return true ;
140+ }
141+ }
142+ // 子逻辑2:文件夹规则 → 精准匹配 + 子路径匹配(仅文件夹规则触发)
143+ else {
144+ const isMatched = (
145+ normalizedPath === normalizedExcludePath || // 文件夹路径精准匹配
146+ normalizedPath . startsWith ( normalizedExcludePath + '/' ) // 子路径匹配
147+ ) ;
148+ if ( isMatched ) {
149+ return true ;
150+ }
101151 }
102152 }
103153 }
104154 return false ;
105155}
106156
107157/**
108- * 工具函数:处理文件名为直接父文件夹同名的情况,仅移除末尾的文件名部分(保留父文件夹路径)
109- * @param {string } filePath Markdown文件的绝对路径
110- * @param {string } cleanRelativePath 处理后的文档相对路径(已移除.md后缀、统一分隔符、数字前缀)
111- * @returns {string } 处理后的路径
158+ * 工具函数:处理文件名为直接父文件夹同名的情况
112159 */
113160function removeSameNameFileNamePart ( filePath , cleanRelativePath ) {
114- // 获取处理后的文件名(不含后缀,已移除数字前缀)
115161 const originalFileName = path . basename ( filePath , '.md' ) ;
116162 const fileName = removeLeadingNumberPrefix ( originalFileName ) ;
117- // 获取文件的直接父文件夹的名称(处理数字前缀)
118163 const parentFolderName = removeLeadingNumberPrefix ( path . basename ( path . dirname ( filePath ) ) ) ;
119- // 获取文件的直接父文件夹的相对路径(相对于DOCS_SRC_DIR,处理数字前缀)
120164 const parentFolderRelativePath = path . relative ( DOCS_SRC_DIR , path . dirname ( filePath ) )
121165 . replace ( / \\ / g, '/' )
122166 . split ( '/' )
123167 . map ( part => removeLeadingNumberPrefix ( part ) )
124168 . join ( '/' ) ;
125169
126- // 仅当文件名与直接父文件夹名相同时处理
127170 if ( fileName === parentFolderName ) {
128- // 情况1:父文件夹是docs根目录(parentFolderRelativePath为空),则保留父文件夹名(即fileName)
129- if ( ! parentFolderRelativePath ) {
130- return fileName ;
131- }
132- // 情况2:父文件夹是子目录,返回父文件夹的相对路径(移除末尾的文件名)
133- return parentFolderRelativePath ;
171+ return parentFolderRelativePath ? parentFolderRelativePath : fileName ;
134172 }
135-
136- // 不满足条件则返回原路径
137173 return cleanRelativePath ;
138174}
139175
140176/**
141- * 获取文件的最后更新时间(通过 git log,若没有 git 则用文件修改时间)
177+ * 获取文件的最后更新时间
142178 */
143179function getFileLastUpdatedTime ( filePath ) {
144180 try {
145- // 执行 git log 获取最后提交时间
146- const log = execSync ( `git log -1 --format=%cd --date=iso "${ filePath } "` , { encoding : 'utf8' } ) ;
181+ const log = execSync ( `git log -1 --format=%cd --date=iso "${ filePath . replace ( / " / g, '\\"' ) } "` , {
182+ encoding : 'utf8' ,
183+ cwd : PROJECT_ROOT
184+ } ) ;
147185 return new Date ( log . trim ( ) ) ;
148186 } catch ( err ) {
149- // 没有 git 则用文件的修改时间
150187 const stats = fs . statSync ( filePath ) ;
151188 return stats . mtime ;
152189 }
@@ -157,90 +194,74 @@ function getFileLastUpdatedTime(filePath) {
157194 */
158195async function generateDocsRSS ( ) {
159196 try {
160- // 1. 检查 Docs 目录是否存在
161197 if ( ! fs . existsSync ( DOCS_SRC_DIR ) ) {
162198 throw new Error ( `Docs 源目录不存在:${ DOCS_SRC_DIR } ` ) ;
163199 }
164200
165- // 2. 确保 build/docs 目录存在
166201 await fs . ensureDir ( path . dirname ( OUTPUT_PATH ) ) ;
167202
168- // 3. 遍历 Docs 目录下的所有 Markdown 文件(排除 node_modules、.git 等)
169- const mdFiles = glob . sync ( `${ DOCS_SRC_DIR } /**/*.md` , {
170- ignore : [ '**/node_modules/**' , '**/.git/**' , '**/_*.md' ] , // 基础排除项
203+ // 遍历docs下的所有md文件(基于项目根目录)
204+ const mdFiles = glob . sync ( 'docs/**/*.md' , {
205+ cwd : PROJECT_ROOT ,
206+ ignore : [ '**/node_modules/**' , '**/.git/**' , '**/_*.md' ] ,
207+ absolute : true
171208 } ) ;
172209
173210 if ( mdFiles . length === 0 ) {
174211 throw new Error ( 'Docs 目录下未找到 Markdown 文档' ) ;
175212 }
176213
177- // 4. 初始化 RSS 实例(使用new URL处理路径,避免重复//)
214+ // 初始化RSS实例
178215 const siteUrl = new URL ( SITE_CONFIG . baseUrl , SITE_CONFIG . url ) . href ;
179216 const feedUrl = new URL ( 'docs/rss.xml' , siteUrl ) . href ;
180-
181217 const feed = new RSS ( {
182218 title : SITE_CONFIG . title ,
183219 description : SITE_CONFIG . description ,
184- site_url : siteUrl , // 处理后的完整站点URL
185- feed_url : feedUrl , // 处理后的RSS文件URL
220+ site_url : siteUrl ,
221+ feed_url : feedUrl ,
186222 language : SITE_CONFIG . language ,
187223 pubDate : new Date ( ) ,
188224 } ) ;
189225
190- // 用于记录已处理的路径,避免重复
191226 const processedPaths = new Set ( ) ;
192- // 5. 处理每个 Markdown 文件
193227 const docsData = [ ] ;
228+
194229 for ( const file of mdFiles ) {
195230 // 第一步:过滤排除路径的文件(原始文件路径)
196- if ( isPathExcluded ( file , SITE_CONFIG . excludePaths , __dirname ) ) {
231+ if ( isPathExcluded ( file , SITE_CONFIG . excludePaths ) ) {
197232 continue ;
198233 }
199234
200- // 读取文件内容
201235 const content = fs . readFileSync ( file , 'utf8' ) ;
202- // 解析前端matter(--- 之间的内容)
203236 const { data : frontMatter } = matter ( content ) ;
204237
205- // 过滤:排除草稿、 隐藏文档
238+ // 过滤草稿/ 隐藏文档
206239 if ( frontMatter . draft || frontMatter . hide ) {
207240 continue ;
208241 }
209242
210- // 获取原始文件名(不含后缀)
243+ // 处理文档标题
211244 const originalFileName = path . basename ( file , '.md' ) ;
212- // 处理文件名:移除开头的数字-前缀
213245 const processedFileName = removeLeadingNumberPrefix ( originalFileName ) ;
214- // 获取文档标题(优先 frontMatter.title,其次处理后的文件名)
215246 const title = frontMatter . title || processedFileName ;
216247
217- // 计算文档的相对路径(适配 Docusaurus 的路由规则)
218- const relativePath = path . relative ( DOCS_SRC_DIR , file ) ;
219- // 步骤1:替换路径分隔符为/,并移除.md后缀
220- let cleanRelativePath = relativePath . replace ( / \\ / g, '/' ) . replace ( / \. m d $ / , '' ) ;
221- // 步骤2:拆分路径片段,逐个移除数字-前缀,再拼接(处理路径中的数字前缀)
222- cleanRelativePath = cleanRelativePath
223- . split ( '/' )
224- . map ( part => removeLeadingNumberPrefix ( part ) )
225- . join ( '/' ) ;
226- // 步骤3:处理文件名与直接父文件夹同名的情况(仅移除文件名,保留父文件夹路径)
248+ // 处理文档相对路径(适配Docusaurus路由)
249+ const relativePath = path . relative ( DOCS_SRC_DIR , file ) . replace ( / \\ / g, '/' ) ;
250+ let cleanRelativePath = relativePath . replace ( / \. m d $ / , '' ) ;
251+ cleanRelativePath = cleanRelativePath . split ( '/' ) . map ( part => removeLeadingNumberPrefix ( part ) ) . join ( '/' ) ;
227252 cleanRelativePath = removeSameNameFileNamePart ( file , cleanRelativePath ) ;
228- // 步骤4:移除末尾的index(最后执行,避免影响路径层级)
229253 cleanRelativePath = removeTrailingIndex ( cleanRelativePath ) ;
230254
231- // 第二步:过滤处理后的路径(包括根路径如docs)
232- // 使用path.posix.join拼接路径,避免手动拼接的分隔符问题
255+ // 拼接处理后的路径
233256 const processedRelativePath = path . posix . join ( 'docs' , cleanRelativePath ) . replace ( / \/ $ / , '' ) ;
234- if ( isPathExcluded ( processedRelativePath , SITE_CONFIG . excludePaths , DOCS_SRC_DIR ) || processedPaths . has ( processedRelativePath ) ) {
257+ // 第二步:过滤已处理的路径或排除的路径
258+ if ( isPathExcluded ( processedRelativePath , SITE_CONFIG . excludePaths ) || processedPaths . has ( processedRelativePath ) ) {
235259 continue ;
236260 }
237- // 记录已处理的路径,避免重复
238261 processedPaths . add ( processedRelativePath ) ;
239262
240- // 步骤5:拼接baseUrl和文档路径,形成完整的permalink(解决baseUrl缺失问题)
241- // 注意:使用posix.join确保路径分隔符为/,且不会出现重复//
263+ // 生成永久链接和更新时间
242264 const permalink = path . posix . join ( SITE_CONFIG . baseUrl , processedRelativePath ) ;
243- // 步骤6:获取最后更新时间
244265 const lastUpdatedAt = getFileLastUpdatedTime ( file ) ;
245266
246267 docsData . push ( {
@@ -251,29 +272,24 @@ async function generateDocsRSS() {
251272 } ) ;
252273 }
253274
254- // 6. 按最后更新时间排序(最新的在前)
275+ // 按更新时间排序并截取最大数量
255276 const sortedDocs = docsData . sort ( ( a , b ) => b . lastUpdatedAt - a . lastUpdatedAt ) ;
256-
257- // 7. 截取指定数量的文章(支持设置最大个数)
258277 const limitedDocs = sortedDocs . slice ( 0 , SITE_CONFIG . maxItems ) ;
259278
260- // 8. 添加 RSS 条目
279+ // 添加RSS条目
261280 limitedDocs . forEach ( ( doc ) => {
262- // 拼接完整 URL(使用new URL确保路径正确,自动处理重复//)
263281 const docUrl = new URL ( doc . permalink , SITE_CONFIG . url ) . href ;
264- // 处理guid的路径分隔符,确保统一为/
265282 const guid = doc . permalink . replace ( / \\ / g, '/' ) ;
266-
267283 feed . item ( {
268284 title : doc . title ,
269- url : docUrl , // link标签对应的值
270- guid : guid , // guid标签对应的值(融入baseUrl,解决缺失问题)
271- guid_isPermaLink : false , // 显式设置isPermaLink属性
285+ url : docUrl ,
286+ guid : guid ,
287+ guid_isPermaLink : false ,
272288 date : doc . lastUpdatedAt ,
273289 } ) ;
274290 } ) ;
275291
276- // 9. 生成 RSS XML 并写入文件
292+ // 写入RSS文件
277293 const xml = feed . xml ( { indent : true } ) ;
278294 await fs . writeFile ( OUTPUT_PATH , xml , 'utf8' ) ;
279295
0 commit comments