canon 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +69 -92
- data/README.adoc +13 -13
- data/docs/.lycheeignore +69 -0
- data/docs/Gemfile +1 -0
- data/docs/_config.yml +90 -1
- data/docs/advanced/diff-classification.adoc +82 -2
- data/docs/advanced/extending-canon.adoc +193 -0
- data/docs/features/match-options/index.adoc +239 -1
- data/docs/internals/diffnode-enrichment.adoc +611 -0
- data/docs/internals/index.adoc +251 -0
- data/docs/lychee.toml +13 -6
- data/docs/understanding/architecture.adoc +749 -33
- data/docs/understanding/comparison-pipeline.adoc +122 -0
- data/lib/canon/cache.rb +129 -0
- data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +68 -0
- data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +68 -0
- data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +171 -0
- data/lib/canon/comparison/dimensions/base_dimension.rb +107 -0
- data/lib/canon/comparison/dimensions/comments_dimension.rb +121 -0
- data/lib/canon/comparison/dimensions/element_position_dimension.rb +90 -0
- data/lib/canon/comparison/dimensions/registry.rb +77 -0
- data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +119 -0
- data/lib/canon/comparison/dimensions/text_content_dimension.rb +96 -0
- data/lib/canon/comparison/dimensions.rb +54 -0
- data/lib/canon/comparison/format_detector.rb +87 -0
- data/lib/canon/comparison/html_comparator.rb +70 -26
- data/lib/canon/comparison/html_compare_profile.rb +8 -2
- data/lib/canon/comparison/html_parser.rb +80 -0
- data/lib/canon/comparison/json_comparator.rb +12 -0
- data/lib/canon/comparison/json_parser.rb +19 -0
- data/lib/canon/comparison/markup_comparator.rb +293 -0
- data/lib/canon/comparison/match_options/base_resolver.rb +150 -0
- data/lib/canon/comparison/match_options/json_resolver.rb +82 -0
- data/lib/canon/comparison/match_options/xml_resolver.rb +151 -0
- data/lib/canon/comparison/match_options/yaml_resolver.rb +87 -0
- data/lib/canon/comparison/match_options.rb +68 -463
- data/lib/canon/comparison/profile_definition.rb +149 -0
- data/lib/canon/comparison/ruby_object_comparator.rb +180 -0
- data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +7 -10
- data/lib/canon/comparison/whitespace_sensitivity.rb +208 -0
- data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +177 -0
- data/lib/canon/comparison/xml_comparator/attribute_filter.rb +136 -0
- data/lib/canon/comparison/xml_comparator/child_comparison.rb +197 -0
- data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +115 -0
- data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +186 -0
- data/lib/canon/comparison/xml_comparator/node_parser.rb +79 -0
- data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +102 -0
- data/lib/canon/comparison/xml_comparator.rb +97 -684
- data/lib/canon/comparison/xml_node_comparison.rb +319 -0
- data/lib/canon/comparison/xml_parser.rb +19 -0
- data/lib/canon/comparison/yaml_comparator.rb +3 -3
- data/lib/canon/comparison.rb +265 -110
- data/lib/canon/diff/diff_classifier.rb +101 -2
- data/lib/canon/diff/diff_node.rb +32 -2
- data/lib/canon/diff/formatting_detector.rb +1 -1
- data/lib/canon/diff/node_serializer.rb +191 -0
- data/lib/canon/diff/path_builder.rb +143 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +251 -0
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +6 -248
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +38 -229
- data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +30 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +579 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +121 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +253 -0
- data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +61 -0
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +31 -1028
- data/lib/canon/diff_formatter.rb +1 -1
- data/lib/canon/rspec_matchers.rb +38 -9
- data/lib/canon/tree_diff/operation_converter.rb +92 -338
- data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +71 -0
- data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +103 -0
- data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +168 -0
- data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +188 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +24 -13
- metadata +48 -2
|
@@ -45,7 +45,7 @@ module Canon
|
|
|
45
45
|
return ""
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
require_relative "../../
|
|
48
|
+
require_relative "../../html/data_model"
|
|
49
49
|
require_relative "../../xml/element_matcher"
|
|
50
50
|
require_relative "../../xml/line_range_mapper"
|
|
51
51
|
require_relative "../../pretty_printer/html"
|
|
@@ -54,10 +54,10 @@ module Canon
|
|
|
54
54
|
|
|
55
55
|
begin
|
|
56
56
|
# Parse to DOM using HTML parser
|
|
57
|
-
root1 = Canon::
|
|
58
|
-
|
|
59
|
-
root2 = Canon::
|
|
60
|
-
|
|
57
|
+
root1 = Canon::Html::DataModel.from_html(doc1,
|
|
58
|
+
version: @html_version)
|
|
59
|
+
root2 = Canon::Html::DataModel.from_html(doc2,
|
|
60
|
+
version: @html_version)
|
|
61
61
|
|
|
62
62
|
# Match elements semantically
|
|
63
63
|
matcher = Canon::Xml::ElementMatcher.new
|
|
@@ -78,22 +78,8 @@ module Canon
|
|
|
78
78
|
lines1 = pretty1.split("\n")
|
|
79
79
|
lines2 = pretty2.split("\n")
|
|
80
80
|
|
|
81
|
-
# DEBUG
|
|
82
|
-
warn "DEBUG: HTML Formatter - lines1.length=#{lines1.length}, lines2.length=#{lines2.length}"
|
|
83
|
-
warn "DEBUG: HTML Formatter - matches.length=#{matches.length}"
|
|
84
|
-
warn "DEBUG: HTML Formatter - map1.size=#{map1.size}, map2.size=#{map2.size}"
|
|
85
|
-
warn "DEBUG: Mapped elements in map1: #{map1.keys.map(&:name).join(', ')}"
|
|
86
|
-
warn "DEBUG: Match types: matched=#{matches.count do |m|
|
|
87
|
-
m.status == :matched
|
|
88
|
-
end}, deleted=#{matches.count do |m|
|
|
89
|
-
m.status == :deleted
|
|
90
|
-
end}, inserted=#{matches.count do |m|
|
|
91
|
-
m.status == :inserted
|
|
92
|
-
end}"
|
|
93
|
-
|
|
94
81
|
# Display diffs based on element matches
|
|
95
82
|
result = format_element_matches(matches, map1, map2, lines1, lines2)
|
|
96
|
-
warn "DEBUG: HTML Formatter - result.length=#{result.length}"
|
|
97
83
|
output << result
|
|
98
84
|
rescue StandardError => e
|
|
99
85
|
# Fall back to simple diff on error
|
|
@@ -271,30 +257,6 @@ module Canon
|
|
|
271
257
|
|
|
272
258
|
private
|
|
273
259
|
|
|
274
|
-
# Check if diff display should be skipped
|
|
275
|
-
# Returns true when:
|
|
276
|
-
# 1. show_diffs is :normative AND there are no normative differences
|
|
277
|
-
# 2. show_diffs is :informative AND there are no informative differences
|
|
278
|
-
def should_skip_diff_display?
|
|
279
|
-
return false if @differences.nil? || @differences.empty?
|
|
280
|
-
|
|
281
|
-
case @show_diffs
|
|
282
|
-
when :normative
|
|
283
|
-
# Skip if no normative diffs
|
|
284
|
-
@differences.none? do |diff|
|
|
285
|
-
diff.is_a?(Canon::Diff::DiffNode) && diff.normative?
|
|
286
|
-
end
|
|
287
|
-
when :informative
|
|
288
|
-
# Skip if no informative diffs
|
|
289
|
-
@differences.none? do |diff|
|
|
290
|
-
diff.is_a?(Canon::Diff::DiffNode) && diff.informative?
|
|
291
|
-
end
|
|
292
|
-
else
|
|
293
|
-
# :all or other - never skip
|
|
294
|
-
false
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
|
|
298
260
|
# Format element matches for display
|
|
299
261
|
def format_element_matches(matches, map1, map2, lines1, lines2)
|
|
300
262
|
output = []
|
|
@@ -321,11 +283,6 @@ module Canon
|
|
|
321
283
|
lines2, elements_to_skip,
|
|
322
284
|
children_of_matched_parents)
|
|
323
285
|
|
|
324
|
-
# DEBUG
|
|
325
|
-
warn "DEBUG: format_element_matches - diff_sections.length=#{diff_sections.length}"
|
|
326
|
-
warn "DEBUG: format_element_matches - elements_to_skip.size=#{elements_to_skip.size}"
|
|
327
|
-
warn "DEBUG: format_element_matches - children_of_matched_parents.size=#{children_of_matched_parents.size}"
|
|
328
|
-
|
|
329
286
|
# Sort by line number
|
|
330
287
|
diff_sections.sort_by! do |section|
|
|
331
288
|
section[:start_line1] || section[:start_line2] || 0
|
|
@@ -335,14 +292,13 @@ module Canon
|
|
|
335
292
|
formatted_diffs = if @diff_grouping_lines
|
|
336
293
|
groups = group_diff_sections(diff_sections,
|
|
337
294
|
@diff_grouping_lines)
|
|
338
|
-
format_diff_groups(groups
|
|
295
|
+
format_diff_groups(groups)
|
|
339
296
|
else
|
|
340
297
|
diff_sections.map do |s|
|
|
341
298
|
s[:formatted]
|
|
342
299
|
end.compact.join("\n\n")
|
|
343
300
|
end
|
|
344
301
|
|
|
345
|
-
warn "DEBUG: format_element_matches - formatted_diffs.length=#{formatted_diffs.length}"
|
|
346
302
|
output << formatted_diffs
|
|
347
303
|
output.join("\n")
|
|
348
304
|
end
|
|
@@ -391,57 +347,6 @@ module Canon
|
|
|
391
347
|
elements_to_skip
|
|
392
348
|
end
|
|
393
349
|
|
|
394
|
-
# Check if an element or its children have semantic diffs
|
|
395
|
-
def has_semantic_diff_in_subtree?(element, elements_with_semantic_diffs)
|
|
396
|
-
# Check the element itself
|
|
397
|
-
return true if elements_with_semantic_diffs.include?(element)
|
|
398
|
-
|
|
399
|
-
# Check all descendants
|
|
400
|
-
if element.respond_to?(:children)
|
|
401
|
-
element.children.any? do |child|
|
|
402
|
-
has_semantic_diff_in_subtree?(child, elements_with_semantic_diffs)
|
|
403
|
-
end
|
|
404
|
-
else
|
|
405
|
-
false
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
# Build set of individual elements (not pairs) that have semantic diffs
|
|
410
|
-
def build_elements_with_semantic_diffs_set
|
|
411
|
-
elements = Set.new
|
|
412
|
-
|
|
413
|
-
return elements if @differences.nil? || @differences.empty?
|
|
414
|
-
|
|
415
|
-
@differences.each do |diff|
|
|
416
|
-
next unless diff.is_a?(Canon::Diff::DiffNode)
|
|
417
|
-
|
|
418
|
-
# Add both nodes if they exist
|
|
419
|
-
elements.add(diff.node1) if diff.node1
|
|
420
|
-
elements.add(diff.node2) if diff.node2
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
elements
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# Build set of children of matched parents
|
|
427
|
-
def build_children_set(matches)
|
|
428
|
-
children = Set.new
|
|
429
|
-
|
|
430
|
-
matches.each do |match|
|
|
431
|
-
next unless match.status == :matched
|
|
432
|
-
|
|
433
|
-
[match.elem1, match.elem2].compact.each do |elem|
|
|
434
|
-
next unless elem.respond_to?(:children)
|
|
435
|
-
|
|
436
|
-
elem.children.each do |child|
|
|
437
|
-
children.add(child) if child.respond_to?(:name)
|
|
438
|
-
end
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
children
|
|
443
|
-
end
|
|
444
|
-
|
|
445
350
|
# Collect diff sections with metadata
|
|
446
351
|
def collect_diff_sections(matches, map1, map2, lines1, lines2,
|
|
447
352
|
elements_to_skip, _children_of_matched_parents)
|
|
@@ -473,7 +378,6 @@ module Canon
|
|
|
473
378
|
range2 = map2[match.elem2]
|
|
474
379
|
if !range1 || !range2
|
|
475
380
|
no_range_count += 1
|
|
476
|
-
warn "DEBUG: No range for #{match.elem1.name} (path: #{match.path.join('/')})" if no_range_count <= 5
|
|
477
381
|
end
|
|
478
382
|
|
|
479
383
|
section = format_matched_element_with_metadata(match, map1,
|
|
@@ -481,7 +385,6 @@ module Canon
|
|
|
481
385
|
lines2)
|
|
482
386
|
if range1 && range2 && !section
|
|
483
387
|
no_diff_count += 1
|
|
484
|
-
warn "DEBUG: No diff for #{match.elem1.name} (path: #{match.path.join('/')})" if no_diff_count <= 5
|
|
485
388
|
end
|
|
486
389
|
diff_sections << section if section
|
|
487
390
|
when :deleted
|
|
@@ -497,67 +400,9 @@ module Canon
|
|
|
497
400
|
end
|
|
498
401
|
end
|
|
499
402
|
|
|
500
|
-
warn "DEBUG: collect_diff_sections - no_range_count=#{no_range_count}, no_diff_count=#{no_diff_count}"
|
|
501
403
|
diff_sections
|
|
502
404
|
end
|
|
503
405
|
|
|
504
|
-
# Format matched element with metadata
|
|
505
|
-
def format_matched_element_with_metadata(match, map1, map2, lines1,
|
|
506
|
-
lines2)
|
|
507
|
-
range1 = map1[match.elem1]
|
|
508
|
-
range2 = map2[match.elem2]
|
|
509
|
-
return nil unless range1 && range2
|
|
510
|
-
|
|
511
|
-
formatted = format_matched_element(match, map1, map2, lines1,
|
|
512
|
-
lines2)
|
|
513
|
-
return nil unless formatted
|
|
514
|
-
|
|
515
|
-
{
|
|
516
|
-
formatted: formatted,
|
|
517
|
-
start_line1: range1.start_line,
|
|
518
|
-
end_line1: range1.end_line,
|
|
519
|
-
start_line2: range2.start_line,
|
|
520
|
-
end_line2: range2.end_line,
|
|
521
|
-
path: match.path.join("/"),
|
|
522
|
-
}
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
# Format deleted element with metadata
|
|
526
|
-
def format_deleted_element_with_metadata(match, map1, lines1)
|
|
527
|
-
range1 = map1[match.elem1]
|
|
528
|
-
return nil unless range1
|
|
529
|
-
|
|
530
|
-
formatted = format_deleted_element(match, map1, lines1)
|
|
531
|
-
return nil unless formatted
|
|
532
|
-
|
|
533
|
-
{
|
|
534
|
-
formatted: formatted,
|
|
535
|
-
start_line1: range1.start_line,
|
|
536
|
-
end_line1: range1.end_line,
|
|
537
|
-
start_line2: nil,
|
|
538
|
-
end_line2: nil,
|
|
539
|
-
path: match.path.join("/"),
|
|
540
|
-
}
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
# Format inserted element with metadata
|
|
544
|
-
def format_inserted_element_with_metadata(match, map2, lines2)
|
|
545
|
-
range2 = map2[match.elem2]
|
|
546
|
-
return nil unless range2
|
|
547
|
-
|
|
548
|
-
formatted = format_inserted_element(match, map2, lines2)
|
|
549
|
-
return nil unless formatted
|
|
550
|
-
|
|
551
|
-
{
|
|
552
|
-
formatted: formatted,
|
|
553
|
-
start_line1: nil,
|
|
554
|
-
end_line1: nil,
|
|
555
|
-
start_line2: range2.start_line,
|
|
556
|
-
end_line2: range2.end_line,
|
|
557
|
-
path: match.path.join("/"),
|
|
558
|
-
}
|
|
559
|
-
end
|
|
560
|
-
|
|
561
406
|
# Format a matched element showing differences
|
|
562
407
|
def format_matched_element(match, map1, map2, lines1, lines2)
|
|
563
408
|
range1 = map1[match.elem1]
|
|
@@ -631,93 +476,6 @@ module Canon
|
|
|
631
476
|
|
|
632
477
|
output.join("\n")
|
|
633
478
|
end
|
|
634
|
-
|
|
635
|
-
# Group diff sections by proximity
|
|
636
|
-
def group_diff_sections(sections, grouping_lines)
|
|
637
|
-
return [] if sections.empty?
|
|
638
|
-
|
|
639
|
-
groups = []
|
|
640
|
-
current_group = [sections[0]]
|
|
641
|
-
|
|
642
|
-
sections[1..].each do |section|
|
|
643
|
-
last_section = current_group.last
|
|
644
|
-
|
|
645
|
-
# Calculate gap
|
|
646
|
-
gap1 = if last_section[:end_line1] && section[:start_line1]
|
|
647
|
-
section[:start_line1] - last_section[:end_line1] - 1
|
|
648
|
-
else
|
|
649
|
-
Float::INFINITY
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
gap2 = if last_section[:end_line2] && section[:start_line2]
|
|
653
|
-
section[:start_line2] - last_section[:end_line2] - 1
|
|
654
|
-
else
|
|
655
|
-
Float::INFINITY
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
max_gap = [gap1, gap2].max
|
|
659
|
-
|
|
660
|
-
if max_gap <= grouping_lines
|
|
661
|
-
current_group << section
|
|
662
|
-
else
|
|
663
|
-
groups << current_group
|
|
664
|
-
current_group = [section]
|
|
665
|
-
end
|
|
666
|
-
end
|
|
667
|
-
|
|
668
|
-
groups << current_group unless current_group.empty?
|
|
669
|
-
groups
|
|
670
|
-
end
|
|
671
|
-
|
|
672
|
-
# Format groups of diffs
|
|
673
|
-
def format_diff_groups(groups, _lines1, _lines2)
|
|
674
|
-
output = []
|
|
675
|
-
|
|
676
|
-
groups.each_with_index do |group, group_idx|
|
|
677
|
-
output << "" if group_idx.positive?
|
|
678
|
-
|
|
679
|
-
if group.length > 1
|
|
680
|
-
output << colorize("Context block has #{group.length} diffs",
|
|
681
|
-
:yellow, :bold)
|
|
682
|
-
output << ""
|
|
683
|
-
group.each do |section|
|
|
684
|
-
output << section[:formatted] if section[:formatted]
|
|
685
|
-
end
|
|
686
|
-
elsif group[0][:formatted]
|
|
687
|
-
output << group[0][:formatted]
|
|
688
|
-
end
|
|
689
|
-
end
|
|
690
|
-
|
|
691
|
-
output.join("\n")
|
|
692
|
-
end
|
|
693
|
-
|
|
694
|
-
# Check if an element or its children have semantic diffs
|
|
695
|
-
def has_semantic_diff_in_subtree?(element, elements_with_semantic_diffs)
|
|
696
|
-
return true if elements_with_semantic_diffs.include?(element)
|
|
697
|
-
|
|
698
|
-
if element.respond_to?(:children)
|
|
699
|
-
element.children.any? do |child|
|
|
700
|
-
has_semantic_diff_in_subtree?(child, elements_with_semantic_diffs)
|
|
701
|
-
end
|
|
702
|
-
else
|
|
703
|
-
false
|
|
704
|
-
end
|
|
705
|
-
end
|
|
706
|
-
|
|
707
|
-
# Build set of individual elements that have semantic diffs
|
|
708
|
-
def build_elements_with_semantic_diffs_set
|
|
709
|
-
elements = Set.new
|
|
710
|
-
return elements if @differences.nil? || @differences.empty?
|
|
711
|
-
|
|
712
|
-
@differences.each do |diff|
|
|
713
|
-
next unless diff.is_a?(Canon::Diff::DiffNode)
|
|
714
|
-
|
|
715
|
-
elements.add(diff.node1) if diff.node1
|
|
716
|
-
elements.add(diff.node2) if diff.node2
|
|
717
|
-
end
|
|
718
|
-
|
|
719
|
-
elements
|
|
720
|
-
end
|
|
721
479
|
end
|
|
722
480
|
end
|
|
723
481
|
end
|
|
@@ -252,30 +252,6 @@ module Canon
|
|
|
252
252
|
|
|
253
253
|
private
|
|
254
254
|
|
|
255
|
-
# Check if diff display should be skipped
|
|
256
|
-
# Returns true when:
|
|
257
|
-
# 1. show_diffs is :normative AND there are no normative differences
|
|
258
|
-
# 2. show_diffs is :informative AND there are no informative differences
|
|
259
|
-
def should_skip_diff_display?
|
|
260
|
-
return false if @differences.nil? || @differences.empty?
|
|
261
|
-
|
|
262
|
-
case @show_diffs
|
|
263
|
-
when :normative
|
|
264
|
-
# Skip if no normative diffs
|
|
265
|
-
@differences.none? do |diff|
|
|
266
|
-
diff.is_a?(Canon::Diff::DiffNode) && diff.normative?
|
|
267
|
-
end
|
|
268
|
-
when :informative
|
|
269
|
-
# Skip if no informative diffs
|
|
270
|
-
@differences.none? do |diff|
|
|
271
|
-
diff.is_a?(Canon::Diff::DiffNode) && diff.informative?
|
|
272
|
-
end
|
|
273
|
-
else
|
|
274
|
-
# :all or other - never skip
|
|
275
|
-
false
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
|
|
279
255
|
# Format element matches for display
|
|
280
256
|
def format_element_matches(matches, map1, map2, lines1, lines2)
|
|
281
257
|
output = []
|
|
@@ -311,7 +287,7 @@ module Canon
|
|
|
311
287
|
formatted_diffs = if @diff_grouping_lines
|
|
312
288
|
groups = group_diff_sections(diff_sections,
|
|
313
289
|
@diff_grouping_lines)
|
|
314
|
-
format_diff_groups(groups
|
|
290
|
+
format_diff_groups(groups)
|
|
315
291
|
else
|
|
316
292
|
diff_sections.map do |s|
|
|
317
293
|
s[:formatted]
|
|
@@ -322,101 +298,6 @@ module Canon
|
|
|
322
298
|
output.join("\n")
|
|
323
299
|
end
|
|
324
300
|
|
|
325
|
-
# Build set of elements to skip (children with parents showing diffs)
|
|
326
|
-
def build_skip_set(matches, map1, map2, lines1, lines2)
|
|
327
|
-
elements_to_skip = Set.new
|
|
328
|
-
elements_with_diffs = Set.new
|
|
329
|
-
|
|
330
|
-
# Build set of element pairs that have semantic diffs
|
|
331
|
-
build_elements_with_semantic_diffs_set
|
|
332
|
-
|
|
333
|
-
# First pass: identify elements with line differences
|
|
334
|
-
# (semantic filtering happens in collect_diff_sections)
|
|
335
|
-
matches.each do |match|
|
|
336
|
-
next unless match.status == :matched
|
|
337
|
-
|
|
338
|
-
range1 = map1[match.elem1]
|
|
339
|
-
range2 = map2[match.elem2]
|
|
340
|
-
next unless range1 && range2
|
|
341
|
-
|
|
342
|
-
elem_lines1 = lines1[range1.start_line..range1.end_line]
|
|
343
|
-
elem_lines2 = lines2[range2.start_line..range2.end_line]
|
|
344
|
-
|
|
345
|
-
# Add if there are line diffs
|
|
346
|
-
# Semantic filtering is done in collect_diff_sections
|
|
347
|
-
if elem_lines1 != elem_lines2
|
|
348
|
-
elements_with_diffs.add(match.elem1)
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
# Second pass: skip children of elements with diffs
|
|
353
|
-
elements_with_diffs.each do |elem|
|
|
354
|
-
if elem.respond_to?(:parent)
|
|
355
|
-
current = elem.parent
|
|
356
|
-
while current
|
|
357
|
-
if current.respond_to?(:name) && elements_with_diffs.include?(current)
|
|
358
|
-
elements_to_skip.add(elem)
|
|
359
|
-
break
|
|
360
|
-
end
|
|
361
|
-
current = current.respond_to?(:parent) ? current.parent : nil
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
elements_to_skip
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# Check if an element or its children have semantic diffs
|
|
370
|
-
def has_semantic_diff_in_subtree?(element, elements_with_semantic_diffs)
|
|
371
|
-
# Check the element itself
|
|
372
|
-
return true if elements_with_semantic_diffs.include?(element)
|
|
373
|
-
|
|
374
|
-
# Check all descendants
|
|
375
|
-
if element.respond_to?(:children)
|
|
376
|
-
element.children.any? do |child|
|
|
377
|
-
has_semantic_diff_in_subtree?(child, elements_with_semantic_diffs)
|
|
378
|
-
end
|
|
379
|
-
else
|
|
380
|
-
false
|
|
381
|
-
end
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
# Build set of individual elements (not pairs) that have semantic diffs
|
|
385
|
-
def build_elements_with_semantic_diffs_set
|
|
386
|
-
elements = Set.new
|
|
387
|
-
|
|
388
|
-
return elements if @differences.nil? || @differences.empty?
|
|
389
|
-
|
|
390
|
-
@differences.each do |diff|
|
|
391
|
-
next unless diff.is_a?(Canon::Diff::DiffNode)
|
|
392
|
-
|
|
393
|
-
# Add both nodes if they exist
|
|
394
|
-
elements.add(diff.node1) if diff.node1
|
|
395
|
-
elements.add(diff.node2) if diff.node2
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
elements
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
# Build set of children of matched parents
|
|
402
|
-
def build_children_set(matches)
|
|
403
|
-
children = Set.new
|
|
404
|
-
|
|
405
|
-
matches.each do |match|
|
|
406
|
-
next unless match.status == :matched
|
|
407
|
-
|
|
408
|
-
[match.elem1, match.elem2].compact.each do |elem|
|
|
409
|
-
next unless elem.respond_to?(:children)
|
|
410
|
-
|
|
411
|
-
elem.children.each do |child|
|
|
412
|
-
children.add(child) if child.respond_to?(:name)
|
|
413
|
-
end
|
|
414
|
-
end
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
children
|
|
418
|
-
end
|
|
419
|
-
|
|
420
301
|
# Collect diff sections with metadata
|
|
421
302
|
def collect_diff_sections(matches, map1, map2, lines1, lines2,
|
|
422
303
|
elements_to_skip, children_of_matched_parents)
|
|
@@ -463,61 +344,48 @@ module Canon
|
|
|
463
344
|
diff_sections
|
|
464
345
|
end
|
|
465
346
|
|
|
466
|
-
#
|
|
467
|
-
def
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
range2 = map2[match.elem2]
|
|
471
|
-
return nil unless range1 && range2
|
|
347
|
+
# Build set of elements to skip (children with parents showing diffs)
|
|
348
|
+
def build_skip_set(matches, map1, map2, lines1, lines2)
|
|
349
|
+
elements_to_skip = Set.new
|
|
350
|
+
elements_with_diffs = Set.new
|
|
472
351
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return nil unless formatted
|
|
476
|
-
|
|
477
|
-
{
|
|
478
|
-
formatted: formatted,
|
|
479
|
-
start_line1: range1.start_line,
|
|
480
|
-
end_line1: range1.end_line,
|
|
481
|
-
start_line2: range2.start_line,
|
|
482
|
-
end_line2: range2.end_line,
|
|
483
|
-
path: match.path.join("/"),
|
|
484
|
-
}
|
|
485
|
-
end
|
|
352
|
+
# Build set of element pairs that have semantic diffs
|
|
353
|
+
build_elements_with_semantic_diffs_set
|
|
486
354
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
355
|
+
# First pass: identify elements with line differences
|
|
356
|
+
# (semantic filtering happens in collect_diff_sections)
|
|
357
|
+
matches.each do |match|
|
|
358
|
+
next unless match.status == :matched
|
|
491
359
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
{
|
|
496
|
-
formatted: formatted,
|
|
497
|
-
start_line1: range1.start_line,
|
|
498
|
-
end_line1: range1.end_line,
|
|
499
|
-
start_line2: nil,
|
|
500
|
-
end_line2: nil,
|
|
501
|
-
path: match.path.join("/"),
|
|
502
|
-
}
|
|
503
|
-
end
|
|
360
|
+
range1 = map1[match.elem1]
|
|
361
|
+
range2 = map2[match.elem2]
|
|
362
|
+
next unless range1 && range2
|
|
504
363
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
range2 = map2[match.elem2]
|
|
508
|
-
return nil unless range2
|
|
364
|
+
elem_lines1 = lines1[range1.start_line..range1.end_line]
|
|
365
|
+
elem_lines2 = lines2[range2.start_line..range2.end_line]
|
|
509
366
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
367
|
+
# Add if there are line diffs
|
|
368
|
+
# Semantic filtering is done in collect_diff_sections
|
|
369
|
+
if elem_lines1 != elem_lines2
|
|
370
|
+
elements_with_diffs.add(match.elem1)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Second pass: skip children of elements with diffs
|
|
375
|
+
elements_with_diffs.each do |elem|
|
|
376
|
+
if elem.respond_to?(:parent)
|
|
377
|
+
current = elem.parent
|
|
378
|
+
while current
|
|
379
|
+
if current.respond_to?(:name) && elements_with_diffs.include?(current)
|
|
380
|
+
elements_to_skip.add(elem)
|
|
381
|
+
break
|
|
382
|
+
end
|
|
383
|
+
current = current.respond_to?(:parent) ? current.parent : nil
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
elements_to_skip
|
|
521
389
|
end
|
|
522
390
|
|
|
523
391
|
# Format a matched element showing differences
|
|
@@ -746,65 +614,6 @@ module Canon
|
|
|
746
614
|
output.join("\n")
|
|
747
615
|
end
|
|
748
616
|
|
|
749
|
-
# Group diff sections by proximity
|
|
750
|
-
def group_diff_sections(sections, grouping_lines)
|
|
751
|
-
return [] if sections.empty?
|
|
752
|
-
|
|
753
|
-
groups = []
|
|
754
|
-
current_group = [sections[0]]
|
|
755
|
-
|
|
756
|
-
sections[1..].each do |section|
|
|
757
|
-
last_section = current_group.last
|
|
758
|
-
|
|
759
|
-
# Calculate gap
|
|
760
|
-
gap1 = if last_section[:end_line1] && section[:start_line1]
|
|
761
|
-
section[:start_line1] - last_section[:end_line1] - 1
|
|
762
|
-
else
|
|
763
|
-
Float::INFINITY
|
|
764
|
-
end
|
|
765
|
-
|
|
766
|
-
gap2 = if last_section[:end_line2] && section[:start_line2]
|
|
767
|
-
section[:start_line2] - last_section[:end_line2] - 1
|
|
768
|
-
else
|
|
769
|
-
Float::INFINITY
|
|
770
|
-
end
|
|
771
|
-
|
|
772
|
-
max_gap = [gap1, gap2].max
|
|
773
|
-
|
|
774
|
-
if max_gap <= grouping_lines
|
|
775
|
-
current_group << section
|
|
776
|
-
else
|
|
777
|
-
groups << current_group
|
|
778
|
-
current_group = [section]
|
|
779
|
-
end
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
groups << current_group unless current_group.empty?
|
|
783
|
-
groups
|
|
784
|
-
end
|
|
785
|
-
|
|
786
|
-
# Format groups of diffs
|
|
787
|
-
def format_diff_groups(groups, _lines1, _lines2)
|
|
788
|
-
output = []
|
|
789
|
-
|
|
790
|
-
groups.each_with_index do |group, group_idx|
|
|
791
|
-
output << "" if group_idx.positive?
|
|
792
|
-
|
|
793
|
-
if group.length > 1
|
|
794
|
-
output << colorize("Context block has #{group.length} diffs",
|
|
795
|
-
:yellow, :bold)
|
|
796
|
-
output << ""
|
|
797
|
-
group.each do |section|
|
|
798
|
-
output << section[:formatted] if section[:formatted]
|
|
799
|
-
end
|
|
800
|
-
elsif group[0][:formatted]
|
|
801
|
-
output << group[0][:formatted]
|
|
802
|
-
end
|
|
803
|
-
end
|
|
804
|
-
|
|
805
|
-
output.join("\n")
|
|
806
|
-
end
|
|
807
|
-
|
|
808
617
|
# Format a unified diff line
|
|
809
618
|
def format_unified_line(old_num, new_num, marker, content, color = nil,
|
|
810
619
|
informative: false, formatting: false)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "paint"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
class DiffFormatter
|
|
7
|
+
module DiffDetailFormatterHelpers
|
|
8
|
+
# Color helper for diff formatting
|
|
9
|
+
#
|
|
10
|
+
# Provides consistent colorization for diff output.
|
|
11
|
+
module ColorHelper
|
|
12
|
+
# Colorize text with optional bold formatting
|
|
13
|
+
#
|
|
14
|
+
# @param text [String] Text to colorize
|
|
15
|
+
# @param color [Symbol] Color name
|
|
16
|
+
# @param use_color [Boolean] Whether to use colors
|
|
17
|
+
# @param bold [Boolean] Whether to make text bold
|
|
18
|
+
# @return [String] Colorized text (or plain text if use_color is false)
|
|
19
|
+
def self.colorize(text, color, use_color, bold: false)
|
|
20
|
+
return text unless use_color
|
|
21
|
+
|
|
22
|
+
args = [color]
|
|
23
|
+
args << :bold if bold
|
|
24
|
+
|
|
25
|
+
Paint[text, *args]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|