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
|
@@ -10,7 +10,7 @@ module Canon
|
|
|
10
10
|
class HtmlFormatter < BaseFormatter
|
|
11
11
|
attr_reader :html_version
|
|
12
12
|
|
|
13
|
-
# rubocop:disable Metrics/ParameterLists
|
|
13
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
14
14
|
def initialize(use_color: true, context_lines: 3,
|
|
15
15
|
diff_grouping_lines: nil, visualization_map: nil,
|
|
16
16
|
html_version: :html4, show_diffs: :all, differences: [],
|
|
@@ -25,7 +25,6 @@ module Canon
|
|
|
25
25
|
character_visualization: character_visualization)
|
|
26
26
|
@html_version = html_version
|
|
27
27
|
end
|
|
28
|
-
# rubocop:enable Metrics/ParameterLists
|
|
29
28
|
|
|
30
29
|
# Format DOM-guided HTML diff
|
|
31
30
|
#
|
|
@@ -9,7 +9,7 @@ module Canon
|
|
|
9
9
|
# Receives preprocessed document strings from the DiffFormatter facade
|
|
10
10
|
# and delegates to format-specific ByLine formatters (XML, HTML, JSON, YAML).
|
|
11
11
|
class ByLineFormatter
|
|
12
|
-
# rubocop:disable Metrics/ParameterLists
|
|
12
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
13
13
|
def initialize(use_color:, visualization_map:, context_lines:,
|
|
14
14
|
diff_grouping_lines:, show_diffs:, character_visualization:,
|
|
15
15
|
legacy_terminal:, diff_mode:)
|
|
@@ -22,7 +22,6 @@ module Canon
|
|
|
22
22
|
@legacy_terminal = legacy_terminal
|
|
23
23
|
@diff_mode = diff_mode
|
|
24
24
|
end
|
|
25
|
-
# rubocop:enable Metrics/ParameterLists
|
|
26
25
|
|
|
27
26
|
# Format a line-by-line diff between two documents.
|
|
28
27
|
#
|
|
@@ -226,7 +226,7 @@ module Canon
|
|
|
226
226
|
end
|
|
227
227
|
|
|
228
228
|
def serialize_backend_node_compact(node)
|
|
229
|
-
if
|
|
229
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
|
|
230
230
|
serialize_nokogiri_node_compact(node)
|
|
231
231
|
elsif node.is_a?(Canon::Xml::Node)
|
|
232
232
|
serialize_node_compact(node)
|
|
@@ -256,7 +256,7 @@ module Canon
|
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
def serialize_backend_open_tag(node)
|
|
259
|
-
if
|
|
259
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
|
|
260
260
|
tag = node.name.to_s
|
|
261
261
|
attrs = node.attribute_nodes.map do |a|
|
|
262
262
|
" #{a.name}=\"#{CGI.escapeHTML(a.value.to_s)}\""
|
|
@@ -125,11 +125,11 @@ module Canon
|
|
|
125
125
|
},
|
|
126
126
|
added: {
|
|
127
127
|
marker: { color: :green, bg: nil, bold: false },
|
|
128
|
-
content: { color: :green,
|
|
128
|
+
content: { color: :green, bg: nil, bold: false,
|
|
129
129
|
underline: false, strikethrough: false },
|
|
130
130
|
},
|
|
131
131
|
changed: {
|
|
132
|
-
marker: { color: :yellow,
|
|
132
|
+
marker: { color: :yellow, bg: nil, bold: true },
|
|
133
133
|
content_old: { color: :bright_red, bg: nil, bold: false,
|
|
134
134
|
underline: false, strikethrough: true },
|
|
135
135
|
content_new: { color: :bright_green, bg: nil, bold: false,
|
data/lib/canon/diff_formatter.rb
CHANGED
|
@@ -311,11 +311,10 @@ module Canon
|
|
|
311
311
|
# In by-line mode with both docs present, always use by-line diff
|
|
312
312
|
if @mode == :by_line && doc1 && doc2
|
|
313
313
|
doc1, doc2 = apply_display_preprocessing(doc1, doc2, format)
|
|
314
|
-
# rubocop:disable Layout/HashAlignment
|
|
314
|
+
# rubocop:disable-next Layout/HashAlignment
|
|
315
315
|
return by_line_formatter.format(doc1, doc2, format: format,
|
|
316
316
|
html_version: html_version,
|
|
317
317
|
differences: differences)
|
|
318
|
-
# rubocop:enable Layout/HashAlignment
|
|
319
318
|
end
|
|
320
319
|
|
|
321
320
|
# In pretty_diff mode, always use text-LCS diff (bypasses DiffNodeMapper).
|
|
@@ -343,11 +342,10 @@ module Canon
|
|
|
343
342
|
doc1, doc2 = apply_display_preprocessing(doc1, doc2,
|
|
344
343
|
format)
|
|
345
344
|
end
|
|
346
|
-
# rubocop:disable Layout/HashAlignment
|
|
345
|
+
# rubocop:disable-next Layout/HashAlignment
|
|
347
346
|
by_line_formatter.format(doc1, doc2, format: format,
|
|
348
347
|
html_version: html_version,
|
|
349
348
|
differences: differences)
|
|
350
|
-
# rubocop:enable Layout/HashAlignment
|
|
351
349
|
when :pretty_diff
|
|
352
350
|
d1, d2 = if doc1 && doc2
|
|
353
351
|
apply_display_preprocessing(doc1, doc2,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Html
|
|
5
|
+
# Nokogiri node utilities for the HTML pipeline.
|
|
6
|
+
#
|
|
7
|
+
# HTML parsing is Nokogiri's job on every CRuby runtime regardless of
|
|
8
|
+
# the XML engine (XmlBackend): moxml has no HTML adapter and leptris no
|
|
9
|
+
# HTML parser. Under Opal Nokogiri is unavailable and these helpers
|
|
10
|
+
# degrade the same way the moxml backend always did (false / nil /
|
|
11
|
+
# raise), which keeps HTML comparison unsupported there.
|
|
12
|
+
module NokogiriSupport
|
|
13
|
+
class << self
|
|
14
|
+
# Whether the node is a document fragment (any variant).
|
|
15
|
+
def document_fragment?(node)
|
|
16
|
+
return false unless defined?(Nokogiri)
|
|
17
|
+
|
|
18
|
+
node.is_a?(Nokogiri::XML::DocumentFragment) ||
|
|
19
|
+
node.is_a?(Nokogiri::HTML4::DocumentFragment) ||
|
|
20
|
+
node.is_a?(Nokogiri::HTML5::DocumentFragment)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether the node is an HTML document (any variant).
|
|
24
|
+
def html_document?(node)
|
|
25
|
+
return false unless defined?(Nokogiri)
|
|
26
|
+
|
|
27
|
+
node.is_a?(Nokogiri::HTML::Document) ||
|
|
28
|
+
node.is_a?(Nokogiri::HTML4::Document) ||
|
|
29
|
+
node.is_a?(Nokogiri::HTML5::Document)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Detect HTML version from a Nokogiri node.
|
|
33
|
+
# Returns :html5 or :html4. Defaults to :html5 for non-Nokogiri nodes.
|
|
34
|
+
def html_version_from_node(node)
|
|
35
|
+
return :html5 unless defined?(Nokogiri)
|
|
36
|
+
|
|
37
|
+
if node.is_a?(Nokogiri::HTML5::Document) ||
|
|
38
|
+
node.is_a?(Nokogiri::HTML5::DocumentFragment)
|
|
39
|
+
:html5
|
|
40
|
+
elsif node.is_a?(Nokogiri::HTML4::Document) ||
|
|
41
|
+
node.is_a?(Nokogiri::HTML4::DocumentFragment)
|
|
42
|
+
:html4
|
|
43
|
+
else
|
|
44
|
+
:html5
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parse an HTML string into an XML fragment for the XML comparator.
|
|
49
|
+
def xml_fragment(html_string)
|
|
50
|
+
unless defined?(Nokogiri)
|
|
51
|
+
raise Canon::Error,
|
|
52
|
+
"HTML fragment parsing requires Nokogiri"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Nokogiri::XML.fragment(html_string)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/canon/html.rb
CHANGED
|
@@ -11,7 +11,12 @@ module Canon
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def format(xml_string)
|
|
14
|
-
|
|
14
|
+
# Output parity: pretty-printed bytes are canon's product. The
|
|
15
|
+
# moxml serializer cannot yet match Nokogiri's output (empty
|
|
16
|
+
# element expansion, declaration placement, tab indent), so the
|
|
17
|
+
# Nokogiri pipeline is used whenever available — the moxml branch
|
|
18
|
+
# is the Opal fallback.
|
|
19
|
+
if defined?(Nokogiri)
|
|
15
20
|
nokogiri_format(xml_string)
|
|
16
21
|
else
|
|
17
22
|
moxml_format(xml_string)
|
|
@@ -153,12 +153,12 @@ module Canon
|
|
|
153
153
|
# @return [String] Serialized XML, one node per line, with content
|
|
154
154
|
# whitespace visualized at line boundaries
|
|
155
155
|
def format(xml_string)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Nokogiri::HTML5(xml_string)
|
|
156
|
+
# Output parity: see PrettyPrinter::Xml — the Nokogiri pipeline
|
|
157
|
+
# produces canon's byte-stable output; moxml is the Opal fallback.
|
|
158
|
+
doc = if defined?(Nokogiri)
|
|
159
|
+
@html_mode ? Nokogiri::HTML5(xml_string) : Nokogiri::XML(xml_string)
|
|
160
160
|
else
|
|
161
|
-
|
|
161
|
+
Canon::XmlParsing.parse(xml_string)
|
|
162
162
|
end
|
|
163
163
|
lines = []
|
|
164
164
|
|
|
@@ -356,10 +356,11 @@ module Canon
|
|
|
356
356
|
end
|
|
357
357
|
|
|
358
358
|
# True when the supplied source node is a Nokogiri node that
|
|
359
|
-
# exposes an XPath via +path+.
|
|
360
|
-
#
|
|
359
|
+
# exposes an XPath via +path+. Nokogiri nodes arrive from user
|
|
360
|
+
# input under any engine, so this is a type check; the constant
|
|
361
|
+
# is guarded for Opal.
|
|
361
362
|
def nokogiri_source?(node)
|
|
362
|
-
return false unless
|
|
363
|
+
return false unless defined?(Nokogiri)
|
|
363
364
|
|
|
364
365
|
node.is_a?(Nokogiri::XML::Node)
|
|
365
366
|
end
|
|
@@ -66,7 +66,7 @@ module Canon
|
|
|
66
66
|
return false unless node
|
|
67
67
|
return false unless Canon::Comparison::NodeInspector.element_node?(node)
|
|
68
68
|
|
|
69
|
-
Canon::
|
|
69
|
+
Canon::XmlParsing.xml_node?(node) && defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
|
|
70
70
|
end
|
|
71
71
|
private_class_method :backend_element?
|
|
72
72
|
|
data/lib/canon/version.rb
CHANGED
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -159,7 +159,7 @@ preserve_whitespace: false)
|
|
|
159
159
|
when Nokogiri::XML::Element
|
|
160
160
|
build_element_node(nokogiri_node,
|
|
161
161
|
preserve_whitespace: preserve_whitespace)
|
|
162
|
-
when Nokogiri::XML::Text
|
|
162
|
+
when Nokogiri::XML::Text, Nokogiri::XML::CDATA
|
|
163
163
|
build_text_node(nokogiri_node,
|
|
164
164
|
preserve_whitespace: preserve_whitespace)
|
|
165
165
|
when Nokogiri::XML::Comment
|
|
@@ -222,7 +222,10 @@ preserve_whitespace: false)
|
|
|
222
222
|
end
|
|
223
223
|
|
|
224
224
|
def self.build_attribute_nodes(nokogiri_element, element)
|
|
225
|
-
|
|
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|
|
|
226
229
|
attr_node = Nodes::AttributeNode.new(
|
|
227
230
|
name: attr.name,
|
|
228
231
|
value: attr.value,
|
|
@@ -259,26 +262,183 @@ preserve_whitespace: false)
|
|
|
259
262
|
|
|
260
263
|
def self.from_moxml_xml(xml_string, preserve_whitespace:)
|
|
261
264
|
doc = Canon::XmlParsing.parse(xml_string)
|
|
262
|
-
|
|
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
|
|
263
290
|
end
|
|
264
291
|
|
|
265
292
|
def self.build_from_moxml(moxml_doc, preserve_whitespace: false)
|
|
266
293
|
root = Nodes::RootNode.new
|
|
267
294
|
|
|
268
295
|
if moxml_doc.is_a?(Moxml::Document) && moxml_doc.root
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
315
|
end
|
|
272
316
|
|
|
273
317
|
root
|
|
274
318
|
end
|
|
275
319
|
|
|
276
|
-
|
|
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)
|
|
277
436
|
case node
|
|
278
437
|
when Moxml::Element
|
|
279
438
|
build_moxml_element_node(node,
|
|
280
|
-
preserve_whitespace: preserve_whitespace
|
|
281
|
-
|
|
439
|
+
preserve_whitespace: preserve_whitespace,
|
|
440
|
+
inherited_namespaces: inherited_namespaces)
|
|
441
|
+
when Moxml::Text, Moxml::Cdata
|
|
282
442
|
build_moxml_text_node(node, preserve_whitespace: preserve_whitespace)
|
|
283
443
|
when Moxml::Comment
|
|
284
444
|
build_moxml_comment_node(node)
|
|
@@ -288,7 +448,8 @@ preserve_whitespace: false)
|
|
|
288
448
|
end
|
|
289
449
|
|
|
290
450
|
def self.build_moxml_element_node(moxml_element,
|
|
291
|
-
preserve_whitespace: false
|
|
451
|
+
preserve_whitespace: false,
|
|
452
|
+
inherited_namespaces: nil)
|
|
292
453
|
ns = moxml_element.namespace
|
|
293
454
|
element = Nodes::ElementNode.new(
|
|
294
455
|
name: moxml_element.name,
|
|
@@ -296,44 +457,56 @@ preserve_whitespace: false)
|
|
|
296
457
|
prefix: ns&.prefix,
|
|
297
458
|
)
|
|
298
459
|
|
|
299
|
-
|
|
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
|
+
|
|
300
468
|
build_moxml_attribute_nodes(moxml_element, element)
|
|
301
469
|
|
|
302
470
|
moxml_element.children.each do |child|
|
|
303
471
|
node = build_moxml_node(child,
|
|
304
|
-
preserve_whitespace: preserve_whitespace
|
|
472
|
+
preserve_whitespace: preserve_whitespace,
|
|
473
|
+
inherited_namespaces: scope)
|
|
305
474
|
element.add_child(node) if node
|
|
306
475
|
end
|
|
307
476
|
|
|
308
477
|
element
|
|
309
478
|
end
|
|
310
479
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
unless element.namespace_nodes.any? do |n|
|
|
321
|
-
n.prefix == "xml"
|
|
322
|
-
end
|
|
323
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
324
|
-
prefix: "xml",
|
|
325
|
-
uri: "http://www.w3.org/XML/1998/namespace",
|
|
326
|
-
))
|
|
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
|
|
327
489
|
end
|
|
490
|
+
scope
|
|
328
491
|
end
|
|
329
492
|
|
|
330
493
|
def self.build_moxml_attribute_nodes(moxml_element, element)
|
|
494
|
+
seen = {}
|
|
331
495
|
moxml_element.attributes.each do |attr|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
))
|
|
337
510
|
end
|
|
338
511
|
end
|
|
339
512
|
|
|
@@ -354,7 +527,7 @@ preserve_whitespace: false)
|
|
|
354
527
|
def self.build_moxml_pi_node(moxml_pi)
|
|
355
528
|
Nodes::ProcessingInstructionNode.new(
|
|
356
529
|
target: moxml_pi.target,
|
|
357
|
-
data: moxml_pi.
|
|
530
|
+
data: moxml_pi.content,
|
|
358
531
|
)
|
|
359
532
|
end
|
|
360
533
|
end
|
data/lib/canon/xml/processor.rb
CHANGED
|
@@ -130,10 +130,18 @@ module Canon
|
|
|
130
130
|
# Add line break before PI if it's outside document element
|
|
131
131
|
output << "\n" if parent_element.nil? && output.length.positive?
|
|
132
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
|
+
|
|
133
141
|
output << "<?" << node.target
|
|
134
142
|
|
|
135
|
-
unless
|
|
136
|
-
output << " " <<
|
|
143
|
+
unless data.empty?
|
|
144
|
+
output << " " << data
|
|
137
145
|
end
|
|
138
146
|
|
|
139
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
|