canon 0.3.18 → 0.3.19
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/lib/canon/comparison/whitespace_sensitivity.rb +50 -11
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +17 -7
- data/lib/canon/xml/sax_builder.rb +3 -2
- data/lib/canon/xml/whitespace_policy.rb +9 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b04c8b44b470bb85f2ec003f434a0fa8ede8eec74f687b388241814e60841415
|
|
4
|
+
data.tar.gz: a944aa18b2dd0993362d15a92cf70488a0618841445873c617c206a5e6fe4f72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfc4366fc597d9a8c33201cf7ed4857f3577e791d0fc80160c0e6dbeda359f5c7ea0d8df49f4dcec22ffe7c596e1a694ec114b8005bce7275f9e6572b3643f77
|
|
7
|
+
data.tar.gz: 3b00f6d66f7c75e3f4dccb224347b3092ea4713aa44906c1bc623b3fee6086f940d9c13b5af10c8c60f063d8e16e82a99adc412e662cb4fe092bb23f8129c1b4
|
|
@@ -39,16 +39,26 @@ module Canon
|
|
|
39
39
|
|
|
40
40
|
class << self
|
|
41
41
|
# Classify the whitespace behaviour for an element using ancestor walk.
|
|
42
|
+
# Results are cached per element per match_opts (classify runs
|
|
43
|
+
# per text node/pair otherwise — once per element is enough;
|
|
44
|
+
# the sets it depends on are memoized alongside in the same
|
|
45
|
+
# entry).
|
|
42
46
|
def classify_element(element, match_opts)
|
|
43
47
|
return :strip unless element
|
|
44
48
|
return :strip unless node_name(element)
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
walk_ancestor_classification(
|
|
51
|
-
|
|
50
|
+
cache = classification_map(match_opts)
|
|
51
|
+
cached = cache[element]
|
|
52
|
+
return cached if cached
|
|
53
|
+
|
|
54
|
+
classification = walk_ancestor_classification(
|
|
55
|
+
element,
|
|
56
|
+
resolved_preserve_elements_set(match_opts),
|
|
57
|
+
resolved_collapse_elements_set(match_opts),
|
|
58
|
+
resolved_strip_elements_set(match_opts),
|
|
59
|
+
)
|
|
60
|
+
cache[element] = classification
|
|
61
|
+
classification
|
|
52
62
|
end
|
|
53
63
|
|
|
54
64
|
# Check if an element is whitespace-sensitive based on configuration.
|
|
@@ -147,12 +157,27 @@ module Canon
|
|
|
147
157
|
parent = NodeInspector.parent(text_node)
|
|
148
158
|
return false unless parent
|
|
149
159
|
|
|
160
|
+
# One pass: find the node by identity while tracking the
|
|
161
|
+
# nearest non-whitespace siblings on each side (index plus
|
|
162
|
+
# two directional scans cost three passes before).
|
|
150
163
|
siblings = NodeInspector.children(parent)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
prev_neighbour = nil
|
|
165
|
+
next_neighbour = nil
|
|
166
|
+
found = false
|
|
167
|
+
|
|
168
|
+
siblings.each do |sibling|
|
|
169
|
+
if found
|
|
170
|
+
if next_neighbour.nil? && !whitespace_text_node?(sibling)
|
|
171
|
+
next_neighbour = sibling
|
|
172
|
+
break
|
|
173
|
+
end
|
|
174
|
+
elsif sibling.equal?(text_node)
|
|
175
|
+
found = true
|
|
176
|
+
elsif !whitespace_text_node?(sibling)
|
|
177
|
+
prev_neighbour = sibling
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
return false unless found
|
|
156
181
|
|
|
157
182
|
inline_element?(prev_neighbour) && inline_element?(next_neighbour)
|
|
158
183
|
end
|
|
@@ -236,6 +261,20 @@ module Canon
|
|
|
236
261
|
cache[match_opts] ||= [nil, nil, nil]
|
|
237
262
|
end
|
|
238
263
|
|
|
264
|
+
# Per-match_opts classification map (slot 3 of the resolved
|
|
265
|
+
# entry). Weak element keys: entries vanish with their trees,
|
|
266
|
+
# so finished comparisons retain nothing. Opal has no
|
|
267
|
+
# ObjectSpace::WeakMap — there the map holds strong references
|
|
268
|
+
# and dies with the bounded resolved-sets cache clear.
|
|
269
|
+
def classification_map(match_opts)
|
|
270
|
+
entry = resolved_sets_entry(match_opts)
|
|
271
|
+
entry[3] ||= if defined?(ObjectSpace::WeakMap)
|
|
272
|
+
ObjectSpace::WeakMap.new
|
|
273
|
+
else
|
|
274
|
+
{}.compare_by_identity
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
239
278
|
def walk_ancestor_classification(element, preserve_set, collapse_set,
|
|
240
279
|
strip_set)
|
|
241
280
|
current = element
|
data/lib/canon/version.rb
CHANGED
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -79,19 +79,29 @@ module Canon
|
|
|
79
79
|
nil
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Encoding-declaration probe: anchored, and it only ever scans the
|
|
83
|
+
# XML declaration prefix — a document without one bails after a
|
|
84
|
+
# few characters.
|
|
85
|
+
XML_ENCODING_DECL = /\A\s*<\?xml[^>]*\bencoding\s*=\s*["']([^"']+)["'][^>]*\?>/i
|
|
86
|
+
|
|
82
87
|
def self.extract_xml_encoding(xml_string)
|
|
83
88
|
# A valid UTF-8 string matches directly (the regex is
|
|
84
89
|
# ASCII-only); anything else still needs the BINARY view so a
|
|
85
|
-
# broken byte sequence cannot raise mid-probe.
|
|
86
|
-
#
|
|
87
|
-
|
|
90
|
+
# broken byte sequence cannot raise mid-probe. The validity is
|
|
91
|
+
# tested by the match itself, not a full-document scan: invalid
|
|
92
|
+
# bytes outside the scanned prefix never raise, and invalid
|
|
93
|
+
# bytes inside it fall back to the BINARY retry.
|
|
94
|
+
if xml_string.encoding.name == "UTF-8"
|
|
95
|
+
begin
|
|
96
|
+
return xml_string[XML_ENCODING_DECL, 1]
|
|
97
|
+
rescue ArgumentError
|
|
98
|
+
xml_string = xml_string.dup.force_encoding("BINARY")
|
|
99
|
+
end
|
|
100
|
+
else
|
|
88
101
|
xml_string = xml_string.dup.force_encoding("BINARY")
|
|
89
102
|
end
|
|
90
|
-
if xml_string =~ /\A\s*<\?xml[^>]*\bencoding\s*=\s*["']([^"']+)["'][^>]*\?>/i
|
|
91
|
-
return Regexp.last_match(1)
|
|
92
|
-
end
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
xml_string[XML_ENCODING_DECL, 1]
|
|
95
105
|
end
|
|
96
106
|
|
|
97
107
|
def self.parse(xml_string)
|
|
@@ -52,8 +52,9 @@ strip_doctype: false)
|
|
|
52
52
|
# @param xml [String] XML string potentially containing DOCTYPE
|
|
53
53
|
# @return [String] XML string with DOCTYPE removed
|
|
54
54
|
def self.strip_doctype_declaration(xml)
|
|
55
|
-
# Find DOCTYPE start (case-insensitive)
|
|
56
|
-
|
|
55
|
+
# Find DOCTYPE start (case-insensitive). A literal + /i index
|
|
56
|
+
# scans in place — upcase would copy the whole document per call.
|
|
57
|
+
doctype_start = xml.index(/<!DOCTYPE/i)
|
|
57
58
|
return xml unless doctype_start
|
|
58
59
|
|
|
59
60
|
# Find the end of DOCTYPE - it ends with >
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Canon
|
|
4
6
|
module Xml
|
|
5
7
|
# Parse-time whitespace policy: whether a character-data node
|
|
@@ -60,13 +62,18 @@ element_parent: true)
|
|
|
60
62
|
# carries NBSP (U+00A0 — never insignificant; strip is ASCII-only
|
|
61
63
|
# so it is checked explicitly).
|
|
62
64
|
HTML_WHITESPACE_SENSITIVE_TAGS = %w[pre code textarea script style].freeze
|
|
65
|
+
# Set form for the hot lookup; element names arrive lowercased
|
|
66
|
+
# from the HTML parser, so the downcase fallback only allocates
|
|
67
|
+
# for unusual (non-lowercased) names.
|
|
68
|
+
HTML_SENSITIVE_TAG_SET = HTML_WHITESPACE_SENSITIVE_TAGS.to_set
|
|
63
69
|
|
|
64
70
|
def keep_html_text?(content, parent_name:, text_node: nil)
|
|
65
71
|
return true unless content.match?(STRIP_ONLY)
|
|
66
72
|
return true if content.include?(" ")
|
|
67
73
|
|
|
68
|
-
parent_name = parent_name.to_s
|
|
69
|
-
return true if
|
|
74
|
+
parent_name = parent_name.to_s
|
|
75
|
+
return true if HTML_SENSITIVE_TAG_SET.include?(parent_name)
|
|
76
|
+
return true if HTML_SENSITIVE_TAG_SET.include?(parent_name.downcase)
|
|
70
77
|
|
|
71
78
|
# Computed last: the sibling scan is O(siblings), so it must
|
|
72
79
|
# not run for the content-bearing text nodes that fail the
|