canon 0.3.101 → 0.3.102

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: 7585f36fdd0ec2e32f315db6393c265e885eeff9e1ad705ad792bfcaeab3373d
4
+ data.tar.gz: f29aa48def44453b036c2dad82883839f19fc846b6e9e4d095f25da4b1a82eb8
5
5
  SHA512:
6
- metadata.gz: efc3813ef9fa91be57ddd8fa537506cb938361e5c582f4bdc06ac272c7ffd299b92172c9bf41cf194f1a5d0c50237b82c8a01de801f652545dc51693ed96f08b
7
- data.tar.gz: a867f50610cb84caa68f469561f9bd5cc15ec49cb4732543fd76efbca201787520245f013edca9454349e7a164be73f8f884bc503458f007f7ee7206bd5fedc9
6
+ metadata.gz: 6e05e33a87145653f7986ee62c076bd12ded0c95ff96dc74cc5ddf81c2bf48b5286d000715b9c7c32da4160c73f419b6fde7597e46f6e4fbf667324bf0a3d5ca
7
+ data.tar.gz: 3c11b01ca57ab4d737aafd8c159040934ab4e7c80d697d9be07c0b1e05bbfc9bbc7db6e1718a0bb54f1629cad16f5a0f4c80431846babceb236d74b63ec4226e
@@ -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
@@ -4,7 +4,9 @@ module Canon
4
4
  module Validators
5
5
  # Validator for HTML input
6
6
  #
7
- # Validates HTML input (HTML4, HTML5, or XHTML) using Nokogiri.
7
+ # Validates HTML input (HTML4, HTML5, or XHTML) through the
8
+ # engine (strict XML parse for XHTML; the WHATWG parse-lint
9
+ # surface for HTML5 — leptris#1771).
8
10
  # Automatically detects the HTML type and applies appropriate validation.
9
11
  # Raises detailed ValidationError with line and column information
10
12
  # when malformed HTML is detected.
@@ -73,74 +75,44 @@ module Canon
73
75
  # @param input [String] The HTML5 string to validate
74
76
  # @raise [Canon::ValidationError] If HTML5 is malformed
75
77
  # @return [void]
78
+ # The HTML5 lint layer is engine-served (leptris#1771 delivered
79
+ # the §13.2.5 parse-error token stream: machine-readable code +
80
+ # line + column, RECOVER class). Engine-direct call — the
81
+ # documented seam exception (moxml's parse_html does not forward
82
+ # the collect_errors option yet; ask filed upstream).
76
83
  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)
84
+ doc = Leptris::XML.parse_html(input, mode: :whatwg,
85
+ collect_errors: true)
86
+ errors = doc.errors
87
+ return if errors.empty?
83
88
 
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)
91
- end
92
-
93
- return if significant_errors.empty?
94
-
95
- error = significant_errors.first
96
- location = extract_location(error)
89
+ error = errors.first
97
90
  raise Canon::ValidationError.new(
98
- error.message,
91
+ "[#{error.code}] #{error.message}",
99
92
  format: :html,
100
- line: location[:line],
101
- column: location[:column],
102
- details: build_error_details(significant_errors),
93
+ line: error.line,
94
+ column: error.column,
95
+ details: build_error_details(errors),
103
96
  )
104
97
  end
105
98
 
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
99
  # Build error details from multiple errors
115
100
  #
116
- # @param errors [Array<Nokogiri::XML::SyntaxError>] Array of errors
101
+ # @param errors [Array<Leptris::XML::SyntaxError>] Array of errors
117
102
  # @return [String, nil] Combined error details
118
103
  def self.build_error_details(errors)
119
104
  return nil if errors.size <= 1
120
105
 
121
- significant = errors.select { |e| e.level >= 2 }
122
- return nil if significant.empty?
123
-
124
- details = significant[1..3].map do |e|
125
- loc = extract_location(e)
106
+ errors[1..3].map do |e|
126
107
  msg = e.message
127
- msg += " (line #{loc[:line]})" if loc[:line]
108
+ msg += " (line #{e.line})" if e.respond_to?(:line) && e.line
128
109
  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)
110
+ end.join("; ")
139
111
  end
140
112
 
141
113
  private_class_method :xhtml?, :validate_xhtml!, :validate_html5!,
142
- :extract_details, :build_error_details,
143
- :doctype_or_warning?, :extract_engine_location
114
+ :build_error_details,
115
+ :extract_engine_location
144
116
  end
145
117
  end
146
118
  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.102"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.102
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.