canon 0.3.12 → 0.3.13
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/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +11 -4
- data/lib/canon/xml/node.rb +15 -3
- data/lib/canon/xml/nodes/element_node.rb +23 -11
- data/lib/canon/xml/nodes/root_node.rb +0 -4
- data/lib/canon/xml/sax_builder.rb +7 -2
- data/lib/canon/xml/tree_builder.rb +51 -21
- data/lib/canon/xml/whitespace_policy.rb +12 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e052ba548bcbe2090cf0ebb538cc50a56ca64cc30a24e875418dbbe9accd053d
|
|
4
|
+
data.tar.gz: 6dfb8f2c9f840d0326271545ffee0ec137f3313469fa78c0cf80c9dc3e14f931
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c041ca7ded4daedf25b1ecc59d6e3be6ebb7df8dfa12f4bf87ae5458fdfb7bdcc2c3aa8bad1eb92f75c365ee91047d075be45cd05694477275ad1f9313b665be
|
|
7
|
+
data.tar.gz: 97d0a91b0fb8629cc57315872b2a8c151c2c06b28eca0f6c686c9ba6258b3405e3717fbbc72fa837794ed1129d2412c684955194de8a5e5112831ad4af695bb1
|
data/lib/canon/version.rb
CHANGED
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -80,8 +80,14 @@ module Canon
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def self.extract_xml_encoding(xml_string)
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
# A valid UTF-8 string matches directly (the regex is
|
|
84
|
+
# 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?
|
|
88
|
+
xml_string = xml_string.dup.force_encoding("BINARY")
|
|
89
|
+
end
|
|
90
|
+
if xml_string =~ /\A\s*<\?xml[^>]*\bencoding\s*=\s*["']([^"']+)["'][^>]*\?>/i
|
|
85
91
|
return Regexp.last_match(1)
|
|
86
92
|
end
|
|
87
93
|
|
|
@@ -320,8 +326,9 @@ preserve_whitespace: false)
|
|
|
320
326
|
children << frames.pop[1] while frames.any? && frames.last[0] > depth
|
|
321
327
|
children.reverse_each { |child| element.add_child(child) }
|
|
322
328
|
|
|
323
|
-
# Copy the declarations out of the reused buffer
|
|
324
|
-
|
|
329
|
+
# Copy the declarations out of the reused buffer (most
|
|
330
|
+
# elements declare none — stash nothing for those).
|
|
331
|
+
own_namespaces[element] = namespaces.each_slice(2).to_a unless namespaces.empty?
|
|
325
332
|
element
|
|
326
333
|
end
|
|
327
334
|
|
data/lib/canon/xml/node.rb
CHANGED
|
@@ -4,17 +4,29 @@ module Canon
|
|
|
4
4
|
module Xml
|
|
5
5
|
# Base class for all XPath data model nodes
|
|
6
6
|
class Node
|
|
7
|
-
attr_reader :parent
|
|
7
|
+
attr_reader :parent
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
10
|
@parent = nil
|
|
11
|
-
@children =
|
|
11
|
+
@children = nil
|
|
12
12
|
@in_node_set = true
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# Leaf nodes (text, attributes, namespaces, comments, PIs) never
|
|
16
|
+
# gain children; allocating the array eagerly cost one Array per
|
|
17
|
+
# node — the single largest retained-allocation source in a built
|
|
18
|
+
# tree — so it materializes on first add or read.
|
|
19
|
+
def children
|
|
20
|
+
@children ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def children=(new_children)
|
|
24
|
+
@children = new_children
|
|
25
|
+
end
|
|
26
|
+
|
|
15
27
|
def add_child(child)
|
|
16
28
|
child.parent = self
|
|
17
|
-
|
|
29
|
+
children << child
|
|
18
30
|
end
|
|
19
31
|
|
|
20
32
|
def in_node_set?
|
|
@@ -3,18 +3,32 @@
|
|
|
3
3
|
module Canon
|
|
4
4
|
module Xml
|
|
5
5
|
module Nodes
|
|
6
|
-
# Element node in the XPath data model
|
|
7
6
|
class ElementNode < Node
|
|
8
|
-
attr_reader :name, :namespace_uri, :prefix
|
|
9
|
-
:attribute_nodes
|
|
7
|
+
attr_reader :name, :namespace_uri, :prefix
|
|
10
8
|
|
|
11
9
|
def initialize(name:, namespace_uri: nil, prefix: nil)
|
|
12
10
|
super()
|
|
13
11
|
@name = name
|
|
14
12
|
@namespace_uri = namespace_uri
|
|
15
13
|
@prefix = prefix
|
|
16
|
-
@namespace_nodes =
|
|
17
|
-
@attribute_nodes =
|
|
14
|
+
@namespace_nodes = nil
|
|
15
|
+
@attribute_nodes = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Lazy: most elements carry no attributes, and inherited
|
|
19
|
+
# namespace nodes arrive as one shared frozen array (see
|
|
20
|
+
# TreeBuilder#attach_namespace_scope) — eager per-element arrays
|
|
21
|
+
# were the largest retained-allocation source in a built tree.
|
|
22
|
+
def namespace_nodes
|
|
23
|
+
@namespace_nodes ||= []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def namespace_nodes=(nodes)
|
|
27
|
+
@namespace_nodes = nodes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def attribute_nodes
|
|
31
|
+
@attribute_nodes ||= []
|
|
18
32
|
end
|
|
19
33
|
|
|
20
34
|
def node_type
|
|
@@ -27,24 +41,22 @@ module Canon
|
|
|
27
41
|
|
|
28
42
|
def add_namespace(namespace_node)
|
|
29
43
|
namespace_node.parent = self
|
|
30
|
-
|
|
44
|
+
namespace_nodes << namespace_node
|
|
31
45
|
end
|
|
32
46
|
|
|
33
47
|
def add_attribute(attribute_node)
|
|
34
48
|
attribute_node.parent = self
|
|
35
|
-
|
|
49
|
+
attribute_nodes << attribute_node
|
|
36
50
|
end
|
|
37
51
|
|
|
38
52
|
# Get namespace nodes in sorted order (lexicographically by local name)
|
|
39
53
|
def sorted_namespace_nodes
|
|
40
|
-
|
|
41
|
-
ns.local_name.to_s
|
|
42
|
-
end
|
|
54
|
+
namespace_nodes.sort_by(&:local_name)
|
|
43
55
|
end
|
|
44
56
|
|
|
45
57
|
# Get attribute nodes in sorted order (by namespace URI then local name)
|
|
46
58
|
def sorted_attribute_nodes
|
|
47
|
-
|
|
59
|
+
attribute_nodes.sort_by do |attr|
|
|
48
60
|
[attr.namespace_uri.to_s, attr.local_name]
|
|
49
61
|
end
|
|
50
62
|
end
|
|
@@ -129,8 +129,13 @@ strip_doctype: false)
|
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
# Push new namespace scope with declarations (own shadows
|
|
132
|
-
# inherited — the same merge the TreeBuilder scope kernel
|
|
133
|
-
|
|
132
|
+
# inherited — the same merge the TreeBuilder scope kernel
|
|
133
|
+
# applies). Elements that declare nothing push the inherited
|
|
134
|
+
# scope object itself, so scopes — and their cached
|
|
135
|
+
# namespace-node arrays in TreeBuilder — are shared down runs
|
|
136
|
+
# of undeclaring elements instead of re-merged per element.
|
|
137
|
+
inherited_scope = @namespace_stack.last
|
|
138
|
+
new_scope = ns_hash.empty? ? inherited_scope : inherited_scope.merge(ns_hash)
|
|
134
139
|
@namespace_stack.push(new_scope)
|
|
135
140
|
|
|
136
141
|
element = TreeBuilder::DEFAULT.element(
|
|
@@ -26,23 +26,41 @@ module Canon
|
|
|
26
26
|
# In-scope namespace bindings: the element's own declarations
|
|
27
27
|
# shadow inherited ones; xml is prebound at the base. Declaration
|
|
28
28
|
# pairs are [prefix-or-nil, uri]; nil and "" both mean the default
|
|
29
|
-
# namespace.
|
|
29
|
+
# namespace. Elements with no declarations return the inherited
|
|
30
|
+
# scope unchanged — most elements — so equal scopes share one
|
|
31
|
+
# object (and one namespace-node array, see attach). The xml
|
|
32
|
+
# binding is materialized at the root even for undeclaring
|
|
33
|
+
# elements: it is in scope on every element per the XPath data
|
|
34
|
+
# model (and the SAX feed's initial stack carries it too).
|
|
30
35
|
def merge_namespace_scope(inherited, declaration_pairs)
|
|
36
|
+
return inherited if declaration_pairs.empty? && inherited
|
|
37
|
+
|
|
31
38
|
scope = inherited ? inherited.dup : { XML_NAMESPACE_PREFIX => XML_NAMESPACE_URI }
|
|
32
39
|
declaration_pairs.each do |prefix, uri|
|
|
33
40
|
scope[prefix || ""] = uri
|
|
34
41
|
end
|
|
35
|
-
scope
|
|
42
|
+
scope.freeze
|
|
36
43
|
end
|
|
37
44
|
|
|
38
|
-
# Attach an in-scope scope to an element as namespace nodes.
|
|
45
|
+
# Attach an in-scope scope to an element as namespace nodes. The
|
|
46
|
+
# node array is cached per scope object: elements that introduced
|
|
47
|
+
# no declarations share their parent's array instead of
|
|
48
|
+
# re-materializing one NamespaceNode per prefix per element.
|
|
49
|
+
# NamespaceNode#parent is never read, so shared nodes carry the
|
|
50
|
+
# first creator as parent by convention. The cache is identity
|
|
51
|
+
# keyed and bounded — stale scopes of dead trees cost a few
|
|
52
|
+
# hundred bytes until the next clear.
|
|
39
53
|
def attach_namespace_scope(element, scope)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
cache = (@namespace_node_cache ||= {}.compare_by_identity)
|
|
55
|
+
cache.clear if cache.size >= 4096
|
|
56
|
+
nodes = cache[scope]
|
|
57
|
+
if nodes.nil?
|
|
58
|
+
nodes = scope.map do |prefix, uri|
|
|
59
|
+
Nodes::NamespaceNode.new(prefix: prefix, uri: uri)
|
|
60
|
+
end.freeze
|
|
61
|
+
cache[scope] = nodes
|
|
45
62
|
end
|
|
63
|
+
element.namespace_nodes = nodes
|
|
46
64
|
end
|
|
47
65
|
|
|
48
66
|
# Build an element. `attributes` are [name, value, namespace_uri,
|
|
@@ -58,21 +76,33 @@ module Canon
|
|
|
58
76
|
attach_namespace_scope(element, namespace_scope) if namespace_scope
|
|
59
77
|
|
|
60
78
|
# Duplicate (name, namespace) pairs are invalid XML — first
|
|
61
|
-
# occurrence wins.
|
|
62
|
-
# scan
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
# occurrence wins. Index arithmetic instead of subarray slices:
|
|
80
|
+
# the scan stays allocation-free on the hot paths.
|
|
81
|
+
index = 0
|
|
82
|
+
while index < attributes.size
|
|
83
|
+
pair = attributes[index]
|
|
84
|
+
attr_name = pair[0]
|
|
85
|
+
attr_namespace_uri = pair[2]
|
|
86
|
+
duplicate = false
|
|
87
|
+
prior = 0
|
|
88
|
+
while prior < index
|
|
89
|
+
candidate = attributes[prior]
|
|
90
|
+
if candidate[0] == attr_name && candidate[2] == attr_namespace_uri
|
|
91
|
+
duplicate = true
|
|
92
|
+
break
|
|
93
|
+
end
|
|
94
|
+
prior += 1
|
|
67
95
|
end
|
|
68
|
-
next if duplicate
|
|
69
96
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
unless duplicate
|
|
98
|
+
element.add_attribute(Nodes::AttributeNode.new(
|
|
99
|
+
name: attr_name,
|
|
100
|
+
value: pair[1],
|
|
101
|
+
namespace_uri: attr_namespace_uri,
|
|
102
|
+
prefix: pair[3],
|
|
103
|
+
))
|
|
104
|
+
end
|
|
105
|
+
index += 1
|
|
76
106
|
end
|
|
77
107
|
|
|
78
108
|
element
|
|
@@ -14,6 +14,13 @@ module Canon
|
|
|
14
14
|
# text; engines that report it (libxml2 does not, libleptris 1.9.38+
|
|
15
15
|
# does) stay byte-compatible through here.
|
|
16
16
|
module WhitespacePolicy
|
|
17
|
+
# Zero-allocation forms of the `content.strip.empty?` /
|
|
18
|
+
# `content.gsub(...).empty?` checks these policies used to run
|
|
19
|
+
# per text node. STRIP_ONLY is exactly String#strip's set
|
|
20
|
+
# (ASCII whitespace plus null); SAX drops space/tab/CR/LF runs.
|
|
21
|
+
STRIP_ONLY = /\A[\0\t\n\v\f\r ]*\z/
|
|
22
|
+
SAX_DROPPED = /\A[ \t\r\n]*\z/
|
|
23
|
+
|
|
17
24
|
module_function
|
|
18
25
|
|
|
19
26
|
# DOM conversion rule: whitespace-only text is dropped unless
|
|
@@ -27,11 +34,11 @@ element_parent: true)
|
|
|
27
34
|
# The XPath data model has no root text children — drop
|
|
28
35
|
# whitespace-only document-level text even when preserving
|
|
29
36
|
# (engines that report it stay byte-compatible with libxml2).
|
|
30
|
-
return false if !element_parent && content.
|
|
37
|
+
return false if !element_parent && content.match?(STRIP_ONLY)
|
|
31
38
|
|
|
32
39
|
return true if preserve_whitespace
|
|
33
40
|
|
|
34
|
-
!content.
|
|
41
|
+
!content.match?(STRIP_ONLY)
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
# SAX rule: same shape, plus CR-bearing content is always kept
|
|
@@ -39,12 +46,12 @@ element_parent: true)
|
|
|
39
46
|
# whitespace (space, tab, CR, LF) are dropped when not preserving.
|
|
40
47
|
def keep_sax_text?(content, preserve_whitespace:,
|
|
41
48
|
element_parent: true)
|
|
42
|
-
return false if !element_parent && content.
|
|
49
|
+
return false if !element_parent && content.match?(STRIP_ONLY)
|
|
43
50
|
|
|
44
51
|
return true if preserve_whitespace
|
|
45
52
|
return true if content.include?("\r")
|
|
46
53
|
|
|
47
|
-
!content.
|
|
54
|
+
!content.match?(SAX_DROPPED)
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
# HTML conversion rule: whitespace-only text is dropped except in
|
|
@@ -55,7 +62,7 @@ element_parent: true)
|
|
|
55
62
|
HTML_WHITESPACE_SENSITIVE_TAGS = %w[pre code textarea script style].freeze
|
|
56
63
|
|
|
57
64
|
def keep_html_text?(content, parent_name:, inline_significant: false)
|
|
58
|
-
return true unless content.
|
|
65
|
+
return true unless content.match?(STRIP_ONLY)
|
|
59
66
|
return true if content.include?(" ")
|
|
60
67
|
|
|
61
68
|
parent_name = parent_name.to_s.downcase
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: canon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|