package cascade
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
CSS generation and manipulation library for OCaml
Install
dune-project
Dependency
Authors
Maintainers
Sources
cascade-1.2.0.tbz
sha256=3315f94068943b3655bf530c1d82b68655524ff3601efedfbaf0a7f886fbf5a1
sha512=0f187c8e17fcbddaa0ad2760930956451062e476b678e1e60daf6ad9e7414fad99a2d70534dfdf8bca0a1092e374d009051f06b0ce9da33f2742cdde0187e75c
doc/src/cascade.diff/string_diff.ml.html
Source file string_diff.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344(** String difference analysis and formatting. *) type config = { max_width : int; short_threshold : int; show_caret : bool; indent : int; } (** Default maximum width for formatted output *) let default_max_width = 60 let default_config = { max_width = default_max_width; short_threshold = 30; show_caret = true; indent = 0; } (* ===== List Utilities ===== *) let rec list_take n = function | [] -> [] | _ when n <= 0 -> [] | h :: t -> h :: list_take (n - 1) t let rec list_drop n = function | [] -> [] | lst when n <= 0 -> lst | _ :: t -> list_drop (n - 1) t let rec zip_with_empty l1 l2 = match (l1, l2) with | [], [] -> [] | h1 :: t1, [] -> (h1, "") :: zip_with_empty t1 [] | [], h2 :: t2 -> ("", h2) :: zip_with_empty [] t2 | h1 :: t1, h2 :: t2 -> (h1, h2) :: zip_with_empty t1 t2 (* ===== Core Functions ===== *) let check_at_end len1 len2 = if len1 <> len2 then Some len1 else None let first_diff_pos s1 s2 = let len1 = String.length s1 in let len2 = String.length s2 in let rec find i = if i >= len1 || i >= len2 then check_at_end (min len1 len2) (max len1 len2) else if s1.[i] <> s2.[i] then Some i else find (i + 1) in find 0 let truncate_middle max_len s = let len = String.length s in if len <= max_len then s else let ellipsis_length = 3 in let half_len = (max_len - ellipsis_length) / 2 in let start_part = String.sub s 0 half_len in let end_part = String.sub s (len - half_len) half_len in String.concat "" [ start_part; "..."; end_part ] (* ===== Main Diff Type ===== *) type t = { position : int; (* Character position of first difference *) line_expected : int; column_expected : int; line_actual : int; column_actual : int; context_before : (string * string) list; diff_lines : string * string; context_after : (string * string) list; } (* Find line number and column for a character position *) let line_and_column lines pos = let rec find line_num char_count = function | [] -> (line_num - 1, pos - char_count, []) | line :: rest -> let line_len = String.length line + 1 in if char_count + line_len > pos then (line_num, pos - char_count, line :: rest) else find (line_num + 1) (char_count + line_len) rest in find 0 0 lines (* Extract context lines before a given line number *) let context_before lines line_num context_size = let before_lines = list_take line_num lines in let context_start = max 0 (List.length before_lines - context_size) in list_drop context_start before_lines let diff ?(context_size = 3) ~expected actual = match first_diff_pos expected actual with | None -> None | Some pos -> let lines_expected = String.split_on_char '\n' expected in let lines_actual = String.split_on_char '\n' actual in (* Find line and column for the diff position *) let line_exp, col_exp, remaining_exp = line_and_column lines_expected pos in let line_act, col_act, remaining_act = line_and_column lines_actual pos in (* Get context lines before the diff *) let context_before_exp = context_before lines_expected line_exp context_size in let context_before_act = context_before lines_actual line_act context_size in let context_before = zip_with_empty context_before_exp context_before_act in (* Get the lines containing the diff *) let diff_line_exp = match remaining_exp with [] -> "" | h :: _ -> h in let diff_line_act = match remaining_act with [] -> "" | h :: _ -> h in (* A file ending in a newline has no line after it: the empty string that [split_on_char] leaves behind is the terminator, not content. *) let drop_final_terminator lines = match List.rev lines with "" :: rest -> List.rev rest | _ -> lines in (* Get context lines after the diff *) let context_after_exp = match remaining_exp with | [] -> [] | _ :: t -> list_take context_size (drop_final_terminator t) in let context_after_act = match remaining_act with | [] -> [] | _ :: t -> list_take context_size (drop_final_terminator t) in let context_after = zip_with_empty context_after_exp context_after_act in Some { position = pos; line_expected = line_exp; column_expected = col_exp; line_actual = line_act; column_actual = col_act; context_before; diff_lines = (diff_line_exp, diff_line_act); context_after; } (* ===== Pretty-printing ===== *) let add_strings buf strings = List.iter (Buffer.add_string buf) strings let pp_caret ?(indent = 0) buf pos = Buffer.add_string buf (String.make (pos + indent) ' '); Buffer.add_string buf "^\n" (* ===== Visualising raw lines ===== *) let hex_digit n = Char.unsafe_chr (if n < 10 then Char.code '0' + n else Char.code 'a' + n - 10) let add_hex_escape buf code = Buffer.add_string buf "\\x"; Buffer.add_char buf (hex_digit (code lsr 4)); Buffer.add_char buf (hex_digit (code land 0xF)) let hex_escape code = let buf = Buffer.create 4 in add_hex_escape buf code; Buffer.contents buf let escape_codepoint u = let cp = Uchar.to_int u in if cp = 0x09 then "\\t" else if cp = 0x0D then "\\r" else if cp <= 0x1F || cp = 0x7F || (cp >= 0x80 && cp <= 0x9F) then hex_escape cp else "" let visualize s = let len = String.length s in let col = ref 0 in let map = Array.make (len + 1) (-1) in let buf = Buffer.create len in let set_positions pos n = for k = 0 to n - 1 do if map.(pos + k) = -1 then map.(pos + k) <- !col done in let emit_escape pos n esc = let width = String.length esc in set_positions pos n; Buffer.add_string buf esc; col := !col + width in (* [width] is in columns, as everywhere else a caret is placed in this repository: a multi-byte scalar occupies one. *) let emit_raw pos n ~width raw = set_positions pos n; Buffer.add_string buf raw; col := !col + width in (* A scalar the source already holds is copied back byte for byte rather than re-encoded, and a byte the decoder rejects is shown as its own escape: the report has to render what the file contains, not a repaired reading of it. *) let rec walk pos = if pos < len then begin let d = String.get_utf_8_uchar s pos in let n = Uchar.utf_decode_length d in if Uchar.utf_decode_is_valid d then begin let esc = escape_codepoint (Uchar.utf_decode_uchar d) in if esc <> "" then emit_escape pos n esc else emit_raw pos n ~width:1 (String.sub s pos n) end else for k = 0 to n - 1 do emit_escape (pos + k) 1 (hex_escape (Char.code s.[pos + k])) done; walk (pos + n) end in walk 0; map.(len) <- !col; (Buffer.contents buf, map) (* Pretty-print a line pair in unified diff format *) let pp_line_pair buf (exp, act) = let exp_vis, _ = visualize exp in let act_vis, _ = visualize act in if exp = act then ( Buffer.add_string buf " "; Buffer.add_string buf exp_vis; Buffer.add_char buf '\n') else ( if exp <> "" then ( Buffer.add_char buf '-'; Buffer.add_string buf exp_vis; Buffer.add_char buf '\n'); if act <> "" then ( Buffer.add_char buf '+'; Buffer.add_string buf act_vis; Buffer.add_char buf '\n')) (* Helper to format diff lines based on their length *) let extract_diff_window s len ~window_start ~window_end = if window_start >= len then ("...", 3) else let actual_end = min len window_end in let snippet = String.sub s window_start (actual_end - window_start) in let has_prefix = window_start > 0 in let has_suffix = window_end < len in let prefix = if has_prefix then "..." else "" in let suffix = if has_suffix then "..." else "" in let full_string = String.concat "" [ prefix; snippet; suffix ] in let prefix_len = if has_prefix then 3 else 0 in (full_string, prefix_len) let format_long_diff_line ~config ~diff_pos ~len1 ~len2 expected actual = let half = config.max_width / 2 in let window_start = max 0 (diff_pos - half) in let window_end = min (max len1 len2) (window_start + config.max_width) in let s1_display, prefix_len1 = extract_diff_window expected len1 ~window_start ~window_end in let s2_display, _prefix_len2 = extract_diff_window actual len2 ~window_start ~window_end in let s1_escaped, s1_map = visualize s1_display in let s2_escaped, _ = visualize s2_display in (* [s1_map] is indexed over the DISPLAYED string, whose leading ellipsis shifts every snippet byte right by [prefix_len1], and it already answers in columns. Indexing it with a bare snippet offset reads the wrong byte, and adding the prefix afterwards counts it twice. *) let last_col = s1_map.(Array.length s1_map - 1) in let visual_pos = if diff_pos < window_start then 0 else if diff_pos >= window_end then last_col else let i = prefix_len1 + (diff_pos - window_start) in if i >= Array.length s1_map then last_col else s1_map.(i) in `Long (s1_escaped, s2_escaped, visual_pos) let format_diff_line ?(config = default_config) expected actual = match first_diff_pos expected actual with | None -> `Equal | Some diff_pos -> let len1 = String.length expected in let len2 = String.length actual in if len1 <= config.short_threshold && len2 <= config.short_threshold then let exp_escaped, exp_map = visualize expected in let act_escaped, _ = visualize actual in `Short (exp_escaped, act_escaped, exp_map.(diff_pos)) else if len1 <= config.max_width && len2 <= config.max_width then let exp_escaped, exp_map = visualize expected in let act_escaped, _ = visualize actual in `Medium (exp_escaped, act_escaped, exp_map.(diff_pos)) else format_long_diff_line ~config ~diff_pos ~len1 ~len2 expected actual let pp ?(config = default_config) ?(expected_label = "Expected") ?(actual_label = "Actual") buf t = add_strings buf [ "Strings differ at position "; string_of_int t.position; " (line "; string_of_int t.line_expected; ", col "; string_of_int t.column_expected; ")\n\n"; ]; (* Git-style diff header *) add_strings buf [ "--- "; expected_label; "\n" ]; add_strings buf [ "+++ "; actual_label; "\n" ]; add_strings buf [ "@@ position "; string_of_int t.position; " @@\n" ]; (* Print context before *) List.iter (pp_line_pair buf) t.context_before; (* Print the diff lines with appropriate formatting *) let diff_exp, diff_act = t.diff_lines in (match format_diff_line ~config diff_exp diff_act with | `Equal -> let exp_vis, _ = visualize diff_exp in let act_vis, _ = visualize diff_act in add_strings buf [ "-"; exp_vis; "\n" ]; add_strings buf [ "+"; act_vis; "\n" ] | `Short (exp, act, pos) -> add_strings buf [ "-"; exp; "\n" ]; add_strings buf [ "+"; act; "\n" ]; if t.line_expected = t.line_actual then pp_caret ~indent:1 buf pos | `Medium (exp, act, pos) | `Long (exp, act, pos) -> add_strings buf [ "-"; exp; "\n" ]; add_strings buf [ "+"; act; "\n" ]; if t.line_expected = t.line_actual then pp_caret ~indent:1 buf pos); (* Print context after *) List.iter (pp_line_pair buf) t.context_after
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>