Class: Canon::Comparison::DiffNodeBuilder
- Inherits:
-
Object
- Object
- Canon::Comparison::DiffNodeBuilder
- Defined in:
- lib/canon/comparison/diff_node_builder.rb
Overview
Single factory for DiffNode creation in the DOM comparison path.
Centralises reason building, metadata enrichment (path, serialization, attributes), and whitespace visualization — previously duplicated across MarkupComparator and XmlComparator.
Class Method Summary collapse
-
.build(node1:, node2:, diff1:, diff2:, dimension:, **_opts) ⇒ Object
Build an enriched DiffNode.
-
.build_attribute_difference_reason(attrs1, attrs2) ⇒ Object
— Attribute reason builders —————————————–.
- .build_attribute_order_reason(node1, node2) ⇒ Object
- .build_attribute_values_reason(node1, node2) ⇒ Object
-
.build_comment_difference_reason(node1, node2) ⇒ Object
— Comment reason —————————————————-.
-
.build_reason(node1, node2, diff1, diff2, dimension) ⇒ Object
— Reason building —————————————————.
-
.build_text_difference_reason(text1, text2) ⇒ Object
— Text diff reason ————————————————–.
-
.build_whitespace_adjacency_reason(node1, node2) ⇒ Object
— Whitespace adjacency reason (#137) ——————————–.
- .comment_text(node) ⇒ Object
- .describe_whitespace(text) ⇒ Object
-
.enrich_metadata(node1, node2) ⇒ Object
— Metadata enrichment ———————————————–.
- .extract_attributes(node) ⇒ Object
-
.extract_text_content(node) ⇒ Object
— Text content extraction ——————————————-.
-
.serialize(node) ⇒ Object
— Node queries (delegate to NodeSerializer) ————————-.
- .truncate(text, max_length = 40) ⇒ Object
-
.visualize_whitespace(text) ⇒ Object
— Whitespace visualization ——————————————.
- .whitespace_only?(text) ⇒ Boolean
Class Method Details
.build(node1:, node2:, diff1:, diff2:, dimension:, **_opts) ⇒ Object
Build an enriched DiffNode.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 14 def self.build(node1:, node2:, diff1:, diff2:, dimension:, **_opts) raise ArgumentError, "dimension required for DiffNode" if dimension.nil? reason = build_reason(node1, node2, diff1, diff2, dimension) = (node1, node2) Canon::Diff::DiffNode.new( node1: node1, node2: node2, dimension: dimension, reason: reason, **, ) end |
.build_attribute_difference_reason(attrs1, attrs2) ⇒ Object
— Attribute reason builders —————————————–
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 96 def self.build_attribute_difference_reason(attrs1, attrs2) unless attrs1 && attrs2 return "#{attrs1&.keys&.size || 0} vs #{attrs2&.keys&.size || 0} attributes" end keys1 = attrs1.keys.to_set keys2 = attrs2.keys.to_set only_in_first = keys1 - keys2 only_in_second = keys2 - keys1 different_values = (keys1 & keys2).reject { |k| attrs1[k] == attrs2[k] } parts = [] parts << "only in first: #{only_in_first.to_a.sort.join(', ')}" if only_in_first.any? parts << "only in second: #{only_in_second.to_a.sort.join(', ')}" if only_in_second.any? parts << "different values: #{different_values.sort.join(', ')}" if different_values.any? parts.empty? ? "#{keys1.size} vs #{keys2.size} attributes (same names)" : parts.join("; ") end |
.build_attribute_order_reason(node1, node2) ⇒ Object
131 132 133 134 135 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 131 def self.build_attribute_order_reason(node1, node2) keys1 = extract_attributes(node1)&.keys || [] keys2 = extract_attributes(node2)&.keys || [] "Attribute order changed: [#{keys1.join(', ')}] → [#{keys2.join(', ')}]" end |
.build_attribute_values_reason(node1, node2) ⇒ Object
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 116 def self.build_attribute_values_reason(node1, node2) attrs1 = extract_attributes(node1) || {} attrs2 = extract_attributes(node2) || {} differing = (attrs1.keys | attrs2.keys).sort.reject do |k| attrs1[k.to_s] == attrs2[k.to_s] end changed = differing.map do |k| "Changed: #{k}=\"#{attrs1[k.to_s]}\" → \"#{attrs2[k.to_s]}\"" end changed.empty? ? "attributes differ" : "Attributes differ (#{changed.join('; ')})" end |
.build_comment_difference_reason(node1, node2) ⇒ Object
— Comment reason —————————————————-
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 176 def self.build_comment_difference_reason(node1, node2) cm1 = node1 && NodeInspector.comment_node?(node1) cm2 = node2 && NodeInspector.comment_node?(node2) return nil unless cm1 || cm2 if cm1 && !cm2 "Comment present on EXPECTED only: <!--#{truncate(comment_text(node1))}-->" elsif cm2 && !cm1 "Comment present on ACTUAL only: <!--#{truncate(comment_text(node2))}-->" else t1 = truncate(comment_text(node1)) t2 = truncate(comment_text(node2)) "Comment text differs: <!--#{t1}--> vs <!--#{t2}-->" end end |
.build_reason(node1, node2, diff1, diff2, dimension) ⇒ Object
— Reason building —————————————————
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 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 31 def self.build_reason(node1, node2, diff1, diff2, dimension) # Nil-node text content with namespace info if dimension == :text_content && (node1.nil? || node2.nil?) node = node1 || node2 if node.is_a?(Canon::Xml::Node) || Canon::XmlParsing.xml_node?(node) ns = Canon::XmlParsing.namespace_uri(node) ns_info = ns.nil? || ns.empty? ? "" : " (namespace: #{ns})" label = Canon::Comparison.code_pair_label(diff1, diff2) return "element '#{node.name}'#{ns_info}: #{label}" end end case dimension when :attribute_presence build_attribute_difference_reason( extract_attributes(node1), extract_attributes(node2) ) when :attribute_values build_attribute_values_reason(node1, node2) when :text_content build_text_difference_reason( extract_text_content(node1), extract_text_content(node2) ) when :attribute_order build_attribute_order_reason(node1, node2) when :comments build_comment_difference_reason(node1, node2) || fallback_reason(diff1, diff2, dimension, node1, node2) when :whitespace_adjacency build_whitespace_adjacency_reason(node1, node2) else fallback_reason(diff1, diff2, dimension, node1, node2) end end |
.build_text_difference_reason(text1, text2) ⇒ Object
— Text diff reason ————————————————–
162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 162 def self.build_text_difference_reason(text1, text2) return "missing vs '#{truncate(text2)}'" if text1.nil? && text2 return "'#{truncate(text1)}' vs missing" if text1 && text2.nil? return "both missing" if text1.nil? && text2.nil? if whitespace_only?(text1) && whitespace_only?(text2) return "whitespace: #{describe_whitespace(text1)} vs #{describe_whitespace(text2)}" end "Text: \"#{visualize_whitespace(text1)}\" vs \"#{visualize_whitespace(text2)}\"" end |
.build_whitespace_adjacency_reason(node1, node2) ⇒ Object
— Whitespace adjacency reason (#137) ——————————–
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 199 def self.build_whitespace_adjacency_reason(node1, node2) text1 = extract_text_content(node1) text2 = extract_text_content(node2) ws_on_first = NodeInspector.whitespace_only_text?(node1) && !NodeInspector.whitespace_only_text?(node2) ws_on_second = NodeInspector.whitespace_only_text?(node2) && !NodeInspector.whitespace_only_text?(node1) unless ws_on_first || ws_on_second return build_text_difference_reason(text1, text2) end if ws_on_first build_adjacency_side(text1, text2, node1, "EXPECTED", "ACTUAL") else build_adjacency_side(text2, text1, node2, "ACTUAL", "EXPECTED") end end |
.comment_text(node) ⇒ Object
193 194 195 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 193 def self.comment_text(node) NodeInspector.text_content(node).to_s end |
.describe_whitespace(text) ⇒ Object
228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 228 def self.describe_whitespace(text) return "0 chars" if text.nil? || text.empty? char_count = text.length parts = [] parts << "#{text.count("\n")} newlines" if text.include?("\n") parts << "#{text.count(' ')} spaces" if text.include?(" ") parts << "#{text.count("\t")} tabs" if text.include?("\t") "#{char_count} chars (#{parts.join(', ')})" end |
.enrich_metadata(node1, node2) ⇒ Object
— Metadata enrichment ———————————————–
69 70 71 72 73 74 75 76 77 78 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 69 def self.(node1, node2) { path: Canon::Diff::PathBuilder.build(node1 || node2, format: :document), serialized_before: serialize(node1), serialized_after: serialize(node2), attributes_before: extract_attributes(node1), attributes_after: extract_attributes(node2), } end |
.extract_attributes(node) ⇒ Object
88 89 90 91 92 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 88 def self.extract_attributes(node) return nil if node.nil? Canon::Diff::NodeSerializer.extract_attributes(node) end |
.extract_text_content(node) ⇒ Object
— Text content extraction ——————————————-
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 139 def self.extract_text_content(node) return nil if node.nil? case node when Canon::Xml::Nodes::TextNode node.value when Canon::Xml::Node node.text_content else if Canon::XmlBackend.nokogiri? && node.is_a?(Nokogiri::XML::Node) node.content.to_s elsif Canon::XmlParsing.xml_node?(node) Canon::XmlParsing.text_content(node) else node.to_s end end rescue StandardError nil end |
.serialize(node) ⇒ Object
— Node queries (delegate to NodeSerializer) ————————-
82 83 84 85 86 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 82 def self.serialize(node) return nil if node.nil? Canon::Diff::NodeSerializer.serialize(node) end |
.truncate(text, max_length = 40) ⇒ Object
246 247 248 249 250 251 252 253 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 246 def self.truncate(text, max_length = 40) return "" if text.nil? text = text.to_s return text if text.length <= max_length "#{text[0...max_length]}..." end |
.visualize_whitespace(text) ⇒ Object
— Whitespace visualization ——————————————
221 222 223 224 225 226 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 221 def self.visualize_whitespace(text) return "" if text.nil? viz_map = character_visualization_map text.chars.map { |char| viz_map[char] || char }.join end |
.whitespace_only?(text) ⇒ Boolean
240 241 242 243 244 |
# File 'lib/canon/comparison/diff_node_builder.rb', line 240 def self.whitespace_only?(text) return false if text.nil? text.to_s.strip.empty? end |