canon 0.2.12 → 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 +54 -7
- data/CLAUDE.md +197 -0
- data/lib/canon/commands/diff_command.rb +1 -2
- data/lib/canon/comparison/comparison_result.rb +1 -2
- data/lib/canon/comparison/diff_node_builder.rb +1 -1
- data/lib/canon/comparison/format_detector.rb +27 -29
- data/lib/canon/comparison/html_comparator.rb +14 -14
- data/lib/canon/comparison/node_inspector.rb +5 -9
- data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +1 -1
- data/lib/canon/comparison/xml_comparator/node_parser.rb +3 -3
- data/lib/canon/comparison/xml_node_comparison.rb +2 -2
- data/lib/canon/comparison.rb +1 -1
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +1 -2
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +1 -2
- data/lib/canon/diff_formatter/by_line_formatter.rb +1 -2
- data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +2 -2
- data/lib/canon/diff_formatter/theme.rb +2 -2
- data/lib/canon/diff_formatter.rb +2 -4
- data/lib/canon/html/nokogiri_support.rb +60 -0
- data/lib/canon/html.rb +1 -0
- data/lib/canon/pretty_printer/xml.rb +6 -1
- data/lib/canon/pretty_printer/xml_normalized.rb +5 -5
- data/lib/canon/tree_diff/core/tree_node.rb +4 -3
- data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +1 -1
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +206 -33
- data/lib/canon/xml/processor.rb +10 -2
- 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 -20
- data/lib/canon/xml.rb +1 -0
- data/lib/canon/xml_backend.rb +39 -54
- data/lib/canon/xml_parsing.rb +89 -118
- data/lib/canon.rb +1 -2
- data/lib/tasks/benchmark_runner.rb +4 -0
- data/lib/tasks/performance.rake +2 -0
- data/lib/tasks/performance_report.rb +12 -8
- metadata +9 -4
|
@@ -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
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri" unless RUBY_ENGINE == "opal"
|
|
4
|
-
|
|
5
3
|
module Canon
|
|
6
4
|
module Xml
|
|
7
|
-
# Builds Canon::Xml::Node tree
|
|
8
|
-
#
|
|
9
|
-
# This is MUCH faster than DOM parsing + conversion because:
|
|
10
|
-
# 1. No intermediate Nokogiri DOM tree (saves ~60ms)
|
|
11
|
-
# 2. No tree traversal to build Canon (saves ~1200ms)
|
|
12
|
-
# 3. No memory overhead of two complete DOM trees
|
|
5
|
+
# Builds Canon::Xml::Node tree from SAX events.
|
|
13
6
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
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.
|
|
16
11
|
#
|
|
17
12
|
# Usage:
|
|
18
13
|
# root = SaxBuilder.parse(xml_string, preserve_whitespace: false)
|
|
@@ -21,7 +16,7 @@ module Canon
|
|
|
21
16
|
# For C14N, use strip_doctype: true to avoid DTD default attribute expansion:
|
|
22
17
|
# root = SaxBuilder.parse(xml_string, strip_doctype: true)
|
|
23
18
|
#
|
|
24
|
-
class SaxBuilder
|
|
19
|
+
class SaxBuilder
|
|
25
20
|
# Parse XML string and return Canon::Xml::Node tree
|
|
26
21
|
#
|
|
27
22
|
# @param xml_string [String] XML content to parse
|
|
@@ -30,7 +25,7 @@ module Canon
|
|
|
30
25
|
# @return [Nodes::RootNode] Root of the data model tree
|
|
31
26
|
def self.parse(xml_string, preserve_whitespace: false,
|
|
32
27
|
strip_doctype: false)
|
|
33
|
-
# Strip DOCTYPE to prevent
|
|
28
|
+
# Strip DOCTYPE to prevent the SAX engine from expanding DTD default attributes
|
|
34
29
|
# This is needed for C14N which should NOT include default attributes from DTD
|
|
35
30
|
# Use string methods instead of complex regex to avoid ReDoS vulnerability
|
|
36
31
|
if strip_doctype
|
|
@@ -38,8 +33,7 @@ strip_doctype: false)
|
|
|
38
33
|
end
|
|
39
34
|
|
|
40
35
|
builder = new(preserve_whitespace: preserve_whitespace)
|
|
41
|
-
|
|
42
|
-
parser.parse(xml_string)
|
|
36
|
+
Canon::Xml::Sax.parse(xml_string, builder)
|
|
43
37
|
builder.result
|
|
44
38
|
end
|
|
45
39
|
|
|
@@ -79,7 +73,6 @@ strip_doctype: false)
|
|
|
79
73
|
#
|
|
80
74
|
# @param preserve_whitespace [Boolean] Whether to preserve whitespace-only text nodes
|
|
81
75
|
def initialize(preserve_whitespace: false)
|
|
82
|
-
super()
|
|
83
76
|
@preserve_whitespace = preserve_whitespace
|
|
84
77
|
@root = Nodes::RootNode.new
|
|
85
78
|
@stack = [@root]
|
|
@@ -168,13 +161,30 @@ strip_doctype: false)
|
|
|
168
161
|
def characters(string)
|
|
169
162
|
return if string.nil?
|
|
170
163
|
|
|
171
|
-
|
|
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?
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
append_text(string, string)
|
|
178
|
+
end
|
|
175
179
|
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
178
188
|
|
|
179
189
|
# Combine with previous text node if adjacent (SAX can split text content)
|
|
180
190
|
# This MUST happen before whitespace check, because SAX may split "foo "
|
|
@@ -208,6 +218,7 @@ strip_doctype: false)
|
|
|
208
218
|
text = Nodes::TextNode.new(value: decoded_string, original: raw_string)
|
|
209
219
|
parent.add_child(text)
|
|
210
220
|
end
|
|
221
|
+
private :append_text
|
|
211
222
|
|
|
212
223
|
# Called for comments
|
|
213
224
|
#
|
data/lib/canon/xml.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Canon
|
|
|
25
25
|
autoload :Node, "canon/xml/node"
|
|
26
26
|
autoload :Nodes, "canon/xml/nodes"
|
|
27
27
|
autoload :Processor, "canon/xml/processor"
|
|
28
|
+
autoload :Sax, "canon/xml/sax"
|
|
28
29
|
autoload :SaxBuilder, "canon/xml/sax_builder"
|
|
29
30
|
autoload :WhitespaceNormalizer, "canon/xml/whitespace_normalizer"
|
|
30
31
|
autoload :XmlBaseHandler, "canon/xml/xml_base_handler"
|
data/lib/canon/xml_backend.rb
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Canon
|
|
4
|
+
# Selection of the XML engine.
|
|
5
|
+
#
|
|
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).
|
|
11
|
+
#
|
|
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).
|
|
22
|
+
#
|
|
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).
|
|
4
26
|
module XmlBackend
|
|
27
|
+
VALID_BACKENDS = %i[nokogiri moxml].freeze
|
|
28
|
+
|
|
5
29
|
class << self
|
|
6
30
|
def active
|
|
7
|
-
@active ||= detect
|
|
31
|
+
@active ||= forced || detect
|
|
8
32
|
end
|
|
9
33
|
|
|
10
34
|
def nokogiri?
|
|
@@ -19,66 +43,27 @@ module Canon
|
|
|
19
43
|
@active = nil
|
|
20
44
|
end
|
|
21
45
|
|
|
22
|
-
|
|
23
|
-
def document_fragment?(node)
|
|
24
|
-
if nokogiri?
|
|
25
|
-
node.is_a?(Nokogiri::XML::DocumentFragment) ||
|
|
26
|
-
node.is_a?(Nokogiri::HTML4::DocumentFragment) ||
|
|
27
|
-
node.is_a?(Nokogiri::HTML5::DocumentFragment)
|
|
28
|
-
else
|
|
29
|
-
false
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Whether the node is an HTML document (any variant).
|
|
34
|
-
def html_document?(node)
|
|
35
|
-
if nokogiri?
|
|
36
|
-
node.is_a?(Nokogiri::HTML::Document) ||
|
|
37
|
-
node.is_a?(Nokogiri::HTML4::Document) ||
|
|
38
|
-
node.is_a?(Nokogiri::HTML5::Document)
|
|
39
|
-
else
|
|
40
|
-
false
|
|
41
|
-
end
|
|
42
|
-
end
|
|
46
|
+
private
|
|
43
47
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if
|
|
48
|
-
if node.is_a?(Nokogiri::HTML5::Document) ||
|
|
49
|
-
node.is_a?(Nokogiri::HTML5::DocumentFragment)
|
|
50
|
-
:html5
|
|
51
|
-
elsif node.is_a?(Nokogiri::HTML4::Document) ||
|
|
52
|
-
node.is_a?(Nokogiri::HTML4::DocumentFragment)
|
|
53
|
-
:html4
|
|
54
|
-
else
|
|
55
|
-
:html5
|
|
56
|
-
end
|
|
57
|
-
else
|
|
58
|
-
:html5
|
|
59
|
-
end
|
|
60
|
-
end
|
|
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?
|
|
61
52
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if nokogiri?
|
|
65
|
-
Nokogiri::XML.fragment(html_string)
|
|
66
|
-
else
|
|
53
|
+
backend = value.to_sym
|
|
54
|
+
unless VALID_BACKENDS.include?(backend)
|
|
67
55
|
raise Canon::Error,
|
|
68
|
-
"
|
|
56
|
+
"Invalid CANON_XML_BACKEND: #{value.inspect}. " \
|
|
57
|
+
"Must be one of: #{VALID_BACKENDS.join(', ')}"
|
|
69
58
|
end
|
|
70
|
-
end
|
|
71
59
|
|
|
72
|
-
|
|
60
|
+
backend
|
|
61
|
+
end
|
|
73
62
|
|
|
74
63
|
def detect
|
|
75
|
-
if RUBY_ENGINE == "opal"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:nokogiri
|
|
79
|
-
else
|
|
80
|
-
:moxml
|
|
81
|
-
end
|
|
64
|
+
return :moxml if RUBY_ENGINE == "opal"
|
|
65
|
+
|
|
66
|
+
:nokogiri
|
|
82
67
|
end
|
|
83
68
|
end
|
|
84
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,28 +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
|
-
raise Canon::Error,
|
|
269
|
-
"C14N canonicalization is not supported by the moxml backend. " \
|
|
270
|
-
"Use the Nokogiri backend or a different preprocessing option."
|
|
241
|
+
moxml_context.parse(xml_string, readonly: true)
|
|
271
242
|
end
|
|
272
243
|
|
|
273
244
|
def moxml_node_type(node)
|
data/lib/canon.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Canon
|
|
|
44
44
|
get_formatter(format).parse(content)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# rubocop:disable Metrics/MethodLength
|
|
47
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
48
48
|
def self.get_formatter(format)
|
|
49
49
|
case format.to_sym
|
|
50
50
|
when :xml
|
|
@@ -63,7 +63,6 @@ module Canon
|
|
|
63
63
|
raise Error, "Unsupported format: #{format}"
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
|
-
# rubocop:enable Metrics/MethodLength
|
|
67
66
|
|
|
68
67
|
# Define shorthand methods for each supported format
|
|
69
68
|
# Creates parse_{format} and format_{format} methods
|
|
@@ -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
|
data/lib/tasks/performance.rake
CHANGED
|
@@ -32,18 +32,22 @@ module Performance
|
|
|
32
32
|
DEFAULT_ITEMS = Integer(ENV.fetch("CANON_PERF_ITEMS", "50"))
|
|
33
33
|
|
|
34
34
|
BENCHMARKS = {
|
|
35
|
-
xml_parse_dom_simple: { category: "xml_parsing",
|
|
36
|
-
xml_parse_sax_simple: { category: "xml_parsing",
|
|
37
|
-
xml_parse_dom_large: { category: "xml_parsing",
|
|
38
|
-
xml_parse_sax_large: { category: "xml_parsing",
|
|
35
|
+
xml_parse_dom_simple: { category: "xml_parsing", name: "DOM (simple)" },
|
|
36
|
+
xml_parse_sax_simple: { category: "xml_parsing", name: "SAX (simple)" },
|
|
37
|
+
xml_parse_dom_large: { category: "xml_parsing", name: "DOM (large)" },
|
|
38
|
+
xml_parse_sax_large: { category: "xml_parsing", name: "SAX (large)" },
|
|
39
39
|
html_parse_simple: { category: "html_parsing", name: "Simple HTML" },
|
|
40
40
|
html_parse_complex: { category: "html_parsing", name: "Complex HTML" },
|
|
41
|
-
xml_compare_identical: { category: "xml_comparison",
|
|
41
|
+
xml_compare_identical: { category: "xml_comparison",
|
|
42
|
+
name: "Identical XML" },
|
|
42
43
|
xml_compare_similar: { category: "xml_comparison", name: "Similar XML" },
|
|
43
|
-
xml_compare_different: { category: "xml_comparison",
|
|
44
|
-
|
|
44
|
+
xml_compare_different: { category: "xml_comparison",
|
|
45
|
+
name: "Different XML" },
|
|
46
|
+
html_compare_identical: { category: "html_comparison",
|
|
47
|
+
name: "Identical HTML" },
|
|
45
48
|
html_compare_similar: { category: "html_comparison", name: "Similar HTML" },
|
|
46
|
-
html_compare_different: { category: "html_comparison",
|
|
49
|
+
html_compare_different: { category: "html_comparison",
|
|
50
|
+
name: "Different HTML" },
|
|
47
51
|
xml_c14n_format: { category: "formatting", name: "XML C14N" },
|
|
48
52
|
json_format: { category: "formatting", name: "JSON" },
|
|
49
53
|
yaml_format: { category: "formatting", name: "YAML" },
|