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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b27c3659e1b75307f170fb50161ce649c54ab4a12600716d3b006fce8b01ad1a
4
- data.tar.gz: 89c288d724f4a3b0b410938d94959880b4fbbcff1ec550b53593840bd33d52c8
3
+ metadata.gz: b04c8b44b470bb85f2ec003f434a0fa8ede8eec74f687b388241814e60841415
4
+ data.tar.gz: a944aa18b2dd0993362d15a92cf70488a0618841445873c617c206a5e6fe4f72
5
5
  SHA512:
6
- metadata.gz: '0922a2a6d6061f7472ac55de8c6e0172a8ce8d2939f8f144f71493b148248fb597c07d916c67dc9a4549d35a6e65b7757e5cb3f0d0ae79a7a39f7b35306bbb22'
7
- data.tar.gz: e5b5b898ca11ef0bdc077d8cfb78d6575bc7c9a6b712f2a0ccbd2a3f338704d66883c730e32651af1776746b751b996c723b29af80044d8cd44944f9b18df883
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
- preserve_set = resolved_preserve_elements_set(match_opts)
47
- collapse_set = resolved_collapse_elements_set(match_opts)
48
- strip_set = resolved_strip_elements_set(match_opts)
49
-
50
- walk_ancestor_classification(element, preserve_set, collapse_set,
51
- strip_set)
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
- idx = siblings.index(text_node)
152
- return false unless idx
153
-
154
- prev_neighbour = nearest_non_whitespace_sibling(siblings, idx, -1)
155
- next_neighbour = nearest_non_whitespace_sibling(siblings, idx, 1)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.18"
4
+ VERSION = "0.3.19"
5
5
  end
@@ -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. Skipping the
86
- # dup avoids a full-document copy on every parse.
87
- unless xml_string.encoding.name == "UTF-8" && xml_string.valid_encoding?
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
- nil
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
- doctype_start = xml.upcase.index("<!DOCTYPE")
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.downcase
69
- return true if HTML_WHITESPACE_SENSITIVE_TAGS.include?(parent_name)
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.18
4
+ version: 0.3.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.