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
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri" unless RUBY_ENGINE == "opal"
|
|
4
|
-
require_relative "nodes/root_node"
|
|
5
|
-
require_relative "nodes/element_node"
|
|
6
|
-
require_relative "nodes/namespace_node"
|
|
7
|
-
require_relative "nodes/attribute_node"
|
|
8
|
-
require_relative "nodes/text_node"
|
|
9
|
-
require_relative "nodes/comment_node"
|
|
10
|
-
require_relative "nodes/processing_instruction_node"
|
|
11
|
-
|
|
12
3
|
module Canon
|
|
13
4
|
module Xml
|
|
14
|
-
# Builds Canon::Xml::Node tree
|
|
15
|
-
#
|
|
16
|
-
# This is MUCH faster than DOM parsing + conversion because:
|
|
17
|
-
# 1. No intermediate Nokogiri DOM tree (saves ~60ms)
|
|
18
|
-
# 2. No tree traversal to build Canon (saves ~1200ms)
|
|
19
|
-
# 3. No memory overhead of two complete DOM trees
|
|
5
|
+
# Builds Canon::Xml::Node tree from SAX events.
|
|
20
6
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
7
|
+
# Engine-neutral: the event protocol is Nokogiri-shaped (qname +
|
|
8
|
+
# attribute pairs with xmlns declarations inline); Canon::Xml::Sax
|
|
9
|
+
# selects the driver. Much faster than DOM parsing + conversion —
|
|
10
|
+
# no intermediate engine DOM tree, no traversal conversion pass.
|
|
23
11
|
#
|
|
24
12
|
# Usage:
|
|
25
13
|
# root = SaxBuilder.parse(xml_string, preserve_whitespace: false)
|
|
@@ -28,7 +16,7 @@ module Canon
|
|
|
28
16
|
# For C14N, use strip_doctype: true to avoid DTD default attribute expansion:
|
|
29
17
|
# root = SaxBuilder.parse(xml_string, strip_doctype: true)
|
|
30
18
|
#
|
|
31
|
-
class SaxBuilder
|
|
19
|
+
class SaxBuilder
|
|
32
20
|
# Parse XML string and return Canon::Xml::Node tree
|
|
33
21
|
#
|
|
34
22
|
# @param xml_string [String] XML content to parse
|
|
@@ -37,7 +25,7 @@ module Canon
|
|
|
37
25
|
# @return [Nodes::RootNode] Root of the data model tree
|
|
38
26
|
def self.parse(xml_string, preserve_whitespace: false,
|
|
39
27
|
strip_doctype: false)
|
|
40
|
-
# Strip DOCTYPE to prevent
|
|
28
|
+
# Strip DOCTYPE to prevent the SAX engine from expanding DTD default attributes
|
|
41
29
|
# This is needed for C14N which should NOT include default attributes from DTD
|
|
42
30
|
# Use string methods instead of complex regex to avoid ReDoS vulnerability
|
|
43
31
|
if strip_doctype
|
|
@@ -45,8 +33,7 @@ strip_doctype: false)
|
|
|
45
33
|
end
|
|
46
34
|
|
|
47
35
|
builder = new(preserve_whitespace: preserve_whitespace)
|
|
48
|
-
|
|
49
|
-
parser.parse(xml_string)
|
|
36
|
+
Canon::Xml::Sax.parse(xml_string, builder)
|
|
50
37
|
builder.result
|
|
51
38
|
end
|
|
52
39
|
|
|
@@ -86,7 +73,6 @@ strip_doctype: false)
|
|
|
86
73
|
#
|
|
87
74
|
# @param preserve_whitespace [Boolean] Whether to preserve whitespace-only text nodes
|
|
88
75
|
def initialize(preserve_whitespace: false)
|
|
89
|
-
super()
|
|
90
76
|
@preserve_whitespace = preserve_whitespace
|
|
91
77
|
@root = Nodes::RootNode.new
|
|
92
78
|
@stack = [@root]
|
|
@@ -175,13 +161,30 @@ strip_doctype: false)
|
|
|
175
161
|
def characters(string)
|
|
176
162
|
return if string.nil?
|
|
177
163
|
|
|
178
|
-
|
|
164
|
+
append_text(decode_character_references(string), string)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Called for CDATA content. CDATA is literal character data:
|
|
168
|
+
# character references inside it are NOT decoded (a literal
|
|
169
|
+
# A stays as written), unlike regular text where they are
|
|
170
|
+
# resolved. Whitespace and adjacency rules match characters so
|
|
171
|
+
# the two forms of character data build identical trees.
|
|
172
|
+
#
|
|
173
|
+
# @param string [String] CDATA content
|
|
174
|
+
def cdata(string)
|
|
175
|
+
return if string.nil?
|
|
179
176
|
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
append_text(string, string)
|
|
178
|
+
end
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
# Append character data to the tree: combine with an adjacent
|
|
181
|
+
# text node if present, else create one (respecting the
|
|
182
|
+
# whitespace-only skip rules).
|
|
183
|
+
#
|
|
184
|
+
# @param decoded_string [String] value with character references resolved
|
|
185
|
+
# @param raw_string [String] value as delivered (pre-resolution)
|
|
186
|
+
def append_text(decoded_string, raw_string)
|
|
187
|
+
parent = @stack.last
|
|
185
188
|
|
|
186
189
|
# Combine with previous text node if adjacent (SAX can split text content)
|
|
187
190
|
# This MUST happen before whitespace check, because SAX may split "foo "
|
|
@@ -215,6 +218,7 @@ strip_doctype: false)
|
|
|
215
218
|
text = Nodes::TextNode.new(value: decoded_string, original: raw_string)
|
|
216
219
|
parent.add_child(text)
|
|
217
220
|
end
|
|
221
|
+
private :append_text
|
|
218
222
|
|
|
219
223
|
# Called for comments
|
|
220
224
|
#
|
data/lib/canon/xml.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
# Native XML data model, parsing, and DOM utilities.
|
|
5
|
+
#
|
|
6
|
+
# This namespace holds Canon's own XML representation (independent of
|
|
7
|
+
# Nokogiri/Moxml), including:
|
|
8
|
+
# * the XPath data model nodes (Canon::Xml::Node and Canon::Xml::Nodes::*)
|
|
9
|
+
# * the SAX builder that produces them
|
|
10
|
+
# * element matching, line-range mapping, xpath, C14N, processors
|
|
11
|
+
# * canonicalization (C14n) and serialization helpers
|
|
12
|
+
#
|
|
13
|
+
# All children are autoloaded from this file. The nested Nodes namespace
|
|
14
|
+
# is itself a sibling and is autoloaded on first reference to
|
|
15
|
+
# Canon::Xml::Nodes.
|
|
16
|
+
module Xml
|
|
17
|
+
autoload :AttributeHandler, "canon/xml/attribute_handler"
|
|
18
|
+
autoload :C14n, "canon/xml/c14n"
|
|
19
|
+
autoload :CharacterEncoder, "canon/xml/character_encoder"
|
|
20
|
+
autoload :DataModel, "canon/xml/data_model"
|
|
21
|
+
autoload :ElementMatcher, "canon/xml/element_matcher"
|
|
22
|
+
autoload :LineRangeMapper, "canon/xml/line_range_mapper"
|
|
23
|
+
autoload :NamespaceHandler, "canon/xml/namespace_handler"
|
|
24
|
+
autoload :NamespaceHelper, "canon/xml/namespace_helper"
|
|
25
|
+
autoload :Node, "canon/xml/node"
|
|
26
|
+
autoload :Nodes, "canon/xml/nodes"
|
|
27
|
+
autoload :Processor, "canon/xml/processor"
|
|
28
|
+
autoload :Sax, "canon/xml/sax"
|
|
29
|
+
autoload :SaxBuilder, "canon/xml/sax_builder"
|
|
30
|
+
autoload :WhitespaceNormalizer, "canon/xml/whitespace_normalizer"
|
|
31
|
+
autoload :XmlBaseHandler, "canon/xml/xml_base_handler"
|
|
32
|
+
autoload :XPathEngine, "canon/xml/xpath_engine"
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/canon/xml_backend.rb
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Canon
|
|
4
|
-
#
|
|
4
|
+
# Selection of the XML engine.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# -
|
|
8
|
-
#
|
|
6
|
+
# Two independent concerns, deliberately separated (MECE):
|
|
7
|
+
# - XML engine — :nokogiri (raw) or :moxml (adapter-mediated). Drives DOM
|
|
8
|
+
# parsing and serialization of XML (see Canon::XmlParsing).
|
|
9
|
+
# - HTML support — always Nokogiri on CRuby; moxml has no HTML adapter and
|
|
10
|
+
# leptris no HTML parser (see Canon::Html::NokogiriSupport).
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
12
|
+
# SSOT: moxml owns adapter preference (Moxml::Config prefers leptris when
|
|
13
|
+
# installed and capable; see XmlParsing.moxml_adapter_name). Canon's
|
|
14
|
+
# default engine is Nokogiri on CRuby until leptris WINS somewhere
|
|
15
|
+
# measurably: conformance parity is complete (engine_parity_spec 12/12),
|
|
16
|
+
# but the leptris SAX driver is blocked by a data-corruption bug
|
|
17
|
+
# (leptris-ruby#95) and the record-based conversion runs ~0.55x Nokogiri
|
|
18
|
+
# (CI performance gate: -42..45% on from_xml) pending lighter record
|
|
19
|
+
# emission upstream. Flipping the default is the one-line change in
|
|
20
|
+
# #detect; CANON_XML_BACKEND=moxml opts in today. Under Opal the engine
|
|
21
|
+
# is moxml (rexml adapter).
|
|
13
22
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# environments where Nokogiri is unavailable.
|
|
23
|
+
# HTML stays on Nokogiri on CRuby (Canon::Html::NokogiriSupport) and the
|
|
24
|
+
# pretty-printers keep the Nokogiri pipeline — pretty-printed bytes are
|
|
25
|
+
# canon's product (moxml#129).
|
|
18
26
|
module XmlBackend
|
|
27
|
+
VALID_BACKENDS = %i[nokogiri moxml].freeze
|
|
28
|
+
|
|
19
29
|
class << self
|
|
20
30
|
def active
|
|
21
|
-
@active ||= detect
|
|
31
|
+
@active ||= forced || detect
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
def nokogiri?
|
|
@@ -35,14 +45,25 @@ module Canon
|
|
|
35
45
|
|
|
36
46
|
private
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
# Escape hatch and A/B test switch: CANON_XML_BACKEND=nokogiri|moxml.
|
|
49
|
+
def forced
|
|
50
|
+
value = ENV.fetch("CANON_XML_BACKEND", nil)
|
|
51
|
+
return nil if value.nil? || value.empty?
|
|
52
|
+
|
|
53
|
+
backend = value.to_sym
|
|
54
|
+
unless VALID_BACKENDS.include?(backend)
|
|
55
|
+
raise Canon::Error,
|
|
56
|
+
"Invalid CANON_XML_BACKEND: #{value.inspect}. " \
|
|
57
|
+
"Must be one of: #{VALID_BACKENDS.join(', ')}"
|
|
45
58
|
end
|
|
59
|
+
|
|
60
|
+
backend
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def detect
|
|
64
|
+
return :moxml if RUBY_ENGINE == "opal"
|
|
65
|
+
|
|
66
|
+
:nokogiri
|
|
46
67
|
end
|
|
47
68
|
end
|
|
48
69
|
end
|
data/lib/canon/xml_parsing.rb
CHANGED
|
@@ -3,18 +3,35 @@
|
|
|
3
3
|
module Canon
|
|
4
4
|
# Backend-agnostic XML parsing, serialization, and type dispatch.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Responsibilities (MECE):
|
|
7
|
+
# - Engine actions: `parse` selects the XML engine chosen by
|
|
8
|
+
# Canon::XmlBackend (raw Nokogiri or moxml with its resolved adapter —
|
|
9
|
+
# leptris when installed, nokogiri otherwise).
|
|
10
|
+
# - Node type dispatch: type queries and traversal answer for ANY
|
|
11
|
+
# recognized node (Nokogiri or moxml) regardless of the active engine —
|
|
12
|
+
# callers may hand us nodes from either library (user input, format
|
|
13
|
+
# detection). Dispatch is by node type, never by backend.
|
|
10
14
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
15
|
+
# `defined?(Nokogiri)` guards keep Nokogiri constants unresolved under
|
|
16
|
+
# Opal (no NameError at runtime).
|
|
17
|
+
#
|
|
18
|
+
# OCP: adding a new engine means extending Canon::XmlBackend detection
|
|
19
|
+
# plus the node-type union here; all comparator/formatter code stays
|
|
20
|
+
# untouched because it goes through this module.
|
|
14
21
|
module XmlParsing
|
|
15
22
|
class << self
|
|
16
23
|
def moxml_context
|
|
17
|
-
|
|
24
|
+
# Opal needs the explicit rexml adapter: stock oga requires its
|
|
25
|
+
# C extension there. CRuby defers to moxml's preferred adapter
|
|
26
|
+
# (leptris when installed and capable, nokogiri otherwise).
|
|
27
|
+
@moxml_context ||= Moxml.new(RUBY_ENGINE == "opal" ? :rexml : nil)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The adapter moxml resolved on this runtime. This is the single
|
|
31
|
+
# source of truth for engine capability: moxml owns the preference
|
|
32
|
+
# order, canon never probes gems itself.
|
|
33
|
+
def moxml_adapter_name
|
|
34
|
+
moxml_context.config.adapter_name
|
|
18
35
|
end
|
|
19
36
|
|
|
20
37
|
# --- Parsing ---
|
|
@@ -31,7 +48,7 @@ module Canon
|
|
|
31
48
|
if XmlBackend.nokogiri?
|
|
32
49
|
Nokogiri::XML.fragment(xml_string).children.to_a
|
|
33
50
|
else
|
|
34
|
-
doc = moxml_context.parse("<__frag__>#{xml_string}</__frag__>")
|
|
51
|
+
doc = moxml_context.parse("<__frag__>#{xml_string}</__frag__>", readonly: true)
|
|
35
52
|
doc.root.children.to_a
|
|
36
53
|
end
|
|
37
54
|
end
|
|
@@ -39,113 +56,88 @@ module Canon
|
|
|
39
56
|
# --- Serialization ---
|
|
40
57
|
|
|
41
58
|
def serialize(node)
|
|
42
|
-
if
|
|
43
|
-
|
|
59
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Document)
|
|
60
|
+
node.to_xml(encoding: "UTF-8")
|
|
44
61
|
else
|
|
45
|
-
|
|
62
|
+
node.to_xml
|
|
46
63
|
end
|
|
47
64
|
end
|
|
48
65
|
|
|
49
|
-
# --- Type checks (
|
|
50
|
-
#
|
|
51
|
-
# Both Nokogiri and Moxml are loaded as dependencies. XmlBackend
|
|
52
|
-
# determines which is used for *parsing*, but nodes from either
|
|
53
|
-
# library may flow through comparison code (e.g. tests, format
|
|
54
|
-
# detection). Under Nokogiri backend, both types are checked.
|
|
66
|
+
# --- Type checks (any recognized engine node) ---
|
|
55
67
|
|
|
56
68
|
def document?(obj)
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
obj.is_a?(Moxml::Document)
|
|
61
|
-
end
|
|
69
|
+
return true if defined?(Nokogiri) && obj.is_a?(Nokogiri::XML::Document)
|
|
70
|
+
|
|
71
|
+
obj.is_a?(Moxml::Document)
|
|
62
72
|
end
|
|
63
73
|
|
|
64
74
|
def xml_node?(obj)
|
|
65
|
-
if
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
obj.is_a?(Moxml::Node)
|
|
69
|
-
end
|
|
75
|
+
return true if defined?(Nokogiri) && obj.is_a?(Nokogiri::XML::Node)
|
|
76
|
+
|
|
77
|
+
obj.is_a?(Moxml::Node)
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
def element?(node)
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
node.is_a?(Moxml::Element)
|
|
77
|
-
end
|
|
81
|
+
return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
|
|
82
|
+
|
|
83
|
+
node.is_a?(Moxml::Element)
|
|
78
84
|
end
|
|
79
85
|
|
|
80
86
|
def text_node?(node)
|
|
81
|
-
if
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
node.is_a?(Moxml::Text)
|
|
85
|
-
end
|
|
87
|
+
return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Text)
|
|
88
|
+
|
|
89
|
+
node.is_a?(Moxml::Text)
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
def comment?(node)
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
node.is_a?(Moxml::Comment)
|
|
93
|
-
end
|
|
93
|
+
return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Comment)
|
|
94
|
+
|
|
95
|
+
node.is_a?(Moxml::Comment)
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def cdata?(node)
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
node.is_a?(Moxml::Cdata)
|
|
101
|
-
end
|
|
99
|
+
return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::CDATA)
|
|
100
|
+
|
|
101
|
+
node.is_a?(Moxml::Cdata)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def processing_instruction?(node)
|
|
105
|
-
if
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
node.is_a?(Moxml::ProcessingInstruction)
|
|
109
|
-
end
|
|
105
|
+
return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::ProcessingInstruction)
|
|
106
|
+
|
|
107
|
+
node.is_a?(Moxml::ProcessingInstruction)
|
|
110
108
|
end
|
|
111
109
|
|
|
112
110
|
def document_fragment?(obj)
|
|
113
|
-
|
|
114
|
-
obj.is_a?(Nokogiri::XML::DocumentFragment)
|
|
115
|
-
else
|
|
116
|
-
false
|
|
117
|
-
end
|
|
111
|
+
defined?(Nokogiri) && obj.is_a?(Nokogiri::XML::DocumentFragment)
|
|
118
112
|
end
|
|
119
113
|
|
|
120
114
|
def dtd?(node)
|
|
121
|
-
|
|
122
|
-
node.is_a?(Nokogiri::XML::DTD)
|
|
123
|
-
else
|
|
124
|
-
false
|
|
125
|
-
end
|
|
115
|
+
defined?(Nokogiri) && node.is_a?(Nokogiri::XML::DTD)
|
|
126
116
|
end
|
|
127
117
|
|
|
128
118
|
# --- Node traversal ---
|
|
129
119
|
|
|
130
120
|
def children(node)
|
|
131
|
-
if
|
|
132
|
-
node.
|
|
121
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
|
|
122
|
+
node.children.to_a
|
|
123
|
+
elsif node.is_a?(Moxml::Node)
|
|
124
|
+
node.children.to_a
|
|
133
125
|
else
|
|
134
|
-
|
|
126
|
+
[]
|
|
135
127
|
end
|
|
136
128
|
end
|
|
137
129
|
|
|
138
130
|
def name(node)
|
|
139
|
-
if
|
|
140
|
-
node.
|
|
141
|
-
|
|
142
|
-
node.
|
|
131
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
|
|
132
|
+
node.name
|
|
133
|
+
elsif node.is_a?(Moxml::Node)
|
|
134
|
+
node.name
|
|
143
135
|
end
|
|
144
136
|
end
|
|
145
137
|
|
|
146
138
|
def text_content(node)
|
|
147
|
-
if
|
|
148
|
-
node.
|
|
139
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
|
|
140
|
+
node.content
|
|
149
141
|
else
|
|
150
142
|
case node
|
|
151
143
|
when Moxml::Text, Moxml::Cdata, Moxml::Comment
|
|
@@ -159,26 +151,30 @@ module Canon
|
|
|
159
151
|
end
|
|
160
152
|
|
|
161
153
|
def attributes(node)
|
|
162
|
-
if
|
|
163
|
-
node.
|
|
154
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
|
|
155
|
+
node.attributes.values
|
|
156
|
+
elsif node.is_a?(Moxml::Element)
|
|
157
|
+
node.attributes
|
|
164
158
|
else
|
|
165
|
-
|
|
159
|
+
[]
|
|
166
160
|
end
|
|
167
161
|
end
|
|
168
162
|
|
|
169
163
|
def attribute_value(node, attr_name)
|
|
170
|
-
if
|
|
171
|
-
node
|
|
172
|
-
|
|
173
|
-
node
|
|
164
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
|
|
165
|
+
node[attr_name.to_s]
|
|
166
|
+
elsif node.is_a?(Moxml::Element)
|
|
167
|
+
node[attr_name.to_s]
|
|
174
168
|
end
|
|
175
169
|
end
|
|
176
170
|
|
|
177
171
|
def namespace_definitions(node)
|
|
178
|
-
if
|
|
179
|
-
node.
|
|
172
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
|
|
173
|
+
node.namespace_definitions
|
|
174
|
+
elsif node.is_a?(Moxml::Element)
|
|
175
|
+
node.namespace_definitions
|
|
180
176
|
else
|
|
181
|
-
|
|
177
|
+
[]
|
|
182
178
|
end
|
|
183
179
|
end
|
|
184
180
|
|
|
@@ -191,34 +187,26 @@ module Canon
|
|
|
191
187
|
end
|
|
192
188
|
|
|
193
189
|
def namespace_uri(node)
|
|
194
|
-
if
|
|
195
|
-
node.namespace&.href
|
|
190
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
|
|
191
|
+
node.namespace&.href
|
|
196
192
|
elsif node.is_a?(Moxml::Element)
|
|
197
193
|
node.namespace_uri
|
|
198
194
|
end
|
|
199
195
|
end
|
|
200
196
|
|
|
201
|
-
# Returns a symbol for all
|
|
197
|
+
# Returns a symbol for all engines (:element, :text, :comment, etc.)
|
|
202
198
|
# or nil for unrecognised nodes.
|
|
203
199
|
def node_type(node)
|
|
204
|
-
if
|
|
200
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
|
|
205
201
|
nokogiri_node_type(node)
|
|
206
|
-
|
|
202
|
+
elsif node.is_a?(Moxml::Node)
|
|
207
203
|
moxml_node_type(node)
|
|
208
204
|
end
|
|
209
205
|
end
|
|
210
206
|
|
|
211
|
-
def canonicalize(node, options = {})
|
|
212
|
-
if XmlBackend.nokogiri?
|
|
213
|
-
node.canonicalize(options)
|
|
214
|
-
else
|
|
215
|
-
moxml_canonicalize(node, options)
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
207
|
private
|
|
220
208
|
|
|
221
|
-
# --- Nokogiri
|
|
209
|
+
# --- Nokogiri engine ---
|
|
222
210
|
|
|
223
211
|
def nokogiri_type_map
|
|
224
212
|
@nokogiri_type_map ||= {
|
|
@@ -235,8 +223,6 @@ module Canon
|
|
|
235
223
|
end
|
|
236
224
|
|
|
237
225
|
def nokogiri_node_type(node)
|
|
238
|
-
return nil unless node.is_a?(Nokogiri::XML::Node)
|
|
239
|
-
|
|
240
226
|
nokogiri_type_map[node.node_type]
|
|
241
227
|
end
|
|
242
228
|
|
|
@@ -246,26 +232,13 @@ module Canon
|
|
|
246
232
|
doc
|
|
247
233
|
end
|
|
248
234
|
|
|
249
|
-
|
|
250
|
-
if node.is_a?(Nokogiri::XML::Document)
|
|
251
|
-
node.to_xml(encoding: "UTF-8")
|
|
252
|
-
else
|
|
253
|
-
node.to_xml
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# --- Moxml backend ---
|
|
235
|
+
# --- Moxml engine ---
|
|
258
236
|
|
|
237
|
+
# Readonly: canon only ever reads engine documents (conversion,
|
|
238
|
+
# serialization) — leptris memoizes reads on readonly documents,
|
|
239
|
+
# and mutation is refused loudly rather than silently corrupting.
|
|
259
240
|
def moxml_parse(xml_string, _options)
|
|
260
|
-
moxml_context.parse(xml_string)
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def moxml_serialize(node)
|
|
264
|
-
node.to_xml
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def moxml_canonicalize(node, _options)
|
|
268
|
-
node.to_xml
|
|
241
|
+
moxml_context.parse(xml_string, readonly: true)
|
|
269
242
|
end
|
|
270
243
|
|
|
271
244
|
def moxml_node_type(node)
|
data/lib/canon.rb
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
require "canon/version"
|
|
4
|
+
require "canon/errors"
|
|
5
|
+
require "nokogiri" unless RUBY_ENGINE == "opal"
|
|
6
|
+
require "canon/xml_backend"
|
|
7
|
+
require "canon/xml_parsing"
|
|
8
|
+
require "canon/config"
|
|
9
|
+
require "canon/data_model"
|
|
10
|
+
require "canon/xml"
|
|
11
|
+
require "canon/html"
|
|
12
|
+
require "canon/formatters"
|
|
13
|
+
require "canon/comparison"
|
|
14
|
+
require "canon/diff"
|
|
15
|
+
require "canon/tree_diff"
|
|
16
|
+
require "canon/validators"
|
|
17
|
+
require "canon/pretty_printer"
|
|
18
|
+
require "canon/options"
|
|
19
|
+
require "canon/commands"
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
require "canon/rspec_matchers" if defined?(RSpec.configure)
|
|
19
22
|
|
|
20
23
|
module Canon
|
|
24
|
+
autoload :Cache, "canon/cache"
|
|
25
|
+
autoload :Cli, "canon/cli"
|
|
26
|
+
autoload :ColorDetector, "canon/color_detector"
|
|
27
|
+
autoload :DiffFormatter, "canon/diff_formatter"
|
|
28
|
+
|
|
21
29
|
SUPPORTED_FORMATS = %i[xml yaml json html html4 html5 string].freeze
|
|
22
30
|
|
|
23
31
|
# Format content based on the specified format type
|
|
@@ -36,7 +44,7 @@ module Canon
|
|
|
36
44
|
get_formatter(format).parse(content)
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
# rubocop:disable Metrics/MethodLength
|
|
47
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
40
48
|
def self.get_formatter(format)
|
|
41
49
|
case format.to_sym
|
|
42
50
|
when :xml
|
|
@@ -55,11 +63,12 @@ module Canon
|
|
|
55
63
|
raise Error, "Unsupported format: #{format}"
|
|
56
64
|
end
|
|
57
65
|
end
|
|
58
|
-
# rubocop:enable Metrics/MethodLength
|
|
59
66
|
|
|
60
67
|
# Define shorthand methods for each supported format
|
|
61
68
|
# Creates parse_{format} and format_{format} methods
|
|
62
69
|
SUPPORTED_FORMATS.each do |format|
|
|
70
|
+
next if format == :string # comparison-only format, no formatter
|
|
71
|
+
|
|
63
72
|
define_singleton_method("parse_#{format}") do |content|
|
|
64
73
|
parse(content, format)
|
|
65
74
|
end
|
|
@@ -96,7 +96,11 @@ class BenchmarkRunner
|
|
|
96
96
|
def self.env_info(ruby_version, platform)
|
|
97
97
|
puts
|
|
98
98
|
puts " #{DIM}Environment:#{CLEAR}"
|
|
99
|
+
engine = Canon::XmlBackend.active
|
|
100
|
+
engine_info = "XML engine: #{engine}"
|
|
101
|
+
engine_info += " (forced)" unless ENV["CANON_XML_BACKEND"].to_s.empty?
|
|
99
102
|
puts " #{VL} Ruby #{ruby_version} on #{platform}#{' ' * (60 - ruby_version.length - platform.length)}#{VL}"
|
|
103
|
+
puts " #{VL} #{engine_info}#{' ' * (60 - engine_info.length)}#{VL}"
|
|
100
104
|
puts " #{DIM}#{BL}#{HL * 76}#{BR}#{CLEAR}"
|
|
101
105
|
puts
|
|
102
106
|
end
|