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"
INTERN_LIMIT =

Repetitive short strings — element/attribute names, prefixes, namespace URIs — are interned so one frozen instance is shared across every tree canon builds: a thousand <p> elements hold one "p". Values (text content, attribute values) are unique and are never interned. Bounded; cleared when full.

8192

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.



149
150
151
152
153
154
155
156
157
158
# File 'lib/canon/xml/tree_builder.rb', line 149

def add_document_children(root, children, document_element,
                          skip_types = [])
  children.each do |child|
    next if Canon::XmlParsing.same_engine_node?(child, 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. The node array is cached per scope object: elements that introduced no declarations share their parent's array instead of re-materializing one NamespaceNode per prefix per element. NamespaceNode#parent is never read, so shared nodes carry the first creator as parent by convention. The cache is identity keyed and bounded — stale scopes of dead trees cost a few hundred bytes until the next clear.



68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/canon/xml/tree_builder.rb', line 68

def attach_namespace_scope(element, scope)
  cache = (@namespace_node_cache ||= {}.compare_by_identity)
  cache.clear if cache.size >= 4096
  nodes = cache[scope]
  if nodes.nil?
    nodes = scope.map do |prefix, uri|
      Nodes::NamespaceNode.new(prefix: intern(prefix), uri: intern(uri))
    end.freeze
    cache[scope] = nodes
  end
  element.namespace_nodes = nodes
end

#comment(content) ⇒ Object



137
138
139
# File 'lib/canon/xml/tree_builder.rb', line 137

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 is a FLAT stride-4 array — [name, value, namespace_uri, prefix, name, value, ...] — so the moxml records feed hands its reused buffer straight through (read synchronously below) and the tree feeds build one flat array instead of one sub-array per attribute. namespace_scope is a merged scope (or nil for no namespace nodes).



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
# File 'lib/canon/xml/tree_builder.rb', line 87

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

  # Duplicate (name, namespace) pairs are invalid XML — first
  # occurrence wins. Flat index arithmetic keeps the scan
  # allocation-free.
  base = 0
  limit = attributes.size
  while base < limit
    attr_name = attributes[base]
    attr_namespace_uri = attributes[base + 2]
    duplicate = false
    prior = 0
    while prior < base
      if attributes[prior] == attr_name &&
          attributes[prior + 2] == attr_namespace_uri
        duplicate = true
        break
      end
      prior += 4
    end

    unless duplicate
      element.add_attribute(Nodes::AttributeNode.new(
                              name: intern(attr_name),
                              value: attributes[base + 1],
                              namespace_uri: intern(attr_namespace_uri),
                              prefix: intern(attributes[base + 3]),
                            ))
    end
    base += 4
  end

  element
end

#intern(string) ⇒ Object



33
34
35
36
37
38
39
# File 'lib/canon/xml/tree_builder.rb', line 33

def intern(string)
  return string if string.nil?

  cache = (@string_intern_cache ||= {})
  cache.clear if cache.size >= INTERN_LIMIT
  cache.fetch(string) { cache[string] = string.freeze }
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. Elements with no declarations return the inherited scope unchanged — most elements — so equal scopes share one object (and one namespace-node array, see attach). The xml binding is materialized at the root even for undeclaring elements: it is in scope on every element per the XPath data model (and the SAX feed's initial stack carries it too).



50
51
52
53
54
55
56
57
58
# File 'lib/canon/xml/tree_builder.rb', line 50

def merge_namespace_scope(inherited, declaration_pairs)
  return inherited if declaration_pairs.empty? && inherited

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

#processing_instruction(target, data) ⇒ Object



141
142
143
# File 'lib/canon/xml/tree_builder.rb', line 141

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).



131
132
133
134
135
# File 'lib/canon/xml/tree_builder.rb', line 131

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

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