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

366 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()