1#[cfg(feature = "wasm")]
9use wasm_bindgen::prelude::*;
10
11use crate::calculate_tokens;
12
13#[cfg_attr(feature = "wasm", wasm_bindgen)]
17#[derive(Debug, Clone)]
18pub struct DiffResult {
19 added_lines: u32,
20 removed_lines: u32,
21 added_tokens: u32,
22 removed_tokens: u32,
23}
24
25#[cfg_attr(feature = "wasm", wasm_bindgen)]
26impl DiffResult {
27 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
29 pub fn added_lines(&self) -> u32 {
30 self.added_lines
31 }
32 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
34 pub fn removed_lines(&self) -> u32 {
35 self.removed_lines
36 }
37 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
39 pub fn added_tokens(&self) -> u32 {
40 self.added_tokens
41 }
42 #[cfg_attr(feature = "wasm", wasm_bindgen(getter))]
44 pub fn removed_tokens(&self) -> u32 {
45 self.removed_tokens
46 }
47}
48
49fn build_lcs_table(old_lines: &[&str], new_lines: &[&str]) -> Vec<Vec<u32>> {
51 let n = old_lines.len();
52 let m = new_lines.len();
53 let mut dp = vec![vec![0u32; m + 1]; n + 1];
54 for i in 1..=n {
55 for j in 1..=m {
56 if old_lines[i - 1] == new_lines[j - 1] {
57 dp[i][j] = dp[i - 1][j - 1] + 1;
58 } else {
59 dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
60 }
61 }
62 }
63 dp
64}
65
66#[cfg_attr(feature = "wasm", wasm_bindgen)]
72pub fn compute_diff(old_text: &str, new_text: &str) -> DiffResult {
73 let old_lines: Vec<&str> = old_text.lines().collect();
74 let new_lines: Vec<&str> = new_text.lines().collect();
75
76 let n = old_lines.len();
77 let m = new_lines.len();
78 let dp = build_lcs_table(&old_lines, &new_lines);
79
80 let mut removed = Vec::new();
82 let mut added = Vec::new();
83 let mut i = n;
84 let mut j = m;
85
86 while i > 0 || j > 0 {
87 if i > 0 && j > 0 && old_lines[i - 1] == new_lines[j - 1] {
88 i -= 1;
89 j -= 1;
90 } else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) {
91 added.push(new_lines[j - 1]);
92 j -= 1;
93 } else {
94 removed.push(old_lines[i - 1]);
95 i -= 1;
96 }
97 }
98
99 let added_tokens: u32 = added.iter().map(|l| calculate_tokens(l)).sum();
100 let removed_tokens: u32 = removed.iter().map(|l| calculate_tokens(l)).sum();
101
102 DiffResult {
103 added_lines: added.len() as u32,
104 removed_lines: removed.len() as u32,
105 added_tokens,
106 removed_tokens,
107 }
108}
109
110#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
114pub struct StructuredDiffLine {
115 #[serde(rename = "type")]
117 pub line_type: String,
118 pub content: String,
120 #[serde(rename = "oldLine")]
122 pub old_line: Option<u32>,
123 #[serde(rename = "newLine")]
125 pub new_line: Option<u32>,
126}
127
128#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
130pub struct StructuredDiffResult {
131 pub lines: Vec<StructuredDiffLine>,
133 #[serde(rename = "addedLineCount")]
135 pub added_line_count: u32,
136 #[serde(rename = "removedLineCount")]
138 pub removed_line_count: u32,
139 #[serde(rename = "addedTokens")]
141 pub added_tokens: u32,
142 #[serde(rename = "removedTokens")]
144 pub removed_tokens: u32,
145}
146
147#[cfg_attr(feature = "wasm", wasm_bindgen)]
156pub fn structured_diff(old_text: &str, new_text: &str) -> String {
157 let result = structured_diff_native(old_text, new_text);
158 serde_json::to_string(&result).unwrap_or_else(|_| {
159 r#"{"lines":[],"addedLineCount":0,"removedLineCount":0,"addedTokens":0,"removedTokens":0}"#
160 .to_string()
161 })
162}
163
164pub fn structured_diff_native(old_text: &str, new_text: &str) -> StructuredDiffResult {
166 let old_lines: Vec<&str> = old_text.lines().collect();
167 let new_lines: Vec<&str> = new_text.lines().collect();
168
169 let n = old_lines.len();
170 let m = new_lines.len();
171 let dp = build_lcs_table(&old_lines, &new_lines);
172
173 let mut entries: Vec<StructuredDiffLine> = Vec::new();
175 let mut i = n;
176 let mut j = m;
177
178 while i > 0 || j > 0 {
179 if i > 0 && j > 0 && old_lines[i - 1] == new_lines[j - 1] {
180 entries.push(StructuredDiffLine {
181 line_type: "context".to_string(),
182 content: old_lines[i - 1].to_string(),
183 old_line: Some(i as u32),
184 new_line: Some(j as u32),
185 });
186 i -= 1;
187 j -= 1;
188 } else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) {
189 entries.push(StructuredDiffLine {
190 line_type: "added".to_string(),
191 content: new_lines[j - 1].to_string(),
192 old_line: None,
193 new_line: Some(j as u32),
194 });
195 j -= 1;
196 } else {
197 entries.push(StructuredDiffLine {
198 line_type: "removed".to_string(),
199 content: old_lines[i - 1].to_string(),
200 old_line: Some(i as u32),
201 new_line: None,
202 });
203 i -= 1;
204 }
205 }
206
207 entries.reverse();
209
210 let mut added_count: u32 = 0;
212 let mut removed_count: u32 = 0;
213 let mut added_tokens: u32 = 0;
214 let mut removed_tokens: u32 = 0;
215
216 for entry in &entries {
217 match entry.line_type.as_str() {
218 "added" => {
219 added_count += 1;
220 added_tokens += calculate_tokens(&entry.content);
221 }
222 "removed" => {
223 removed_count += 1;
224 removed_tokens += calculate_tokens(&entry.content);
225 }
226 _ => {}
227 }
228 }
229
230 StructuredDiffResult {
231 lines: entries,
232 added_line_count: added_count,
233 removed_line_count: removed_count,
234 added_tokens,
235 removed_tokens,
236 }
237}
238
239#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
243pub struct MultiDiffVariant {
244 #[serde(rename = "versionIndex")]
246 pub version_index: usize,
247 pub content: String,
249}
250
251#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
253pub struct MultiDiffLine {
254 #[serde(rename = "lineNumber")]
256 pub line_number: usize,
257 #[serde(rename = "type")]
259 pub line_type: String,
260 pub content: String,
262 pub agreement: usize,
264 pub total: usize,
266 pub variants: Vec<MultiDiffVariant>,
268}
269
270#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
272pub struct MultiDiffStats {
273 #[serde(rename = "totalLines")]
274 pub total_lines: usize,
275 #[serde(rename = "consensusLines")]
276 pub consensus_lines: usize,
277 #[serde(rename = "divergentLines")]
278 pub divergent_lines: usize,
279 #[serde(rename = "consensusPercentage")]
280 pub consensus_percentage: f64,
281}
282
283#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
285pub struct MultiDiffResult {
286 #[serde(rename = "baseVersion")]
288 pub base_version: usize,
289 #[serde(rename = "versionCount")]
291 pub version_count: usize,
292 pub lines: Vec<MultiDiffLine>,
293 pub stats: MultiDiffStats,
294}
295
296pub fn multi_way_diff_native(base: &str, versions_json: &str) -> Result<MultiDiffResult, String> {
309 let additional: Vec<String> =
310 serde_json::from_str(versions_json).map_err(|e| format!("Invalid versions JSON: {e}"))?;
311
312 if additional.len() > 4 {
313 return Err(format!(
314 "Too many versions: got {}, max is 4 additional (5 total)",
315 additional.len()
316 ));
317 }
318
319 let version_count = 1 + additional.len();
320 let base_lines: Vec<&str> = base.lines().collect();
321
322 if base_lines.is_empty() && additional.iter().all(|v| v.is_empty()) {
323 return Ok(MultiDiffResult {
324 base_version: 0,
325 version_count,
326 lines: vec![],
327 stats: MultiDiffStats {
328 total_lines: 0,
329 consensus_lines: 0,
330 divergent_lines: 0,
331 consensus_percentage: 100.0,
332 },
333 });
334 }
335
336 let n_base = base_lines.len();
337 let alignments: Vec<crate::diff_multi::VersionAlignment> = additional
338 .iter()
339 .map(|v| crate::diff_multi::align_version(base, v.as_str(), n_base))
340 .collect();
341
342 let grid = crate::diff_multi::build_aligned_grid(&base_lines, &alignments, version_count);
343 let (lines, stats) = crate::diff_multi::score_grid(&grid, version_count);
344
345 Ok(MultiDiffResult {
346 base_version: 0,
347 version_count,
348 lines,
349 stats,
350 })
351}
352
353pub fn multi_way_diff(base: &str, versions_json: &str) -> String {
358 match multi_way_diff_native(base, versions_json) {
359 Ok(result) => serde_json::to_string(&result)
360 .unwrap_or_else(|e| format!(r#"{{"error":"serialization failed: {e}"}}"#)),
361 Err(e) => format!(r#"{{"error":{}}}"#, serde_json::json!(e)),
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_compute_diff_identical() {
371 let text = "line 1\nline 2\nline 3";
372 let result = compute_diff(text, text);
373 assert_eq!(result.added_lines(), 0);
374 assert_eq!(result.removed_lines(), 0);
375 assert_eq!(result.added_tokens(), 0);
376 assert_eq!(result.removed_tokens(), 0);
377 }
378
379 #[test]
380 fn test_compute_diff_empty_to_content() {
381 let result = compute_diff("", "line 1\nline 2");
382 assert_eq!(result.added_lines(), 2);
383 assert_eq!(result.removed_lines(), 0);
384 }
385
386 #[test]
387 fn test_compute_diff_content_to_empty() {
388 let result = compute_diff("line 1\nline 2", "");
389 assert_eq!(result.added_lines(), 0);
390 assert_eq!(result.removed_lines(), 2);
391 }
392
393 #[test]
394 fn test_compute_diff_mixed_changes() {
395 let old = "line 1\nline 2\nline 3\nline 4";
396 let new = "line 1\nmodified 2\nline 3\nline 5\nline 6";
397 let result = compute_diff(old, new);
398 assert_eq!(result.removed_lines(), 2);
399 assert_eq!(result.added_lines(), 3);
400 assert!(result.added_tokens() > 0);
401 assert!(result.removed_tokens() > 0);
402 }
403
404 #[test]
405 fn test_compute_diff_tokens() {
406 let old = "short";
407 let new = "this is a much longer replacement line";
408 let result = compute_diff(old, new);
409 assert_eq!(result.removed_lines(), 1);
410 assert_eq!(result.added_lines(), 1);
411 assert_eq!(result.removed_tokens(), calculate_tokens("short"));
412 assert_eq!(
413 result.added_tokens(),
414 calculate_tokens("this is a much longer replacement line")
415 );
416 }
417
418 #[test]
419 fn test_structured_diff_identical() {
420 let text = "line 1\nline 2\nline 3";
421 let result = structured_diff_native(text, text);
422 assert_eq!(result.lines.len(), 3);
423 assert!(result.lines.iter().all(|l| l.line_type == "context"));
424 assert_eq!(result.added_line_count, 0);
425 assert_eq!(result.removed_line_count, 0);
426 assert_eq!(result.lines[0].old_line, Some(1));
427 assert_eq!(result.lines[0].new_line, Some(1));
428 assert_eq!(result.lines[2].old_line, Some(3));
429 assert_eq!(result.lines[2].new_line, Some(3));
430 }
431
432 #[test]
433 fn test_structured_diff_additions() {
434 let old = "line 1\nline 3";
435 let new = "line 1\nline 2\nline 3";
436 let result = structured_diff_native(old, new);
437 assert_eq!(result.added_line_count, 1);
438 assert_eq!(result.removed_line_count, 0);
439 let types: Vec<&str> = result.lines.iter().map(|l| l.line_type.as_str()).collect();
440 assert_eq!(types, vec!["context", "added", "context"]);
441 let added = &result.lines[1];
442 assert_eq!(added.content, "line 2");
443 assert_eq!(added.old_line, None);
444 assert_eq!(added.new_line, Some(2));
445 }
446
447 #[test]
448 fn test_structured_diff_removals() {
449 let old = "line 1\nline 2\nline 3";
450 let new = "line 1\nline 3";
451 let result = structured_diff_native(old, new);
452 assert_eq!(result.added_line_count, 0);
453 assert_eq!(result.removed_line_count, 1);
454 let types: Vec<&str> = result.lines.iter().map(|l| l.line_type.as_str()).collect();
455 assert_eq!(types, vec!["context", "removed", "context"]);
456 let removed = &result.lines[1];
457 assert_eq!(removed.content, "line 2");
458 assert_eq!(removed.old_line, Some(2));
459 assert_eq!(removed.new_line, None);
460 }
461
462 #[test]
463 fn test_structured_diff_mixed() {
464 let old = "line 1\nline 2\nline 3\nline 4";
465 let new = "line 1\nmodified 2\nline 3\nline 5";
466 let result = structured_diff_native(old, new);
467 assert_eq!(result.added_line_count, 2);
468 assert_eq!(result.removed_line_count, 2);
469 assert!(result.added_tokens > 0);
470 assert!(result.removed_tokens > 0);
471 }
472
473 #[test]
474 fn test_structured_diff_empty_to_content() {
475 let result = structured_diff_native("", "line 1\nline 2");
476 assert_eq!(result.added_line_count, 2);
477 assert_eq!(result.removed_line_count, 0);
478 assert!(result.lines.iter().all(|l| l.line_type == "added"));
479 }
480
481 #[test]
482 fn test_structured_diff_content_to_empty() {
483 let result = structured_diff_native("line 1\nline 2", "");
484 assert_eq!(result.added_line_count, 0);
485 assert_eq!(result.removed_line_count, 2);
486 assert!(result.lines.iter().all(|l| l.line_type == "removed"));
487 }
488
489 #[test]
490 fn test_structured_diff_json_serialization() {
491 let json = structured_diff("hello\n", "hello\nworld\n");
492 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
493 assert!(parsed["lines"].is_array());
494 assert_eq!(parsed["addedLineCount"], 1);
495 assert_eq!(parsed["removedLineCount"], 0);
496 }
497
498 #[test]
501 fn test_multi_way_diff_all_identical() {
502 let base = "line 1\nline 2\nline 3";
503 let v1 = "line 1\nline 2\nline 3";
504 let v2 = "line 1\nline 2\nline 3";
505 let versions_json = serde_json::to_string(&vec![v1, v2]).unwrap();
506 let result = multi_way_diff_native(base, &versions_json).unwrap();
507
508 assert_eq!(result.base_version, 0);
509 assert_eq!(result.version_count, 3);
510 assert_eq!(result.lines.len(), 3);
511 assert!(result.lines.iter().all(|l| l.line_type == "consensus"));
512 assert!(result.lines.iter().all(|l| l.agreement == 3));
513 assert!(result.lines.iter().all(|l| l.total == 3));
514 assert!(result.lines.iter().all(|l| l.variants.is_empty()));
515 assert_eq!(result.stats.consensus_lines, 3);
516 assert_eq!(result.stats.divergent_lines, 0);
517 assert_eq!(result.stats.total_lines, 3);
518 assert_eq!(result.stats.consensus_percentage, 100.0);
519 }
520
521 #[test]
522 fn test_multi_way_diff_one_divergent_line() {
523 let base = "alpha\nbeta\ngamma";
526 let v1 = "alpha\nBETA\ngamma";
527 let v2 = "alpha\nBETA\ngamma";
528 let versions_json = serde_json::to_string(&vec![v1, v2]).unwrap();
529 let result = multi_way_diff_native(base, &versions_json).unwrap();
530
531 assert_eq!(result.version_count, 3);
532 assert_eq!(result.stats.consensus_lines, 2);
533 assert_eq!(result.stats.divergent_lines, 1);
534
535 let line1 = &result.lines[0];
537 assert_eq!(line1.line_type, "consensus");
538 assert_eq!(line1.content, "alpha");
539 assert_eq!(line1.agreement, 3);
540
541 let line2 = &result.lines[1];
543 assert_eq!(line2.line_type, "divergent");
544 assert_eq!(line2.content, "BETA");
545 assert_eq!(line2.agreement, 2);
546 assert_eq!(line2.total, 3);
547 assert_eq!(line2.variants.len(), 3);
548 }
549
550 #[test]
551 fn test_multi_way_diff_three_way_split() {
552 let base = "same\nbase_line2\nsame";
554 let v1 = "same\nv1_line2\nsame";
555 let v2 = "same\nv2_line2\nsame";
556 let versions_json = serde_json::to_string(&vec![v1, v2]).unwrap();
557 let result = multi_way_diff_native(base, &versions_json).unwrap();
558
559 let line2 = &result.lines[1];
560 assert_eq!(line2.line_type, "divergent");
561 assert_eq!(line2.agreement, 1); assert_eq!(line2.total, 3);
563 assert_eq!(line2.variants.len(), 3);
564 }
565
566 #[test]
567 fn test_multi_way_diff_different_lengths_lcs_aligned() {
568 let base = "a\nb\nc";
570 let v1 = "a\nb\nc\nd";
571 let v2 = "a\nb";
572 let versions_json = serde_json::to_string(&vec![v1, v2]).unwrap();
573 let result = multi_way_diff_native(base, &versions_json).unwrap();
574
575 assert_eq!(result.stats.total_lines, 4);
577 assert_eq!(result.stats.consensus_lines, 2);
578 assert_eq!(result.stats.divergent_lines, 1);
579
580 assert_eq!(result.lines[0].line_type, "consensus");
581 assert_eq!(result.lines[0].content, "a");
582 assert_eq!(result.lines[1].line_type, "consensus");
583 assert_eq!(result.lines[1].content, "b");
584
585 assert_eq!(result.lines[2].line_type, "divergent");
587 assert_eq!(result.lines[2].content, "c");
588
589 assert_eq!(result.lines[3].line_type, "insertion");
591 assert_eq!(result.lines[3].content, "d");
592 }
593
594 #[test]
595 fn test_multi_way_diff_empty_base_and_versions() {
596 let versions_json = serde_json::to_string(&vec!["", ""]).unwrap();
597 let result = multi_way_diff_native("", &versions_json).unwrap();
598 assert_eq!(result.stats.total_lines, 0);
599 assert!(result.lines.is_empty());
600 assert_eq!(result.stats.consensus_percentage, 100.0);
601 }
602
603 #[test]
604 fn test_multi_way_diff_single_version() {
605 let base = "hello\nworld";
606 let v1 = "hello\nearth";
607 let versions_json = serde_json::to_string(&vec![v1]).unwrap();
608 let result = multi_way_diff_native(base, &versions_json).unwrap();
609
610 assert_eq!(result.version_count, 2);
611 assert_eq!(result.stats.total_lines, 2);
612 assert_eq!(result.lines[0].line_type, "consensus");
613 assert_eq!(result.lines[1].line_type, "divergent");
614 assert_eq!(result.lines[1].agreement, 1);
615 assert_eq!(result.lines[1].variants.len(), 2);
616 }
617
618 #[test]
619 fn test_multi_way_diff_rejects_too_many_versions() {
620 let versions: Vec<&str> = vec!["a", "b", "c", "d", "e"]; let versions_json = serde_json::to_string(&versions).unwrap();
622 let err = multi_way_diff_native("base", &versions_json).unwrap_err();
623 assert!(err.contains("Too many versions"));
624 }
625
626 #[test]
627 fn test_multi_way_diff_max_versions_accepted() {
628 let versions: Vec<&str> = vec!["a\nb", "a\nc", "a\nd", "a\ne"];
630 let versions_json = serde_json::to_string(&versions).unwrap();
631 let result = multi_way_diff_native("a\nb", &versions_json).unwrap();
632 assert_eq!(result.version_count, 5);
633 }
634
635 #[test]
636 fn test_multi_way_diff_json_output_shape() {
637 let base = "x\ny";
638 let v1 = "x\nz";
639 let versions_json = serde_json::to_string(&vec![v1]).unwrap();
640 let json = multi_way_diff(base, &versions_json);
641 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
642
643 assert!(parsed["lines"].is_array());
644 assert_eq!(parsed["baseVersion"], 0);
645 assert_eq!(parsed["versionCount"], 2);
646 assert!(parsed["stats"]["totalLines"].is_number());
647 assert!(parsed["stats"]["consensusLines"].is_number());
648 assert!(parsed["stats"]["divergentLines"].is_number());
649 assert!(parsed["stats"]["consensusPercentage"].is_number());
650 }
651
652 #[test]
653 fn test_multi_way_diff_line_numbers_are_one_based() {
654 let base = "a\nb\nc";
655 let v1 = "a\nb\nc";
656 let versions_json = serde_json::to_string(&vec![v1]).unwrap();
657 let result = multi_way_diff_native(base, &versions_json).unwrap();
658
659 for (idx, line) in result.lines.iter().enumerate() {
660 assert_eq!(line.line_number, idx + 1);
661 }
662 }
663
664 #[test]
665 fn test_multi_way_diff_consensus_percentage_rounding() {
666 let base = "a\nb\nc";
668 let v1 = "a\nX\nc";
669 let versions_json = serde_json::to_string(&vec![v1]).unwrap();
670 let result = multi_way_diff_native(base, &versions_json).unwrap();
671 assert_eq!(result.stats.consensus_percentage, 66.7);
672 }
673
674 #[test]
677 fn test_multi_way_diff_insertion_after_first_line() {
678 let base = "A\nB\nC\nD\nE";
681 let v1 = "A\nX\nY\nB\nC\nD\nE";
682 let versions_json = serde_json::to_string(&vec![v1]).unwrap();
683 let result = multi_way_diff_native(base, &versions_json).unwrap();
684
685 assert_eq!(result.stats.total_lines, 7); assert_eq!(result.stats.consensus_lines, 5);
687 assert_eq!(result.stats.divergent_lines, 0);
688 assert_eq!(result.stats.consensus_percentage, 100.0);
689 assert_eq!(result.lines[0].content, "A");
690 assert_eq!(result.lines[1].line_type, "insertion");
691 assert_eq!(result.lines[1].content, "X");
692 assert_eq!(result.lines[2].line_type, "insertion");
693 assert_eq!(result.lines[2].content, "Y");
694 for (i, expected) in ["B", "C", "D", "E"].iter().enumerate() {
695 assert_eq!(result.lines[3 + i].line_type, "consensus");
696 assert_eq!(result.lines[3 + i].content, *expected);
697 }
698 }
699
700 #[test]
701 fn test_multi_way_diff_deletion_aligned() {
702 let base = "A\nB\nC\nD\nE";
704 let v1 = "A\nB\nD\nE";
705 let versions_json = serde_json::to_string(&vec![v1]).unwrap();
706 let result = multi_way_diff_native(base, &versions_json).unwrap();
707
708 assert_eq!(result.stats.total_lines, 5);
709 assert_eq!(result.stats.consensus_lines, 4);
710 assert_eq!(result.stats.divergent_lines, 1);
711 let row_c = result
712 .lines
713 .iter()
714 .find(|l| l.line_type == "divergent")
715 .expect("expected one divergent row for C");
716 assert_eq!(row_c.content, "C");
717 for line in result.lines.iter().filter(|l| l.line_type != "divergent") {
718 assert_eq!(line.line_type, "consensus");
719 }
720 }
721
722 #[test]
723 fn test_multi_way_diff_mixed_insertions_two_versions() {
724 let base = "A\nB\nC";
726 let v1 = "A\nX\nB\nC";
727 let v2 = "A\nB\nY\nC";
728 let versions_json = serde_json::to_string(&vec![v1, v2]).unwrap();
729 let result = multi_way_diff_native(base, &versions_json).unwrap();
730
731 assert_eq!(result.stats.total_lines, 5); assert_eq!(result.stats.consensus_lines, 3);
733 assert_eq!(result.stats.divergent_lines, 0);
734 assert_eq!(result.stats.consensus_percentage, 100.0);
735
736 let insertions: Vec<&MultiDiffLine> = result
737 .lines
738 .iter()
739 .filter(|l| l.line_type == "insertion")
740 .collect();
741 assert_eq!(insertions.len(), 2);
742
743 let insertion_contents: std::collections::HashSet<&str> =
744 insertions.iter().map(|l| l.content.as_str()).collect();
745 assert!(insertion_contents.contains("X"));
746 assert!(insertion_contents.contains("Y"));
747
748 for line in result.lines.iter().filter(|l| l.line_type != "insertion") {
749 assert_eq!(line.line_type, "consensus");
750 }
751 }
752
753 #[test]
754 fn test_multi_way_diff_markdown_section_insertion() {
755 let base = "# Title\n\nFirst paragraph.\n\n# Conclusion\n\nEnd.";
757 let v1 = "# Title\n\nFirst paragraph.\n\n# New Section\n\nMiddle content.\n\n# Conclusion\n\nEnd.";
758 let versions_json = serde_json::to_string(&vec![v1]).unwrap();
759 let result = multi_way_diff_native(base, &versions_json).unwrap();
760
761 assert_eq!(result.stats.consensus_lines, 7);
764 assert_eq!(result.stats.divergent_lines, 0);
765 assert_eq!(result.stats.consensus_percentage, 100.0);
766 let insertion_lines: Vec<&MultiDiffLine> = result
767 .lines
768 .iter()
769 .filter(|l| l.line_type == "insertion")
770 .collect();
771 assert_eq!(insertion_lines.len(), 4);
772 let contents: Vec<&str> = insertion_lines.iter().map(|l| l.content.as_str()).collect();
773 assert!(contents.contains(&"# New Section"));
774 assert!(contents.contains(&"Middle content."));
775 }
776}