canon 0.3.101 → 0.3.103

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: dc66e03ef02db4a7cbb21e496076e2794027a7a52996eb797b90b697c77013ee
4
- data.tar.gz: 6ab70c76adfd5fe466b0fa8a54f542097344f4726c34192b1f85f59a542b5752
3
+ metadata.gz: b077068461a0f0a8ec81bf0e81ee5243e11958321e3f15613e52efa439279252
4
+ data.tar.gz: 341d395989cf2831a81b099a50eb64f4a088a3ffcee8d0ae15395d0fcb8e9c11
5
5
  SHA512:
6
- metadata.gz: efc3813ef9fa91be57ddd8fa537506cb938361e5c582f4bdc06ac272c7ffd299b92172c9bf41cf194f1a5d0c50237b82c8a01de801f652545dc51693ed96f08b
7
- data.tar.gz: a867f50610cb84caa68f469561f9bd5cc15ec49cb4732543fd76efbca201787520245f013edca9454349e7a164be73f8f884bc503458f007f7ee7206bd5fedc9
6
+ metadata.gz: b30353f6ac5ac3bebb4ddbe4f59ebd01b0bf2699b9ce42832be0b349f41e94bfad5efc93008fb4ab687dbabd2c2fedd567694221e2cd59343ae5e9623d2f0d6f
7
+ data.tar.gz: de33802e262f5829d0a1e33af25ce1f44fe229f5194e788d6a415cb49b59ba85de2befa2a3f4a7bb4741c35ae3f5bc340f8d32b144c2d77ce440cd03aa304f04
@@ -571,8 +571,8 @@ module Canon
571
571
  return Comparison::UNEQUAL_NODES_TYPES
572
572
  end
573
573
 
574
- content1 = Canon::XmlParsing.xml_node?(n1) ? n1.content.to_s.strip : ""
575
- content2 = Canon::XmlParsing.xml_node?(n2) ? n2.content.to_s.strip : ""
574
+ content1 = pi_content(n1)
575
+ content2 = pi_content(n2)
576
576
 
577
577
  if content1 == content2
578
578
  Comparison::EQUIVALENT
@@ -597,8 +597,60 @@ module Canon
597
597
  return Comparison::MISSING_NODE
598
598
  end
599
599
 
600
- compare_nodes(root1, root2, opts, child_opts, diff_children,
601
- differences)
600
+ status = compare_nodes(root1, root2, opts, child_opts, diff_children,
601
+ differences)
602
+ compare_document_siblings(n1, n2, root1, root2, opts, differences)
603
+ status
604
+ end
605
+
606
+ # Document-level processing instructions and comments (the XML
607
+ # declaration never appears here - it is stripped at parse time
608
+ # by policy) participate in the comparison as an ordered sibling
609
+ # sequence around the root element.
610
+ def compare_document_siblings(n1, n2, root1, root2, opts, differences)
611
+ sibs1 = document_siblings(n1, root1)
612
+ sibs2 = document_siblings(n2, root2)
613
+
614
+ if sibs1.length != sibs2.length
615
+ add_difference(n1, n2, Comparison::MISSING_NODE,
616
+ Comparison::MISSING_NODE, :text_content, opts,
617
+ differences)
618
+ return
619
+ end
620
+
621
+ sibs1.zip(sibs2).each do |a, b|
622
+ next unless a.class != b.class ||
623
+ (a.is_a?(Canon::Xml::Nodes::ProcessingInstructionNode) &&
624
+ compare_processing_instruction_nodes(a, b, opts,
625
+ differences) ==
626
+ Comparison::UNEQUAL_TEXT_CONTENTS) ||
627
+ (a.is_a?(Canon::Xml::Nodes::CommentNode) &&
628
+ compare_comment_nodes(a, b, opts, differences) ==
629
+ Comparison::UNEQUAL_COMMENTS)
630
+
631
+ add_difference(a, b, Comparison::UNEQUAL_NODES_TYPES,
632
+ Comparison::UNEQUAL_NODES_TYPES, :text_content,
633
+ opts, differences)
634
+ end
635
+ end
636
+
637
+ def document_siblings(doc, root)
638
+ return [] unless doc.respond_to?(:children)
639
+
640
+ doc.children.reject do |child|
641
+ child.equal?(root) || child.is_a?(Canon::Xml::Nodes::TextNode) ||
642
+ child.is_a?(Canon::Xml::Nodes::ElementNode)
643
+ end
644
+ end
645
+
646
+ # PI content lives in different accessors per node flavour:
647
+ # Canon::Xml::Nodes::ProcessingInstructionNode#data, engine
648
+ # processing instructions expose #content.
649
+ def pi_content(node)
650
+ return node.data.to_s if node.is_a?(Canon::Xml::Nodes::ProcessingInstructionNode)
651
+ return node.content.to_s if Canon::XmlParsing.xml_node?(node)
652
+
653
+ node.respond_to?(:data) ? node.data.to_s : ""
602
654
  end
603
655
 
604
656
  # Compare children of two nodes using semantic matching
@@ -98,29 +98,24 @@ module Canon
98
98
  # indentation; +AS_XHTML+ produces well-shaped output (void
99
99
  # elements self-closed, non-void paired); +NO_DECLARATION+
100
100
  # suppresses the +<?xml ...?>+ prefix.
101
- # Engine parse + significance-aware strip + the #354 indent
102
- # knobs + a canon-side void self-closing pass for the AS_XHTML
103
- # contract (an engine XHTML mode is asked upstream as moxml#390;
104
- # until then the shape transform is ours).
101
+ # Engine parse + significance-aware strip + the engine's
102
+ # XHTML serialization mode (moxml#390 delivered to_xhtml /
103
+ # to_html(xhtml: true)): void elements self-closed, non-void
104
+ # paired — the AS_XHTML contract, engine-served.
105
105
  def format_fixture_ready(html_string)
106
106
  doc = Canon::Html::DataModel.engine_html_parse(html_string)
107
107
  Canon::Html::DataModel.strip_insignificant_whitespace!(doc.root)
108
108
  html = if @indent_type == "tab"
109
- doc.to_html(indent: 1, indent_text: "\t")
109
+ doc.to_xhtml(indent: 1, indent_text: "\t")
110
110
  else
111
- doc.to_html(indent: @indent)
111
+ doc.to_xhtml(indent: @indent)
112
112
  end
113
- self_closing_voids(html)
114
- end
115
-
116
- # AS_XHTML: void elements self-closed (<br/>, <img ... />).
117
- def self_closing_voids(html)
118
- html.gsub(%r{<([A-Za-z][A-Za-z0-9:-]*)((?:\s+[^<>"]*(?:"[^"]*"[^<>"]*)*)?)>}) do
119
- name = ::Regexp.last_match(1)
120
- next ::Regexp.last_match(0) unless HtmlVoidElements.void?(name)
121
-
122
- "<#{name}#{::Regexp.last_match(2)}/>"
123
- end
113
+ # The AS_XHTML contract pairs empty NON-void elements (<a .../>)
114
+ # for browser-safe pasting; the engine's to_xhtml self-closes
115
+ # every empty element. The existing transform restores the
116
+ # pairing for non-voids while voids stay self-closed — the
117
+ # #135 regression pins it.
118
+ expand_non_void_self_closing(html)
124
119
  end
125
120
 
126
121
  # Rewrite +<tag …/>+ into +<tag …></tag>+ for every element name
@@ -49,6 +49,12 @@ module Canon
49
49
  return to_tree_from_canon_processing_instruction(node)
50
50
  end
51
51
 
52
+ # Raw moxml documents/elements (the leptris engine's parse
53
+ # output) convert through the shared data model.
54
+ if defined?(Moxml) && node.is_a?(Moxml::Node)
55
+ return to_tree(Canon::Xml::DataModel.build_from_moxml(node))
56
+ end
57
+
52
58
  # Fallback to raw engine documents (legacy support). Nokogiri
53
59
  # is optional — under the moxml/leptris engine a raw Nokogiri
54
60
  # object cannot exist, so the constants stay unresolved.
@@ -200,10 +206,34 @@ module Canon
200
206
  # @param root_node [Canon::Xml::Nodes::RootNode] Root node
201
207
  # @return [Core::TreeNode, nil] Tree node for first child (document element)
202
208
  def to_tree_from_canon_root(root_node)
203
- # Root node: process first child (document element)
209
+ # Root node: convert the document element, keeping
210
+ # document-level processing instructions / comments as
211
+ # siblings of it (before it first, then after) so they
212
+ # participate in the diff. The XML declaration never reaches
213
+ # here - it is stripped at parse time by policy.
204
214
  return nil if root_node.children.empty?
205
215
 
206
- to_tree(root_node.children.first)
216
+ element_child = root_node.children.find do |child|
217
+ child.is_a?(Canon::Xml::Nodes::ElementNode)
218
+ end
219
+ return to_tree(root_node.children.first) if element_child.nil?
220
+
221
+ tree = to_tree(element_child)
222
+ seen_element = false
223
+ root_node.children.each do |child|
224
+ if child.equal?(element_child)
225
+ seen_element = true
226
+ next
227
+ end
228
+ next if child.is_a?(Canon::Xml::Nodes::ElementNode)
229
+ next if child.is_a?(Canon::Xml::Nodes::TextNode)
230
+
231
+ sibling = to_tree(child)
232
+ next if sibling.nil?
233
+
234
+ seen_element ? tree.children << sibling : tree.children.unshift(sibling)
235
+ end
236
+ tree
207
237
  end
208
238
 
209
239
  # Convert Canon::Xml::Nodes::ElementNode to TreeNode
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "leptris" # a hard runtime dependency — the HTML5 lint lane
4
+ # uses the engine's parse-lint face (leptris#1771)
5
+
3
6
  module Canon
4
7
  module Validators
5
8
  # Validator for HTML input
6
9
  #
7
- # Validates HTML input (HTML4, HTML5, or XHTML) using Nokogiri.
10
+ # Validates HTML input (HTML4, HTML5, or XHTML) through the
11
+ # engine (strict XML parse for XHTML; the WHATWG parse-lint
12
+ # surface for HTML5 — leptris#1771).
8
13
  # Automatically detects the HTML type and applies appropriate validation.
9
14
  # Raises detailed ValidationError with line and column information
10
15
  # when malformed HTML is detected.
@@ -73,74 +78,58 @@ module Canon
73
78
  # @param input [String] The HTML5 string to validate
74
79
  # @raise [Canon::ValidationError] If HTML5 is malformed
75
80
  # @return [void]
81
+ # Doctype-presence codes: fragments are first-class input and
82
+ # the old gumbo lane filtered doctype warnings — the engine's
83
+ # spec-correct tokens for the same condition are warnings here.
84
+ DOCTYPE_CLASS_CODES = %w[
85
+ expected-doctype-but-got-start-tag
86
+ expected-doctype-but-got-chars
87
+ expected-doctype-token
88
+ ].freeze
89
+
90
+ # The HTML5 lint layer is engine-served (leptris#1771 delivered
91
+ # the §13.2.5 parse-error token stream: machine-readable code +
92
+ # line + column, RECOVER class). Engine-direct call — the
93
+ # documented seam exception (moxml's parse_html does not forward
94
+ # the collect_errors option yet; ask filed upstream).
76
95
  def self.validate_html5!(input)
77
- # The HTML5 lint layer stays on Nokogiri/gumbo: the WHATWG
78
- # parser never fails, so the engine has no parse-error token
79
- # stream to classify (filed as leptris#1771 - the ask carries
80
- # canon's acceptance protocol).
81
- Canon::NokogiriLoader.require!("HTML5 validation (leptris#1771)")
82
- doc = Nokogiri::HTML5(input, max_errors: 100)
83
-
84
- # Check for parse errors
85
- return unless doc.errors.any?
86
-
87
- # Find first significant error (level 2 = error, level 1 = warning)
88
- # Filter out doctype warnings and other non-critical issues
89
- significant_errors = doc.errors.select do |e|
90
- e.level >= 2 && !doctype_or_warning?(e)
96
+ doc = Leptris::XML.parse_html(input, mode: :whatwg,
97
+ collect_errors: true)
98
+ errors = doc.errors.reject do |e|
99
+ DOCTYPE_CLASS_CODES.include?(e.code)
91
100
  end
101
+ return if errors.empty?
92
102
 
93
- return if significant_errors.empty?
94
-
95
- error = significant_errors.first
96
- location = extract_location(error)
103
+ error = errors.first
97
104
  raise Canon::ValidationError.new(
98
- error.message,
105
+ "[#{error.code}] #{error.message}",
99
106
  format: :html,
100
- line: location[:line],
101
- column: location[:column],
102
- details: build_error_details(significant_errors),
107
+ line: error.line,
108
+ column: error.column,
109
+ details: build_error_details(errors),
103
110
  )
104
111
  end
105
112
 
106
- # Extract additional error details
107
- #
108
- # @param error [Nokogiri::XML::SyntaxError] The syntax error
109
- # @return [String, nil] Additional details about the error
110
- def self.extract_details(_error)
111
- nil
112
- end
113
-
114
113
  # Build error details from multiple errors
115
114
  #
116
- # @param errors [Array<Nokogiri::XML::SyntaxError>] Array of errors
115
+ # @param errors [Array<Leptris::XML::SyntaxError>] Array of errors
117
116
  # @return [String, nil] Combined error details
118
117
  def self.build_error_details(errors)
119
- return nil if errors.size <= 1
120
-
121
- significant = errors.select { |e| e.level >= 2 }
122
- return nil if significant.empty?
118
+ significant = errors.reject do |e|
119
+ DOCTYPE_CLASS_CODES.include?(e.code)
120
+ end
121
+ return nil if significant.size <= 1
123
122
 
124
- details = significant[1..3].map do |e|
125
- loc = extract_location(e)
123
+ significant[1..3].map do |e|
126
124
  msg = e.message
127
- msg += " (line #{loc[:line]})" if loc[:line]
125
+ msg += " (line #{e.line})" if e.respond_to?(:line) && e.line
128
126
  msg
129
- end
130
- details.join("; ")
131
- end
132
-
133
- # Check if error is a doctype or other non-critical warning
134
- #
135
- # @param error [Nokogiri::XML::SyntaxError] The error to check
136
- # @return [Boolean] true if error is non-critical
137
- def self.doctype_or_warning?(error)
138
- error.message.match?(/doctype|Expected a doctype token/i)
127
+ end.join("; ")
139
128
  end
140
129
 
141
130
  private_class_method :xhtml?, :validate_xhtml!, :validate_html5!,
142
- :extract_details, :build_error_details,
143
- :doctype_or_warning?, :extract_engine_location
131
+ :build_error_details,
132
+ :extract_engine_location
144
133
  end
145
134
  end
146
135
  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.101"
4
+ VERSION = "0.3.103"
5
5
  end
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.101
4
+ version: 0.3.103
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-19 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.9.368
47
+ version: 1.9.373
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.9.368
54
+ version: 1.9.373
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: moxml
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.5.141
61
+ version: 0.5.147
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.5.141
68
+ version: 0.5.147
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: paint
71
71
  requirement: !ruby/object:Gem::Requirement