Class: Canon::Html::DataModel
- Inherits:
-
DataModel
- Object
- DataModel
- Canon::Html::DataModel
- Defined in:
- lib/canon/html/data_model.rb
Overview
Builds XPath data model from HTML HTML-specific parsing with lowercase element/attribute names, whitespace-sensitive element handling, and fragment parsing
Class Method Summary collapse
-
.build_from_nokogiri(nokogiri_doc) ⇒ Object
Build XPath data model from a Nokogiri HTML document or fragment — a TreeBuilder walk like the XML extractors, with the HTML whitespace policy and xmlns-free attributes.
-
.from_html(html_string, version: :html4) ⇒ Canon::Xml::Nodes::RootNode
Build XPath data model from HTML string.
-
.parse(html_string, version: :html4) ⇒ Object
Alias for compatibility.
-
.serialize(node) ⇒ Object
Serialize HTML node to string.
- .walk(builder, node, inherited_namespaces: nil) ⇒ Object
Class Method Details
.build_from_nokogiri(nokogiri_doc) ⇒ Object
Build XPath data model from a Nokogiri HTML document or fragment — a TreeBuilder walk like the XML extractors, with the HTML whitespace policy and xmlns-free attributes.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/canon/html/data_model.rb', line 82 def self.build_from_nokogiri(nokogiri_doc) builder = Canon::Xml::TreeBuilder::DEFAULT root = Canon::Xml::Nodes::RootNode.new skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : [] if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root root.add_child(walk(builder, nokogiri_doc.root)) builder.add_document_children(root, nokogiri_doc.children, nokogiri_doc.root, skip_types) do |child| walk(builder, child) end else builder.add_document_children(root, nokogiri_doc.children, nil, skip_types) do |child| walk(builder, child) end end root end |
.from_html(html_string, version: :html4) ⇒ Canon::Xml::Nodes::RootNode
Build XPath data model from HTML string
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/canon/html/data_model.rb', line 16 def self.from_html(html_string, version: :html4) # Detect if this is a full document (has <html> tag) or fragment # Full documents should use document parser to preserve structure # Fragments should use fragment parser to avoid adding implicit wrappers is_full_document = html_string.match?(%r{<html[\s>]}i) # Parse with Nokogiri using appropriate parser doc = if is_full_document # CRITICAL FIX: For full HTML documents, parse as document first # and extract the body element. This avoids Nokogiri::HTML.fragment() # incorrectly moving head elements (like meta) to the body. # Parse as full document to get proper structure full_doc = if version == :html5 Nokogiri::HTML5(html_string) else Nokogiri::HTML4(html_string) end # Extract body element and create fragment from it body = full_doc.at_css("body") if body # Create a fragment and copy body children to it # This preserves the body structure without head elements frag = if version == :html5 Nokogiri::HTML5::DocumentFragment.new(full_doc) else Nokogiri::HTML4::DocumentFragment.new(full_doc) end body.children.each do |child| frag.add_child(child.dup) end frag elsif version == :html5 # No body found, fall back to fragment parsing Nokogiri::HTML5.fragment(html_string) else Nokogiri::HTML4.fragment(html_string) end elsif version == :html5 # Fragment - use fragment parser to avoid implicit wrappers Nokogiri::HTML5.fragment(html_string) else Nokogiri::HTML4.fragment(html_string) end # HTML doesn't have strict namespace requirements like XML, # so skip the relative namespace URI check # Convert to XPath data model (reuse XML infrastructure) build_from_nokogiri(doc) end |
.parse(html_string, version: :html4) ⇒ Object
Alias for compatibility
68 69 70 |
# File 'lib/canon/html/data_model.rb', line 68 def self.parse(html_string, version: :html4) from_html(html_string, version: version) end |
.serialize(node) ⇒ Object
Serialize HTML node to string
73 74 75 76 77 |
# File 'lib/canon/html/data_model.rb', line 73 def self.serialize(node) # HTML nodes use the same serialization as XML # Delegate to XML serialization implementation Canon::Xml::DataModel.serialize(node) end |
.walk(builder, node, inherited_namespaces: nil) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/canon/html/data_model.rb', line 103 def self.walk(builder, node, inherited_namespaces: nil) case node when Nokogiri::XML::Element scope = builder.merge_namespace_scope( inherited_namespaces, node.namespace_definitions.map { |ns| [ns.prefix, ns.href] }, ) element = builder.element( name: node.name, prefix: node.namespace&.prefix, namespace_uri: node.namespace&.href, # HTML attributes are namespace-free; xmlns declarations are # not reported as attributes. attributes: node.attribute_nodes.filter_map do |attr| next if attr.name.start_with?("xmlns") [attr.name, attr.value, nil, nil] end, namespace_scope: scope, ) node.children.each do |child| built = walk(builder, child, inherited_namespaces: scope) element.add_child(built) if built end element when Nokogiri::XML::Text builder.text( node.content, keep: Canon::Xml::WhitespacePolicy.keep_html_text?( node.content, parent_name: node.parent.is_a?(Nokogiri::XML::Element) ? node.parent.name : nil, inline_significant: Canon::Comparison::WhitespaceSensitivity.inline_whitespace_significant?(node), ), ) when Nokogiri::XML::Comment builder.comment(node.content) when Nokogiri::XML::ProcessingInstruction builder.processing_instruction(node.name, node.content) end end |