canon 0.3.19 → 0.3.21

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: b04c8b44b470bb85f2ec003f434a0fa8ede8eec74f687b388241814e60841415
4
- data.tar.gz: a944aa18b2dd0993362d15a92cf70488a0618841445873c617c206a5e6fe4f72
3
+ metadata.gz: fd899c17d1512c815d189076fe08721dbe66b7d964f8bac69abde35f207b8c76
4
+ data.tar.gz: 205ad0b365288bf883f2ce57f68b612217151e30ae90fb0f959b29f252063826
5
5
  SHA512:
6
- metadata.gz: dfc4366fc597d9a8c33201cf7ed4857f3577e791d0fc80160c0e6dbeda359f5c7ea0d8df49f4dcec22ffe7c596e1a694ec114b8005bce7275f9e6572b3643f77
7
- data.tar.gz: 3b00f6d66f7c75e3f4dccb224347b3092ea4713aa44906c1bc623b3fee6086f940d9c13b5af10c8c60f063d8e16e82a99adc412e662cb4fe092bb23f8129c1b4
6
+ metadata.gz: 5aa4d552592cda5a55417415efb225151eb0d39d899fe0471eb26f10ceeca64ae56de786d8e807e0085264ba800cfd5937a6ec15e182e71957381414f3036105
7
+ data.tar.gz: b52ceb55e3ca6c6affcca3c7091a0e3f6e22d1c077b65732e7073915c3f6914cb86a61e675b11636baeb1ea09e94c223c933e961c0b366fb3e621c592a50170b
data/lib/canon/cache.rb CHANGED
@@ -44,7 +44,8 @@ module Canon
44
44
  cache.delete(oldest_key) if oldest_key
45
45
  end
46
46
 
47
- cache[key] = { value: value, accessed: Time.now }
47
+ @clock = (@clock || 0) + 1
48
+ cache[key] = { value: value, accessed: @clock }
48
49
  value
49
50
  end
50
51
 
@@ -77,12 +78,6 @@ module Canon
77
78
  "doc:#{format}:#{preprocessing}:#{content_hash(content)}"
78
79
  end
79
80
 
80
- # Generate cache key for format detection
81
- def key_for_format_detection(content)
82
- preview = content[0..100].b
83
- "fmt:#{content_hash(preview + content.length.to_s)}"
84
- end
85
-
86
81
  # Generate cache key for XML canonicalization
87
82
  def key_for_c14n(content, with_comments)
88
83
  "c14n:#{with_comments}:#{content_hash(content)}"
@@ -53,15 +53,16 @@ module Canon
53
53
  raise Canon::Error, "Unknown format for object: #{obj.class}"
54
54
  end
55
55
 
56
- # Detect the format of a string with caching
56
+ # Detect the format of a string
57
57
  #
58
58
  # @param str [String] String to detect format of
59
59
  # @return [Symbol] Format type
60
60
  def detect_string(str)
61
- # Use cache for format detection
62
- Cache.fetch(:format_detect, Cache.key_for_format_detection(str)) do # rubocop:disable Lint/UselessDefaultValueArgument
63
- detect_string_uncached(str)
64
- end
61
+ # Detection is a handful of prefix checks and one anchored
62
+ # regex building the SHA256 cache key (plus LRU clock
63
+ # bookkeeping) cost several times the detection itself, so
64
+ # the answer is computed directly.
65
+ detect_string_uncached(str)
65
66
  end
66
67
 
67
68
  # Detect the format of a string without caching
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.19"
4
+ VERSION = "0.3.21"
5
5
  end
@@ -5,10 +5,18 @@ module Canon
5
5
  # Character encoder for C14N 1.1
6
6
  # Handles UTF-8 encoding and character reference encoding per spec
7
7
  class CharacterEncoder
8
+ # Most text and attribute values contain nothing to escape —
9
+ # the zero-allocation guard keeps those from paying the gsub
10
+ # copy on every render.
11
+ TEXT_ESCAPABLE = /[&<>\r]/
12
+ ATTRIBUTE_ESCAPABLE = /[&<"\t\n\r]/
13
+
8
14
  # Encode text node content
9
15
  # Replace: & → &amp;, < → &lt;, > → &gt;, #xD → &#xD;
10
16
  def encode_text(text)
11
- text.gsub(/[&<>\r]/) do |char|
17
+ return text unless text.match?(TEXT_ESCAPABLE)
18
+
19
+ text.gsub(TEXT_ESCAPABLE) do |char|
12
20
  case char
13
21
  when "&" then "&amp;"
14
22
  when "<" then "&lt;"
@@ -22,7 +30,9 @@ module Canon
22
30
  # Replace: & → &amp;, < → &lt;, " → &quot;,
23
31
  # #x9 → &#x9;, #xA → &#xA;, #xD → &#xD;
24
32
  def encode_attribute(value)
25
- value.gsub(/[&<"\t\n\r]/) do |char|
33
+ return value unless value.match?(ATTRIBUTE_ESCAPABLE)
34
+
35
+ value.gsub(ATTRIBUTE_ESCAPABLE) do |char|
26
36
  case char
27
37
  when "&" then "&amp;"
28
38
  when "<" then "&lt;"
@@ -14,7 +14,7 @@ module Canon
14
14
  def process_namespaces(element, output, parent_element = nil)
15
15
  return unless element.in_node_set?
16
16
 
17
- namespaces = element.sorted_namespace_nodes.select(&:in_node_set?)
17
+ namespaces = renderable_namespaces(element)
18
18
 
19
19
  # Check if we need to emit xmlns="" for empty default namespace
20
20
  if should_emit_empty_default_namespace?(element, namespaces,
@@ -61,26 +61,45 @@ module Canon
61
61
  end
62
62
 
63
63
  # Check if a namespace node should be skipped
64
- def should_skip_namespace?(ns, _element, parent_element)
64
+ def should_skip_namespace?(ns, element, parent_element)
65
65
  # Skip xml namespace with standard URI
66
66
  return true if ns.xml_namespace?
67
67
 
68
68
  # Skip if an ancestor already declared this namespace
69
- return true if namespace_declared_by_ancestor?(ns, parent_element)
69
+ return true if namespace_declared_by_ancestor?(ns, element,
70
+ parent_element)
70
71
 
71
72
  false
72
73
  end
73
74
 
74
75
  # Check if a namespace is already declared by an ancestor
75
- def namespace_declared_by_ancestor?(ns, parent_element)
76
+ def namespace_declared_by_ancestor?(ns, element, parent_element)
76
77
  return false unless parent_element
77
78
 
78
- parent_ns = parent_element.namespace_nodes.find do |parent_ns|
79
+ parent_nodes = parent_element.namespace_nodes
80
+ # Elements that declared nothing share their parent's scope
81
+ # object (TreeBuilder scope sharing), so identical arrays mean
82
+ # identical bindings — every namespace of this element is
83
+ # already declared above and will be skipped without a scan.
84
+ return true if parent_nodes.equal?(element.namespace_nodes)
85
+
86
+ parent_ns = parent_nodes.find do |parent_ns|
79
87
  parent_ns.prefix == ns.prefix && parent_ns.in_node_set?
80
88
  end
81
89
 
82
90
  parent_ns && parent_ns.uri == ns.uri
83
91
  end
92
+
93
+ # Sorted, in-node-set namespaces for one element. Undeclaring
94
+ # elements share one frozen array object, and node-set marking is
95
+ # fixed before processing runs, so the rendered form is cached
96
+ # per array identity for the lifetime of this handler (one
97
+ # canonicalization run).
98
+ def renderable_namespaces(element)
99
+ cache = (@renderable_namespaces ||= {}.compare_by_identity)
100
+ nodes = element.namespace_nodes
101
+ cache[nodes] ||= nodes.sort_by(&:local_name).select(&:in_node_set?)
102
+ end
84
103
  end
85
104
  end
86
105
  end
@@ -53,15 +53,26 @@ module Canon
53
53
  (@attribute_nodes ||= []) << attribute_node
54
54
  end
55
55
 
56
- # Get namespace nodes in sorted order (lexicographically by local name)
56
+ # Get namespace nodes in sorted order (lexicographically by local name).
57
57
  def sorted_namespace_nodes
58
- namespace_nodes.sort_by(&:local_name)
58
+ nodes = namespace_nodes
59
+ return nodes if nodes.empty?
60
+
61
+ nodes.sort_by(&:local_name)
59
62
  end
60
63
 
61
- # Get attribute nodes in sorted order (by namespace URI then local name)
64
+ # Get attribute nodes in sorted order (by namespace URI then local
65
+ # name). A comparator block instead of sort_by keys allocates no
66
+ # per-attribute key arrays; (uri, name) pairs are unique per
67
+ # element (duplicates are resolved at build), so the order is
68
+ # identical.
62
69
  def sorted_attribute_nodes
63
- attribute_nodes.sort_by do |attr|
64
- [attr.namespace_uri.to_s, attr.local_name]
70
+ attrs = attribute_nodes
71
+ return attrs if attrs.empty?
72
+
73
+ attrs.sort do |a, b|
74
+ (a.namespace_uri.to_s <=> b.namespace_uri.to_s).nonzero? ||
75
+ (a.local_name <=> b.local_name)
65
76
  end
66
77
  end
67
78
 
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.19
4
+ version: 0.3.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.