11#!/usr/bin/env python3
22# -*- coding: utf-8 -*-
33"""
4- 自动生成 README.md、docs/all-articles.md、mkdocs.yml nav
4+ 自动生成 README.md、docs/all-articles.md、docs/all-categories.md、 mkdocs.yml nav
55
66用法:
77 python scripts/generate_readme.py # 同时生成 readme + all-articles
88 python scripts/generate_readme.py --target readme
99 python scripts/generate_readme.py --target all-articles
10+ python scripts/generate_readme.py --target all-categories
1011 python scripts/generate_readme.py --target mkdocs-nav
1112 python scripts/generate_readme.py --target both # readme + all-articles
12- python scripts/generate_readme.py --target all # readme + all-articles + mkdocs-nav
13+ python scripts/generate_readme.py --target all # readme + all-articles + all-categories + mkdocs-nav
1314"""
1415
1516import argparse
2829 {'首页' : 'index.md' },
2930 {'关于' : 'about.md' },
3031 {'全部栏目' : 'all-articles.md' },
32+ {'全部分类' : 'all-categories.md' },
3133 {'MkDocs 博客方案' : 'MkDocs博客方案.md' },
3234 {'标签' : 'tags/index.md' },
3335]
@@ -92,7 +94,7 @@ def parse_front_matter(filepath, content):
9294# ──────────────────────────────────────────────
9395
9496def get_article_info (filepath ):
95- """返回 (title, date) 或 None(需跳过)"""
97+ """返回文章信息 dict 或 None(需跳过)"""
9698 try :
9799 content = Path (filepath ).read_text (encoding = 'utf-8' )
98100 except Exception as e :
@@ -107,15 +109,25 @@ def get_article_info(filepath):
107109
108110 title = None
109111 date = None
112+ categories = []
110113
111114 if fm is not None :
112115 raw_title = fm .get ('title' )
113116 raw_date = fm .get ('date' )
117+ raw_categories = fm .get ('categories' )
118+
114119 if raw_title is not None :
115120 title = str (raw_title ).strip ()
116121 if raw_date is not None :
117122 date = str (raw_date ).strip ()
118123
124+ if isinstance (raw_categories , list ):
125+ categories = [str (x ).strip () for x in raw_categories if str (x ).strip ()]
126+ elif raw_categories is not None :
127+ c = str (raw_categories ).strip ()
128+ if c :
129+ categories = [c ]
130+
119131 if not title :
120132 m = re .search (r'title:\s*["\']?([^"\'\n]+)["\']?' , content )
121133 if m :
@@ -129,24 +141,29 @@ def get_article_info(filepath):
129141 if not date :
130142 date = datetime .fromtimestamp (os .path .getmtime (filepath )).strftime ('%Y-%m-%d' )
131143
132- return title , date
144+ return {
145+ 'title' : title ,
146+ 'date' : date ,
147+ 'categories' : categories ,
148+ }
133149
134150
135151# ──────────────────────────────────────────────
136152# 扫描 docs/
137153# ──────────────────────────────────────────────
138154
139155def scan_docs ():
140- """返回 (articles, skipped_files, dir_order)"""
156+ """返回 (articles, category_articles, skipped_files, dir_order)"""
141157 docs_dir = Path ('docs' ).resolve ()
142158 repo_root = docs_dir .parent
143159 articles = defaultdict (list )
160+ category_articles = defaultdict (list )
144161 skipped_files = []
145162 dir_order = [] # 保留扫描到的目录名顺序
146163
147164 if not docs_dir .exists ():
148165 print (f'error: { docs_dir } does not exist' )
149- return articles , skipped_files , dir_order
166+ return articles , category_articles , skipped_files , dir_order
150167
151168 for category_dir in sorted (docs_dir .iterdir ()):
152169 if not category_dir .is_dir () or category_dir .name .startswith ('.' ):
@@ -161,15 +178,26 @@ def scan_docs():
161178 if info is None :
162179 skipped_files .append (str (md_file .relative_to (repo_root )).replace ('\\ ' , '/' ))
163180 continue
164- title , date = info
181+
182+ title = info ['title' ]
183+ date = info ['date' ]
184+ fm_categories = info ['categories' ]
185+
165186 readme_path = str (md_file .relative_to (repo_root )).replace ('\\ ' , '/' )
166187 docs_path = str (md_file .relative_to (docs_dir )).replace ('\\ ' , '/' )
167- articles [category_name ].append ((title , date , readme_path , docs_path , dir_name ))
188+
189+ entry = (title , date , readme_path , docs_path , dir_name )
190+ articles [category_name ].append (entry )
168191 has_article = True
192+
193+ article_categories = fm_categories if fm_categories else ['未分类' ]
194+ for c in article_categories :
195+ category_articles [c ].append (entry )
196+
169197 if has_article or (category_dir / 'index.md' ).exists ():
170198 dir_order .append (dir_name )
171199
172- return articles , skipped_files , dir_order
200+ return articles , category_articles , skipped_files , dir_order
173201
174202
175203# ──────────────────────────────────────────────
@@ -199,10 +227,16 @@ def update_readme(new_section):
199227 end_marker = '## \U0001f3d7 \ufe0f 仓库定位'
200228 start_idx = content .find (start_marker )
201229 end_idx = content .find (end_marker )
202- if start_idx == - 1 or end_idx == - 1 :
230+ if start_idx == - 1 :
203231 print ('error: cannot find article index section in README.md' )
204232 return False
205- readme_path .write_text (content [:start_idx ] + new_section + '\n \n ' + content [end_idx :], encoding = 'utf-8' )
233+
234+ if end_idx == - 1 or end_idx < start_idx :
235+ # 如果 README 里没有后续“仓库定位”分节,则替换到文件末尾
236+ readme_path .write_text (content [:start_idx ] + new_section + '\n ' , encoding = 'utf-8' )
237+ else :
238+ readme_path .write_text (content [:start_idx ] + new_section + '\n \n ' + content [end_idx :], encoding = 'utf-8' )
239+
206240 print ('README.md updated' )
207241 return True
208242
@@ -240,6 +274,37 @@ def create_all_articles_page(content):
240274 return True
241275
242276
277+ def generate_all_categories_page (category_articles ):
278+ lines = [
279+ '---' ,
280+ 'title: "全部分类"' ,
281+ 'date: "2026-03-25"' ,
282+ 'comments: false' ,
283+ '---' ,
284+ '' ,
285+ '# 全部分类' ,
286+ '' ,
287+ '这里列出博客中的全部文章,按文章 front matter 中定义的 `categories` 分组。' ,
288+ '' ,
289+ ]
290+
291+ for category in sorted (category_articles .keys ()):
292+ items = category_articles [category ]
293+ sorted_items = sorted (items , key = lambda x : x [1 ], reverse = True )
294+ lines .append (f'## { category } ({ len (sorted_items )} )\n ' )
295+ for title , date , readme_path , docs_path , dir_name in sorted_items :
296+ lines .append ('- **' + date + '** - [' + title + '](' + docs_path + ')' )
297+ lines .append ('' )
298+
299+ return '\n ' .join (lines )
300+
301+
302+ def create_all_categories_page (content ):
303+ Path ('docs/all-categories.md' ).write_text (content , encoding = 'utf-8' )
304+ print ('docs/all-categories.md updated' )
305+ return True
306+
307+
243308# ──────────────────────────────────────────────
244309# mkdocs.yml nav
245310# ──────────────────────────────────────────────
@@ -307,20 +372,20 @@ def main():
307372 parser = argparse .ArgumentParser ()
308373 parser .add_argument (
309374 '--target' ,
310- choices = ['readme' , 'all-articles' , 'mkdocs-nav' , 'both' , 'all' ],
375+ choices = ['readme' , 'all-articles' , 'all-categories' , ' mkdocs-nav' , 'both' , 'all' ],
311376 default = 'both' ,
312- help = '选择要生成的目标(both=readme+all-articles,all=readme+all-articles+mkdocs-nav)' ,
377+ help = '选择要生成的目标(both=readme+all-articles,all=readme+all-articles+all-categories+ mkdocs-nav)' ,
313378 )
314379 args = parser .parse_args ()
315380
316381 print ('scanning docs/...' )
317- articles , skipped_files , dir_order = scan_docs ()
382+ articles , category_articles , skipped_files , dir_order = scan_docs ()
318383
319384 if not articles :
320385 print ('warning: no articles found' )
321386 else :
322387 total = sum (len (items ) for items in articles .values ())
323- print (f'found { total } articles in { len (articles )} categories' )
388+ print (f'found { total } articles in { len (articles )} directory categories and { len ( category_articles ) } front-matter categories' )
324389
325390 if skipped_files :
326391 print (f'warning: skipped { len (skipped_files )} file(s) due to invalid front matter' )
@@ -329,6 +394,7 @@ def main():
329394
330395 do_readme = args .target in ('readme' , 'both' , 'all' )
331396 do_all_articles = args .target in ('all-articles' , 'both' , 'all' )
397+ do_all_categories = args .target in ('all-categories' , 'all' )
332398 do_nav = args .target in ('mkdocs-nav' , 'all' )
333399
334400 if do_readme :
@@ -347,6 +413,14 @@ def main():
347413 print ('error: failed to write docs/all-articles.md' )
348414 raise SystemExit (1 )
349415
416+ if do_all_categories :
417+ print ('generating all-categories page...' )
418+ all_categories_content = generate_all_categories_page (category_articles )
419+ print ('writing docs/all-categories.md...' )
420+ if not create_all_categories_page (all_categories_content ):
421+ print ('error: failed to write docs/all-categories.md' )
422+ raise SystemExit (1 )
423+
350424 if do_nav :
351425 print ('generating mkdocs.yml nav...' )
352426 nav = generate_mkdocs_nav (articles , dir_order )
0 commit comments