canon 0.3.5 → 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 +1 -1
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +40 -42
- data/lib/canon/xml/sax_builder.rb +23 -60
- data/lib/canon/xml/tree_builder.rb +12 -5
- 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: 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
|
|
@@ -80,7 +80,7 @@ module Canon
|
|
|
80
80
|
# fragment — a TreeBuilder walk like the XML extractors, with the
|
|
81
81
|
# HTML whitespace policy and xmlns-free attributes.
|
|
82
82
|
def self.build_from_nokogiri(nokogiri_doc)
|
|
83
|
-
builder = Canon::Xml::TreeBuilder
|
|
83
|
+
builder = Canon::Xml::TreeBuilder::DEFAULT
|
|
84
84
|
root = Canon::Xml::Nodes::RootNode.new
|
|
85
85
|
skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : []
|
|
86
86
|
|
data/lib/canon/version.rb
CHANGED
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -134,8 +134,6 @@ module Canon
|
|
|
134
134
|
# node kinds, document ordering) live in TreeBuilder and
|
|
135
135
|
# WhitespacePolicy — one dialect, three feeds.
|
|
136
136
|
|
|
137
|
-
TREE_BUILDER = TreeBuilder.new
|
|
138
|
-
|
|
139
137
|
# -- Nokogiri walk: top-down, scope flows down the recursion --
|
|
140
138
|
|
|
141
139
|
def self.build_from_nokogiri(nokogiri_doc, preserve_whitespace: false)
|
|
@@ -145,13 +143,13 @@ module Canon
|
|
|
145
143
|
if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root
|
|
146
144
|
root.add_child(walk_nokogiri(nokogiri_doc.root,
|
|
147
145
|
preserve_whitespace: preserve_whitespace))
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
TreeBuilder::DEFAULT.add_document_children(root, nokogiri_doc.children,
|
|
147
|
+
nokogiri_doc.root, skip_types) do |child|
|
|
150
148
|
walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
|
|
151
149
|
end
|
|
152
150
|
else
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
TreeBuilder::DEFAULT.add_document_children(root, nokogiri_doc.children,
|
|
152
|
+
nil, skip_types) do |child|
|
|
155
153
|
walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
|
|
156
154
|
end
|
|
157
155
|
end
|
|
@@ -163,11 +161,11 @@ module Canon
|
|
|
163
161
|
inherited_namespaces: nil)
|
|
164
162
|
case node
|
|
165
163
|
when Nokogiri::XML::Element
|
|
166
|
-
scope =
|
|
164
|
+
scope = TreeBuilder::DEFAULT.merge_namespace_scope(
|
|
167
165
|
inherited_namespaces,
|
|
168
166
|
node.namespace_definitions.map { |ns| [ns.prefix, ns.href] },
|
|
169
167
|
)
|
|
170
|
-
element =
|
|
168
|
+
element = TreeBuilder::DEFAULT.element(
|
|
171
169
|
name: node.name,
|
|
172
170
|
prefix: node.namespace&.prefix,
|
|
173
171
|
namespace_uri: node.namespace&.href,
|
|
@@ -184,17 +182,17 @@ inherited_namespaces: nil)
|
|
|
184
182
|
end
|
|
185
183
|
element
|
|
186
184
|
when Nokogiri::XML::Text, Nokogiri::XML::CDATA
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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)
|
|
194
192
|
when Nokogiri::XML::Comment
|
|
195
|
-
|
|
193
|
+
TreeBuilder::DEFAULT.comment(node.content)
|
|
196
194
|
when Nokogiri::XML::ProcessingInstruction
|
|
197
|
-
|
|
195
|
+
TreeBuilder::DEFAULT.processing_instruction(node.name, node.content)
|
|
198
196
|
end
|
|
199
197
|
end
|
|
200
198
|
|
|
@@ -243,13 +241,13 @@ inherited_namespaces: nil)
|
|
|
243
241
|
element = build_moxml_subtree_from_records(moxml_doc.root,
|
|
244
242
|
preserve_whitespace: preserve_whitespace)
|
|
245
243
|
root.add_child(element) if element
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
TreeBuilder::DEFAULT.add_document_children(root, moxml_doc.children,
|
|
245
|
+
moxml_doc.root, skip_types) do |child|
|
|
248
246
|
walk_moxml(child, preserve_whitespace: preserve_whitespace)
|
|
249
247
|
end
|
|
250
248
|
else
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
TreeBuilder::DEFAULT.add_document_children(root, moxml_doc.children,
|
|
250
|
+
nil, skip_types) do |child|
|
|
253
251
|
walk_moxml(child, preserve_whitespace: preserve_whitespace)
|
|
254
252
|
end
|
|
255
253
|
end
|
|
@@ -276,15 +274,15 @@ preserve_whitespace: false)
|
|
|
276
274
|
own_namespaces)
|
|
277
275
|
when :text, :cdata
|
|
278
276
|
content = record[:text].to_s
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
TreeBuilder::DEFAULT.text(content,
|
|
278
|
+
keep: WhitespacePolicy.keep_dom_text?(
|
|
279
|
+
content, preserve_whitespace: preserve_whitespace
|
|
280
|
+
))
|
|
283
281
|
when :comment
|
|
284
|
-
|
|
282
|
+
TreeBuilder::DEFAULT.comment(record[:text])
|
|
285
283
|
when :processing_instruction
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
TreeBuilder::DEFAULT.processing_instruction(record[:qname],
|
|
285
|
+
record[:text] || "")
|
|
288
286
|
end
|
|
289
287
|
|
|
290
288
|
frames.push([record[:depth], node]) if node
|
|
@@ -298,7 +296,7 @@ preserve_whitespace: false)
|
|
|
298
296
|
end
|
|
299
297
|
|
|
300
298
|
def self.build_moxml_element_from_record(record, frames, own_namespaces)
|
|
301
|
-
element =
|
|
299
|
+
element = TreeBuilder::DEFAULT.element(
|
|
302
300
|
name: record[:qname],
|
|
303
301
|
prefix: record[:prefix],
|
|
304
302
|
namespace_uri: record[:namespace_uri],
|
|
@@ -316,9 +314,9 @@ preserve_whitespace: false)
|
|
|
316
314
|
end
|
|
317
315
|
|
|
318
316
|
def self.assign_moxml_namespace_scopes(element, inherited, own_namespaces)
|
|
319
|
-
scope =
|
|
320
|
-
|
|
321
|
-
|
|
317
|
+
scope = TreeBuilder::DEFAULT.merge_namespace_scope(inherited,
|
|
318
|
+
own_namespaces[element].to_a)
|
|
319
|
+
TreeBuilder::DEFAULT.attach_namespace_scope(element, scope)
|
|
322
320
|
|
|
323
321
|
element.children.each do |child|
|
|
324
322
|
assign_moxml_namespace_scopes(child, scope, own_namespaces) if child.is_a?(Nodes::ElementNode)
|
|
@@ -331,11 +329,11 @@ preserve_whitespace: false)
|
|
|
331
329
|
inherited_namespaces: nil)
|
|
332
330
|
case node
|
|
333
331
|
when Moxml::Element
|
|
334
|
-
scope =
|
|
332
|
+
scope = TreeBuilder::DEFAULT.merge_namespace_scope(
|
|
335
333
|
inherited_namespaces,
|
|
336
334
|
node.namespace_definitions.map { |ns| [ns.prefix, ns.uri] },
|
|
337
335
|
)
|
|
338
|
-
element =
|
|
336
|
+
element = TreeBuilder::DEFAULT.element(
|
|
339
337
|
name: node.name,
|
|
340
338
|
prefix: node.namespace&.prefix,
|
|
341
339
|
namespace_uri: node.namespace&.uri,
|
|
@@ -352,16 +350,16 @@ inherited_namespaces: nil)
|
|
|
352
350
|
end
|
|
353
351
|
element
|
|
354
352
|
when Moxml::Text, Moxml::Cdata
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
))
|
|
361
359
|
when Moxml::Comment
|
|
362
|
-
|
|
360
|
+
TreeBuilder::DEFAULT.comment(node.content)
|
|
363
361
|
when Moxml::ProcessingInstruction
|
|
364
|
-
|
|
362
|
+
TreeBuilder::DEFAULT.processing_instruction(node.target, node.content)
|
|
365
363
|
end
|
|
366
364
|
end
|
|
367
365
|
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
|
|
@@ -206,8 +208,9 @@ strip_doctype: false)
|
|
|
206
208
|
preserve_whitespace: @preserve_whitespace,
|
|
207
209
|
element_parent: parent.node_type == :element)
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
parent.add_child(
|
|
212
|
+
TreeBuilder::DEFAULT.text(decoded_string, keep: true, original: raw_string),
|
|
213
|
+
)
|
|
211
214
|
end
|
|
212
215
|
private :append_text
|
|
213
216
|
|
|
@@ -215,9 +218,7 @@ strip_doctype: false)
|
|
|
215
218
|
#
|
|
216
219
|
# @param string [String] Comment content
|
|
217
220
|
def comment(string)
|
|
218
|
-
|
|
219
|
-
comment_node = Nodes::CommentNode.new(value: string)
|
|
220
|
-
parent.add_child(comment_node)
|
|
221
|
+
@stack.last.add_child(TreeBuilder::DEFAULT.comment(string))
|
|
221
222
|
end
|
|
222
223
|
|
|
223
224
|
# Called for processing instructions
|
|
@@ -225,10 +226,9 @@ strip_doctype: false)
|
|
|
225
226
|
# @param name [String] PI target
|
|
226
227
|
# @param content [String] PI content
|
|
227
228
|
def processing_instruction(name, content)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
parent.add_child(pi)
|
|
229
|
+
@stack.last.add_child(
|
|
230
|
+
TreeBuilder::DEFAULT.processing_instruction(name, content || ""),
|
|
231
|
+
)
|
|
232
232
|
end
|
|
233
233
|
|
|
234
234
|
# Return the built tree
|
|
@@ -314,44 +314,7 @@ strip_doctype: false)
|
|
|
314
314
|
[ns_decls, regular_attrs]
|
|
315
315
|
end
|
|
316
316
|
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
# @param element [Nodes::ElementNode] Element to add namespaces to
|
|
320
|
-
# @param scope [Hash] Current namespace scope
|
|
321
|
-
def add_namespace_nodes(element, scope)
|
|
322
|
-
scope.each do |prefix, uri|
|
|
323
|
-
ns_node = Nodes::NamespaceNode.new(prefix: prefix, uri: uri)
|
|
324
|
-
element.add_namespace(ns_node)
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Add attribute nodes to element
|
|
329
|
-
#
|
|
330
|
-
# @param element [Nodes::ElementNode] Element to add attributes to
|
|
331
|
-
# @param attrs [Array] Array of [name, value] pairs
|
|
332
|
-
def add_attribute_nodes(element, attrs)
|
|
333
|
-
attrs.each do |attr_name, attr_value|
|
|
334
|
-
attr_prefix, attr_local = parse_qname(attr_name)
|
|
335
|
-
|
|
336
|
-
# Find namespace for attribute (if prefixed)
|
|
337
|
-
attr_ns_uri = attr_prefix ? @namespace_stack.last[attr_prefix] : nil
|
|
338
|
-
|
|
339
|
-
# Decode numeric character references (e.g., & → &)
|
|
340
|
-
# Nokogiri SAX leaves these as-is, but we need them decoded for C14N
|
|
341
|
-
decoded_value = decode_character_references(attr_value || "")
|
|
342
|
-
|
|
343
|
-
attr_node = Nodes::AttributeNode.new(
|
|
344
|
-
name: attr_local,
|
|
345
|
-
value: decoded_value,
|
|
346
|
-
namespace_uri: attr_ns_uri,
|
|
347
|
-
prefix: attr_prefix,
|
|
348
|
-
)
|
|
349
|
-
element.add_attribute(attr_node)
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Decode numeric character references in a string
|
|
354
|
-
# Handles both &#decimal; and &#xhex; forms
|
|
317
|
+
# Decode numeric character references (e.g., & → &)
|
|
355
318
|
#
|
|
356
319
|
# @param value [String] String potentially containing character references
|
|
357
320
|
# @return [String] String with character references decoded
|
|
@@ -16,6 +16,9 @@ module Canon
|
|
|
16
16
|
# repeats, libleptris deduplicates), so the builder resolves them
|
|
17
17
|
# once — first occurrence wins.
|
|
18
18
|
class TreeBuilder
|
|
19
|
+
# Stateless module: one shared instance serves every feed.
|
|
20
|
+
DEFAULT = new
|
|
21
|
+
|
|
19
22
|
NO_ATTRIBUTES = [].freeze
|
|
20
23
|
XML_NAMESPACE_PREFIX = "xml"
|
|
21
24
|
XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"
|
|
@@ -54,12 +57,16 @@ module Canon
|
|
|
54
57
|
)
|
|
55
58
|
attach_namespace_scope(element, namespace_scope) if namespace_scope
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
69
|
|
|
62
|
-
seen[key] = true
|
|
63
70
|
element.add_attribute(Nodes::AttributeNode.new(
|
|
64
71
|
name: attr_name,
|
|
65
72
|
value: value,
|