canon 0.3.108 → 0.3.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58b0a315d811eb36769d72ef0949180ae5c6eb3b23e4732887783d36483dc8f8
4
- data.tar.gz: 739ec6e878fb031f6ca8814587f3763952422679858a992d66b45a541e85600f
3
+ metadata.gz: 831a2d23d844114132e9a234a4e1452f08aacc36ef0947d80c7100358bbbe209
4
+ data.tar.gz: ff423e64f21b9e5e6c93694c124fc404b95e31ad5b4ed3162262c62023bd5c35
5
5
  SHA512:
6
- metadata.gz: 0a57c1c8062483a762edec9caaef0ee8432f03f88b518163ab9b7836b52c6c671025bf9db6525e870362976c380e1fe0ba7487eb1447e4cdaf2f88538744e89d
7
- data.tar.gz: 17a52ab1a74f3d34d34fcf9f36fd5c76104be437e5e9bd16d74500306d539863a0233048ddde79078be6fb993e25b3530287006cf735007945200cef29acdaeb
6
+ metadata.gz: 35762c78b2ba5bd8090cb799566ecabec4d863bd956c27c0a9119bf8025cda2bad8d49f9a93b4242ece7bfc5de7e8ef5a939bce5e3f955e46af925857379e704
7
+ data.tar.gz: 4b932b9b03940bc536887cbf4a750ce265b2992afc775b446ea9a56b675d23f52724b7b7a6f710ae3258a4c5e73c69bfc01bd67b56087554ca0a3c47a5f35343
@@ -198,6 +198,12 @@ module Canon
198
198
  )
199
199
  has_mismatch = false
200
200
 
201
+ # The ORIGINAL direction, captured before
202
+ # determine_mismatch_children returns reduced sets of equal
203
+ # length — deriving the side from the reduced sets reports
204
+ # every mid-content removal as an insertion (canon#285).
205
+ before_longer = children1.length > children2.length
206
+
201
207
  # Length check
202
208
  unless children1.length == children2.length
203
209
  has_mismatch = true
@@ -228,7 +234,7 @@ module Canon
228
234
  else
229
235
  mismatched_children.each do |child|
230
236
  child_dim = comparator.determine_node_dimension(child)
231
- if children1.length > children2.length
237
+ if before_longer
232
238
  comparator.add_difference(child, nil,
233
239
  Comparison::MISSING_NODE,
234
240
  Comparison::MISSING_NODE,
@@ -63,6 +63,12 @@ module Canon
63
63
  # regex per format call cost more than the gsub it drives.
64
64
  BLOCK_SPACING_PATTERN =
65
65
  Regexp.new("(</(?:#{BLOCK_ELEMENTS.join('|')})>)(?:\n*)(<(?:#{BLOCK_ELEMENTS.join('|')})[\s>])").freeze
66
+ # Engine-inserted newlines after a block close, before ANY next
67
+ # tag (the serializer's indent model breaks lines where canon's
68
+ # normalize step kept no text node) — folded to the canonical
69
+ # single space so serialization is idempotent.
70
+ AFTER_BLOCK_NEWLINE_PATTERN =
71
+ Regexp.new("(</(?:#{BLOCK_ELEMENTS.join('|')})>)(\n+)(<(?!/))").freeze
66
72
  # Format HTML using canonical form
67
73
  # @param html [String] HTML document to canonicalize
68
74
  # @return [String] Canonical form of HTML
@@ -108,21 +114,10 @@ module Canon
108
114
  end
109
115
 
110
116
  # Sort element attributes alphabetically throughout document
117
+ # (the shared canonical attribute order — TODO.sota/03)
111
118
  # @param doc [Moxml::Document] Parsed document
112
119
  def self.sort_attributes(doc)
113
- walk_elements(doc.root) do |element|
114
- attrs = element.attributes
115
- next if attrs.empty?
116
-
117
- names = attrs.map(&:name)
118
- # Already-sorted is the common case — removing and re-adding
119
- # every attribute is expensive, so check first.
120
- next if names.each_cons(2).all? { |a, b| (a <=> b) <= 0 }
121
-
122
- sorted_attrs = attrs.sort_by(&:name)
123
- names.each { |name| element.remove_attribute(name) }
124
- sorted_attrs.each { |attr| element[attr.name] = attr.value }
125
- end
120
+ Canon::Xml::AttributeSorter.sort!(doc)
126
121
  end
127
122
 
128
123
  # Depth-first element walk (pre-order).
@@ -211,9 +206,17 @@ module Canon
211
206
  # @param html [String] Serialized HTML string
212
207
  # @return [String] HTML with proper spacing between block elements
213
208
  def self.ensure_block_element_spacing(html)
214
- # Add space between closing and opening block element tags
209
+ # Space between closing and opening block element tags
215
210
  # (pattern compiled once — see BLOCK_SPACING_PATTERN)
216
- html.gsub(BLOCK_SPACING_PATTERN, '\1 \2')
211
+ html = html.gsub(BLOCK_SPACING_PATTERN, '\1 \2')
212
+ # The engine's serializer inserts its own newlines after a
213
+ # block close when the tree has no whitespace text node there
214
+ # (normalize_whitespace keeps a single space only where one
215
+ # existed). Collapse those self-inserted newlines to the same
216
+ # canonical space — without this, format is not idempotent:
217
+ # pass 1 emits "</p>\n<br>", pass 2 answers "</p> <br>"
218
+ # (the property suite, TODO.sota/03).
219
+ html.gsub(AFTER_BLOCK_NEWLINE_PATTERN, '\1 \3')
217
220
  end
218
221
 
219
222
  # Check if a node is a block-level element
@@ -11,13 +11,9 @@ module Canon
11
11
  end
12
12
  end
13
13
 
14
- def self.parse(xml)
14
+ def self.parse(xml, options = {})
15
15
  Canon::Validators::XmlValidator.validate!(xml)
16
- if XmlBackend.nokogiri?
17
- Nokogiri::XML(xml)
18
- else
19
- XmlParsing.parse(xml)
20
- end
16
+ XmlParsing.parse(xml, options)
21
17
  end
22
18
  end
23
19
  end
data/lib/canon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.108"
4
+ VERSION = "0.3.110"
5
5
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Xml
5
+ # Canonical attribute ordering: every element's attributes are
6
+ # re-attached in lexicographic name order. Both canonical lanes
7
+ # consume this — the HTML formatter base and the XML pretty
8
+ # printer — so attribute order is invariant across formats and
9
+ # across input orderings (the equivalence-respect property,
10
+ # TODO.sota/03).
11
+ module AttributeSorter
12
+ module_function
13
+
14
+ def sort!(doc)
15
+ walk_elements(doc.root) do |element|
16
+ attrs = element.attributes
17
+ next if attrs.empty?
18
+
19
+ names = attrs.map(&:name)
20
+ # Already-sorted is the common case — removing and re-adding
21
+ # every attribute is expensive, so check first.
22
+ next if names.each_cons(2).all? { |a, b| (a <=> b) <= 0 }
23
+
24
+ sorted_attrs = attrs.sort_by(&:name)
25
+ names.each { |name| element.remove_attribute(name) }
26
+ sorted_attrs.each { |attr| element[attr.name] = attr.value }
27
+ end
28
+ doc
29
+ end
30
+
31
+ def walk_elements(node, &block)
32
+ return unless node.is_a?(Moxml::Element)
33
+
34
+ yield(node)
35
+ node.children.to_a.each { |child| walk_elements(child, &block) }
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/canon/xml.rb CHANGED
@@ -15,6 +15,7 @@ module Canon
15
15
  # Canon::Xml::Nodes.
16
16
  module Xml
17
17
  autoload :AttributeHandler, "canon/xml/attribute_handler"
18
+ autoload :AttributeSorter, "canon/xml/attribute_sorter"
18
19
  autoload :C14n, "canon/xml/c14n"
19
20
  autoload :DigestGate, "canon/xml/digest_gate"
20
21
  autoload :CharacterEncoder, "canon/xml/character_encoder"
@@ -303,11 +303,14 @@ module Canon
303
303
 
304
304
  # --- Moxml engine ---
305
305
 
306
- # Readonly: canon only ever reads engine documents (conversion,
306
+ # Readonly by default: canon reads engine documents (conversion,
307
307
  # serialization) — leptris memoizes reads on readonly documents,
308
308
  # and mutation is refused loudly rather than silently corrupting.
309
- def moxml_parse(xml_string, _options)
310
- moxml_context.parse(xml_string, readonly: true)
309
+ # Mutating lanes (the attribute sorter, TODO.sota/03) opt out
310
+ # explicitly with readonly: false.
311
+ def moxml_parse(xml_string, options)
312
+ readonly = options.fetch(:readonly, true)
313
+ moxml_context.parse(xml_string, readonly: readonly)
311
314
  end
312
315
 
313
316
  def moxml_node_type(node)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.108
4
+ version: 0.3.110
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-21 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -405,6 +405,7 @@ files:
405
405
  - lib/canon/version.rb
406
406
  - lib/canon/xml.rb
407
407
  - lib/canon/xml/attribute_handler.rb
408
+ - lib/canon/xml/attribute_sorter.rb
408
409
  - lib/canon/xml/c14n.rb
409
410
  - lib/canon/xml/character_encoder.rb
410
411
  - lib/canon/xml/data_model.rb