canon 0.2.11 → 0.3.0
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/.rubocop_todo.yml +62 -25
- data/CLAUDE.md +197 -0
- data/Rakefile +5 -2
- data/lib/canon/cache.rb +3 -1
- data/lib/canon/cli.rb +0 -3
- data/lib/canon/commands/diff_command.rb +1 -8
- data/lib/canon/commands/format_command.rb +0 -4
- data/lib/canon/commands.rb +9 -0
- data/lib/canon/comparison/child_realignment.rb +0 -2
- data/lib/canon/comparison/compare_profile.rb +30 -36
- data/lib/canon/comparison/comparison_result.rb +1 -4
- data/lib/canon/comparison/diff_node_builder.rb +353 -0
- data/lib/canon/comparison/dimensions/dimension.rb +51 -0
- data/lib/canon/comparison/dimensions/dimension_set.rb +49 -0
- data/lib/canon/comparison/dimensions/registry.rb +101 -60
- data/lib/canon/comparison/dimensions.rb +15 -46
- data/lib/canon/comparison/format_detector.rb +27 -29
- data/lib/canon/comparison/html_comparator.rb +20 -143
- data/lib/canon/comparison/html_compare_profile.rb +15 -18
- data/lib/canon/comparison/json_comparator.rb +4 -165
- data/lib/canon/comparison/json_parser.rb +0 -2
- data/lib/canon/comparison/markup_comparator.rb +14 -210
- data/lib/canon/comparison/match_options/base_resolver.rb +18 -29
- data/lib/canon/comparison/match_options/json_resolver.rb +4 -28
- data/lib/canon/comparison/match_options/xml_resolver.rb +4 -45
- data/lib/canon/comparison/match_options/yaml_resolver.rb +4 -30
- data/lib/canon/comparison/match_options.rb +13 -88
- data/lib/canon/comparison/node_inspector.rb +5 -9
- data/lib/canon/comparison/pipeline.rb +269 -0
- data/lib/canon/comparison/profile_definition.rb +0 -2
- data/lib/canon/comparison/ruby_object_comparator.rb +1 -1
- data/lib/canon/comparison/strategies/match_strategy_factory.rb +9 -58
- data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +4 -11
- data/lib/canon/comparison/strategies.rb +16 -0
- data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +1 -4
- data/lib/canon/comparison/xml_comparator/attribute_filter.rb +0 -3
- data/lib/canon/comparison/xml_comparator/child_comparison.rb +0 -6
- data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +1 -6
- data/lib/canon/comparison/xml_comparator/node_parser.rb +3 -7
- data/lib/canon/comparison/xml_comparator.rb +4 -492
- data/lib/canon/comparison/xml_comparator_helpers.rb +21 -0
- data/lib/canon/comparison/xml_node_comparison.rb +6 -121
- data/lib/canon/comparison/yaml_comparator.rb +0 -3
- data/lib/canon/comparison.rb +144 -267
- data/lib/canon/config/config_dsl.rb +159 -0
- data/lib/canon/config/env_provider.rb +0 -3
- data/lib/canon/config/env_schema.rb +48 -58
- data/lib/canon/config/profile_loader.rb +0 -1
- data/lib/canon/config.rb +116 -468
- data/lib/canon/diff/diff_block_builder.rb +0 -2
- data/lib/canon/diff/diff_classifier.rb +0 -5
- data/lib/canon/diff/diff_context.rb +0 -2
- data/lib/canon/diff/diff_context_builder.rb +0 -2
- data/lib/canon/diff/diff_line_builder.rb +0 -3
- data/lib/canon/diff/diff_node_enricher.rb +0 -4
- data/lib/canon/diff/diff_node_mapper.rb +0 -4
- data/lib/canon/diff/diff_report_builder.rb +0 -4
- data/lib/canon/diff/formatting_detector.rb +0 -1
- data/lib/canon/diff/node_serializer.rb +0 -7
- data/lib/canon/diff.rb +39 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +5 -19
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +8 -21
- data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -3
- data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -3
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +7 -26
- data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -3
- data/lib/canon/diff_formatter/by_line_formatter.rb +1 -2
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +8 -15
- data/lib/canon/diff_formatter/by_object/json_formatter.rb +0 -2
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +0 -2
- data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +0 -2
- data/lib/canon/diff_formatter/debug_output.rb +0 -2
- data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +24 -58
- data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +0 -2
- data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +3 -4
- data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +1 -7
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +0 -7
- data/lib/canon/diff_formatter/diff_detail_formatter_helpers.rb +23 -0
- data/lib/canon/diff_formatter/theme.rb +2 -2
- data/lib/canon/diff_formatter.rb +13 -13
- data/lib/canon/formatters/html4_formatter.rb +0 -2
- data/lib/canon/formatters/html5_formatter.rb +0 -2
- data/lib/canon/formatters/html_formatter.rb +0 -3
- data/lib/canon/formatters/json_formatter.rb +0 -1
- data/lib/canon/formatters/xml_formatter.rb +0 -4
- data/lib/canon/formatters/yaml_formatter.rb +0 -1
- data/lib/canon/formatters.rb +16 -0
- data/lib/canon/html/data_model.rb +0 -10
- data/lib/canon/html/nokogiri_support.rb +60 -0
- data/lib/canon/html.rb +5 -3
- data/lib/canon/options/cli_generator.rb +0 -2
- data/lib/canon/options/registry.rb +0 -2
- data/lib/canon/options.rb +9 -0
- data/lib/canon/pretty_printer/html.rb +0 -1
- data/lib/canon/pretty_printer/xml.rb +6 -1
- data/lib/canon/pretty_printer/xml_normalized.rb +5 -7
- data/lib/canon/pretty_printer.rb +12 -0
- data/lib/canon/tree_diff/adapters/html_adapter.rb +1 -1
- data/lib/canon/tree_diff/adapters.rb +14 -0
- data/lib/canon/tree_diff/core/attribute_comparator.rb +0 -6
- data/lib/canon/tree_diff/core/node_signature.rb +1 -1
- data/lib/canon/tree_diff/core/tree_node.rb +13 -5
- data/lib/canon/tree_diff/core.rb +17 -0
- data/lib/canon/tree_diff/matchers/hash_matcher.rb +0 -7
- data/lib/canon/tree_diff/matchers/similarity_matcher.rb +1 -5
- data/lib/canon/tree_diff/matchers/structural_propagator.rb +1 -5
- data/lib/canon/tree_diff/matchers.rb +15 -0
- data/lib/canon/tree_diff/operation_converter.rb +0 -8
- data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +2 -12
- data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +13 -7
- data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +2 -2
- data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +4 -6
- data/lib/canon/tree_diff/operation_converter_helpers.rb +18 -0
- data/lib/canon/tree_diff/operations/operation_detector.rb +2 -5
- data/lib/canon/tree_diff/operations.rb +13 -0
- data/lib/canon/tree_diff.rb +26 -27
- data/lib/canon/validators/base_validator.rb +0 -2
- data/lib/canon/validators/html_validator.rb +0 -1
- data/lib/canon/validators/json_validator.rb +0 -1
- data/lib/canon/validators/xml_validator.rb +0 -1
- data/lib/canon/validators/yaml_validator.rb +0 -1
- data/lib/canon/validators.rb +12 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/c14n.rb +0 -4
- data/lib/canon/xml/data_model.rb +206 -43
- data/lib/canon/xml/line_range_mapper.rb +0 -2
- data/lib/canon/xml/nodes/attribute_node.rb +0 -2
- data/lib/canon/xml/nodes/comment_node.rb +0 -2
- data/lib/canon/xml/nodes/element_node.rb +0 -2
- data/lib/canon/xml/nodes/namespace_node.rb +0 -2
- data/lib/canon/xml/nodes/processing_instruction_node.rb +0 -2
- data/lib/canon/xml/nodes/root_node.rb +0 -2
- data/lib/canon/xml/nodes/text_node.rb +0 -2
- data/lib/canon/xml/nodes.rb +19 -0
- data/lib/canon/xml/processor.rb +10 -7
- data/lib/canon/xml/sax/moxml_driver.rb +58 -0
- data/lib/canon/xml/sax/nokogiri_driver.rb +54 -0
- data/lib/canon/xml/sax.rb +37 -0
- data/lib/canon/xml/sax_builder.rb +31 -27
- data/lib/canon/xml.rb +34 -0
- data/lib/canon/xml_backend.rb +40 -19
- data/lib/canon/xml_parsing.rb +89 -116
- data/lib/canon.rb +26 -17
- data/lib/tasks/benchmark_runner.rb +4 -0
- data/lib/tasks/performance.rake +2 -58
- data/lib/tasks/performance_comparator.rb +132 -65
- data/lib/tasks/performance_helpers.rb +4 -249
- data/lib/tasks/performance_report.rb +313 -0
- metadata +31 -13
- data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +0 -64
- data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +0 -64
- data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +0 -167
- data/lib/canon/comparison/dimensions/base_dimension.rb +0 -107
- data/lib/canon/comparison/dimensions/comments_dimension.rb +0 -117
- data/lib/canon/comparison/dimensions/element_position_dimension.rb +0 -86
- data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +0 -115
- data/lib/canon/comparison/dimensions/text_content_dimension.rb +0 -102
- data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +0 -300
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -2,16 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "nokogiri" unless RUBY_ENGINE == "opal"
|
|
4
4
|
require "set"
|
|
5
|
-
require_relative "../data_model"
|
|
6
|
-
require_relative "../xml_backend"
|
|
7
|
-
require_relative "../xml_parsing"
|
|
8
|
-
require_relative "nodes/root_node"
|
|
9
|
-
require_relative "nodes/element_node"
|
|
10
|
-
require_relative "nodes/namespace_node"
|
|
11
|
-
require_relative "nodes/attribute_node"
|
|
12
|
-
require_relative "nodes/text_node"
|
|
13
|
-
require_relative "nodes/comment_node"
|
|
14
|
-
require_relative "nodes/processing_instruction_node"
|
|
15
5
|
|
|
16
6
|
module Canon
|
|
17
7
|
module Xml
|
|
@@ -169,7 +159,7 @@ preserve_whitespace: false)
|
|
|
169
159
|
when Nokogiri::XML::Element
|
|
170
160
|
build_element_node(nokogiri_node,
|
|
171
161
|
preserve_whitespace: preserve_whitespace)
|
|
172
|
-
when Nokogiri::XML::Text
|
|
162
|
+
when Nokogiri::XML::Text, Nokogiri::XML::CDATA
|
|
173
163
|
build_text_node(nokogiri_node,
|
|
174
164
|
preserve_whitespace: preserve_whitespace)
|
|
175
165
|
when Nokogiri::XML::Comment
|
|
@@ -232,7 +222,10 @@ preserve_whitespace: false)
|
|
|
232
222
|
end
|
|
233
223
|
|
|
234
224
|
def self.build_attribute_nodes(nokogiri_element, element)
|
|
235
|
-
|
|
225
|
+
# attribute_nodes, not the attributes hash: the hash is keyed by
|
|
226
|
+
# local name and collapses e.g. attr / a:attr / b:attr into one
|
|
227
|
+
# entry, losing namespaced attributes (W3C C14N ex 3.3).
|
|
228
|
+
nokogiri_element.attribute_nodes.each do |attr|
|
|
236
229
|
attr_node = Nodes::AttributeNode.new(
|
|
237
230
|
name: attr.name,
|
|
238
231
|
value: attr.value,
|
|
@@ -269,26 +262,183 @@ preserve_whitespace: false)
|
|
|
269
262
|
|
|
270
263
|
def self.from_moxml_xml(xml_string, preserve_whitespace:)
|
|
271
264
|
doc = Canon::XmlParsing.parse(xml_string)
|
|
272
|
-
|
|
265
|
+
check_moxml_relative_namespace_uris(doc)
|
|
266
|
+
result = build_from_moxml(doc, preserve_whitespace: preserve_whitespace)
|
|
267
|
+
# Canon owns this document's full lifecycle: the canon tree holds
|
|
268
|
+
# no engine references once built, so release the native tree now
|
|
269
|
+
# instead of waiting for the GC finalizer (moxml#134; no-op on
|
|
270
|
+
# GC-managed adapters).
|
|
271
|
+
doc.free
|
|
272
|
+
result
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def self.check_moxml_relative_namespace_uris(node)
|
|
276
|
+
case node
|
|
277
|
+
when Moxml::Element
|
|
278
|
+
node.namespace_definitions.each do |ns|
|
|
279
|
+
href = ns.uri
|
|
280
|
+
next if href.nil? || href.empty?
|
|
281
|
+
if relative_uri?(href)
|
|
282
|
+
raise Canon::Error,
|
|
283
|
+
"Relative namespace URI not allowed: #{href}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
node.children.each { |child| check_moxml_relative_namespace_uris(child) }
|
|
287
|
+
when Moxml::Document
|
|
288
|
+
node.children.each { |child| check_moxml_relative_namespace_uris(child) }
|
|
289
|
+
end
|
|
273
290
|
end
|
|
274
291
|
|
|
275
292
|
def self.build_from_moxml(moxml_doc, preserve_whitespace: false)
|
|
276
293
|
root = Nodes::RootNode.new
|
|
277
294
|
|
|
278
295
|
if moxml_doc.is_a?(Moxml::Document) && moxml_doc.root
|
|
279
|
-
|
|
280
|
-
|
|
296
|
+
element = build_moxml_subtree_from_records(moxml_doc.root,
|
|
297
|
+
preserve_whitespace: preserve_whitespace)
|
|
298
|
+
root.add_child(element) if element
|
|
299
|
+
moxml_doc.children.each do |child|
|
|
300
|
+
next if child.equal?(moxml_doc.root)
|
|
301
|
+
next if child.is_a?(Moxml::Doctype)
|
|
302
|
+
|
|
303
|
+
node = build_moxml_node(child,
|
|
304
|
+
preserve_whitespace: preserve_whitespace)
|
|
305
|
+
root.add_child(node) if node
|
|
306
|
+
end
|
|
307
|
+
else
|
|
308
|
+
moxml_doc.children.each do |child|
|
|
309
|
+
next if child.is_a?(Moxml::Doctype)
|
|
310
|
+
|
|
311
|
+
node = build_moxml_node(child,
|
|
312
|
+
preserve_whitespace: preserve_whitespace)
|
|
313
|
+
root.add_child(node) if node
|
|
314
|
+
end
|
|
281
315
|
end
|
|
282
316
|
|
|
283
317
|
root
|
|
284
318
|
end
|
|
285
319
|
|
|
286
|
-
|
|
320
|
+
# Document path: build the root subtree from materialize records —
|
|
321
|
+
# one flattened record stream (moxml#132/#138) instead of a wrapper
|
|
322
|
+
# walk, so conversion costs no Moxml::Node allocation per node.
|
|
323
|
+
#
|
|
324
|
+
# Records arrive post-order (children before parents): completed
|
|
325
|
+
# nodes stack up with their depth, and an element record adopts
|
|
326
|
+
# every completed node deeper than itself. Each element's OWN
|
|
327
|
+
# namespace declarations ride an identity-keyed stash; afterwards
|
|
328
|
+
# assign_moxml_namespace_scopes expands them to in-scope scopes
|
|
329
|
+
# with the same first-wins/xml-prebound semantics as the Nokogiri
|
|
330
|
+
# collector.
|
|
331
|
+
def self.build_moxml_subtree_from_records(moxml_element,
|
|
332
|
+
preserve_whitespace: false)
|
|
333
|
+
frames = []
|
|
334
|
+
own_namespaces = {}.compare_by_identity
|
|
335
|
+
|
|
336
|
+
moxml_element.materialize do |record|
|
|
337
|
+
# The record contract is root-subtree-only since moxml 0.5.11
|
|
338
|
+
# (moxml#140). Older 0.5.x releases — still allowed by canon's
|
|
339
|
+
# gemspec floor — leaked epilog document-level records at depth
|
|
340
|
+
# 0, which the document-children enumeration below also covers;
|
|
341
|
+
# one integer compare per record keeps those working.
|
|
342
|
+
next if record[:depth].zero? && record[:kind] != :element
|
|
343
|
+
|
|
344
|
+
node = case record[:kind]
|
|
345
|
+
when :element
|
|
346
|
+
build_moxml_element_from_record(record, frames,
|
|
347
|
+
own_namespaces)
|
|
348
|
+
when :text, :cdata
|
|
349
|
+
build_moxml_text_from_record(record, preserve_whitespace)
|
|
350
|
+
when :comment
|
|
351
|
+
Nodes::CommentNode.new(value: record[:text])
|
|
352
|
+
when :processing_instruction
|
|
353
|
+
Nodes::ProcessingInstructionNode.new(
|
|
354
|
+
target: record[:qname],
|
|
355
|
+
data: record[:text] || "",
|
|
356
|
+
)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
frames.push([record[:depth], node]) if node
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
top = frames.last
|
|
363
|
+
return nil unless top
|
|
364
|
+
|
|
365
|
+
element = top[1]
|
|
366
|
+
assign_moxml_namespace_scopes(element,
|
|
367
|
+
{ "xml" => "http://www.w3.org/XML/1998/namespace" },
|
|
368
|
+
own_namespaces)
|
|
369
|
+
element
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def self.build_moxml_element_from_record(record, frames, own_namespaces)
|
|
373
|
+
element = Nodes::ElementNode.new(
|
|
374
|
+
name: record[:qname],
|
|
375
|
+
namespace_uri: record[:namespace_uri],
|
|
376
|
+
prefix: record[:prefix],
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
seen = {}
|
|
380
|
+
record[:attributes].each do |name, value, namespace_uri, prefix|
|
|
381
|
+
key = [name, namespace_uri]
|
|
382
|
+
next if seen.key?(key)
|
|
383
|
+
|
|
384
|
+
seen[key] = true
|
|
385
|
+
element.add_attribute(Nodes::AttributeNode.new(
|
|
386
|
+
name: name,
|
|
387
|
+
value: value,
|
|
388
|
+
namespace_uri: namespace_uri,
|
|
389
|
+
prefix: prefix,
|
|
390
|
+
))
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Adopt completed children (they stack in reverse: unshift
|
|
394
|
+
# restores document order).
|
|
395
|
+
children = []
|
|
396
|
+
children.unshift(frames.pop[1]) while frames.any? && frames.last[0] > record[:depth]
|
|
397
|
+
children.each { |child| element.add_child(child) }
|
|
398
|
+
|
|
399
|
+
own_namespaces[element] = record[:namespaces]
|
|
400
|
+
element
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Text records inside a document subtree always have an element
|
|
404
|
+
# parent (only the document element sits at depth 0), so the
|
|
405
|
+
# whitespace-only skip rule matches the wrapper path exactly.
|
|
406
|
+
def self.build_moxml_text_from_record(record, preserve_whitespace)
|
|
407
|
+
content = record[:text].to_s
|
|
408
|
+
return nil if !preserve_whitespace && content.strip.empty?
|
|
409
|
+
|
|
410
|
+
Nodes::TextNode.new(value: content, original: content)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Expand each element's own declarations to in-scope bindings:
|
|
414
|
+
# the element's declarations shadow inherited ones; xml is
|
|
415
|
+
# prebound. Scope flows down the canon tree — no engine calls.
|
|
416
|
+
def self.assign_moxml_namespace_scopes(element, inherited, own_namespaces)
|
|
417
|
+
scope = inherited.dup
|
|
418
|
+
own_namespaces[element].to_a.each do |prefix, uri|
|
|
419
|
+
scope[prefix || ""] = uri
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
scope.each do |prefix, uri|
|
|
423
|
+
element.add_namespace(Nodes::NamespaceNode.new(
|
|
424
|
+
prefix: prefix,
|
|
425
|
+
uri: uri,
|
|
426
|
+
))
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
element.children.each do |child|
|
|
430
|
+
assign_moxml_namespace_scopes(child, scope, own_namespaces) if child.is_a?(Nodes::ElementNode)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def self.build_moxml_node(node, preserve_whitespace: false,
|
|
435
|
+
inherited_namespaces: nil)
|
|
287
436
|
case node
|
|
288
437
|
when Moxml::Element
|
|
289
438
|
build_moxml_element_node(node,
|
|
290
|
-
preserve_whitespace: preserve_whitespace
|
|
291
|
-
|
|
439
|
+
preserve_whitespace: preserve_whitespace,
|
|
440
|
+
inherited_namespaces: inherited_namespaces)
|
|
441
|
+
when Moxml::Text, Moxml::Cdata
|
|
292
442
|
build_moxml_text_node(node, preserve_whitespace: preserve_whitespace)
|
|
293
443
|
when Moxml::Comment
|
|
294
444
|
build_moxml_comment_node(node)
|
|
@@ -298,7 +448,8 @@ preserve_whitespace: false)
|
|
|
298
448
|
end
|
|
299
449
|
|
|
300
450
|
def self.build_moxml_element_node(moxml_element,
|
|
301
|
-
preserve_whitespace: false
|
|
451
|
+
preserve_whitespace: false,
|
|
452
|
+
inherited_namespaces: nil)
|
|
302
453
|
ns = moxml_element.namespace
|
|
303
454
|
element = Nodes::ElementNode.new(
|
|
304
455
|
name: moxml_element.name,
|
|
@@ -306,44 +457,56 @@ preserve_whitespace: false)
|
|
|
306
457
|
prefix: ns&.prefix,
|
|
307
458
|
)
|
|
308
459
|
|
|
309
|
-
|
|
460
|
+
scope = moxml_namespace_scope(moxml_element, inherited_namespaces)
|
|
461
|
+
scope.each do |prefix, uri|
|
|
462
|
+
element.add_namespace(Nodes::NamespaceNode.new(
|
|
463
|
+
prefix: prefix,
|
|
464
|
+
uri: uri,
|
|
465
|
+
))
|
|
466
|
+
end
|
|
467
|
+
|
|
310
468
|
build_moxml_attribute_nodes(moxml_element, element)
|
|
311
469
|
|
|
312
470
|
moxml_element.children.each do |child|
|
|
313
471
|
node = build_moxml_node(child,
|
|
314
|
-
preserve_whitespace: preserve_whitespace
|
|
472
|
+
preserve_whitespace: preserve_whitespace,
|
|
473
|
+
inherited_namespaces: scope)
|
|
315
474
|
element.add_child(node) if node
|
|
316
475
|
end
|
|
317
476
|
|
|
318
477
|
element
|
|
319
478
|
end
|
|
320
479
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
unless element.namespace_nodes.any? do |n|
|
|
331
|
-
n.prefix == "xml"
|
|
332
|
-
end
|
|
333
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
334
|
-
prefix: "xml",
|
|
335
|
-
uri: "http://www.w3.org/XML/1998/namespace",
|
|
336
|
-
))
|
|
480
|
+
# In-scope namespace bindings: the element's own declarations
|
|
481
|
+
# shadow inherited ones, xml is prebound. The scope is passed down
|
|
482
|
+
# the recursion so each element pays one declaration fetch instead
|
|
483
|
+
# of an ancestor walk through the adapter layer (the Nokogiri
|
|
484
|
+
# collector walks ancestors because those calls are cheap there).
|
|
485
|
+
def self.moxml_namespace_scope(moxml_element, inherited)
|
|
486
|
+
scope = inherited ? inherited.dup : { "xml" => "http://www.w3.org/XML/1998/namespace" }
|
|
487
|
+
moxml_element.namespace_definitions.each do |decl|
|
|
488
|
+
scope[decl.prefix || ""] = decl.uri
|
|
337
489
|
end
|
|
490
|
+
scope
|
|
338
491
|
end
|
|
339
492
|
|
|
340
493
|
def self.build_moxml_attribute_nodes(moxml_element, element)
|
|
494
|
+
seen = {}
|
|
341
495
|
moxml_element.attributes.each do |attr|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
496
|
+
# Duplicate attribute names are invalid XML; engines expose
|
|
497
|
+
# them differently (libxml2 lists repeats, libleptris dedups
|
|
498
|
+
# last-wins). Canon follows the Nokogiri presentation contract:
|
|
499
|
+
# first occurrence wins, keyed by name + namespace.
|
|
500
|
+
key = [attr.name, attr.namespace&.uri]
|
|
501
|
+
next if seen.key?(key)
|
|
502
|
+
|
|
503
|
+
seen[key] = true
|
|
504
|
+
element.add_attribute(Nodes::AttributeNode.new(
|
|
505
|
+
name: attr.name,
|
|
506
|
+
value: attr.value,
|
|
507
|
+
namespace_uri: attr.namespace&.uri,
|
|
508
|
+
prefix: attr.namespace&.prefix,
|
|
509
|
+
))
|
|
347
510
|
end
|
|
348
511
|
end
|
|
349
512
|
|
|
@@ -364,7 +527,7 @@ preserve_whitespace: false)
|
|
|
364
527
|
def self.build_moxml_pi_node(moxml_pi)
|
|
365
528
|
Nodes::ProcessingInstructionNode.new(
|
|
366
529
|
target: moxml_pi.target,
|
|
367
|
-
data: moxml_pi.
|
|
530
|
+
data: moxml_pi.content,
|
|
368
531
|
)
|
|
369
532
|
end
|
|
370
533
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Xml
|
|
5
|
+
# XPath data model node types. All nodes inherit from
|
|
6
|
+
# {Canon::Xml::Node}. Children are autoloaded — never
|
|
7
|
+
# `require_relative` them.
|
|
8
|
+
module Nodes
|
|
9
|
+
autoload :AttributeNode, "canon/xml/nodes/attribute_node"
|
|
10
|
+
autoload :CommentNode, "canon/xml/nodes/comment_node"
|
|
11
|
+
autoload :ElementNode, "canon/xml/nodes/element_node"
|
|
12
|
+
autoload :NamespaceNode, "canon/xml/nodes/namespace_node"
|
|
13
|
+
autoload :ProcessingInstructionNode,
|
|
14
|
+
"canon/xml/nodes/processing_instruction_node"
|
|
15
|
+
autoload :RootNode, "canon/xml/nodes/root_node"
|
|
16
|
+
autoload :TextNode, "canon/xml/nodes/text_node"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/canon/xml/processor.rb
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "character_encoder"
|
|
4
|
-
require_relative "namespace_handler"
|
|
5
|
-
require_relative "attribute_handler"
|
|
6
|
-
require_relative "xml_base_handler"
|
|
7
|
-
|
|
8
3
|
module Canon
|
|
9
4
|
module Xml
|
|
10
5
|
# C14N 1.1 processor
|
|
@@ -135,10 +130,18 @@ module Canon
|
|
|
135
130
|
# Add line break before PI if it's outside document element
|
|
136
131
|
output << "\n" if parent_element.nil? && output.length.positive?
|
|
137
132
|
|
|
133
|
+
# C14N renders "<?target S data?>". Both CRuby engines now
|
|
134
|
+
# consume the leading whitespace run after the PI target
|
|
135
|
+
# (leptris-ruby#85), but the canonical writer owns the rule —
|
|
136
|
+
# it must not depend on engine pre-processing (and the pure-Ruby
|
|
137
|
+
# adapters don't). Trailing whitespace and internal newlines are
|
|
138
|
+
# preserved (W3C ex 3.1 keeps them).
|
|
139
|
+
data = node.data.to_s.sub(/\A[ \t\r\n]+/, "")
|
|
140
|
+
|
|
138
141
|
output << "<?" << node.target
|
|
139
142
|
|
|
140
|
-
unless
|
|
141
|
-
output << " " <<
|
|
143
|
+
unless data.empty?
|
|
144
|
+
output << " " << data
|
|
142
145
|
end
|
|
143
146
|
|
|
144
147
|
output << "?>"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Xml
|
|
5
|
+
module Sax
|
|
6
|
+
# moxml SAX driver (rexml adapter under Opal). moxml splits namespace
|
|
7
|
+
# declarations out of the attribute hash and reports them separately,
|
|
8
|
+
# so this driver recombines them as inline "xmlns"/"xmlns:prefix"
|
|
9
|
+
# pairs to match the builder's engine-neutral protocol.
|
|
10
|
+
class MoxmlDriver < Moxml::SAX::Handler
|
|
11
|
+
def initialize(builder)
|
|
12
|
+
super()
|
|
13
|
+
@builder = builder
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse(xml_string)
|
|
17
|
+
Canon::XmlParsing.moxml_context.sax_parse(xml_string, self)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def on_start_element(name, attributes = {}, namespaces = {})
|
|
21
|
+
attrs = attributes.to_a
|
|
22
|
+
namespaces.each do |prefix, uri|
|
|
23
|
+
attrs << if prefix.nil? || prefix.empty?
|
|
24
|
+
["xmlns", uri]
|
|
25
|
+
else
|
|
26
|
+
["xmlns:#{prefix}", uri]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
@builder.start_element(name, attrs)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on_end_element(name)
|
|
33
|
+
@builder.end_element(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def on_characters(text)
|
|
37
|
+
@builder.characters(text)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_cdata(text)
|
|
41
|
+
@builder.cdata(text)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def on_comment(text)
|
|
45
|
+
@builder.comment(text)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_processing_instruction(name, content)
|
|
49
|
+
@builder.processing_instruction(name, content || "")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def on_error(error)
|
|
53
|
+
@builder.error(error.message.to_s)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri" unless RUBY_ENGINE == "opal"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
module Xml
|
|
7
|
+
module Sax
|
|
8
|
+
# Nokogiri SAX driver: the builder's protocol already matches
|
|
9
|
+
# Nokogiri::XML::SAX::Document, so every event is a pass-through.
|
|
10
|
+
class NokogiriDriver < Nokogiri::XML::SAX::Document
|
|
11
|
+
def initialize(builder)
|
|
12
|
+
super()
|
|
13
|
+
@builder = builder
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse(xml_string)
|
|
17
|
+
Nokogiri::XML::SAX::Parser.new(self).parse(xml_string)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def error(string)
|
|
21
|
+
@builder.error(string)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def warning(string)
|
|
25
|
+
@builder.warning(string)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start_element(name, attrs = [])
|
|
29
|
+
@builder.start_element(name, attrs)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def end_element(name)
|
|
33
|
+
@builder.end_element(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def characters(string)
|
|
37
|
+
@builder.characters(string)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cdata_block(string)
|
|
41
|
+
@builder.cdata(string)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def comment(string)
|
|
45
|
+
@builder.comment(string)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def processing_instruction(name, content)
|
|
49
|
+
@builder.processing_instruction(name, content)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Xml
|
|
5
|
+
# SAX engine selection and driving for the data-model builder.
|
|
6
|
+
#
|
|
7
|
+
# The builder (SaxBuilder) owns tree construction and speaks a single
|
|
8
|
+
# engine-neutral event protocol (Nokogiri-shaped: qname + attribute
|
|
9
|
+
# pairs with xmlns entries inline). Each driver adapts one engine's SAX
|
|
10
|
+
# API to that protocol. OCP: a new engine is a new driver plus one line
|
|
11
|
+
# here; the builder never changes.
|
|
12
|
+
#
|
|
13
|
+
# Engine choice: leptris SAX outperforms Nokogiri SAX (1.10x min-of-N
|
|
14
|
+
# on canon's builder, 53KB doc) through moxml 0.5.12's
|
|
15
|
+
# interest-proportional callbacks — BUT the batch-attribute transport
|
|
16
|
+
# reads uninitialized memory at certain buffer offsets, corrupting the
|
|
17
|
+
# first N attributes of an element (leptris-ruby#95). Until that is
|
|
18
|
+
# fixed, CRuby keeps the Nokogiri driver; flipping is the one-line
|
|
19
|
+
# change below.
|
|
20
|
+
module Sax
|
|
21
|
+
autoload :NokogiriDriver, "canon/xml/sax/nokogiri_driver"
|
|
22
|
+
autoload :MoxmlDriver, "canon/xml/sax/moxml_driver"
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Drive `builder` with the runtime's SAX engine.
|
|
26
|
+
def parse(xml_string, builder)
|
|
27
|
+
if RUBY_ENGINE == "opal"
|
|
28
|
+
MoxmlDriver.new(builder).parse(xml_string)
|
|
29
|
+
else
|
|
30
|
+
NokogiriDriver.new(builder).parse(xml_string)
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|