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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8912c851a2e2b0e250fcae120cda428d7705f9c6262b57ec7fe1218aaf186229
4
- data.tar.gz: 98a7b6c64818395e18c1de7b8918255368b6b4bf8766ecaa567b5502c7c084b4
3
+ metadata.gz: 04012234db38cae77206ba33f856aca1262a0ad0111e7d233d5b814e90c22187
4
+ data.tar.gz: 3700758afcb5619f59ba7e2f896963bc63695d534f60af7d922dfce8c26076ec
5
5
  SHA512:
6
- metadata.gz: 2fee687e2469d42193c4f58a294179dc8332aca3fefca5c3ad476cdc061e40c2370d2183790b20c1e43ae83b420ae78567cf7f27114c128f21b0ab8d76be46fe
7
- data.tar.gz: 61a58938d48f48ce066c93b09e734bc62a05cfccec1c7fea80ecdb4de97663e589940108f89f0d8f947737d096ca9c21b9f41edbe51569da5bcc2ad5c990fd0f
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.new
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.5"
4
+ VERSION = "0.3.6"
5
5
  end
@@ -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
- TREE_BUILDER.add_document_children(root, nokogiri_doc.children,
149
- nokogiri_doc.root, skip_types) do |child|
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
- TREE_BUILDER.add_document_children(root, nokogiri_doc.children,
154
- nil, skip_types) do |child|
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 = TREE_BUILDER.merge_namespace_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 = TREE_BUILDER.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
- TREE_BUILDER.text(node.content,
188
- keep: WhitespacePolicy.keep_dom_text?(
189
- node.content,
190
- preserve_whitespace: preserve_whitespace,
191
- element_parent: node.parent.is_a?(Nokogiri::XML::Element),
192
- ),
193
- original: node.to_xml)
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
- TREE_BUILDER.comment(node.content)
193
+ TreeBuilder::DEFAULT.comment(node.content)
196
194
  when Nokogiri::XML::ProcessingInstruction
197
- TREE_BUILDER.processing_instruction(node.name, node.content)
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
- TREE_BUILDER.add_document_children(root, moxml_doc.children,
247
- moxml_doc.root, skip_types) do |child|
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
- TREE_BUILDER.add_document_children(root, moxml_doc.children,
252
- nil, skip_types) do |child|
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
- TREE_BUILDER.text(content,
280
- keep: WhitespacePolicy.keep_dom_text?(
281
- content, preserve_whitespace: preserve_whitespace
282
- ))
277
+ TreeBuilder::DEFAULT.text(content,
278
+ keep: WhitespacePolicy.keep_dom_text?(
279
+ content, preserve_whitespace: preserve_whitespace
280
+ ))
283
281
  when :comment
284
- TREE_BUILDER.comment(record[:text])
282
+ TreeBuilder::DEFAULT.comment(record[:text])
285
283
  when :processing_instruction
286
- TREE_BUILDER.processing_instruction(record[:qname],
287
- record[:text] || "")
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 = TREE_BUILDER.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 = TREE_BUILDER.merge_namespace_scope(inherited,
320
- own_namespaces[element].to_a)
321
- TREE_BUILDER.attach_namespace_scope(element, scope)
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 = TREE_BUILDER.merge_namespace_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 = TREE_BUILDER.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
- TREE_BUILDER.text(node.content,
356
- keep: WhitespacePolicy.keep_dom_text?(
357
- node.content,
358
- preserve_whitespace: preserve_whitespace,
359
- element_parent: node.parent.is_a?(Moxml::Element),
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
- TREE_BUILDER.comment(node.content)
360
+ TreeBuilder::DEFAULT.comment(node.content)
363
361
  when Moxml::ProcessingInstruction
364
- TREE_BUILDER.processing_instruction(node.target, node.content)
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
- # Find namespace URI from current scope
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
- text = Nodes::TextNode.new(value: decoded_string, original: raw_string)
210
- parent.add_child(text)
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
- parent = @stack.last
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
- parent = @stack.last
229
- pi = Nodes::ProcessingInstructionNode.new(target: name,
230
- data: content || "")
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
- # Add namespace nodes to element
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
- seen = {}
58
- attributes.each do |attr_name, value, attr_namespace_uri, attr_prefix|
59
- key = [attr_name, attr_namespace_uri]
60
- next if seen.key?(key)
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,
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.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.