Class: Canon::Xml::TreeBuilder
- Inherits:
-
Object
- Object
- Canon::Xml::TreeBuilder
- 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
-
#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.
-
#attach_namespace_scope(element, scope) ⇒ Object
Attach an in-scope scope to an element as namespace nodes.
- #comment(content) ⇒ Object
-
#element(name:, prefix: nil, namespace_uri: nil, attributes: NO_ATTRIBUTES, namespace_scope: nil) ⇒ Object
Build an element.
- #intern(string) ⇒ Object
-
#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.
- #processing_instruction(target, data) ⇒ Object
-
#text(content, keep:, original: content) ⇒ Object
Build a text node;
keepcomes from WhitespacePolicy (the caller knows the policy and parent context).
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 |