This commit is contained in:
parent
d519612ded
commit
200a90c859
624
sync.py
624
sync.py
|
|
@ -296,7 +296,7 @@ def search_chinese_page(title):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None):
|
def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None):
|
||||||
"""创建双语对比的HTML页面 - 使用精确的行号映射"""
|
"""创建双语对比的HTML页面 - Word批注风格,英文变更直接显示在对应中文行右侧"""
|
||||||
# 准备中文内容行
|
# 准备中文内容行
|
||||||
cn_lines = []
|
cn_lines = []
|
||||||
if cn_content:
|
if cn_content:
|
||||||
|
|
@ -305,6 +305,126 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None
|
||||||
# 解析diff并获取行号信息
|
# 解析diff并获取行号信息
|
||||||
parsed_diff = parse_diff_with_line_numbers(en_diff) if en_diff else []
|
parsed_diff = parse_diff_with_line_numbers(en_diff) if en_diff else []
|
||||||
|
|
||||||
|
# 构建行号到diff内容的映射 - 科学处理连续diff块
|
||||||
|
en_changes_by_line = {}
|
||||||
|
blank_lines_to_insert = {} # 记录需要在某行前插入的空白行及其对应的新增内容
|
||||||
|
|
||||||
|
if parsed_diff:
|
||||||
|
i = 0
|
||||||
|
while i < len(parsed_diff):
|
||||||
|
# 收集连续的diff块
|
||||||
|
diff_block = []
|
||||||
|
start_index = i
|
||||||
|
|
||||||
|
# 收集连续的添加/删除操作(跳过hunk和header)
|
||||||
|
while i < len(parsed_diff):
|
||||||
|
item = parsed_diff[i]
|
||||||
|
if item['type'] in ['added', 'removed']:
|
||||||
|
diff_block.append(item)
|
||||||
|
elif item['type'] in ['hunk', 'header'] or item['type'] == 'context':
|
||||||
|
if diff_block: # 如果已经有diff块,就停止
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# 处理连续的diff块
|
||||||
|
if diff_block:
|
||||||
|
# 计算行数平衡
|
||||||
|
line_balance = 0
|
||||||
|
for item in diff_block:
|
||||||
|
if item['type'] == 'added':
|
||||||
|
line_balance += 1
|
||||||
|
elif item['type'] == 'removed':
|
||||||
|
line_balance -= 1
|
||||||
|
|
||||||
|
# 如果平衡为正数,需要在中文侧添加空白行
|
||||||
|
if line_balance > 0:
|
||||||
|
# 找到基准行号(第一个操作的行号)
|
||||||
|
base_line = None
|
||||||
|
for item in diff_block:
|
||||||
|
if item['old_line']: # 优先使用删除行的行号
|
||||||
|
base_line = item['old_line']
|
||||||
|
break
|
||||||
|
elif item['new_line'] and base_line is None:
|
||||||
|
base_line = item['new_line']
|
||||||
|
|
||||||
|
if base_line:
|
||||||
|
# 收集需要分配到空白行的新增内容
|
||||||
|
additions_for_blank_lines = []
|
||||||
|
remaining_additions = []
|
||||||
|
|
||||||
|
for item in diff_block:
|
||||||
|
if item['type'] == 'added':
|
||||||
|
additions_for_blank_lines.append(item['content'])
|
||||||
|
|
||||||
|
# 记录需要插入的空白行和对应的内容
|
||||||
|
blank_lines_to_insert[base_line] = additions_for_blank_lines
|
||||||
|
|
||||||
|
# 处理具体的diff项
|
||||||
|
j = 0
|
||||||
|
while j < len(diff_block):
|
||||||
|
item = diff_block[j]
|
||||||
|
|
||||||
|
# 检查是否是替换操作(删除后紧跟新增)
|
||||||
|
if (item['type'] == 'removed' and j + 1 < len(diff_block) and
|
||||||
|
diff_block[j + 1]['type'] == 'added'):
|
||||||
|
next_item = diff_block[j + 1]
|
||||||
|
|
||||||
|
# 这是同一行的替换操作
|
||||||
|
target_line = item['old_line'] # 使用删除行的行号作为目标行号
|
||||||
|
|
||||||
|
if target_line not in en_changes_by_line:
|
||||||
|
en_changes_by_line[target_line] = []
|
||||||
|
|
||||||
|
en_changes_by_line[target_line].append({
|
||||||
|
'type': 'replaced',
|
||||||
|
'old_content': item['content'],
|
||||||
|
'new_content': next_item['content']
|
||||||
|
})
|
||||||
|
|
||||||
|
j += 2 # 跳过下一个项目,因为已经处理了
|
||||||
|
|
||||||
|
# 处理普通的添加操作(不包括需要分配到空白行的)
|
||||||
|
elif item['type'] == 'added' and item['new_line']:
|
||||||
|
# 如果这个新增内容已经被分配到空白行,就跳过
|
||||||
|
if line_balance > 0 and item['content'] in blank_lines_to_insert.get(base_line, []):
|
||||||
|
j += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item['new_line'] not in en_changes_by_line:
|
||||||
|
en_changes_by_line[item['new_line']] = []
|
||||||
|
en_changes_by_line[item['new_line']].append({
|
||||||
|
'type': 'added',
|
||||||
|
'content': item['content']
|
||||||
|
})
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
# 处理普通的删除操作(没有对应的新增)
|
||||||
|
elif item['type'] == 'removed' and item['old_line']:
|
||||||
|
if item['old_line'] not in en_changes_by_line:
|
||||||
|
en_changes_by_line[item['old_line']] = []
|
||||||
|
en_changes_by_line[item['old_line']].append({
|
||||||
|
'type': 'removed',
|
||||||
|
'content': item['content']
|
||||||
|
})
|
||||||
|
j += 1
|
||||||
|
else:
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
# 继续处理剩余项
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# HTML转义函数
|
||||||
|
def html_escape(text):
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
return (str(text)
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace("'", "'"))
|
||||||
|
|
||||||
# 生成HTML
|
# 生成HTML
|
||||||
html = f'''<!DOCTYPE html>
|
html = f'''<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
@ -323,6 +443,7 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
padding: 20px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.header {{
|
.header {{
|
||||||
|
|
@ -330,6 +451,7 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.header h1 {{
|
.header h1 {{
|
||||||
|
|
@ -343,165 +465,149 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.container {{
|
.content-container {{
|
||||||
display: flex;
|
max-width: 1200px;
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
min-height: calc(100vh - 100px);
|
border-radius: 8px;
|
||||||
}}
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
|
||||||
.column {{
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.column-header {{
|
.content-header {{
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
padding: 12px 20px;
|
padding: 15px 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
border-bottom: 1px solid #dee2e6;
|
border-bottom: 1px solid #dee2e6;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.diff-content {{
|
.diff-content {{
|
||||||
flex: 1;
|
padding: 0;
|
||||||
overflow-y: auto;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line {{
|
.line-wrapper {{
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 20px;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
.line-wrapper:hover {{
|
||||||
|
background-color: rgba(0, 123, 255, 0.02);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.has-changes {{
|
||||||
|
background-color: rgba(255, 193, 7, 0.05);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.main-line {{
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 24px;
|
||||||
|
align-items: center;
|
||||||
|
}}
|
||||||
|
|
||||||
.line-number {{
|
.line-number {{
|
||||||
width: 60px;
|
width: 60px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 0 10px;
|
padding: 8px 12px;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
border-right: 1px solid #dee2e6;
|
font-size: 12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}}
|
border-right: 1px solid #e9ecef;
|
||||||
|
|
||||||
.line.highlight {{
|
|
||||||
background-color: rgba(255, 235, 59, 0.3) !important;
|
|
||||||
animation: highlight 2s ease-in-out;
|
|
||||||
}}
|
|
||||||
|
|
||||||
@keyframes highlight {{
|
|
||||||
0% {{ background-color: rgba(255, 235, 59, 0.8); }}
|
|
||||||
100% {{ background-color: rgba(255, 235, 59, 0.3); }}
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line-content {{
|
.line-content {{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 10px;
|
padding: 8px 12px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
color: #333;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/* Diff specific styles */
|
/* 批注样式 */
|
||||||
.line.diff-added {{
|
.annotation {{
|
||||||
|
width: 400px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 1px solid #dee2e6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.has-changes .annotation {{
|
||||||
|
display: block;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.annotation-item {{
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.annotation-item:last-child {{
|
||||||
|
margin-bottom: 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.annotation-item.added {{
|
||||||
background-color: #e6ffec;
|
background-color: #e6ffec;
|
||||||
}}
|
|
||||||
|
|
||||||
.line.diff-added .line-content {{
|
|
||||||
background-color: #cdffd8;
|
|
||||||
border-left: 3px solid #28a745;
|
border-left: 3px solid #28a745;
|
||||||
|
color: #155724;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line.diff-removed {{
|
.annotation-item.removed {{
|
||||||
background-color: #ffeef0;
|
background-color: #ffeef0;
|
||||||
}}
|
|
||||||
|
|
||||||
.line.diff-removed .line-content {{
|
|
||||||
background-color: #fdb8c0;
|
|
||||||
border-left: 3px solid #dc3545;
|
border-left: 3px solid #dc3545;
|
||||||
|
color: #721c24;
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line.diff-context {{
|
.annotation-item.replaced {{
|
||||||
background-color: #ffffff;
|
margin-bottom: 8px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line.diff-context .line-content {{
|
.annotation-item.replaced .old-content {{
|
||||||
background-color: #ffffff;
|
background-color: #ffeef0;
|
||||||
|
border-left: 3px solid #dc3545;
|
||||||
|
color: #721c24;
|
||||||
|
text-decoration: line-through;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 4px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line.diff-hunk {{
|
.annotation-item.replaced .new-content {{
|
||||||
background-color: #f8f9fa;
|
background-color: #e6ffec;
|
||||||
|
border-left: 3px solid #28a745;
|
||||||
|
color: #155724;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.annotation-header {{
|
||||||
|
font-size: 10px;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
font-style: italic;
|
margin-bottom: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line.diff-hunk .line-content {{
|
/* 新页面提示 */
|
||||||
background-color: #f1f3f4;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.line.diff-header {{
|
|
||||||
background-color: #e9ecef;
|
|
||||||
color: #495057;
|
|
||||||
font-style: italic;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.line.diff-header .line-content {{
|
|
||||||
background-color: #e9ecef;
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Separator between columns */
|
|
||||||
.separator {{
|
|
||||||
width: 1px;
|
|
||||||
background-color: #dee2e6;
|
|
||||||
box-shadow: 0 0 5px rgba(0,0,0,0.1);
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
|
||||||
.diff-content::-webkit-scrollbar {{
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.diff-content::-webkit-scrollbar-track {{
|
|
||||||
background: #f1f1f1;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.diff-content::-webkit-scrollbar-thumb {{
|
|
||||||
background: #888;
|
|
||||||
border-radius: 4px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.diff-content::-webkit-scrollbar-thumb:hover {{
|
|
||||||
background: #555;
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {{
|
|
||||||
.container {{
|
|
||||||
flex-direction: column;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.separator {{
|
|
||||||
width: 100%;
|
|
||||||
height: 1px;
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Special styling for new page */
|
|
||||||
.new-page-notice {{
|
.new-page-notice {{
|
||||||
background-color: #d4edda;
|
background-color: #d4edda;
|
||||||
color: #155724;
|
color: #155724;
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
margin-bottom: 20px;
|
margin: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
border-left: 4px solid #28a745;
|
border-left: 4px solid #28a745;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
@ -509,17 +615,126 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None
|
||||||
background-color: #fff3cd;
|
background-color: #fff3cd;
|
||||||
color: #856404;
|
color: #856404;
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
margin-bottom: 20px;
|
margin: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
border-left: 4px solid #ffc107;
|
border-left: 4px solid #ffc107;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/* Line linking styles */
|
/* 响应式设计 */
|
||||||
.line[data-cn-line] {{
|
@media (max-width: 1024px) {{
|
||||||
cursor: pointer;
|
.annotation {{
|
||||||
|
width: 300px;
|
||||||
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.line:hover {{
|
@media (max-width: 768px) {{
|
||||||
background-color: rgba(0, 123, 255, 0.05);
|
body {{
|
||||||
|
padding: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.annotation {{
|
||||||
|
width: 100%;
|
||||||
|
display: block !important;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper {{
|
||||||
|
flex-direction: column;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.main-line {{
|
||||||
|
border-bottom: none;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* 高亮效果 */
|
||||||
|
.line-wrapper.highlight {{
|
||||||
|
background-color: rgba(255, 235, 59, 0.3) !important;
|
||||||
|
animation: highlight 2s ease-in-out;
|
||||||
|
}}
|
||||||
|
|
||||||
|
@keyframes highlight {{
|
||||||
|
0% {{ background-color: rgba(255, 235, 59, 0.6); }}
|
||||||
|
100% {{ background-color: rgba(255, 235, 59, 0.3); }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* 空行样式 */
|
||||||
|
.line-wrapper.empty-line .line-content {{
|
||||||
|
min-height: 24px;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* 空白占位行样式 */
|
||||||
|
.line-wrapper.blank-placeholder {{
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder .main-line {{
|
||||||
|
min-height: 24px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder .line-number {{
|
||||||
|
color: #dee2e6;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder .line-content {{
|
||||||
|
color: #dee2e6;
|
||||||
|
font-style: italic;
|
||||||
|
min-height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* 空白占位行的批注样式 */
|
||||||
|
.line-wrapper.blank-placeholder .annotation {{
|
||||||
|
width: 400px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 1px solid #dee2e6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: block;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder .annotation .annotation-item {{
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder .annotation .annotation-item:last-child {{
|
||||||
|
margin-bottom: 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder .annotation .annotation-item.added {{
|
||||||
|
background-color: #e6ffec;
|
||||||
|
border-left: 3px solid #28a745;
|
||||||
|
color: #155724;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder .annotation .annotation-header {{
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder:hover {{
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.line-wrapper.blank-placeholder:hover .main-line {{
|
||||||
|
background-color: rgba(0, 123, 255, 0.02);
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -532,58 +747,71 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="content-container">
|
||||||
<div class="column">
|
<div class="content-header">中文翻译(含英文变更批注)</div>
|
||||||
<div class="column-header">English Diff</div>
|
<div class="diff-content">
|
||||||
<div class="diff-content" id="en-diff">
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# 生成英文diff内容
|
# 添加中文内容和英文变更批注
|
||||||
if parsed_diff:
|
|
||||||
for item in parsed_diff:
|
|
||||||
if item['type'] == 'hunk':
|
|
||||||
html += f'<div class="line diff-hunk"><span class="line-content">{item["content"]}</span></div>'
|
|
||||||
elif item['type'] == 'header':
|
|
||||||
html += f'<div class="line diff-header"><span class="line-content">{item["content"]}</span></div>'
|
|
||||||
elif item['type'] == 'added':
|
|
||||||
cn_line_attr = f'data-cn-line="{item["new_line"]}"' if item["new_line"] and cn_lines and item["new_line"] <= len(cn_lines) else ''
|
|
||||||
cn_title = f'中文第{item["new_line"]}行' if item["new_line"] and cn_lines and item["new_line"] <= len(cn_lines) else ''
|
|
||||||
html += f'<div class="line diff-added" {cn_line_attr} title="{cn_title}"><span class="line-number">{item["new_line"] or ""}</span><span class="line-content">{item["content"]}</span></div>'
|
|
||||||
elif item['type'] == 'removed':
|
|
||||||
html += f'<div class="line diff-removed" title="已删除"><span class="line-number">{item["old_line"] or ""}</span><span class="line-content">{item["content"]}</span></div>'
|
|
||||||
elif item['type'] == 'context':
|
|
||||||
cn_line_attr = f'data-cn-line="{item["new_line"]}"' if item["new_line"] and cn_lines and item["new_line"] <= len(cn_lines) else ''
|
|
||||||
cn_title = f'中文第{item["new_line"]}行' if item["new_line"] and cn_lines and item["new_line"] <= len(cn_lines) else ''
|
|
||||||
html += f'<div class="line diff-context" {cn_line_attr} title="{cn_title}"><span class="line-number">{item["new_line"]}</span><span class="line-content">{item["content"]}</span></div>'
|
|
||||||
else:
|
|
||||||
html += f'<div class="line"><span class="line-content">{item["content"]}</span></div>'
|
|
||||||
else:
|
|
||||||
# 新页面或无diff
|
|
||||||
if en_diff and en_diff.startswith("新创建页面"):
|
|
||||||
html += '<div class="new-page-notice">新创建页面</div>'
|
|
||||||
|
|
||||||
# 显示完整内容(新页面或无diff时)
|
|
||||||
for i, line in enumerate(en_new_lines or [], 1):
|
|
||||||
cn_line_attr = f'data-cn-line="{i}"' if cn_lines and i <= len(cn_lines) else ''
|
|
||||||
cn_title = f'中文第{i}行' if cn_lines and i <= len(cn_lines) else ''
|
|
||||||
html += f'<div class="line diff-context" {cn_line_attr} title="{cn_title}"><span class="line-number">{i}</span><span class="line-content">{line}</span></div>'
|
|
||||||
|
|
||||||
html += '''
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<div class="column">
|
|
||||||
<div class="column-header">中文翻译</div>
|
|
||||||
<div class="diff-content" id="cn-content">
|
|
||||||
'''
|
|
||||||
|
|
||||||
# 添加中文内容
|
|
||||||
if cn_content:
|
if cn_content:
|
||||||
html += '<div id="cn-lines">'
|
|
||||||
for i, line in enumerate(cn_lines, 1):
|
for i, line in enumerate(cn_lines, 1):
|
||||||
html += f'<div class="line diff-context" id="cn-line-{i}"><span class="line-number">{i}</span><span class="line-content">{line}</span></div>'
|
# 检查是否需要在此行之前插入空白行
|
||||||
|
if i in blank_lines_to_insert:
|
||||||
|
additions_list = blank_lines_to_insert[i]
|
||||||
|
for addition_content in additions_list:
|
||||||
|
html += f'<div class="line-wrapper blank-placeholder">'
|
||||||
|
html += '<div class="main-line">'
|
||||||
|
html += '<span class="line-number"> </span>' # 不显示行号
|
||||||
|
html += f'<span class="line-content">(新增英文内容占位)</span>'
|
||||||
|
html += '</div>'
|
||||||
|
|
||||||
|
# 为空白行添加对应的新增批注
|
||||||
|
escaped_addition = html_escape(addition_content)
|
||||||
|
html += '<div class="annotation">'
|
||||||
|
html += f'<div class="annotation-item added">'
|
||||||
|
html += f'<div class="annotation-header">新增</div>'
|
||||||
|
html += f'<div>{escaped_addition}</div>'
|
||||||
|
html += '</div>'
|
||||||
|
html += '</div>'
|
||||||
|
|
||||||
|
html += '</div>'
|
||||||
|
|
||||||
|
escaped_line = html_escape(line)
|
||||||
|
has_changes = i in en_changes_by_line
|
||||||
|
changes = en_changes_by_line.get(i, [])
|
||||||
|
|
||||||
|
# 判断是否为空行
|
||||||
|
is_empty = not line.strip()
|
||||||
|
|
||||||
|
html += f'<div class="line-wrapper {"has-changes" if has_changes else ""} {"empty-line" if is_empty else ""}">'
|
||||||
|
html += f'<div class="main-line">'
|
||||||
|
html += f'<span class="line-number">{i}</span>'
|
||||||
|
html += f'<span class="line-content">{escaped_line if not is_empty else "(空行)"}</span>'
|
||||||
|
html += '</div>'
|
||||||
|
|
||||||
|
# 添加英文变更批注(只显示替换和删除操作,新增操作已经在空白行中显示)
|
||||||
|
if has_changes:
|
||||||
|
html += '<div class="annotation">'
|
||||||
|
for change in changes:
|
||||||
|
if change['type'] == 'added':
|
||||||
|
# 新增内容已经在空白行中显示,这里跳过
|
||||||
|
continue
|
||||||
|
elif change['type'] == 'removed':
|
||||||
|
escaped_change = html_escape(change['content'])
|
||||||
|
html += f'<div class="annotation-item removed">'
|
||||||
|
html += f'<div class="annotation-header">删除</div>'
|
||||||
|
html += f'<div>{escaped_change}</div>'
|
||||||
|
html += '</div>'
|
||||||
|
elif change['type'] == 'replaced':
|
||||||
|
escaped_old = html_escape(change['old_content'])
|
||||||
|
escaped_new = html_escape(change['new_content'])
|
||||||
|
html += f'<div class="annotation-item replaced">'
|
||||||
|
html += f'<div class="annotation-header">替换</div>'
|
||||||
|
html += f'<div class="old-content">{escaped_old}</div>'
|
||||||
|
html += f'<div class="new-content">{escaped_new}</div>'
|
||||||
|
html += '</div>'
|
||||||
|
html += '</div>'
|
||||||
|
|
||||||
html += '</div>'
|
html += '</div>'
|
||||||
else:
|
else:
|
||||||
html += '<div class="no-translation">未找到对应的中文翻译页面</div>'
|
html += '<div class="no-translation">未找到对应的中文翻译页面</div>'
|
||||||
|
|
@ -591,80 +819,18 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None
|
||||||
html += '''
|
html += '''
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 同步滚动功能
|
// 点击有变更的行时高亮
|
||||||
const enDiff = document.querySelector('#en-diff');
|
document.querySelectorAll('.line-wrapper.has-changes').forEach(lineWrapper => {{
|
||||||
const cnContent = document.querySelector('#cn-content');
|
lineWrapper.addEventListener('click', () => {{
|
||||||
const cnLines = {};
|
|
||||||
|
|
||||||
// 构建中文行的位置映射
|
|
||||||
if (document.getElementById('cn-lines')) {{
|
|
||||||
document.querySelectorAll('#cn-lines .line').forEach(line => {{
|
|
||||||
const lineNum = line.querySelector('.line-number').textContent;
|
|
||||||
if (lineNum) {{
|
|
||||||
cnLines[lineNum] = line.offsetTop;
|
|
||||||
}}
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
|
|
||||||
// 同步滚动
|
|
||||||
if (enDiff && cnContent) {{
|
|
||||||
enDiff.addEventListener('scroll', () => {{
|
|
||||||
cnContent.scrollTop = enDiff.scrollTop;
|
|
||||||
}});
|
|
||||||
|
|
||||||
cnContent.addEventListener('scroll', () => {{
|
|
||||||
enDiff.scrollTop = cnContent.scrollTop;
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
|
|
||||||
// 点击英文行时,高亮对应的中文行
|
|
||||||
document.querySelectorAll('[data-cn-line]').forEach(enLine => {{
|
|
||||||
enLine.addEventListener('click', () => {{
|
|
||||||
const cnLineNum = enLine.getAttribute('data-cn-line');
|
|
||||||
if (cnLineNum) {{
|
|
||||||
const cnLine = document.getElementById(`cn-line-${cnLineNum}`);
|
|
||||||
if (cnLine) {{
|
|
||||||
// 移除所有高亮
|
// 移除所有高亮
|
||||||
document.querySelectorAll('.line.highlight').forEach(line => {{
|
document.querySelectorAll('.line-wrapper.highlight').forEach(line => {{
|
||||||
line.classList.remove('highlight');
|
line.classList.remove('highlight');
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 高亮英文行和中文行
|
// 高亮当前行
|
||||||
enLine.classList.add('highlight');
|
lineWrapper.classList.add('highlight');
|
||||||
cnLine.classList.add('highlight');
|
|
||||||
|
|
||||||
// 滚动到中文行的位置
|
|
||||||
cnLine.scrollIntoView({{ behavior: 'smooth', block: 'center' }});
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}});
|
|
||||||
|
|
||||||
// 鼠标悬停时显示预览
|
|
||||||
enLine.addEventListener('mouseenter', () => {{
|
|
||||||
const cnLineNum = enLine.getAttribute('data-cn-line');
|
|
||||||
if (cnLineNum) {{
|
|
||||||
const cnLine = document.getElementById(`cn-line-${cnLineNum}`);
|
|
||||||
if (cnLine) {{
|
|
||||||
enLine.style.backgroundColor = 'rgba(0, 123, 255, 0.1)';
|
|
||||||
cnLine.style.backgroundColor = 'rgba(0, 123, 255, 0.1)';
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}});
|
|
||||||
|
|
||||||
enLine.addEventListener('mouseleave', () => {{
|
|
||||||
if (!enLine.classList.contains('highlight')) {{
|
|
||||||
enLine.style.backgroundColor = '';
|
|
||||||
}}
|
|
||||||
const cnLineNum = enLine.getAttribute('data-cn-line');
|
|
||||||
if (cnLineNum) {{
|
|
||||||
const cnLine = document.getElementById(`cn-line-${cnLineNum}`);
|
|
||||||
if (cnLine && !cnLine.classList.contains('highlight')) {{
|
|
||||||
cnLine.style.backgroundColor = '';
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue