88
99
1010TITLE_RE = re .compile (r"^#\s+(.+)$" , re .MULTILINE )
11+ FENCED_BLOCK_RE = re .compile (
12+ r"(^```.*?$.*?^```$|^~~~.*?$.*?^~~~$)" ,
13+ re .MULTILINE | re .DOTALL ,
14+ )
15+ INLINE_CODE_RE = re .compile (r"(`[^`]*`)" )
1116MANIFEST_NAME = ".astrbot-wiki-sync-manifest"
1217SOURCE_ALIASES = {
1318 "zh/config/providers/start.md" : "zh/providers/start.md" ,
@@ -317,27 +322,80 @@ def rewrite_link_target(target: str, source_path: str, resolver: LinkResolver) -
317322 return f"{ page_name_for_source (resolved )} { anchor } "
318323
319324
320- def rewrite_links (
321- content : str ,
325+ def rewrite_links_in_segment (
326+ segment : str ,
322327 source_path : str ,
323328 resolver : LinkResolver ,
324329) -> str :
325- links = list (iter_markdown_links (content ))
330+ links = list (iter_markdown_links (segment ))
326331 if not links :
327- return content
332+ return segment
328333
329334 result : list [str ] = []
330335 previous_end = 0
331336 for link in links :
332- result .append (content [previous_end : link .start ])
337+ result .append (segment [previous_end : link .start ])
333338 result .append (
334339 f"{ link .prefix } { rewrite_link_target (link .target , source_path , resolver )} { link .suffix } " ,
335340 )
336341 previous_end = link .end
337- result .append (content [previous_end :])
342+ result .append (segment [previous_end :])
338343 return "" .join (result )
339344
340345
346+ def rewrite_links (
347+ content : str ,
348+ source_path : str ,
349+ resolver : LinkResolver ,
350+ ) -> str :
351+ parts : list [tuple [str , str ]] = []
352+ last_end = 0
353+
354+ for fenced_match in FENCED_BLOCK_RE .finditer (content ):
355+ before = content [last_end : fenced_match .start ()]
356+ if before :
357+ parts .append (("text" , before ))
358+ parts .append (("code" , fenced_match .group (0 )))
359+ last_end = fenced_match .end ()
360+
361+ tail = content [last_end :]
362+ if tail :
363+ parts .append (("text" , tail ))
364+
365+ output : list [str ] = []
366+ for kind , chunk in parts :
367+ if kind == "code" :
368+ output .append (chunk )
369+ continue
370+
371+ last_inline_end = 0
372+ for inline_match in INLINE_CODE_RE .finditer (chunk ):
373+ before_inline = chunk [last_inline_end : inline_match .start ()]
374+ if before_inline :
375+ output .append (
376+ rewrite_links_in_segment (
377+ before_inline ,
378+ source_path = source_path ,
379+ resolver = resolver ,
380+ )
381+ )
382+
383+ output .append (inline_match .group (0 ))
384+ last_inline_end = inline_match .end ()
385+
386+ after_inline = chunk [last_inline_end :]
387+ if after_inline :
388+ output .append (
389+ rewrite_links_in_segment (
390+ after_inline ,
391+ source_path = source_path ,
392+ resolver = resolver ,
393+ )
394+ )
395+
396+ return "" .join (output )
397+
398+
341399def find_unresolved_doc_links (source_root : Path ) -> list [str ]:
342400 unresolved : list [str ] = []
343401 root = Path (source_root )
@@ -362,6 +420,15 @@ def find_unresolved_doc_links(source_root: Path) -> list[str]:
362420 return unresolved
363421
364422
423+ def check_unresolved_doc_links (source_root : Path ) -> None :
424+ unresolved = find_unresolved_doc_links (source_root )
425+ if not unresolved :
426+ return
427+
428+ issues = "\n " .join (f"- { item } " for item in unresolved )
429+ raise ValueError (f"Unresolved internal doc links found:\n { issues } " )
430+
431+
365432def page_name_for_source (source_path : str ) -> str :
366433 if not source_path .endswith (".md" ):
367434 raise ValueError (f"Unsupported source path: { source_path } " )
@@ -417,12 +484,6 @@ def build_home_page(language: str) -> str:
417484 return normalize_content ("\n " .join (lines ))
418485
419486
420- def sidebar_group_name (group : str ) -> str :
421- if group == "root" :
422- return "Top Level"
423- return group .replace ("-" , " " )
424-
425-
426487def build_sidebar (page_infos : list [PageInfo ]) -> str :
427488 lines : list [str ] = []
428489
@@ -449,7 +510,7 @@ def build_sidebar(page_infos: list[PageInfo]) -> str:
449510 grouped .setdefault (info .group , []).append (info )
450511
451512 for group_name in sorted (grouped ):
452- lines .append (f"- { sidebar_group_name ( group_name ) } " )
513+ lines .append (f"- { group_name } " )
453514 for info in grouped [group_name ]:
454515 lines .append (f" - [{ info .title } ]({ info .page_name } )" )
455516
@@ -469,7 +530,7 @@ def build_page_info(
469530
470531 relative = PurePosixPath (source_path )
471532 parts = relative .parts
472- group = "root " if len (parts ) <= 2 else parts [1 ]
533+ group = "Top Level " if len (parts ) <= 2 else parts [1 ]. replace ( "-" , " " )
473534
474535 return PageInfo (
475536 source_path = source_path ,
@@ -553,11 +614,23 @@ def main() -> int:
553614 )
554615 parser .add_argument (
555616 "--wiki-root" ,
556- required = True ,
557617 help = "Path to the checked out wiki repository." ,
558618 )
619+ parser .add_argument (
620+ "--check-links-only" ,
621+ action = "store_true" ,
622+ help = "Validate internal doc links without writing wiki files." ,
623+ )
559624 args = parser .parse_args ()
560625
626+ if not args .check_links_only and not args .wiki_root :
627+ parser .error ("--wiki-root is required unless --check-links-only is set" )
628+
629+ check_unresolved_doc_links (Path (args .source_root ))
630+
631+ if args .check_links_only :
632+ return 0
633+
561634 sync_docs_to_wiki (
562635 source_root = Path (args .source_root ), wiki_root = Path (args .wiki_root )
563636 )
0 commit comments