mirror of
https://github.com/langgenius/dify-docs.git
synced 2026-03-27 13:28:32 +07:00
366 lines
13 KiB
Python
366 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
交互式Markdown链接修复工具
|
||
|
||
这个脚本用于交互式地修复Markdown文件中的相对路径引用,将它们转换为
|
||
从根目录开始的绝对路径格式(如 /zh-hans/xxx),以符合Mintlify文档要求。
|
||
脚本支持处理单个文件或指定目录内的所有.mdx文件。
|
||
|
||
特点:
|
||
- 交互式操作,精确可控
|
||
- 提供修改预览
|
||
- 支持单文件或目录处理
|
||
- 将相对路径转换为绝对路径
|
||
- 支持锚点保留
|
||
- 移除文件扩展名
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
import glob
|
||
|
||
# 正则表达式来匹配Markdown链接引用,支持.md和.mdx文件
|
||
MD_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\(([^)]+\.(md|mdx))(?:#([^)]*))?(\))')
|
||
REL_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\(([^)/][^)]+)(?:#([^)]*))?(\))') # 匹配不以/开头的相对路径
|
||
|
||
# 颜色代码,用于美化终端输出
|
||
class Colors:
|
||
HEADER = '\033[95m'
|
||
BLUE = '\033[94m'
|
||
CYAN = '\033[96m'
|
||
GREEN = '\033[92m'
|
||
WARNING = '\033[93m'
|
||
FAIL = '\033[91m'
|
||
ENDC = '\033[0m'
|
||
BOLD = '\033[1m'
|
||
UNDERLINE = '\033[4m'
|
||
|
||
|
||
def find_file_in_project(root_dir, rel_path, current_file_dir):
|
||
"""
|
||
根据相对路径在项目中查找实际文件
|
||
|
||
Args:
|
||
root_dir: 项目根目录
|
||
rel_path: 相对路径引用
|
||
current_file_dir: 当前文件所在目录
|
||
|
||
Returns:
|
||
找到的文件绝对路径,或None如果未找到
|
||
"""
|
||
# 移除扩展名,稍后会添加回.mdx
|
||
if rel_path.endswith(('.md', '.mdx')):
|
||
extension = '.md' if rel_path.endswith('.md') else '.mdx'
|
||
rel_path = rel_path[:-len(extension)]
|
||
|
||
# 如果是以../或./开头的相对路径
|
||
if rel_path.startswith(('./','../')):
|
||
# 计算实际路径
|
||
actual_path = os.path.normpath(os.path.join(current_file_dir, rel_path))
|
||
|
||
# 尝试匹配.mdx文件
|
||
matches = glob.glob(f"{actual_path}.mdx")
|
||
if matches:
|
||
return matches[0]
|
||
|
||
# 尝试匹配.md文件
|
||
matches = glob.glob(f"{actual_path}.md")
|
||
if matches:
|
||
return matches[0]
|
||
|
||
# 尝试在项目中搜索匹配的文件名
|
||
basename = os.path.basename(rel_path)
|
||
# 搜索所有.mdx文件
|
||
mdx_matches = []
|
||
md_matches = []
|
||
|
||
for root, _, files in os.walk(root_dir):
|
||
for file in files:
|
||
if file.endswith('.mdx') and os.path.splitext(file)[0] == basename:
|
||
mdx_matches.append(os.path.join(root, file))
|
||
elif file.endswith('.md') and os.path.splitext(file)[0] == basename:
|
||
md_matches.append(os.path.join(root, file))
|
||
|
||
# 优先使用.mdx文件
|
||
if mdx_matches:
|
||
return mdx_matches[0]
|
||
elif md_matches:
|
||
return md_matches[0]
|
||
|
||
return None
|
||
|
||
def get_absolute_path(file_path, root_dir):
|
||
"""
|
||
获取相对于项目根目录的绝对路径
|
||
|
||
Args:
|
||
file_path: 文件的完整路径
|
||
root_dir: 项目根目录
|
||
|
||
Returns:
|
||
/zh-hans/xxx 格式的绝对路径
|
||
"""
|
||
# 获取相对于根目录的路径
|
||
rel_path = os.path.relpath(file_path, root_dir)
|
||
# 移除扩展名
|
||
rel_path = os.path.splitext(rel_path)[0]
|
||
# 添加前导斜杠
|
||
abs_path = f"/{rel_path}"
|
||
|
||
return abs_path
|
||
|
||
def process_file(file_path, root_dir, dry_run=False, auto_confirm=False):
|
||
"""
|
||
处理单个Markdown文件中的链接引用
|
||
|
||
Args:
|
||
file_path: 要处理的文件路径
|
||
root_dir: 项目根目录
|
||
dry_run: 是否只预览修改,不实际写入
|
||
auto_confirm: 是否自动确认所有修改
|
||
|
||
Returns:
|
||
修改的链接数量
|
||
"""
|
||
print(f"\n{Colors.HEADER}处理文件:{Colors.ENDC} {file_path}")
|
||
|
||
# 获取文件内容
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
except Exception as e:
|
||
print(f"{Colors.FAIL}错误: 无法读取文件 - {e}{Colors.ENDC}")
|
||
return 0
|
||
|
||
# 当前文件所在目录
|
||
current_file_dir = os.path.dirname(file_path)
|
||
|
||
# 存储所有要修改的内容
|
||
changes = []
|
||
|
||
# 查找带有.md或.mdx后缀的链接
|
||
for m in MD_LINK_PATTERN.finditer(content):
|
||
link_text = m.group(1)
|
||
link_path = m.group(2)
|
||
fragment = m.group(4) or "" # 锚点可能不存在
|
||
full_match = m.group(0)
|
||
|
||
# 跳过外部链接
|
||
if link_path.startswith(('http://', 'https://', 'mailto:', 'ftp://')):
|
||
continue
|
||
|
||
# 查找实际文件
|
||
actual_file = find_file_in_project(root_dir, link_path, current_file_dir)
|
||
if actual_file:
|
||
# 转换为绝对路径
|
||
abs_path = get_absolute_path(actual_file, root_dir)
|
||
fragment_text = f"#{fragment}" if fragment else ""
|
||
new_link = f"[{link_text}]({abs_path}{fragment_text})"
|
||
changes.append((full_match, new_link, actual_file))
|
||
|
||
# 查找其他相对路径链接(不带.md或.mdx后缀)
|
||
for m in REL_LINK_PATTERN.finditer(content):
|
||
link_text = m.group(1)
|
||
link_path = m.group(2)
|
||
fragment = m.group(3) or "" # 锚点可能不存在
|
||
full_match = m.group(0)
|
||
|
||
# 跳过已经是绝对路径的链接
|
||
if link_path.startswith('/'):
|
||
continue
|
||
|
||
# 跳过外部链接
|
||
if link_path.startswith(('http://', 'https://', 'mailto:', 'ftp://')):
|
||
continue
|
||
|
||
# 查找实际文件
|
||
actual_file = find_file_in_project(root_dir, link_path, current_file_dir)
|
||
if actual_file:
|
||
# 转换为绝对路径
|
||
abs_path = get_absolute_path(actual_file, root_dir)
|
||
fragment_text = f"#{fragment}" if fragment else ""
|
||
new_link = f"[{link_text}]({abs_path}{fragment_text})"
|
||
changes.append((full_match, new_link, actual_file))
|
||
|
||
# 如果没有找到需要修改的链接
|
||
if not changes:
|
||
print(f"{Colors.GREEN}没有找到需要修改的链接{Colors.ENDC}")
|
||
return 0
|
||
|
||
# 显示找到的修改
|
||
print(f"\n{Colors.BLUE}找到 {len(changes)} 个需要修改的链接:{Colors.ENDC}")
|
||
for i, (old, new, target) in enumerate(changes):
|
||
print(f"{Colors.CYAN}修改 {i+1}:{Colors.ENDC}")
|
||
print(f" - 原始链接: {Colors.WARNING}{old}{Colors.ENDC}")
|
||
print(f" - 新链接: {Colors.GREEN}{new}{Colors.ENDC}")
|
||
print(f" - 目标文件: {os.path.relpath(target, root_dir)}\n")
|
||
|
||
# 如果是预览模式,返回
|
||
if dry_run:
|
||
print(f"{Colors.BLUE}预览模式 - 未执行实际修改{Colors.ENDC}")
|
||
return len(changes)
|
||
|
||
# 确认修改
|
||
if not auto_confirm:
|
||
response = input(f"{Colors.BOLD}是否应用这些修改? (y/n/部分修改输入数字如1,3,5): {Colors.ENDC}")
|
||
|
||
if response.lower() == 'n':
|
||
print(f"{Colors.BLUE}已取消修改{Colors.ENDC}")
|
||
return 0
|
||
elif response.lower() == 'y':
|
||
selected_changes = changes
|
||
else:
|
||
try:
|
||
# 解析用户选择的修改索引
|
||
indices = [int(i.strip()) - 1 for i in response.split(',')]
|
||
selected_changes = [changes[i] for i in indices if 0 <= i < len(changes)]
|
||
if not selected_changes:
|
||
print(f"{Colors.WARNING}未选择任何有效修改,操作取消{Colors.ENDC}")
|
||
return 0
|
||
except:
|
||
print(f"{Colors.WARNING}输入格式有误,操作取消{Colors.ENDC}")
|
||
return 0
|
||
else:
|
||
selected_changes = changes
|
||
|
||
# 应用修改
|
||
modified_content = content
|
||
for old, new, _ in selected_changes:
|
||
modified_content = modified_content.replace(old, new)
|
||
|
||
# 写入文件
|
||
try:
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
f.write(modified_content)
|
||
print(f"{Colors.GREEN}成功应用 {len(selected_changes)} 个修改到文件{Colors.ENDC}")
|
||
return len(selected_changes)
|
||
except Exception as e:
|
||
print(f"{Colors.FAIL}错误: 无法写入文件 - {e}{Colors.ENDC}")
|
||
return 0
|
||
|
||
def scan_directory(dir_path, root_dir, dry_run=False, auto_confirm=False):
|
||
"""
|
||
扫描目录中的所有.mdx文件
|
||
|
||
Args:
|
||
dir_path: 要扫描的目录路径
|
||
root_dir: 项目根目录
|
||
dry_run: 是否只预览修改
|
||
auto_confirm: 是否自动确认所有修改
|
||
|
||
Returns:
|
||
处理的文件数量,修改的链接总数
|
||
"""
|
||
file_count = 0
|
||
total_changes = 0
|
||
|
||
print(f"{Colors.HEADER}扫描目录: {dir_path}{Colors.ENDC}")
|
||
|
||
# 获取所有.mdx文件
|
||
mdx_files = []
|
||
for root, _, files in os.walk(dir_path):
|
||
for file in files:
|
||
if file.endswith('.mdx'):
|
||
mdx_files.append(os.path.join(root, file))
|
||
|
||
if not mdx_files:
|
||
print(f"{Colors.WARNING}在目录中未找到.mdx文件{Colors.ENDC}")
|
||
return 0, 0
|
||
|
||
print(f"{Colors.BLUE}找到 {len(mdx_files)} 个.mdx文件{Colors.ENDC}")
|
||
|
||
# 处理每个文件
|
||
for file_path in mdx_files:
|
||
# 显示文件的相对路径
|
||
rel_path = os.path.relpath(file_path, root_dir)
|
||
print(f"\n{Colors.BOLD}处理文件 ({file_count+1}/{len(mdx_files)}): {rel_path}{Colors.ENDC}")
|
||
|
||
# 询问是否处理此文件
|
||
if not auto_confirm:
|
||
response = input(f"{Colors.BOLD}是否处理此文件? (y/n/q-退出): {Colors.ENDC}")
|
||
if response.lower() == 'n':
|
||
print(f"{Colors.BLUE}跳过此文件{Colors.ENDC}")
|
||
continue
|
||
elif response.lower() == 'q':
|
||
print(f"{Colors.BLUE}退出处理{Colors.ENDC}")
|
||
break
|
||
|
||
# 处理文件
|
||
changes = process_file(file_path, root_dir, dry_run, auto_confirm)
|
||
if changes > 0:
|
||
file_count += 1
|
||
total_changes += changes
|
||
|
||
return file_count, total_changes
|
||
|
||
def main():
|
||
"""主程序入口"""
|
||
# 确定项目根目录
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
project_root = os.path.dirname(script_dir) # 脚本在scripts目录下,上一级是项目根目录
|
||
|
||
# 显示欢迎信息
|
||
print(f"\n{Colors.HEADER}{'='*60}{Colors.ENDC}")
|
||
print(f"{Colors.HEADER} Mintlify文档链接修复工具 {Colors.ENDC}")
|
||
print(f"{Colors.HEADER}{'='*60}{Colors.ENDC}")
|
||
print(f"项目根目录: {project_root}\n")
|
||
|
||
# 交互式菜单
|
||
while True:
|
||
print(f"\n{Colors.BOLD}请选择操作模式:{Colors.ENDC}")
|
||
print("1. 处理单个文件")
|
||
print("2. 处理指定目录中的所有.mdx文件")
|
||
print("3. 退出")
|
||
|
||
choice = input(f"{Colors.BOLD}请输入选项 (1-3): {Colors.ENDC}")
|
||
|
||
if choice == '1':
|
||
# 处理单个文件
|
||
file_path = input(f"{Colors.BOLD}请输入文件路径 (相对于项目根目录): {Colors.ENDC}")
|
||
file_path = os.path.join(project_root, file_path)
|
||
|
||
if not os.path.isfile(file_path):
|
||
print(f"{Colors.FAIL}错误: 文件不存在{Colors.ENDC}")
|
||
continue
|
||
|
||
# 询问是否只预览修改
|
||
dry_run = input(f"{Colors.BOLD}是否只预览修改而不实际写入? (y/n): {Colors.ENDC}").lower() == 'y'
|
||
|
||
# 处理文件
|
||
changes = process_file(file_path, project_root, dry_run)
|
||
|
||
print(f"\n{Colors.GREEN}处理完成! 共发现 {changes} 个需要修改的链接{Colors.ENDC}")
|
||
|
||
elif choice == '2':
|
||
# 处理目录
|
||
dir_path = input(f"{Colors.BOLD}请输入目录路径 (相对于项目根目录): {Colors.ENDC}")
|
||
dir_path = os.path.join(project_root, dir_path)
|
||
|
||
if not os.path.isdir(dir_path):
|
||
print(f"{Colors.FAIL}错误: 目录不存在{Colors.ENDC}")
|
||
continue
|
||
|
||
# 询问是否只预览修改
|
||
dry_run = input(f"{Colors.BOLD}是否只预览修改而不实际写入? (y/n): {Colors.ENDC}").lower() == 'y'
|
||
|
||
# 询问是否自动确认所有修改
|
||
auto_confirm = input(f"{Colors.BOLD}是否自动确认所有修改? (y/n): {Colors.ENDC}").lower() == 'y'
|
||
|
||
# 处理目录
|
||
file_count, total_changes = scan_directory(dir_path, project_root, dry_run, auto_confirm)
|
||
|
||
print(f"\n{Colors.GREEN}处理完成! 共处理 {file_count} 个文件,修改了 {total_changes} 个链接{Colors.ENDC}")
|
||
|
||
elif choice == '3':
|
||
# 退出
|
||
print(f"{Colors.BLUE}感谢使用,再见!{Colors.ENDC}")
|
||
break
|
||
|
||
else:
|
||
print(f"{Colors.WARNING}无效选项,请重试{Colors.ENDC}")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|