"""Unified diff generator for squid.conf changes. Provides a small, pure-Python unified-diff renderer. We avoid Python's ``difflib.unified_diff`` because we want tight control over hunk sizing and color classes used by the UI. """ from __future__ import annotations import difflib from typing import Iterable def unified_diff(old: str, new: str, context: int = 3, lineterm: str = "\n") -> list[dict]: """Return a list of diff lines as dicts: {kind, text, old_lineno, new_lineno}. kind: 'equal' | 'add' | 'del' | 'hunk' | 'meta' """ old_lines = old.splitlines(keepends=False) new_lines = new.splitlines(keepends=False) sm = difflib.SequenceMatcher(a=old_lines, b=new_lines, autojunk=False) out: list[dict] = [] old_no = 0 new_no = 0 def push_meta(text: str): out.append({"kind": "meta", "text": text, "old_lineno": None, "new_lineno": None}) push_meta("--- 当前配置 (existing)") push_meta("+++ 新配置 (proposed)") for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag == "equal": # show context lines (limit to first/last of long equal runs) block = old_lines[i1:i2] if len(block) <= context * 2 + 1: for k, ln in enumerate(block): old_no += 1 new_no += 1 out.append({ "kind": "equal", "text": ln, "old_lineno": old_no, "new_lineno": new_no, }) else: # head for k in range(context): old_no += 1; new_no += 1 out.append({ "kind": "equal", "text": old_lines[i1 + k], "old_lineno": old_no, "new_lineno": new_no, }) out.append({ "kind": "meta", "text": f"... ({i2 - i1 - 2 * context} equal lines skipped) ...", "old_lineno": None, "new_lineno": None, }) old_no += (i2 - i1 - 2 * context) new_no += (i2 - i1 - 2 * context) # tail for k in range(i2 - context, i2): old_no += 1; new_no += 1 out.append({ "kind": "equal", "text": old_lines[k], "old_lineno": old_no, "new_lineno": new_no, }) elif tag == "replace": for ln in old_lines[i1:i2]: old_no += 1 out.append({ "kind": "del", "text": ln, "old_lineno": old_no, "new_lineno": None, }) for ln in new_lines[j1:j2]: new_no += 1 out.append({ "kind": "add", "text": ln, "old_lineno": None, "new_lineno": new_no, }) elif tag == "delete": for ln in old_lines[i1:i2]: old_no += 1 out.append({ "kind": "del", "text": ln, "old_lineno": old_no, "new_lineno": None, }) elif tag == "insert": for ln in new_lines[j1:j2]: new_no += 1 out.append({ "kind": "add", "text": ln, "old_lineno": None, "new_lineno": new_no, }) return out def summarize(diff: list[dict]) -> dict: """Return {adds, dels, total_changed}.""" adds = sum(1 for d in diff if d["kind"] == "add") dels = sum(1 for d in diff if d["kind"] == "del") return {"adds": adds, "dels": dels, "total_changed": adds + dels} def render_html(diff: list[dict]) -> str: """Render diff to HTML with classes used by static/style.css.""" parts = ['
'] for d in diff: kind = d["kind"] text = d["text"] # escape HTML esc = (text.replace("&", "&").replace("<", "<").replace(">", ">")) old_n = d.get("old_lineno") or "" new_n = d.get("new_lineno") or "" if kind == "add": parts.append(f'
{old_n:>4}{new_n:>4} + {esc}
') elif kind == "del": parts.append(f'
{old_n:>4}{new_n:>4} - {esc}
') elif kind == "equal": parts.append(f'
{old_n:>4}{new_n:>4} {esc}
') else: # meta parts.append(f'
{esc}
') parts.append("
") return "\n".join(parts)