Module: Canon::Comparison::WhitespaceSensitivity
- Defined in:
- lib/canon/comparison/whitespace_sensitivity.rb
Overview
Whitespace sensitivity utilities for element-level control
Constant Summary collapse
- HTML_COLLAPSE_ELEMENTS =
HTML mixed-content "leaf block" elements where whitespace presence matters but all forms are equivalent (CSS block whitespace collapsing).
%w[ p li dt dd td th caption figcaption label legend summary h1 h2 h3 h4 h5 h6 blockquote address button ].freeze
- HTML_PRESERVE_ELEMENTS =
HTML elements where every whitespace character is significant.
%w[pre code textarea script style].freeze
- INLINE_ELEMENTS =
HTML inline elements — whitespace between these is semantically significant (renders as a visible space). Whitespace-only text nodes that sit between two inline siblings must not be stripped.
%w[ a abbr acronym b bdo big br button cite code dfn em i img input kbd label map object output q s samp select small span strong sub sup time tt u var wbr ].freeze
- HTML_PRESERVE_SYMBOLS =
Precomputed symbol forms of the default lists: format_default_* used to map(&:to_sym) per call — one array per classified node.
HTML_PRESERVE_ELEMENTS.map(&:to_sym).freeze
- HTML_COLLAPSE_SYMBOLS =
HTML_COLLAPSE_ELEMENTS.map(&:to_sym).freeze
- RESOLVED_SETS_LIMIT =
The resolved element sets depend only on match_opts, which is resolved once per comparison — but classification runs per node, so the sets are memoized per match_opts object (identity-keyed; side-flag merges produce distinct objects and their own entries). Bounded: stale entries of finished comparisons cost a few hundred bytes until the next clear.
1024
Class Method Summary collapse
-
.classify_element(element, match_opts) ⇒ Object
Classify the whitespace behaviour for an element using ancestor walk.
-
.classify_text_node(node, opts) ⇒ Object
Return the whitespace class for a text node used during comparison.
- .contains_nbsp?(text) ⇒ Boolean
- .default_sensitive_element?(element_name, match_opts) ⇒ Boolean
-
.element_sensitive?(node, opts) ⇒ Boolean
Check if an element is whitespace-sensitive based on configuration.
- .format_default_collapse_elements(match_opts) ⇒ Object
- .format_default_preserve_elements(match_opts) ⇒ Object
-
.inline_whitespace_significant?(text_node) ⇒ Boolean
Check if whitespace-only text node sits between two inline element siblings, making the whitespace semantically significant.
- .nearest_non_whitespace_sibling(siblings, idx, direction) ⇒ Object
-
.preserve_whitespace_node?(node, opts) ⇒ Boolean
Check if whitespace-only text node should be filtered.
- .resolved_collapse_elements(match_opts) ⇒ Object
- .resolved_preserve_elements(match_opts) ⇒ Object
-
.whitespace_preserved?(element, match_opts) ⇒ Boolean
Check if structural whitespace is preserved (not stripped) for an element.
Class Method Details
.classify_element(element, match_opts) ⇒ Object
Classify the whitespace behaviour for an element using ancestor walk. Results are cached per element per match_opts (classify runs per text node/pair otherwise — once per element is enough; the sets it depends on are memoized alongside in the same entry).
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 46 def classify_element(element, match_opts) return :strip unless element return :strip unless node_name(element) cache = classification_map(match_opts) cached = cache[element] return cached if cached classification = walk_ancestor_classification( element, resolved_preserve_elements_set(match_opts), resolved_collapse_elements_set(match_opts), resolved_strip_elements_set(match_opts), ) cache[element] = classification classification end |
.classify_text_node(node, opts) ⇒ Object
Return the whitespace class for a text node used during comparison.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 92 def classify_text_node(node, opts) match_opts = opts[:match_opts] return :strip unless match_opts return :strip unless text_node_parent?(node) parent = node_parent(node) unless respect_xml_space?(match_opts) return user_config_sensitive?(parent, match_opts) ? :preserve : :strip end return :preserve if xml_space_preserve?(parent) return :strip if xml_space_default?(parent) classify_element(parent, match_opts) end |
.contains_nbsp?(text) ⇒ Boolean
198 199 200 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 198 def contains_nbsp?(text) text.to_s.include?(" ") end |
.default_sensitive_element?(element_name, match_opts) ⇒ Boolean
149 150 151 152 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 149 def default_sensitive_element?(element_name, match_opts) format_default_preserve_elements(match_opts) .include?(element_name.to_sym) end |
.element_sensitive?(node, opts) ⇒ Boolean
Check if an element is whitespace-sensitive based on configuration.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 65 def element_sensitive?(node, opts) match_opts = opts[:match_opts] return false unless match_opts return false unless text_node_parent?(node) parent = node_parent(node) unless respect_xml_space?(match_opts) return user_config_sensitive?(parent, match_opts) end return true if xml_space_preserve?(parent) return false if xml_space_default?(parent) classification = classify_element(parent, match_opts) %i[preserve collapse].include?(classification) end |
.format_default_collapse_elements(match_opts) ⇒ Object
139 140 141 142 143 144 145 146 147 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 139 def format_default_collapse_elements(match_opts) format = match_opts[:format] || :xml case format when :html, :html4, :html5 HTML_COLLAPSE_SYMBOLS else [].freeze end end |
.format_default_preserve_elements(match_opts) ⇒ Object
129 130 131 132 133 134 135 136 137 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 129 def format_default_preserve_elements(match_opts) format = match_opts[:format] || :xml case format when :html, :html4, :html5 HTML_PRESERVE_SYMBOLS else [].freeze end end |
.inline_whitespace_significant?(text_node) ⇒ Boolean
Check if whitespace-only text node sits between two inline element siblings, making the whitespace semantically significant.
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 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 156 def inline_whitespace_significant?(text_node) parent = NodeInspector.parent(text_node) return false unless parent # One pass: find the node by identity while tracking the # nearest non-whitespace siblings on each side (index plus # two directional scans cost three passes before). siblings = NodeInspector.children(parent) prev_neighbour = nil next_neighbour = nil found = false siblings.each do |sibling| if found if next_neighbour.nil? && !whitespace_text_node?(sibling) next_neighbour = sibling break end elsif sibling.equal?(text_node) found = true elsif !whitespace_text_node?(sibling) prev_neighbour = sibling end end return false unless found inline_element?(prev_neighbour) && inline_element?(next_neighbour) end |
.nearest_non_whitespace_sibling(siblings, idx, direction) ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 185 def nearest_non_whitespace_sibling(siblings, idx, direction) i = idx + direction while i >= 0 && i < siblings.length s = siblings[i] unless whitespace_text_node?(s) return s end i += direction end nil end |
.preserve_whitespace_node?(node, opts) ⇒ Boolean
Check if whitespace-only text node should be filtered
84 85 86 87 88 89 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 84 def preserve_whitespace_node?(node, opts) parent = node_parent(node) return false unless parent element_sensitive?(node, opts) end |
.resolved_collapse_elements(match_opts) ⇒ Object
125 126 127 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 125 def resolved_collapse_elements(match_opts) resolved_collapse_elements_set(match_opts).to_a end |
.resolved_preserve_elements(match_opts) ⇒ Object
121 122 123 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 121 def resolved_preserve_elements(match_opts) resolved_preserve_elements_set(match_opts).to_a end |
.whitespace_preserved?(element, match_opts) ⇒ Boolean
Check if structural whitespace is preserved (not stripped) for an element.
111 112 113 114 115 116 117 118 119 |
# File 'lib/canon/comparison/whitespace_sensitivity.rb', line 111 def whitespace_preserved?(element, match_opts) if respect_xml_space?(match_opts) return true if xml_space_preserve?(element) return false if xml_space_default?(element) end classification = classify_element(element, match_opts) %i[preserve collapse].include?(classification) end |