canon 0.3.4 → 0.3.6
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/html/data_model.rb +48 -145
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +114 -282
- data/lib/canon/xml/sax_builder.rb +25 -61
- data/lib/canon/xml/tree_builder.rb +113 -0
- data/lib/canon/xml/whitespace_policy.rb +37 -5
- data/lib/canon/xml.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04012234db38cae77206ba33f856aca1262a0ad0111e7d233d5b814e90c22187
|
|
4
|
+
data.tar.gz: 3700758afcb5619f59ba7e2f896963bc63695d534f60af7d922dfce8c26076ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6452b462325e65c3f38f31438656b1cf60790646413314d8e835f6a27f78026bc10de2474065627b26a602962c424fd6858e1bc97ed3bca43e8a009b1779291
|
|
7
|
+
data.tar.gz: 749b78e282b4b2c7d440f9dff4a2361ee8d7742afcdab54b490bf836ae9417afc5f677e2039143020a6e50eceaf12d42950372c7a5f62dfcd335223243962fef
|
|
@@ -76,171 +76,74 @@ module Canon
|
|
|
76
76
|
Canon::Xml::DataModel.serialize(node)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
# Build XPath data model from Nokogiri document or
|
|
79
|
+
# Build XPath data model from a Nokogiri HTML document or
|
|
80
|
+
# fragment — a TreeBuilder walk like the XML extractors, with the
|
|
81
|
+
# HTML whitespace policy and xmlns-free attributes.
|
|
80
82
|
def self.build_from_nokogiri(nokogiri_doc)
|
|
83
|
+
builder = Canon::Xml::TreeBuilder::DEFAULT
|
|
81
84
|
root = Canon::Xml::Nodes::RootNode.new
|
|
85
|
+
skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : []
|
|
82
86
|
|
|
83
87
|
if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
nokogiri_doc.children.each do |child|
|
|
89
|
-
next if child == nokogiri_doc.root
|
|
90
|
-
next if child.is_a?(Nokogiri::XML::DTD)
|
|
91
|
-
|
|
92
|
-
node = build_node_from_nokogiri(child)
|
|
93
|
-
root.add_child(node) if node
|
|
88
|
+
root.add_child(walk(builder, nokogiri_doc.root))
|
|
89
|
+
builder.add_document_children(root, nokogiri_doc.children,
|
|
90
|
+
nokogiri_doc.root, skip_types) do |child|
|
|
91
|
+
walk(builder, child)
|
|
94
92
|
end
|
|
95
93
|
else
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
next if child.is_a?(Nokogiri::XML::DTD)
|
|
100
|
-
|
|
101
|
-
node = build_node_from_nokogiri(child)
|
|
102
|
-
root.add_child(node) if node
|
|
94
|
+
builder.add_document_children(root, nokogiri_doc.children,
|
|
95
|
+
nil, skip_types) do |child|
|
|
96
|
+
walk(builder, child)
|
|
103
97
|
end
|
|
104
98
|
end
|
|
105
99
|
|
|
106
100
|
root
|
|
107
101
|
end
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
case nokogiri_node
|
|
103
|
+
def self.walk(builder, node, inherited_namespaces: nil)
|
|
104
|
+
case node
|
|
112
105
|
when Nokogiri::XML::Element
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
when Nokogiri::XML::Comment
|
|
117
|
-
build_comment_node(nokogiri_node)
|
|
118
|
-
when Nokogiri::XML::ProcessingInstruction
|
|
119
|
-
build_pi_node(nokogiri_node)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Build element node from Nokogiri element
|
|
124
|
-
def self.build_element_node(nokogiri_element)
|
|
125
|
-
element = Canon::Xml::Nodes::ElementNode.new(
|
|
126
|
-
name: nokogiri_element.name,
|
|
127
|
-
namespace_uri: nokogiri_element.namespace&.href,
|
|
128
|
-
prefix: nokogiri_element.namespace&.prefix,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
# Build namespace nodes (includes inherited namespaces)
|
|
132
|
-
build_namespace_nodes(nokogiri_element, element)
|
|
133
|
-
|
|
134
|
-
# Build attribute nodes
|
|
135
|
-
build_attribute_nodes(nokogiri_element, element)
|
|
136
|
-
|
|
137
|
-
# Build child nodes
|
|
138
|
-
nokogiri_element.children.each do |child|
|
|
139
|
-
node = build_node_from_nokogiri(child)
|
|
140
|
-
element.add_child(node) if node
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
element
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Build namespace nodes for an element
|
|
147
|
-
def self.build_namespace_nodes(nokogiri_element, element)
|
|
148
|
-
# Collect all in-scope namespaces
|
|
149
|
-
namespaces = collect_in_scope_namespaces(nokogiri_element)
|
|
150
|
-
|
|
151
|
-
namespaces.each do |prefix, uri|
|
|
152
|
-
ns_node = Canon::Xml::Nodes::NamespaceNode.new(
|
|
153
|
-
prefix: prefix,
|
|
154
|
-
uri: uri,
|
|
106
|
+
scope = builder.merge_namespace_scope(
|
|
107
|
+
inherited_namespaces,
|
|
108
|
+
node.namespace_definitions.map { |ns| [ns.prefix, ns.href] },
|
|
155
109
|
)
|
|
156
|
-
element.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
current.namespace_definitions.each do |ns|
|
|
169
|
-
prefix = ns.prefix || ""
|
|
170
|
-
# Only add if not already defined (child overrides parent)
|
|
171
|
-
unless namespaces.key?(prefix)
|
|
172
|
-
namespaces[prefix] = ns.href
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
current = current.parent
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Always include xml namespace
|
|
180
|
-
namespaces["xml"] ||= "http://www.w3.org/XML/1998/namespace"
|
|
181
|
-
|
|
182
|
-
namespaces
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Build attribute nodes for an element
|
|
186
|
-
def self.build_attribute_nodes(nokogiri_element, element)
|
|
187
|
-
nokogiri_element.attributes.each do |name, attr|
|
|
188
|
-
next if name.start_with?("xmlns")
|
|
189
|
-
|
|
190
|
-
attr_node = Canon::Xml::Nodes::AttributeNode.new(
|
|
191
|
-
name: attr.name,
|
|
192
|
-
value: attr.value,
|
|
193
|
-
namespace_uri: attr.namespace&.href,
|
|
194
|
-
prefix: attr.namespace&.prefix,
|
|
110
|
+
element = builder.element(
|
|
111
|
+
name: node.name,
|
|
112
|
+
prefix: node.namespace&.prefix,
|
|
113
|
+
namespace_uri: node.namespace&.href,
|
|
114
|
+
# HTML attributes are namespace-free; xmlns declarations are
|
|
115
|
+
# not reported as attributes.
|
|
116
|
+
attributes: node.attribute_nodes.filter_map do |attr|
|
|
117
|
+
next if attr.name.start_with?("xmlns")
|
|
118
|
+
|
|
119
|
+
[attr.name, attr.value, nil, nil]
|
|
120
|
+
end,
|
|
121
|
+
namespace_scope: scope,
|
|
195
122
|
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
# Build text node from Nokogiri text node
|
|
201
|
-
# HTML-specific: handles whitespace-sensitive elements (pre, code, textarea, script, style)
|
|
202
|
-
# and preserves whitespace between inline element siblings.
|
|
203
|
-
def self.build_text_node(nokogiri_text)
|
|
204
|
-
# Skip text nodes that are only whitespace between elements
|
|
205
|
-
# EXCEPT in whitespace-sensitive elements (pre, code, textarea, script, style)
|
|
206
|
-
# and when whitespace is between inline element siblings (semantically significant)
|
|
207
|
-
content = nokogiri_text.content
|
|
208
|
-
|
|
209
|
-
# NBSP (U+00A0) is never insignificant whitespace
|
|
210
|
-
if content.strip.empty? && nokogiri_text.parent.is_a?(Nokogiri::XML::Element) && !content.include?("\u00A0")
|
|
211
|
-
# Check if parent is whitespace-sensitive
|
|
212
|
-
parent_name = nokogiri_text.parent.name.downcase
|
|
213
|
-
whitespace_sensitive_tags = %w[pre code textarea script style]
|
|
214
|
-
|
|
215
|
-
# Check if whitespace is between inline siblings
|
|
216
|
-
unless whitespace_sensitive_tags.include?(parent_name) ||
|
|
217
|
-
Canon::Comparison::WhitespaceSensitivity.inline_whitespace_significant?(nokogiri_text)
|
|
218
|
-
return nil
|
|
123
|
+
node.children.each do |child|
|
|
124
|
+
built = walk(builder, child, inherited_namespaces: scope)
|
|
125
|
+
element.add_child(built) if built
|
|
219
126
|
end
|
|
127
|
+
element
|
|
128
|
+
when Nokogiri::XML::Text
|
|
129
|
+
builder.text(
|
|
130
|
+
node.content,
|
|
131
|
+
keep: Canon::Xml::WhitespacePolicy.keep_html_text?(
|
|
132
|
+
node.content,
|
|
133
|
+
parent_name: node.parent.is_a?(Nokogiri::XML::Element) ? node.parent.name : nil,
|
|
134
|
+
inline_significant:
|
|
135
|
+
Canon::Comparison::WhitespaceSensitivity.inline_whitespace_significant?(node),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
when Nokogiri::XML::Comment
|
|
139
|
+
builder.comment(node.content)
|
|
140
|
+
when Nokogiri::XML::ProcessingInstruction
|
|
141
|
+
builder.processing_instruction(node.name, node.content)
|
|
220
142
|
end
|
|
221
|
-
|
|
222
|
-
# Nokogiri already handles CDATA conversion and entity resolution
|
|
223
|
-
Canon::Xml::Nodes::TextNode.new(value: content)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Build comment node from Nokogiri comment
|
|
227
|
-
def self.build_comment_node(nokogiri_comment)
|
|
228
|
-
Canon::Xml::Nodes::CommentNode.new(value: nokogiri_comment.content)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Build PI node from Nokogiri PI
|
|
232
|
-
def self.build_pi_node(nokogiri_pi)
|
|
233
|
-
Canon::Xml::Nodes::ProcessingInstructionNode.new(
|
|
234
|
-
target: nokogiri_pi.name,
|
|
235
|
-
data: nokogiri_pi.content,
|
|
236
|
-
)
|
|
237
143
|
end
|
|
238
144
|
|
|
239
145
|
class << self
|
|
240
|
-
private :build_from_nokogiri, :
|
|
241
|
-
:build_element_node, :build_namespace_nodes,
|
|
242
|
-
:collect_in_scope_namespaces, :build_attribute_nodes,
|
|
243
|
-
:build_text_node, :build_comment_node, :build_pi_node
|
|
146
|
+
private :build_from_nokogiri, :walk
|
|
244
147
|
end
|
|
245
148
|
end
|
|
246
149
|
end
|
data/lib/canon/version.rb
CHANGED
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -126,149 +126,83 @@ module Canon
|
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
# ---
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
skip_types, converter)
|
|
137
|
-
children.each do |child|
|
|
138
|
-
next if child.equal?(document_element)
|
|
139
|
-
next if skip_types.any? { |type| child.is_a?(type) }
|
|
140
|
-
|
|
141
|
-
node = converter.call(child)
|
|
142
|
-
root.add_child(node) if node
|
|
143
|
-
end
|
|
144
|
-
end
|
|
129
|
+
# --- Engine extractors ---
|
|
130
|
+
#
|
|
131
|
+
# Traversal and engine mapping only. Each walk maps its engine's
|
|
132
|
+
# shapes onto TreeBuilder's interface; all construction semantics
|
|
133
|
+
# (namespace scopes, attribute normalization, whitespace policy,
|
|
134
|
+
# node kinds, document ordering) live in TreeBuilder and
|
|
135
|
+
# WhitespacePolicy — one dialect, three feeds.
|
|
145
136
|
|
|
146
|
-
#
|
|
147
|
-
# shadow inherited ones; xml is prebound at the base. Declaration
|
|
148
|
-
# pairs are [prefix-or-nil, uri]; nil and "" both mean the default
|
|
149
|
-
# namespace.
|
|
150
|
-
def self.merge_namespace_scope(inherited, declaration_pairs)
|
|
151
|
-
scope = inherited ? inherited.dup : { "xml" => "http://www.w3.org/XML/1998/namespace" }
|
|
152
|
-
declaration_pairs.each do |prefix, uri|
|
|
153
|
-
scope[prefix || ""] = uri
|
|
154
|
-
end
|
|
155
|
-
scope
|
|
156
|
-
end
|
|
137
|
+
# -- Nokogiri walk: top-down, scope flows down the recursion --
|
|
157
138
|
|
|
158
139
|
def self.build_from_nokogiri(nokogiri_doc, preserve_whitespace: false)
|
|
159
140
|
root = Nodes::RootNode.new
|
|
160
|
-
|
|
161
|
-
converter = ->(child) { build_node_from_nokogiri(child, preserve_whitespace: preserve_whitespace) }
|
|
162
141
|
skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : []
|
|
163
142
|
|
|
164
143
|
if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root
|
|
165
|
-
root.add_child(
|
|
166
|
-
|
|
167
|
-
add_document_children(root, nokogiri_doc.children,
|
|
168
|
-
|
|
144
|
+
root.add_child(walk_nokogiri(nokogiri_doc.root,
|
|
145
|
+
preserve_whitespace: preserve_whitespace))
|
|
146
|
+
TreeBuilder::DEFAULT.add_document_children(root, nokogiri_doc.children,
|
|
147
|
+
nokogiri_doc.root, skip_types) do |child|
|
|
148
|
+
walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
|
|
149
|
+
end
|
|
169
150
|
else
|
|
170
|
-
add_document_children(root, nokogiri_doc.children,
|
|
171
|
-
|
|
151
|
+
TreeBuilder::DEFAULT.add_document_children(root, nokogiri_doc.children,
|
|
152
|
+
nil, skip_types) do |child|
|
|
153
|
+
walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
|
|
154
|
+
end
|
|
172
155
|
end
|
|
173
156
|
|
|
174
157
|
root
|
|
175
158
|
end
|
|
176
159
|
|
|
177
|
-
def self.
|
|
178
|
-
|
|
179
|
-
case
|
|
160
|
+
def self.walk_nokogiri(node, preserve_whitespace:,
|
|
161
|
+
inherited_namespaces: nil)
|
|
162
|
+
case node
|
|
180
163
|
when Nokogiri::XML::Element
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
164
|
+
scope = TreeBuilder::DEFAULT.merge_namespace_scope(
|
|
165
|
+
inherited_namespaces,
|
|
166
|
+
node.namespace_definitions.map { |ns| [ns.prefix, ns.href] },
|
|
167
|
+
)
|
|
168
|
+
element = TreeBuilder::DEFAULT.element(
|
|
169
|
+
name: node.name,
|
|
170
|
+
prefix: node.namespace&.prefix,
|
|
171
|
+
namespace_uri: node.namespace&.href,
|
|
172
|
+
attributes: node.attribute_nodes.map do |attr|
|
|
173
|
+
[attr.name, attr.value, attr.namespace&.href, attr.namespace&.prefix]
|
|
174
|
+
end,
|
|
175
|
+
namespace_scope: scope,
|
|
176
|
+
)
|
|
177
|
+
node.children.each do |child|
|
|
178
|
+
built = walk_nokogiri(child,
|
|
179
|
+
preserve_whitespace: preserve_whitespace,
|
|
180
|
+
inherited_namespaces: scope)
|
|
181
|
+
element.add_child(built) if built
|
|
182
|
+
end
|
|
183
|
+
element
|
|
184
184
|
when Nokogiri::XML::Text, Nokogiri::XML::CDATA
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
TreeBuilder::DEFAULT.text(node.content,
|
|
186
|
+
keep: WhitespacePolicy.keep_dom_text?(
|
|
187
|
+
node.content,
|
|
188
|
+
preserve_whitespace: preserve_whitespace,
|
|
189
|
+
element_parent: node.parent.is_a?(Nokogiri::XML::Element),
|
|
190
|
+
),
|
|
191
|
+
original: node.to_xml)
|
|
187
192
|
when Nokogiri::XML::Comment
|
|
188
|
-
|
|
193
|
+
TreeBuilder::DEFAULT.comment(node.content)
|
|
189
194
|
when Nokogiri::XML::ProcessingInstruction
|
|
190
|
-
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def self.build_element_node(nokogiri_element, preserve_whitespace: false,
|
|
195
|
-
inherited_namespaces: nil)
|
|
196
|
-
element = Nodes::ElementNode.new(
|
|
197
|
-
name: nokogiri_element.name,
|
|
198
|
-
namespace_uri: nokogiri_element.namespace&.href,
|
|
199
|
-
prefix: nokogiri_element.namespace&.prefix,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
scope = nokogiri_namespace_scope(nokogiri_element, inherited_namespaces)
|
|
203
|
-
scope.each do |prefix, uri|
|
|
204
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
205
|
-
prefix: prefix,
|
|
206
|
-
uri: uri,
|
|
207
|
-
))
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
build_attribute_nodes(nokogiri_element, element)
|
|
211
|
-
|
|
212
|
-
nokogiri_element.children.each do |child|
|
|
213
|
-
node = build_node_from_nokogiri(child,
|
|
214
|
-
preserve_whitespace: preserve_whitespace,
|
|
215
|
-
inherited_namespaces: scope)
|
|
216
|
-
element.add_child(node) if node
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
element
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# In-scope bindings via the shared kernel; the scope is passed down
|
|
223
|
-
# the recursion — one definition fetch per element instead of an
|
|
224
|
-
# ancestor walk per element (O(n) total, not O(n * depth)).
|
|
225
|
-
def self.nokogiri_namespace_scope(nokogiri_element, inherited)
|
|
226
|
-
merge_namespace_scope(inherited,
|
|
227
|
-
nokogiri_element.namespace_definitions.map do |ns|
|
|
228
|
-
[ns.prefix, ns.href]
|
|
229
|
-
end)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def self.build_attribute_nodes(nokogiri_element, element)
|
|
233
|
-
# attribute_nodes, not the attributes hash: the hash is keyed by
|
|
234
|
-
# local name and collapses e.g. attr / a:attr / b:attr into one
|
|
235
|
-
# entry, losing namespaced attributes (W3C C14N ex 3.3).
|
|
236
|
-
nokogiri_element.attribute_nodes.each do |attr|
|
|
237
|
-
attr_node = Nodes::AttributeNode.new(
|
|
238
|
-
name: attr.name,
|
|
239
|
-
value: attr.value,
|
|
240
|
-
namespace_uri: attr.namespace&.href,
|
|
241
|
-
prefix: attr.namespace&.prefix,
|
|
242
|
-
)
|
|
243
|
-
element.add_attribute(attr_node)
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def self.build_text_node(nokogiri_text, preserve_whitespace: false)
|
|
248
|
-
content = nokogiri_text.content
|
|
249
|
-
|
|
250
|
-
unless WhitespacePolicy.keep_dom_text?(content,
|
|
251
|
-
preserve_whitespace: preserve_whitespace,
|
|
252
|
-
element_parent: nokogiri_text.parent.is_a?(Nokogiri::XML::Element))
|
|
253
|
-
return nil
|
|
195
|
+
TreeBuilder::DEFAULT.processing_instruction(node.name, node.content)
|
|
254
196
|
end
|
|
255
|
-
|
|
256
|
-
original = nokogiri_text.to_xml
|
|
257
|
-
Nodes::TextNode.new(value: content, original: original)
|
|
258
197
|
end
|
|
259
198
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
data: nokogiri_pi.content,
|
|
268
|
-
)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
# --- Moxml path ---
|
|
199
|
+
# -- moxml record stream: post-order frames adopt children --
|
|
200
|
+
# One flattened record stream (moxml#132/#138) instead of a
|
|
201
|
+
# wrapper walk, so document conversion costs no Moxml::Node
|
|
202
|
+
# allocation per node. Own namespace declarations ride an
|
|
203
|
+
# identity-keyed stash; assign_moxml_namespace_scopes expands
|
|
204
|
+
# them to in-scope scopes afterwards (post-order arrival means
|
|
205
|
+
# scopes cannot flow down during the stream).
|
|
272
206
|
|
|
273
207
|
def self.from_moxml_xml(xml_string, preserve_whitespace:)
|
|
274
208
|
doc = Canon::XmlParsing.moxml_context.parse(xml_string, readonly: true)
|
|
@@ -301,35 +235,26 @@ inherited_namespaces: nil)
|
|
|
301
235
|
|
|
302
236
|
def self.build_from_moxml(moxml_doc, preserve_whitespace: false)
|
|
303
237
|
root = Nodes::RootNode.new
|
|
304
|
-
|
|
305
|
-
converter = ->(child) { build_moxml_node(child, preserve_whitespace: preserve_whitespace) }
|
|
306
238
|
skip_types = [Moxml::Doctype]
|
|
307
239
|
|
|
308
240
|
if moxml_doc.is_a?(Moxml::Document) && moxml_doc.root
|
|
309
241
|
element = build_moxml_subtree_from_records(moxml_doc.root,
|
|
310
242
|
preserve_whitespace: preserve_whitespace)
|
|
311
243
|
root.add_child(element) if element
|
|
312
|
-
add_document_children(root, moxml_doc.children,
|
|
313
|
-
|
|
244
|
+
TreeBuilder::DEFAULT.add_document_children(root, moxml_doc.children,
|
|
245
|
+
moxml_doc.root, skip_types) do |child|
|
|
246
|
+
walk_moxml(child, preserve_whitespace: preserve_whitespace)
|
|
247
|
+
end
|
|
314
248
|
else
|
|
315
|
-
add_document_children(root, moxml_doc.children,
|
|
316
|
-
|
|
249
|
+
TreeBuilder::DEFAULT.add_document_children(root, moxml_doc.children,
|
|
250
|
+
nil, skip_types) do |child|
|
|
251
|
+
walk_moxml(child, preserve_whitespace: preserve_whitespace)
|
|
252
|
+
end
|
|
317
253
|
end
|
|
318
254
|
|
|
319
255
|
root
|
|
320
256
|
end
|
|
321
257
|
|
|
322
|
-
# Document path: build the root subtree from materialize records —
|
|
323
|
-
# one flattened record stream (moxml#132/#138) instead of a wrapper
|
|
324
|
-
# walk, so conversion costs no Moxml::Node allocation per node.
|
|
325
|
-
#
|
|
326
|
-
# Records arrive post-order (children before parents): completed
|
|
327
|
-
# nodes stack up with their depth, and an element record adopts
|
|
328
|
-
# every completed node deeper than itself. Each element's OWN
|
|
329
|
-
# namespace declarations ride an identity-keyed stash; afterwards
|
|
330
|
-
# assign_moxml_namespace_scopes expands them to in-scope scopes
|
|
331
|
-
# with the same first-wins/xml-prebound semantics as the Nokogiri
|
|
332
|
-
# collector.
|
|
333
258
|
def self.build_moxml_subtree_from_records(moxml_element,
|
|
334
259
|
preserve_whitespace: false)
|
|
335
260
|
frames = []
|
|
@@ -339,8 +264,8 @@ preserve_whitespace: false)
|
|
|
339
264
|
# The record contract is root-subtree-only since moxml 0.5.11
|
|
340
265
|
# (moxml#140). Older 0.5.x releases — still allowed by canon's
|
|
341
266
|
# gemspec floor — leaked epilog document-level records at depth
|
|
342
|
-
# 0, which the document-children enumeration
|
|
343
|
-
#
|
|
267
|
+
# 0, which the document-children enumeration also covers; one
|
|
268
|
+
# integer compare per record keeps those working.
|
|
344
269
|
next if record[:depth].zero? && record[:kind] != :element
|
|
345
270
|
|
|
346
271
|
node = case record[:kind]
|
|
@@ -348,14 +273,16 @@ preserve_whitespace: false)
|
|
|
348
273
|
build_moxml_element_from_record(record, frames,
|
|
349
274
|
own_namespaces)
|
|
350
275
|
when :text, :cdata
|
|
351
|
-
|
|
276
|
+
content = record[:text].to_s
|
|
277
|
+
TreeBuilder::DEFAULT.text(content,
|
|
278
|
+
keep: WhitespacePolicy.keep_dom_text?(
|
|
279
|
+
content, preserve_whitespace: preserve_whitespace
|
|
280
|
+
))
|
|
352
281
|
when :comment
|
|
353
|
-
|
|
282
|
+
TreeBuilder::DEFAULT.comment(record[:text])
|
|
354
283
|
when :processing_instruction
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
data: record[:text] || "",
|
|
358
|
-
)
|
|
284
|
+
TreeBuilder::DEFAULT.processing_instruction(record[:qname],
|
|
285
|
+
record[:text] || "")
|
|
359
286
|
end
|
|
360
287
|
|
|
361
288
|
frames.push([record[:depth], node]) if node
|
|
@@ -364,34 +291,18 @@ preserve_whitespace: false)
|
|
|
364
291
|
top = frames.last
|
|
365
292
|
return nil unless top
|
|
366
293
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
{ "xml" => "http://www.w3.org/XML/1998/namespace" },
|
|
370
|
-
own_namespaces)
|
|
371
|
-
element
|
|
294
|
+
assign_moxml_namespace_scopes(top[1], nil, own_namespaces)
|
|
295
|
+
top[1]
|
|
372
296
|
end
|
|
373
297
|
|
|
374
298
|
def self.build_moxml_element_from_record(record, frames, own_namespaces)
|
|
375
|
-
element =
|
|
299
|
+
element = TreeBuilder::DEFAULT.element(
|
|
376
300
|
name: record[:qname],
|
|
377
|
-
namespace_uri: record[:namespace_uri],
|
|
378
301
|
prefix: record[:prefix],
|
|
302
|
+
namespace_uri: record[:namespace_uri],
|
|
303
|
+
attributes: record[:attributes],
|
|
379
304
|
)
|
|
380
305
|
|
|
381
|
-
seen = {}
|
|
382
|
-
record[:attributes].each do |name, value, namespace_uri, prefix|
|
|
383
|
-
key = [name, namespace_uri]
|
|
384
|
-
next if seen.key?(key)
|
|
385
|
-
|
|
386
|
-
seen[key] = true
|
|
387
|
-
element.add_attribute(Nodes::AttributeNode.new(
|
|
388
|
-
name: name,
|
|
389
|
-
value: value,
|
|
390
|
-
namespace_uri: namespace_uri,
|
|
391
|
-
prefix: prefix,
|
|
392
|
-
))
|
|
393
|
-
end
|
|
394
|
-
|
|
395
306
|
# Adopt completed children: they pop in reverse order; reversing
|
|
396
307
|
# once is O(n) (unshift per child would be O(n^2) on wide trees).
|
|
397
308
|
children = []
|
|
@@ -402,133 +313,54 @@ preserve_whitespace: false)
|
|
|
402
313
|
element
|
|
403
314
|
end
|
|
404
315
|
|
|
405
|
-
# Text records inside a document subtree always have an element
|
|
406
|
-
# parent (only the document element sits at depth 0), so the
|
|
407
|
-
# whitespace-only skip rule matches the wrapper path exactly.
|
|
408
|
-
def self.build_moxml_text_from_record(record, preserve_whitespace)
|
|
409
|
-
content = record[:text].to_s
|
|
410
|
-
return nil unless WhitespacePolicy.keep_dom_text?(content, preserve_whitespace: preserve_whitespace)
|
|
411
|
-
|
|
412
|
-
Nodes::TextNode.new(value: content, original: content)
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
# Expand each element's own declarations to in-scope bindings via
|
|
416
|
-
# the shared kernel. Scope flows down the canon tree — no engine
|
|
417
|
-
# calls.
|
|
418
316
|
def self.assign_moxml_namespace_scopes(element, inherited, own_namespaces)
|
|
419
|
-
scope = merge_namespace_scope(inherited,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
423
|
-
prefix: prefix,
|
|
424
|
-
uri: uri,
|
|
425
|
-
))
|
|
426
|
-
end
|
|
317
|
+
scope = TreeBuilder::DEFAULT.merge_namespace_scope(inherited,
|
|
318
|
+
own_namespaces[element].to_a)
|
|
319
|
+
TreeBuilder::DEFAULT.attach_namespace_scope(element, scope)
|
|
427
320
|
|
|
428
321
|
element.children.each do |child|
|
|
429
322
|
assign_moxml_namespace_scopes(child, scope, own_namespaces) if child.is_a?(Nodes::ElementNode)
|
|
430
323
|
end
|
|
431
324
|
end
|
|
432
325
|
|
|
433
|
-
|
|
326
|
+
# -- moxml wrapper walk: fragments and user-supplied nodes --
|
|
327
|
+
|
|
328
|
+
def self.walk_moxml(node, preserve_whitespace: false,
|
|
434
329
|
inherited_namespaces: nil)
|
|
435
330
|
case node
|
|
436
331
|
when Moxml::Element
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
332
|
+
scope = TreeBuilder::DEFAULT.merge_namespace_scope(
|
|
333
|
+
inherited_namespaces,
|
|
334
|
+
node.namespace_definitions.map { |ns| [ns.prefix, ns.uri] },
|
|
335
|
+
)
|
|
336
|
+
element = TreeBuilder::DEFAULT.element(
|
|
337
|
+
name: node.name,
|
|
338
|
+
prefix: node.namespace&.prefix,
|
|
339
|
+
namespace_uri: node.namespace&.uri,
|
|
340
|
+
attributes: node.attributes.map do |attr|
|
|
341
|
+
[attr.name, attr.value, attr.namespace&.uri, attr.namespace&.prefix]
|
|
342
|
+
end,
|
|
343
|
+
namespace_scope: scope,
|
|
344
|
+
)
|
|
345
|
+
node.children.each do |child|
|
|
346
|
+
built = walk_moxml(child,
|
|
347
|
+
preserve_whitespace: preserve_whitespace,
|
|
348
|
+
inherited_namespaces: scope)
|
|
349
|
+
element.add_child(built) if built
|
|
350
|
+
end
|
|
351
|
+
element
|
|
440
352
|
when Moxml::Text, Moxml::Cdata
|
|
441
|
-
|
|
353
|
+
TreeBuilder::DEFAULT.text(node.content,
|
|
354
|
+
keep: WhitespacePolicy.keep_dom_text?(
|
|
355
|
+
node.content,
|
|
356
|
+
preserve_whitespace: preserve_whitespace,
|
|
357
|
+
element_parent: node.parent.is_a?(Moxml::Element),
|
|
358
|
+
))
|
|
442
359
|
when Moxml::Comment
|
|
443
|
-
|
|
360
|
+
TreeBuilder::DEFAULT.comment(node.content)
|
|
444
361
|
when Moxml::ProcessingInstruction
|
|
445
|
-
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
def self.build_moxml_element_node(moxml_element,
|
|
450
|
-
preserve_whitespace: false,
|
|
451
|
-
inherited_namespaces: nil)
|
|
452
|
-
ns = moxml_element.namespace
|
|
453
|
-
element = Nodes::ElementNode.new(
|
|
454
|
-
name: moxml_element.name,
|
|
455
|
-
namespace_uri: ns&.uri,
|
|
456
|
-
prefix: ns&.prefix,
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
scope = moxml_namespace_scope(moxml_element, inherited_namespaces)
|
|
460
|
-
scope.each do |prefix, uri|
|
|
461
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
462
|
-
prefix: prefix,
|
|
463
|
-
uri: uri,
|
|
464
|
-
))
|
|
362
|
+
TreeBuilder::DEFAULT.processing_instruction(node.target, node.content)
|
|
465
363
|
end
|
|
466
|
-
|
|
467
|
-
build_moxml_attribute_nodes(moxml_element, element)
|
|
468
|
-
|
|
469
|
-
moxml_element.children.each do |child|
|
|
470
|
-
node = build_moxml_node(child,
|
|
471
|
-
preserve_whitespace: preserve_whitespace,
|
|
472
|
-
inherited_namespaces: scope)
|
|
473
|
-
element.add_child(node) if node
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
element
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
# In-scope namespace bindings: the element's own declarations
|
|
480
|
-
# shadow inherited ones, xml is prebound. The scope is passed down
|
|
481
|
-
# the recursion so each element pays one declaration fetch instead
|
|
482
|
-
# of an ancestor walk through the adapter layer (the Nokogiri
|
|
483
|
-
# collector walks ancestors because those calls are cheap there).
|
|
484
|
-
def self.moxml_namespace_scope(moxml_element, inherited)
|
|
485
|
-
merge_namespace_scope(inherited,
|
|
486
|
-
moxml_element.namespace_definitions.map do |decl|
|
|
487
|
-
[decl.prefix, decl.uri]
|
|
488
|
-
end)
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def self.build_moxml_attribute_nodes(moxml_element, element)
|
|
492
|
-
seen = {}
|
|
493
|
-
moxml_element.attributes.each do |attr|
|
|
494
|
-
# Duplicate attribute names are invalid XML; engines expose
|
|
495
|
-
# them differently (libxml2 lists repeats, libleptris dedups
|
|
496
|
-
# last-wins). Canon follows the Nokogiri presentation contract:
|
|
497
|
-
# first occurrence wins, keyed by name + namespace.
|
|
498
|
-
key = [attr.name, attr.namespace&.uri]
|
|
499
|
-
next if seen.key?(key)
|
|
500
|
-
|
|
501
|
-
seen[key] = true
|
|
502
|
-
element.add_attribute(Nodes::AttributeNode.new(
|
|
503
|
-
name: attr.name,
|
|
504
|
-
value: attr.value,
|
|
505
|
-
namespace_uri: attr.namespace&.uri,
|
|
506
|
-
prefix: attr.namespace&.prefix,
|
|
507
|
-
))
|
|
508
|
-
end
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
def self.build_moxml_text_node(moxml_text, preserve_whitespace: false)
|
|
512
|
-
content = moxml_text.content
|
|
513
|
-
|
|
514
|
-
unless WhitespacePolicy.keep_dom_text?(content,
|
|
515
|
-
preserve_whitespace: preserve_whitespace,
|
|
516
|
-
element_parent: moxml_text.parent.is_a?(Moxml::Element))
|
|
517
|
-
return nil
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
Nodes::TextNode.new(value: content, original: content)
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
def self.build_moxml_comment_node(moxml_comment)
|
|
524
|
-
Nodes::CommentNode.new(value: moxml_comment.content)
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
def self.build_moxml_pi_node(moxml_pi)
|
|
528
|
-
Nodes::ProcessingInstructionNode.new(
|
|
529
|
-
target: moxml_pi.target,
|
|
530
|
-
data: moxml_pi.content,
|
|
531
|
-
)
|
|
532
364
|
end
|
|
533
365
|
end
|
|
534
366
|
end
|
|
@@ -9,6 +9,11 @@ module Canon
|
|
|
9
9
|
# selects the driver. Much faster than DOM parsing + conversion —
|
|
10
10
|
# no intermediate engine DOM tree, no traversal conversion pass.
|
|
11
11
|
#
|
|
12
|
+
# Construction goes through TreeBuilder like every other feed:
|
|
13
|
+
# this class owns only what is SAX-specific — qname parsing, xmlns
|
|
14
|
+
# separation, character-reference decoding, adjacency combining,
|
|
15
|
+
# the namespace stack, and document-level reordering.
|
|
16
|
+
#
|
|
12
17
|
# Usage:
|
|
13
18
|
# root = SaxBuilder.parse(xml_string, preserve_whitespace: false)
|
|
14
19
|
# # root is a Canon::Xml::Nodes::RootNode
|
|
@@ -123,26 +128,23 @@ strip_doctype: false)
|
|
|
123
128
|
end
|
|
124
129
|
end
|
|
125
130
|
|
|
126
|
-
# Push new namespace scope with declarations
|
|
131
|
+
# Push new namespace scope with declarations (own shadows
|
|
132
|
+
# inherited — the same merge the TreeBuilder scope kernel applies)
|
|
127
133
|
new_scope = @namespace_stack.last.merge(ns_hash)
|
|
128
134
|
@namespace_stack.push(new_scope)
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
ns_uri = new_scope[prefix.to_s]
|
|
132
|
-
|
|
133
|
-
# Create element node
|
|
134
|
-
element = Nodes::ElementNode.new(
|
|
136
|
+
element = TreeBuilder::DEFAULT.element(
|
|
135
137
|
name: local_name,
|
|
136
|
-
namespace_uri: ns_uri,
|
|
137
138
|
prefix: prefix,
|
|
139
|
+
namespace_uri: new_scope[prefix.to_s],
|
|
140
|
+
namespace_scope: new_scope,
|
|
141
|
+
attributes: regular_attrs.map do |attr_name, attr_value|
|
|
142
|
+
attr_prefix, attr_local = parse_qname(attr_name)
|
|
143
|
+
attr_ns_uri = attr_prefix ? new_scope[attr_prefix] : nil
|
|
144
|
+
[attr_local, decode_character_references(attr_value || ""), attr_ns_uri, attr_prefix]
|
|
145
|
+
end,
|
|
138
146
|
)
|
|
139
147
|
|
|
140
|
-
# Add namespace nodes from current scope
|
|
141
|
-
add_namespace_nodes(element, new_scope)
|
|
142
|
-
|
|
143
|
-
# Build and add attribute nodes (excluding xmlns declarations)
|
|
144
|
-
add_attribute_nodes(element, regular_attrs)
|
|
145
|
-
|
|
146
148
|
parent.add_child(element)
|
|
147
149
|
@stack.push(element)
|
|
148
150
|
end
|
|
@@ -200,13 +202,15 @@ strip_doctype: false)
|
|
|
200
202
|
|
|
201
203
|
# Skip whitespace-only text nodes unless preserving, per the SAX
|
|
202
204
|
# policy (CR-bearing content always survives for C14N;
|
|
203
|
-
# non-ASCII whitespace is meaningful content
|
|
205
|
+
# non-ASCII whitespace is meaningful content; document-level
|
|
206
|
+
# whitespace drops with every policy).
|
|
204
207
|
return unless WhitespacePolicy.keep_sax_text?(decoded_string,
|
|
205
208
|
preserve_whitespace: @preserve_whitespace,
|
|
206
209
|
element_parent: parent.node_type == :element)
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
parent.add_child(
|
|
212
|
+
TreeBuilder::DEFAULT.text(decoded_string, keep: true, original: raw_string),
|
|
213
|
+
)
|
|
210
214
|
end
|
|
211
215
|
private :append_text
|
|
212
216
|
|
|
@@ -214,9 +218,7 @@ strip_doctype: false)
|
|
|
214
218
|
#
|
|
215
219
|
# @param string [String] Comment content
|
|
216
220
|
def comment(string)
|
|
217
|
-
|
|
218
|
-
comment_node = Nodes::CommentNode.new(value: string)
|
|
219
|
-
parent.add_child(comment_node)
|
|
221
|
+
@stack.last.add_child(TreeBuilder::DEFAULT.comment(string))
|
|
220
222
|
end
|
|
221
223
|
|
|
222
224
|
# Called for processing instructions
|
|
@@ -224,10 +226,9 @@ strip_doctype: false)
|
|
|
224
226
|
# @param name [String] PI target
|
|
225
227
|
# @param content [String] PI content
|
|
226
228
|
def processing_instruction(name, content)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
parent.add_child(pi)
|
|
229
|
+
@stack.last.add_child(
|
|
230
|
+
TreeBuilder::DEFAULT.processing_instruction(name, content || ""),
|
|
231
|
+
)
|
|
231
232
|
end
|
|
232
233
|
|
|
233
234
|
# Return the built tree
|
|
@@ -313,44 +314,7 @@ strip_doctype: false)
|
|
|
313
314
|
[ns_decls, regular_attrs]
|
|
314
315
|
end
|
|
315
316
|
|
|
316
|
-
#
|
|
317
|
-
#
|
|
318
|
-
# @param element [Nodes::ElementNode] Element to add namespaces to
|
|
319
|
-
# @param scope [Hash] Current namespace scope
|
|
320
|
-
def add_namespace_nodes(element, scope)
|
|
321
|
-
scope.each do |prefix, uri|
|
|
322
|
-
ns_node = Nodes::NamespaceNode.new(prefix: prefix, uri: uri)
|
|
323
|
-
element.add_namespace(ns_node)
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
# Add attribute nodes to element
|
|
328
|
-
#
|
|
329
|
-
# @param element [Nodes::ElementNode] Element to add attributes to
|
|
330
|
-
# @param attrs [Array] Array of [name, value] pairs
|
|
331
|
-
def add_attribute_nodes(element, attrs)
|
|
332
|
-
attrs.each do |attr_name, attr_value|
|
|
333
|
-
attr_prefix, attr_local = parse_qname(attr_name)
|
|
334
|
-
|
|
335
|
-
# Find namespace for attribute (if prefixed)
|
|
336
|
-
attr_ns_uri = attr_prefix ? @namespace_stack.last[attr_prefix] : nil
|
|
337
|
-
|
|
338
|
-
# Decode numeric character references (e.g., & → &)
|
|
339
|
-
# Nokogiri SAX leaves these as-is, but we need them decoded for C14N
|
|
340
|
-
decoded_value = decode_character_references(attr_value || "")
|
|
341
|
-
|
|
342
|
-
attr_node = Nodes::AttributeNode.new(
|
|
343
|
-
name: attr_local,
|
|
344
|
-
value: decoded_value,
|
|
345
|
-
namespace_uri: attr_ns_uri,
|
|
346
|
-
prefix: attr_prefix,
|
|
347
|
-
)
|
|
348
|
-
element.add_attribute(attr_node)
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
# Decode numeric character references in a string
|
|
353
|
-
# Handles both &#decimal; and &#xhex; forms
|
|
317
|
+
# Decode numeric character references (e.g., & → &)
|
|
354
318
|
#
|
|
355
319
|
# @param value [String] String potentially containing character references
|
|
356
320
|
# @return [String] String with character references decoded
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Xml
|
|
5
|
+
# The one place canon tree nodes are constructed from parsed XML.
|
|
6
|
+
#
|
|
7
|
+
# Owns construction semantics: namespace scopes, attribute
|
|
8
|
+
# normalization, node kinds, and document-level ordering. Keep/strip
|
|
9
|
+
# decisions come from WhitespacePolicy (one home for all three
|
|
10
|
+
# policies); engine walks — the Nokogiri and moxml extractors in
|
|
11
|
+
# Xml::DataModel, the HTML walk in Html::DataModel — only map their
|
|
12
|
+
# engine's shapes onto this interface.
|
|
13
|
+
#
|
|
14
|
+
# Attribute normalization: duplicate (name, namespace) pairs are
|
|
15
|
+
# invalid XML; engines expose them differently (libxml2 lists
|
|
16
|
+
# repeats, libleptris deduplicates), so the builder resolves them
|
|
17
|
+
# once — first occurrence wins.
|
|
18
|
+
class TreeBuilder
|
|
19
|
+
# Stateless module: one shared instance serves every feed.
|
|
20
|
+
DEFAULT = new
|
|
21
|
+
|
|
22
|
+
NO_ATTRIBUTES = [].freeze
|
|
23
|
+
XML_NAMESPACE_PREFIX = "xml"
|
|
24
|
+
XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"
|
|
25
|
+
|
|
26
|
+
# In-scope namespace bindings: the element's own declarations
|
|
27
|
+
# shadow inherited ones; xml is prebound at the base. Declaration
|
|
28
|
+
# pairs are [prefix-or-nil, uri]; nil and "" both mean the default
|
|
29
|
+
# namespace.
|
|
30
|
+
def merge_namespace_scope(inherited, declaration_pairs)
|
|
31
|
+
scope = inherited ? inherited.dup : { XML_NAMESPACE_PREFIX => XML_NAMESPACE_URI }
|
|
32
|
+
declaration_pairs.each do |prefix, uri|
|
|
33
|
+
scope[prefix || ""] = uri
|
|
34
|
+
end
|
|
35
|
+
scope
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Attach an in-scope scope to an element as namespace nodes.
|
|
39
|
+
def attach_namespace_scope(element, scope)
|
|
40
|
+
scope.each do |prefix, uri|
|
|
41
|
+
element.add_namespace(Nodes::NamespaceNode.new(
|
|
42
|
+
prefix: prefix,
|
|
43
|
+
uri: uri,
|
|
44
|
+
))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Build an element. `attributes` are [name, value, namespace_uri,
|
|
49
|
+
# prefix] pairs; `namespace_scope` is a merged scope (or nil for
|
|
50
|
+
# no namespace nodes).
|
|
51
|
+
def element(name:, prefix: nil, namespace_uri: nil,
|
|
52
|
+
attributes: NO_ATTRIBUTES, namespace_scope: nil)
|
|
53
|
+
element = Nodes::ElementNode.new(
|
|
54
|
+
name: name,
|
|
55
|
+
namespace_uri: namespace_uri,
|
|
56
|
+
prefix: prefix,
|
|
57
|
+
)
|
|
58
|
+
attach_namespace_scope(element, namespace_scope) if namespace_scope
|
|
59
|
+
|
|
60
|
+
# Duplicate (name, namespace) pairs are invalid XML — first
|
|
61
|
+
# occurrence wins. Attributes per element are few, so a pairwise
|
|
62
|
+
# scan beats a per-element seen-hash on allocation (this is the
|
|
63
|
+
# hot SAX path).
|
|
64
|
+
attributes.each_with_index do |(attr_name, value, attr_namespace_uri, attr_prefix), index|
|
|
65
|
+
duplicate = attributes[0...index].any? do |prior_name, _v, prior_ns, _p|
|
|
66
|
+
prior_name == attr_name && prior_ns == attr_namespace_uri
|
|
67
|
+
end
|
|
68
|
+
next if duplicate
|
|
69
|
+
|
|
70
|
+
element.add_attribute(Nodes::AttributeNode.new(
|
|
71
|
+
name: attr_name,
|
|
72
|
+
value: value,
|
|
73
|
+
namespace_uri: attr_namespace_uri,
|
|
74
|
+
prefix: attr_prefix,
|
|
75
|
+
))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
element
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Build a text node; `keep` comes from WhitespacePolicy (the
|
|
82
|
+
# caller knows the policy and parent context).
|
|
83
|
+
def text(content, keep:, original: content)
|
|
84
|
+
return nil unless keep
|
|
85
|
+
|
|
86
|
+
Nodes::TextNode.new(value: content, original: original)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def comment(content)
|
|
90
|
+
Nodes::CommentNode.new(value: content)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def processing_instruction(target, data)
|
|
94
|
+
Nodes::ProcessingInstructionNode.new(target: target, data: data)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Attach document-level children (prolog/epilog PIs, comments,
|
|
98
|
+
# document-level text) to the canon root, in document order,
|
|
99
|
+
# skipping the document element and the given types. Yields each
|
|
100
|
+
# child to the caller's converter; nil results are dropped.
|
|
101
|
+
def add_document_children(root, children, document_element,
|
|
102
|
+
skip_types = [])
|
|
103
|
+
children.each do |child|
|
|
104
|
+
next if child.equal?(document_element)
|
|
105
|
+
next if skip_types.any? { |type| child.is_a?(type) }
|
|
106
|
+
|
|
107
|
+
node = yield child
|
|
108
|
+
root.add_child(node) if node
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -4,8 +4,15 @@ module Canon
|
|
|
4
4
|
module Xml
|
|
5
5
|
# Parse-time whitespace policy: whether a character-data node
|
|
6
6
|
# survives conversion. One home for the keep/strip rules so the
|
|
7
|
-
# DOM/SAX differences are visible here instead of implied by
|
|
7
|
+
# DOM/SAX/HTML differences are visible here instead of implied by
|
|
8
8
|
# copy-paste across conversion sites.
|
|
9
|
+
#
|
|
10
|
+
# Document-level character data (outside the root element) can only
|
|
11
|
+
# be whitespace per the XML grammar, and the XPath data model —
|
|
12
|
+
# which canon's tree and C14N follow — has no root text children at
|
|
13
|
+
# all. Every policy therefore drops whitespace-only document-level
|
|
14
|
+
# text; engines that report it (libxml2 does not, libleptris 1.9.38+
|
|
15
|
+
# does) stay byte-compatible through here.
|
|
9
16
|
module WhitespacePolicy
|
|
10
17
|
module_function
|
|
11
18
|
|
|
@@ -15,9 +22,14 @@ module Canon
|
|
|
15
22
|
#
|
|
16
23
|
# NOTE: CR-only nodes are dropped on this path; the SAX rule keeps
|
|
17
24
|
# them (character references must survive for C14N).
|
|
18
|
-
def keep_dom_text?(content, preserve_whitespace:,
|
|
25
|
+
def keep_dom_text?(content, preserve_whitespace:,
|
|
26
|
+
element_parent: true)
|
|
27
|
+
# The XPath data model has no root text children — drop
|
|
28
|
+
# whitespace-only document-level text even when preserving
|
|
29
|
+
# (engines that report it stay byte-compatible with libxml2).
|
|
30
|
+
return false if !element_parent && content.strip.empty?
|
|
31
|
+
|
|
19
32
|
return true if preserve_whitespace
|
|
20
|
-
return true unless element_parent
|
|
21
33
|
|
|
22
34
|
!content.strip.empty?
|
|
23
35
|
end
|
|
@@ -25,13 +37,33 @@ module Canon
|
|
|
25
37
|
# SAX rule: same shape, plus CR-bearing content is always kept
|
|
26
38
|
# (
 must survive parsing for C14N) — only runs of pure ASCII
|
|
27
39
|
# whitespace (space, tab, CR, LF) are dropped when not preserving.
|
|
28
|
-
def keep_sax_text?(content, preserve_whitespace:,
|
|
40
|
+
def keep_sax_text?(content, preserve_whitespace:,
|
|
41
|
+
element_parent: true)
|
|
42
|
+
return false if !element_parent && content.strip.empty?
|
|
43
|
+
|
|
29
44
|
return true if preserve_whitespace
|
|
30
|
-
return true unless element_parent
|
|
31
45
|
return true if content.include?("\r")
|
|
32
46
|
|
|
33
47
|
!content.gsub(/[ \t\r\n]/, "").empty?
|
|
34
48
|
end
|
|
49
|
+
|
|
50
|
+
# HTML conversion rule: whitespace-only text is dropped except in
|
|
51
|
+
# whitespace-sensitive elements (pre/code/textarea/script/style),
|
|
52
|
+
# between inline siblings (semantically significant), and when it
|
|
53
|
+
# carries NBSP (U+00A0 — never insignificant; strip is ASCII-only
|
|
54
|
+
# so it is checked explicitly).
|
|
55
|
+
HTML_WHITESPACE_SENSITIVE_TAGS = %w[pre code textarea script style].freeze
|
|
56
|
+
|
|
57
|
+
def keep_html_text?(content, parent_name:, inline_significant: false)
|
|
58
|
+
return true unless content.strip.empty?
|
|
59
|
+
return true if content.include?(" ")
|
|
60
|
+
|
|
61
|
+
parent_name = parent_name.to_s.downcase
|
|
62
|
+
return true if HTML_WHITESPACE_SENSITIVE_TAGS.include?(parent_name)
|
|
63
|
+
return true if inline_significant
|
|
64
|
+
|
|
65
|
+
false
|
|
66
|
+
end
|
|
35
67
|
end
|
|
36
68
|
end
|
|
37
69
|
end
|
data/lib/canon/xml.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Canon
|
|
|
27
27
|
autoload :Processor, "canon/xml/processor"
|
|
28
28
|
autoload :Sax, "canon/xml/sax"
|
|
29
29
|
autoload :SaxBuilder, "canon/xml/sax_builder"
|
|
30
|
+
autoload :TreeBuilder, "canon/xml/tree_builder"
|
|
30
31
|
autoload :WhitespaceNormalizer, "canon/xml/whitespace_normalizer"
|
|
31
32
|
autoload :WhitespacePolicy, "canon/xml/whitespace_policy"
|
|
32
33
|
autoload :XmlBaseHandler, "canon/xml/xml_base_handler"
|
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.
|
|
4
|
+
version: 0.3.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -400,6 +400,7 @@ files:
|
|
|
400
400
|
- lib/canon/xml/sax/moxml_driver.rb
|
|
401
401
|
- lib/canon/xml/sax/nokogiri_driver.rb
|
|
402
402
|
- lib/canon/xml/sax_builder.rb
|
|
403
|
+
- lib/canon/xml/tree_builder.rb
|
|
403
404
|
- lib/canon/xml/whitespace_normalizer.rb
|
|
404
405
|
- lib/canon/xml/whitespace_policy.rb
|
|
405
406
|
- lib/canon/xml/xml_base_handler.rb
|