diff --git a/sync.py b/sync.py index db7023e..0140e24 100644 --- a/sync.py +++ b/sync.py @@ -296,7 +296,7 @@ def search_chinese_page(title): return None def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None): - """创建双语对比的HTML页面 - 使用精确的行号映射""" + """创建双语对比的HTML页面 - Word批注风格,英文变更直接显示在对应中文行右侧""" # 准备中文内容行 cn_lines = [] if cn_content: @@ -305,6 +305,126 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None # 解析diff并获取行号信息 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 = f''' @@ -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; background-color: #f5f5f5; line-height: 1.6; + padding: 20px; }} .header {{ @@ -330,6 +451,7 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; + border-radius: 8px; }} .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; }} - .container {{ - display: flex; - max-width: 100%; + .content-container {{ + max-width: 1200px; margin: 0 auto; background-color: #fff; - min-height: calc(100vh - 100px); - }} - - .column {{ - flex: 1; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); overflow: hidden; - display: flex; - flex-direction: column; }} - .column-header {{ + .content-header {{ background-color: #e9ecef; - padding: 12px 20px; + padding: 15px 20px; font-weight: bold; color: #495057; border-bottom: 1px solid #dee2e6; }} .diff-content {{ - flex: 1; - overflow-y: auto; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 13px; - line-height: 1.4; + padding: 0; }} - .line {{ + .line-wrapper {{ display: flex; - min-height: 20px; + border-bottom: 1px solid #f0f0f0; 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 {{ width: 60px; text-align: right; - padding: 0 10px; + padding: 8px 12px; background-color: #f8f9fa; color: #6c757d; - border-right: 1px solid #dee2e6; + font-size: 12px; user-select: none; flex-shrink: 0; - }} - - .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); }} + border-right: 1px solid #e9ecef; }} .line-content {{ 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; 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; - }} - - .line.diff-added .line-content {{ - background-color: #cdffd8; border-left: 3px solid #28a745; + color: #155724; }} - .line.diff-removed {{ + .annotation-item.removed {{ background-color: #ffeef0; - }} - - .line.diff-removed .line-content {{ - background-color: #fdb8c0; border-left: 3px solid #dc3545; + color: #721c24; text-decoration: line-through; }} - .line.diff-context {{ - background-color: #ffffff; + .annotation-item.replaced {{ + margin-bottom: 8px; }} - .line.diff-context .line-content {{ - background-color: #ffffff; + .annotation-item.replaced .old-content {{ + 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 {{ - background-color: #f8f9fa; + .annotation-item.replaced .new-content {{ + background-color: #e6ffec; + border-left: 3px solid #28a745; + color: #155724; + padding: 4px 6px; + border-radius: 3px; + }} + + .annotation-header {{ + font-size: 10px; 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 {{ background-color: #d4edda; color: #155724; padding: 15px 20px; - margin-bottom: 20px; + margin: 15px; + border-radius: 4px; 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; color: #856404; padding: 15px 20px; - margin-bottom: 20px; + margin: 15px; + border-radius: 4px; border-left: 4px solid #ffc107; }} - /* Line linking styles */ - .line[data-cn-line] {{ - cursor: pointer; + /* 响应式设计 */ + @media (max-width: 1024px) {{ + .annotation {{ + width: 300px; + }} }} - .line:hover {{ - background-color: rgba(0, 123, 255, 0.05); + @media (max-width: 768px) {{ + 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); }} @@ -532,139 +747,90 @@ def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None -
-
-
English Diff
-
+
+
中文翻译(含英文变更批注)
+
''' - # 生成英文diff内容 - if parsed_diff: - for item in parsed_diff: - if item['type'] == 'hunk': - html += f'
{item["content"]}
' - elif item['type'] == 'header': - html += f'
{item["content"]}
' - 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'
{item["new_line"] or ""}{item["content"]}
' - elif item['type'] == 'removed': - html += f'
{item["old_line"] or ""}{item["content"]}
' - 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'
{item["new_line"]}{item["content"]}
' - else: - html += f'
{item["content"]}
' - else: - # 新页面或无diff - if en_diff and en_diff.startswith("新创建页面"): - html += '
新创建页面
' - - # 显示完整内容(新页面或无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'
{i}{line}
' - - html += ''' -
-
- -
- -
-
中文翻译
-
-''' - - # 添加中文内容 + # 添加中文内容和英文变更批注 if cn_content: - html += '
' for i, line in enumerate(cn_lines, 1): - html += f'
{i}{line}
' - html += '
' + # 检查是否需要在此行之前插入空白行 + if i in blank_lines_to_insert: + additions_list = blank_lines_to_insert[i] + for addition_content in additions_list: + html += f'
' + html += '
' + html += ' ' # 不显示行号 + html += f'(新增英文内容占位)' + html += '
' + + # 为空白行添加对应的新增批注 + escaped_addition = html_escape(addition_content) + html += '
' + html += f'
' + html += f'
新增
' + html += f'
{escaped_addition}
' + html += '
' + html += '
' + + html += '
' + + 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'
' + html += f'
' + html += f'{i}' + html += f'{escaped_line if not is_empty else "(空行)"}' + html += '
' + + # 添加英文变更批注(只显示替换和删除操作,新增操作已经在空白行中显示) + if has_changes: + html += '
' + for change in changes: + if change['type'] == 'added': + # 新增内容已经在空白行中显示,这里跳过 + continue + elif change['type'] == 'removed': + escaped_change = html_escape(change['content']) + html += f'
' + html += f'
删除
' + html += f'
{escaped_change}
' + html += '
' + elif change['type'] == 'replaced': + escaped_old = html_escape(change['old_content']) + escaped_new = html_escape(change['new_content']) + html += f'
' + html += f'
替换
' + html += f'
{escaped_old}
' + html += f'
{escaped_new}
' + html += '
' + html += '
' + + html += '
' else: html += '
未找到对应的中文翻译页面
' html += ''' -