Files
dify-docs/scripts/fix_links_interactive.py
2025-03-25 12:09:33 +08:00

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()