Class: Canon::Xml::TreeBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/canon/xml/tree_builder.rb

Overview

The one place canon tree nodes are constructed from parsed XML.

Owns construction semantics: namespace scopes, attribute normalization, node kinds, and document-level ordering. Keep/strip decisions come from WhitespacePolicy (one home for all three policies); engine walks — the Nokogiri and moxml extractors in Xml::DataModel, the HTML walk in Html::DataModel — only map their engine's shapes onto this interface.

Attribute normalization: duplicate (name, namespace) pairs are invalid XML; engines expose them differently (libxml2 lists repeats, libleptris deduplicates), so the builder resolves them once — first occurrence wins.

Constant Summary collapse

DEFAULT =

Stateless module: one shared instance serves every feed.

new
NO_ATTRIBUTES =
[].freeze
XML_NAMESPACE_PREFIX =
"xml"
XML_NAMESPACE_URI =
"http://www.w3.org/XML/1998/namespace"

Instance Method Summary collapse

Instance Method Details

#add_document_children(root, children, document_element, skip_types = []) ⇒ Object

Attach document-level children (prolog/epilog PIs, comments, document-level text) to the canon root, in document order, skipping the document element and the given types. Yields each child to the caller's converter; nil results are dropped.



101
102
103
104
105
106
107
108
109
110
# File 'lib/canon/xml/tree_builder.rb', line 101

def add_document_children(root, children, document_element,
                          skip_types = [])
  children.each do |child|
    next if child.equal?(document_element)
    next if skip_types.any? { |type| child.is_a?(type) }

    node = yield child
    root.add_child(node) if node
  end
end

#attach_namespace_scope(element, scope) ⇒ Object

Attach an in-scope scope to an element as namespace nodes.



39
40
41
42
43
44
45
46
# File 'lib/canon/xml/tree_builder.rb', line 39

def attach_namespace_scope(element, scope)
  scope.each do |prefix, uri|
    element.add_namespace(Nodes::NamespaceNode.new(
                            prefix: prefix,
                            uri: uri,
                          ))
  end
end

#comment(content) ⇒ Object



89
90
91
# File 'lib/canon/xml/tree_builder.rb', line 89

def comment(content)
  Nodes::CommentNode.new(value: content)
end

#element(name:, prefix: nil, namespace_uri: nil, attributes: NO_ATTRIBUTES, namespace_scope: nil) ⇒ Object

Build an element. attributes are [name, value, namespace_uri, prefix] pairs; namespace_scope is a merged scope (or nil for no namespace nodes).



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/canon/xml/tree_builder.rb', line 51

def element(name:, prefix: nil, namespace_uri: nil,
            attributes: NO_ATTRIBUTES, namespace_scope: nil)
  element = Nodes::ElementNode.new(
    name: name,
    namespace_uri: namespace_uri,
    prefix: prefix,
  )
  attach_namespace_scope(element, namespace_scope) if namespace_scope

  # Duplicate (name, namespace) pairs are invalid XML — first
  # occurrence wins. Attributes per element are few, so a pairwise
  # scan beats a per-element seen-hash on allocation (this is the
  # hot SAX path).
  attributes.each_with_index do |(attr_name, value, attr_namespace_uri, attr_prefix), index|
    duplicate = attributes[0...index].any? do |prior_name, _v, prior_ns, _p|
      prior_name == attr_name && prior_ns == attr_namespace_uri
    end
    next if duplicate

    element.add_attribute(Nodes::AttributeNode.new(
                            name: attr_name,
                            value: value,
                            namespace_uri: attr_namespace_uri,
                            prefix: attr_prefix,
                          ))
  end

  element
end

#merge_namespace_scope(inherited, declaration_pairs) ⇒ Object

In-scope namespace bindings: the element's own declarations shadow inherited ones; xml is prebound at the base. Declaration pairs are [prefix-or-nil, uri]; nil and "" both mean the default namespace.



30
31
32
33
34
35
36
# File 'lib/canon/xml/tree_builder.rb', line 30

def merge_namespace_scope(inherited, declaration_pairs)
  scope = inherited ? inherited.dup : { XML_NAMESPACE_PREFIX => XML_NAMESPACE_URI }
  declaration_pairs.each do |prefix, uri|
    scope[prefix || ""] = uri
  end
  scope
end

#processing_instruction(target, data) ⇒ Object



93
94
95
# File 'lib/canon/xml/tree_builder.rb', line 93

def processing_instruction(target, data)
  Nodes::ProcessingInstructionNode.new(target: target, data: data)
end

#text(content, keep:, original: content) ⇒ Object

Build a text node; keep comes from WhitespacePolicy (the caller knows the policy and parent context).



83
84
85
86
87
# File 'lib/canon/xml/tree_builder.rb', line 83

def text(content, keep:, original: content)
  return nil unless keep

  Nodes::TextNode.new(value: content, original: original)
end