canon 0.1.7 → 0.1.8

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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +25 -135
  3. data/README.adoc +13 -13
  4. data/docs/.lycheeignore +69 -0
  5. data/docs/advanced/extending-canon.adoc +193 -0
  6. data/docs/internals/diffnode-enrichment.adoc +611 -0
  7. data/docs/internals/index.adoc +251 -0
  8. data/docs/lychee.toml +13 -6
  9. data/docs/plans/2025-01-17-html-parser-selection-fix.adoc +250 -0
  10. data/docs/understanding/architecture.adoc +749 -33
  11. data/docs/understanding/comparison-pipeline.adoc +122 -0
  12. data/false_positive_analysis.txt +0 -0
  13. data/file1.html +1 -0
  14. data/file2.html +1 -0
  15. data/lib/canon/cache.rb +129 -0
  16. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +68 -0
  17. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +68 -0
  18. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +171 -0
  19. data/lib/canon/comparison/dimensions/base_dimension.rb +107 -0
  20. data/lib/canon/comparison/dimensions/comments_dimension.rb +121 -0
  21. data/lib/canon/comparison/dimensions/element_position_dimension.rb +90 -0
  22. data/lib/canon/comparison/dimensions/registry.rb +77 -0
  23. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +119 -0
  24. data/lib/canon/comparison/dimensions/text_content_dimension.rb +96 -0
  25. data/lib/canon/comparison/dimensions.rb +54 -0
  26. data/lib/canon/comparison/format_detector.rb +86 -0
  27. data/lib/canon/comparison/html_comparator.rb +51 -18
  28. data/lib/canon/comparison/html_parser.rb +80 -0
  29. data/lib/canon/comparison/json_comparator.rb +12 -0
  30. data/lib/canon/comparison/json_parser.rb +19 -0
  31. data/lib/canon/comparison/markup_comparator.rb +293 -0
  32. data/lib/canon/comparison/match_options/base_resolver.rb +143 -0
  33. data/lib/canon/comparison/match_options/json_resolver.rb +82 -0
  34. data/lib/canon/comparison/match_options/xml_resolver.rb +151 -0
  35. data/lib/canon/comparison/match_options/yaml_resolver.rb +87 -0
  36. data/lib/canon/comparison/match_options.rb +68 -463
  37. data/lib/canon/comparison/profile_definition.rb +149 -0
  38. data/lib/canon/comparison/ruby_object_comparator.rb +180 -0
  39. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +7 -10
  40. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +177 -0
  41. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +136 -0
  42. data/lib/canon/comparison/xml_comparator/child_comparison.rb +189 -0
  43. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +115 -0
  44. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +186 -0
  45. data/lib/canon/comparison/xml_comparator/node_parser.rb +74 -0
  46. data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +95 -0
  47. data/lib/canon/comparison/xml_comparator.rb +52 -664
  48. data/lib/canon/comparison/xml_node_comparison.rb +297 -0
  49. data/lib/canon/comparison/xml_parser.rb +19 -0
  50. data/lib/canon/comparison/yaml_comparator.rb +3 -3
  51. data/lib/canon/comparison.rb +265 -110
  52. data/lib/canon/diff/diff_node.rb +32 -2
  53. data/lib/canon/diff/node_serializer.rb +191 -0
  54. data/lib/canon/diff/path_builder.rb +143 -0
  55. data/lib/canon/diff_formatter/by_line/base_formatter.rb +251 -0
  56. data/lib/canon/diff_formatter/by_line/html_formatter.rb +6 -248
  57. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +38 -229
  58. data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +30 -0
  59. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +579 -0
  60. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +121 -0
  61. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +253 -0
  62. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +61 -0
  63. data/lib/canon/diff_formatter/diff_detail_formatter.rb +31 -1028
  64. data/lib/canon/diff_formatter.rb +1 -1
  65. data/lib/canon/rspec_matchers.rb +1 -1
  66. data/lib/canon/tree_diff/operation_converter.rb +92 -338
  67. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +71 -0
  68. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +103 -0
  69. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +168 -0
  70. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +188 -0
  71. data/lib/canon/version.rb +1 -1
  72. data/old-docs/ADVANCED_TOPICS.adoc +20 -0
  73. data/old-docs/BASIC_USAGE.adoc +16 -0
  74. data/old-docs/CHARACTER_VISUALIZATION.adoc +567 -0
  75. data/old-docs/CLI.adoc +497 -0
  76. data/old-docs/CUSTOMIZING_BEHAVIOR.adoc +19 -0
  77. data/old-docs/DIFF_ARCHITECTURE.adoc +435 -0
  78. data/old-docs/DIFF_FORMATTING.adoc +540 -0
  79. data/old-docs/DIFF_PARAMETERS.adoc +261 -0
  80. data/old-docs/DOM_DIFF.adoc +1017 -0
  81. data/old-docs/ENV_CONFIG.adoc +876 -0
  82. data/old-docs/FORMATS.adoc +867 -0
  83. data/old-docs/INPUT_VALIDATION.adoc +477 -0
  84. data/old-docs/MATCHER_BEHAVIOR.adoc +90 -0
  85. data/old-docs/MATCH_ARCHITECTURE.adoc +463 -0
  86. data/old-docs/MATCH_OPTIONS.adoc +912 -0
  87. data/old-docs/MODES.adoc +432 -0
  88. data/old-docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +219 -0
  89. data/old-docs/OPTIONS.adoc +1387 -0
  90. data/old-docs/PREPROCESSING.adoc +491 -0
  91. data/old-docs/README.old.adoc +2831 -0
  92. data/old-docs/RSPEC.adoc +814 -0
  93. data/old-docs/RUBY_API.adoc +485 -0
  94. data/old-docs/SEMANTIC_DIFF_REPORT.adoc +646 -0
  95. data/old-docs/SEMANTIC_TREE_DIFF.adoc +765 -0
  96. data/old-docs/STRING_COMPARE.adoc +345 -0
  97. data/old-docs/TMP.adoc +3384 -0
  98. data/old-docs/TREE_DIFF.adoc +1080 -0
  99. data/old-docs/UNDERSTANDING_CANON.adoc +17 -0
  100. data/old-docs/VERBOSE.adoc +482 -0
  101. data/old-docs/VISUALIZATION_MAP.adoc +625 -0
  102. data/old-docs/WHITESPACE_TREATMENT.adoc +1155 -0
  103. data/scripts/analyze_current_state.rb +85 -0
  104. data/scripts/analyze_false_positives.rb +114 -0
  105. data/scripts/analyze_remaining_failures.rb +105 -0
  106. data/scripts/compare_current_failures.rb +95 -0
  107. data/scripts/compare_dom_tree_diff.rb +158 -0
  108. data/scripts/compare_failures.rb +151 -0
  109. data/scripts/debug_attribute_extraction.rb +66 -0
  110. data/scripts/debug_blocks_839.rb +115 -0
  111. data/scripts/debug_meta_matching.rb +52 -0
  112. data/scripts/debug_p_matching.rb +192 -0
  113. data/scripts/debug_signature_matching.rb +118 -0
  114. data/scripts/debug_sourcecode_124.rb +32 -0
  115. data/scripts/debug_whitespace_sensitive.rb +192 -0
  116. data/scripts/extract_false_positives.rb +138 -0
  117. data/scripts/find_actual_false_positives.rb +125 -0
  118. data/scripts/investigate_all_false_positives.rb +161 -0
  119. data/scripts/investigate_batch1.rb +127 -0
  120. data/scripts/investigate_classification.rb +150 -0
  121. data/scripts/investigate_classification_detailed.rb +190 -0
  122. data/scripts/investigate_common_failures.rb +342 -0
  123. data/scripts/investigate_false_negative.rb +80 -0
  124. data/scripts/investigate_false_positive.rb +83 -0
  125. data/scripts/investigate_false_positives.rb +227 -0
  126. data/scripts/investigate_false_positives_batch.rb +163 -0
  127. data/scripts/investigate_mixed_content.rb +125 -0
  128. data/scripts/investigate_remaining_16.rb +214 -0
  129. data/scripts/run_single_test.rb +29 -0
  130. data/scripts/test_all_false_positives.rb +95 -0
  131. data/scripts/test_attribute_details.rb +61 -0
  132. data/scripts/test_both_algorithms.rb +49 -0
  133. data/scripts/test_both_simple.rb +49 -0
  134. data/scripts/test_enhanced_semantic_output.rb +125 -0
  135. data/scripts/test_readme_examples.rb +131 -0
  136. data/scripts/test_semantic_tree_diff.rb +99 -0
  137. data/scripts/test_semantic_ux_improvements.rb +135 -0
  138. data/scripts/test_single_false_positive.rb +119 -0
  139. data/scripts/test_size_limits.rb +99 -0
  140. data/test_html_1.html +21 -0
  141. data/test_html_2.html +21 -0
  142. data/test_nokogiri.rb +33 -0
  143. data/test_normalize.rb +45 -0
  144. metadata +123 -2
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Comparison
5
+ # XML Node Comparison Utilities
6
+ #
7
+ # Provides public comparison methods for XML/HTML nodes.
8
+ # This module extracts shared comparison logic that was previously
9
+ # accessed via send() from HtmlComparator.
10
+ module XmlNodeComparison
11
+ # Main comparison dispatcher for XML nodes
12
+ #
13
+ # This method handles the high-level comparison logic, delegating
14
+ # to specific comparison methods based on node types.
15
+ #
16
+ # @param node1 [Object] First node
17
+ # @param node2 [Object] Second node
18
+ # @param opts [Hash] Comparison options
19
+ # @param child_opts [Hash] Options for child comparison
20
+ # @param diff_children [Boolean] Whether to diff children
21
+ # @param differences [Array] Array to append differences to
22
+ # @return [Symbol] Comparison result constant
23
+ def self.compare_nodes(node1, node2, opts, child_opts, diff_children,
24
+ differences)
25
+ # Handle DocumentFragment nodes - compare their children instead
26
+ if node1.is_a?(Nokogiri::XML::DocumentFragment) &&
27
+ node2.is_a?(Nokogiri::XML::DocumentFragment)
28
+ return compare_document_fragments(node1, node2, opts, child_opts,
29
+ diff_children, differences)
30
+ end
31
+
32
+ # Check if nodes should be excluded
33
+ return Comparison::EQUIVALENT if node_excluded?(node1, opts) &&
34
+ node_excluded?(node2, opts)
35
+
36
+ if node_excluded?(node1, opts) || node_excluded?(node2, opts)
37
+ add_difference(node1, node2, Comparison::MISSING_NODE,
38
+ Comparison::MISSING_NODE, :text_content, opts,
39
+ differences)
40
+ return Comparison::MISSING_NODE
41
+ end
42
+
43
+ # Check node types match
44
+ unless same_node_type?(node1, node2)
45
+ add_difference(node1, node2, Comparison::UNEQUAL_NODES_TYPES,
46
+ Comparison::UNEQUAL_NODES_TYPES, :text_content, opts,
47
+ differences)
48
+ return Comparison::UNEQUAL_NODES_TYPES
49
+ end
50
+
51
+ # Dispatch based on node type
52
+ dispatch_by_node_type(node1, node2, opts, child_opts, diff_children,
53
+ differences)
54
+ end
55
+
56
+ # Filter children based on options
57
+ #
58
+ # Removes nodes that should be excluded from comparison based on
59
+ # options like :ignore_nodes, :ignore_comments, etc.
60
+ #
61
+ # @param children [Array] Array of child nodes
62
+ # @param opts [Hash] Comparison options
63
+ # @return [Array] Filtered array of children
64
+ def self.filter_children(children, opts)
65
+ children.reject do |child|
66
+ node_excluded?(child, opts)
67
+ end
68
+ end
69
+
70
+ # Compare document fragments by comparing their children
71
+ #
72
+ # @param node1 [Nokogiri::XML::DocumentFragment] First fragment
73
+ # @param node2 [Nokogiri::XML::DocumentFragment] Second fragment
74
+ # @param opts [Hash] Comparison options
75
+ # @param child_opts [Hash] Options for child comparison
76
+ # @param diff_children [Boolean] Whether to diff children
77
+ # @param differences [Array] Array to append differences to
78
+ # @return [Symbol] Comparison result constant
79
+ def self.compare_document_fragments(node1, node2, opts, child_opts,
80
+ diff_children, differences)
81
+ childrenode1 = node1.children.to_a
82
+ childrenode2 = node2.children.to_a
83
+
84
+ if childrenode1.length != childrenode2.length
85
+ add_difference(node1, node2, Comparison::UNEQUAL_ELEMENTS,
86
+ Comparison::UNEQUAL_ELEMENTS, :text_content, opts,
87
+ differences)
88
+ Comparison::UNEQUAL_ELEMENTS
89
+ elsif childrenode1.empty?
90
+ Comparison::EQUIVALENT
91
+ else
92
+ # Compare each pair of children
93
+ result = Comparison::EQUIVALENT
94
+ childrenode1.zip(childrenode2).each do |child1, child2|
95
+ child_result = compare_nodes(child1, child2, opts, child_opts,
96
+ diff_children, differences)
97
+ result = child_result unless result == Comparison::EQUIVALENT
98
+ end
99
+ result
100
+ end
101
+ end
102
+
103
+ # Dispatch comparison based on node type
104
+ #
105
+ # @param node1 [Object] First node
106
+ # @param node2 [Object] Second node
107
+ # @param opts [Hash] Comparison options
108
+ # @param child_opts [Hash] Options for child comparison
109
+ # @param diff_children [Boolean] Whether to diff children
110
+ # @param differences [Array] Array to append differences to
111
+ # @return [Symbol] Comparison result constant
112
+ def self.dispatch_by_node_type(node1, node2, opts, child_opts,
113
+ diff_children, differences)
114
+ # Canon::Xml::Node types use .node_type method that returns symbols
115
+ # Nokogiri also has .node_type but returns integers, so check for Symbol
116
+ if node1.respond_to?(:node_type) && node2.respond_to?(:node_type) &&
117
+ node1.node_type.is_a?(Symbol) && node2.node_type.is_a?(Symbol)
118
+ dispatch_canon_node_type(node1, node2, opts, child_opts,
119
+ diff_children, differences)
120
+ # Moxml/Nokogiri types use .element?, .text?, etc. methods
121
+ else
122
+ dispatch_legacy_node_type(node1, node2, opts, child_opts,
123
+ diff_children, differences)
124
+ end
125
+ end
126
+
127
+ # Private helper methods
128
+
129
+ # Check if a node should be excluded from comparison
130
+ #
131
+ # @param node [Object] Node to check
132
+ # @param opts [Hash] Comparison options
133
+ # @return [Boolean] true if node should be excluded
134
+ def self.node_excluded?(node, opts)
135
+ return false if node.nil?
136
+ return true if opts[:ignore_nodes]&.include?(node)
137
+ return true if opts[:ignore_comments] && comment_node?(node)
138
+ return true if opts[:ignore_text_nodes] && text_node?(node)
139
+
140
+ # Check structural_whitespace match option
141
+ match_opts = opts[:match_opts]
142
+ # Filter out whitespace-only text nodes
143
+ if match_opts && %i[ignore
144
+ normalize].include?(match_opts[:structural_whitespace]) && text_node?(node)
145
+ text = node_text(node)
146
+ return true if MatchOptions.normalize_text(text).empty?
147
+ end
148
+
149
+ false
150
+ end
151
+
152
+ # Check if two nodes are of the same type
153
+ #
154
+ # @param node1 [Object] First node
155
+ # @param node2 [Object] Second node
156
+ # @return [Boolean] true if nodes are same type
157
+ def self.same_node_type?(node1, node2)
158
+ return false if node1.class != node2.class
159
+
160
+ # For Nokogiri/Canon::Xml nodes, check node type
161
+ if node1.respond_to?(:node_type) && node2.respond_to?(:node_type)
162
+ node1.node_type == node2.node_type
163
+ else
164
+ true
165
+ end
166
+ end
167
+
168
+ # Check if a node is a comment node
169
+ #
170
+ # @param node [Object] Node to check
171
+ # @return [Boolean] true if node is a comment
172
+ def self.comment_node?(node)
173
+ node.respond_to?(:comment?) && node.comment? ||
174
+ node.respond_to?(:node_type) && node.node_type == :comment
175
+ end
176
+
177
+ # Check if a node is a text node
178
+ #
179
+ # @param node [Object] Node to check
180
+ # @return [Boolean] true if node is a text node
181
+ def self.text_node?(node)
182
+ node.respond_to?(:text?) && node.text? &&
183
+ !node.respond_to?(:element?) ||
184
+ node.respond_to?(:node_type) && node.node_type == :text
185
+ end
186
+
187
+ # Dispatch by Canon::Xml::Node type
188
+ def self.dispatch_canon_node_type(node1, node2, opts, child_opts,
189
+ diff_children, differences)
190
+ # Import XmlComparator to use its comparison methods
191
+ require_relative "xml_comparator"
192
+
193
+ case node1.node_type
194
+ when :root
195
+ XmlComparator.compare_children(node1, node2, opts, child_opts,
196
+ diff_children, differences)
197
+ when :element
198
+ XmlComparator.compare_element_nodes(node1, node2, opts, child_opts,
199
+ diff_children, differences)
200
+ when :text
201
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
202
+ when :comment
203
+ XmlComparator.compare_comment_nodes(node1, node2, opts, differences)
204
+ when :cdata
205
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
206
+ when :processing_instruction
207
+ XmlComparator.compare_processing_instruction_nodes(node1, node2,
208
+ opts, differences)
209
+ else
210
+ Comparison::EQUIVALENT
211
+ end
212
+ end
213
+
214
+ # Dispatch by legacy Nokogiri/Moxml node type
215
+ def self.dispatch_legacy_node_type(node1, node2, opts, child_opts,
216
+ diff_children, differences)
217
+ # Import XmlComparator to use its comparison methods
218
+ require_relative "xml_comparator"
219
+
220
+ if node1.respond_to?(:element?) && node1.element?
221
+ XmlComparator.compare_element_nodes(node1, node2, opts, child_opts,
222
+ diff_children, differences)
223
+ elsif node1.respond_to?(:text?) && node1.text?
224
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
225
+ elsif node1.respond_to?(:comment?) && node1.comment?
226
+ XmlComparator.compare_comment_nodes(node1, node2, opts, differences)
227
+ elsif node1.respond_to?(:cdata?) && node1.cdata?
228
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
229
+ elsif node1.respond_to?(:processing_instruction?) && node1.processing_instruction?
230
+ XmlComparator.compare_processing_instruction_nodes(node1, node2,
231
+ opts, differences)
232
+ elsif node1.respond_to?(:root)
233
+ XmlComparator.compare_document_nodes(node1, node2, opts, child_opts,
234
+ diff_children, differences)
235
+ else
236
+ Comparison::EQUIVALENT
237
+ end
238
+ end
239
+
240
+ # Add a difference to the differences array
241
+ #
242
+ # @param node1 [Object] First node
243
+ # @param node2 [Object] Second node
244
+ # @param diff1 [Symbol] Difference type for node1
245
+ # @param diff2 [Symbol] Difference type for node2
246
+ # @param dimension [Symbol] The dimension of the difference
247
+ # @param opts [Hash] Comparison options
248
+ # @param differences [Array] Array to append difference to
249
+ def self.add_difference(node1, node2, diff1, diff2, dimension, opts,
250
+ differences)
251
+ return unless opts[:verbose]
252
+
253
+ require_relative "xml_comparator"
254
+ XmlComparator.add_difference(node1, node2, diff1, diff2, dimension,
255
+ opts, differences)
256
+ end
257
+
258
+ # Serialize a Canon::Xml::Node to XML string
259
+ #
260
+ # This utility method handles serialization of different node types
261
+ # to their string representation for display and debugging purposes.
262
+ #
263
+ # @param node [Canon::Xml::Node, Object] Node to serialize
264
+ # @return [String] XML string representation
265
+ def self.serialize_node_to_xml(node)
266
+ if node.is_a?(Canon::Xml::Nodes::RootNode)
267
+ # Serialize all children of root
268
+ node.children.map { |child| serialize_node_to_xml(child) }.join
269
+ elsif node.is_a?(Canon::Xml::Nodes::ElementNode)
270
+ # Serialize element with attributes and children
271
+ attrs = node.attribute_nodes.map do |a|
272
+ " #{a.name}=\"#{a.value}\""
273
+ end.join
274
+ children_xml = node.children.map do |c|
275
+ serialize_node_to_xml(c)
276
+ end.join
277
+
278
+ if children_xml.empty?
279
+ "<#{node.name}#{attrs}/>"
280
+ else
281
+ "<#{node.name}#{attrs}>#{children_xml}</#{node.name}>"
282
+ end
283
+ elsif node.is_a?(Canon::Xml::Nodes::TextNode)
284
+ node.value
285
+ elsif node.is_a?(Canon::Xml::Nodes::CommentNode)
286
+ "<!--#{node.value}-->"
287
+ elsif node.is_a?(Canon::Xml::Nodes::ProcessingInstructionNode)
288
+ "<?#{node.target} #{node.data}?>"
289
+ elsif node.respond_to?(:to_xml)
290
+ node.to_xml
291
+ else
292
+ node.to_s
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Comparison
5
+ # Public API for XML parsing operations
6
+ # Provides access to parsing functionality without using send()
7
+ class XmlParser
8
+ # Parse an object to Canon::Xml::Node with preprocessing
9
+ #
10
+ # @param obj [String, Object] Object to parse
11
+ # @param preprocessing [Symbol] Preprocessing mode
12
+ # @return [Canon::Xml::Node] Parsed node
13
+ def self.parse_node(obj, preprocessing = :none)
14
+ # Delegate to XmlComparator's private method via public API
15
+ XmlComparator::NodeParser.parse(obj, preprocessing)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require_relative "json_comparator"
5
4
  require_relative "match_options"
6
5
  require_relative "comparison_result"
6
+ require_relative "ruby_object_comparator"
7
7
 
8
8
  module Canon
9
9
  module Comparison
@@ -60,8 +60,8 @@ module Canon
60
60
  obj2 = parse_yaml(yaml2)
61
61
 
62
62
  differences = []
63
- result = JsonComparator.send(:compare_ruby_objects, obj1, obj2, opts,
64
- differences, "")
63
+ result = RubyObjectComparator.compare_objects(obj1, obj2, opts,
64
+ differences, "")
65
65
 
66
66
  if opts[:verbose]
67
67
  # Format YAML for display