Module: Moxml::Materializer

Defined in:
lib/moxml/materializer.rb

Overview

Flattened subtree records for conversion-style consumers (issue #132): one record per node, emitted in post-order (children before parents — build with a stack), carrying everything a consumer tree needs without allocating Moxml::Node or Moxml::Attribute wrappers.

Two emission forms share one walk (issue #143):

document.materialize { |record| ... }     # Hash snapshot per node
document.materialize.to_a                 # Enumerator of snapshots
document.materialize_fields { |kind, qname, prefix, namespace_uri,
                             namespaces, attributes, text, depth| }

materialize_fields is the conversion hot path: the two list arguments are flat reused buffers — attributes stride 4 (name, value, namespace_uri, prefix), namespaces stride 2 (prefix, uri; nil prefix = default) — valid only inside the block. Copy anything you intend to keep, or use materialize, whose records are independent Hash snapshots.

Record shape (both forms):

kind: :element|:text|:cdata|:comment|:processing_instruction,
qname: "tag"|"pi-target"|nil, prefix: "p"|nil,
namespace_uri: "urn:x"|nil,
namespaces: [[prefix, uri], ...],        # element's OWN declarations
attributes: [[name, value, namespace_uri, prefix], ...],
text: String|nil, depth: Integer

Adapters with a bulk path (leptris: one leptris_node_traverse FFI call for the whole subtree, reading properties straight off the C handles) answer bulk_materialize?; the others walk the wrapper tree generically. Both emit identical streams — spec-pinned.

Defined Under Namespace

Modules: Record Classes: Buffers

Constant Summary collapse

EMPTY_ATTRIBUTES =

Shared frozen empty list for records with no list fields; adapter bulk paths yield it too.

[].freeze

Class Method Summary collapse

Class Method Details

.fill_element_buffers(element, buffers) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/moxml/materializer.rb', line 133

def fill_element_buffers(element, buffers)
  attrs = buffers.attributes
  attrs.clear
  element.attributes.each do |attr|
    ns = attr.namespace
    attrs << attr.name << attr.value << ns&.uri << ns&.prefix
  end

  ns_buf = buffers.namespaces
  ns_buf.clear
  # declared_namespaces: the element's OWN declarations ([prefix,
  # uri] pairs; nil prefix = default), not the in-scope set —
  # enough for a consumer to rebuild scope while walking (#138).
  element.declared_namespaces.each do |prefix, uri|
    ns_buf << prefix << uri
  end
end

.materialize(node, &block) ⇒ Object

Hash-snapshot form: every record is an independent Hash.



82
83
84
85
86
87
88
89
# File 'lib/moxml/materializer.rb', line 82

def materialize(node, &block)
  return enum_for(:materialize, node) unless block

  materialize_fields(node) do |kind, qname, prefix, namespace_uri, namespaces, attributes, text, depth|
    yield(Record.from_fields(kind, qname, prefix, namespace_uri,
                             namespaces, attributes, text, depth))
  end
end

.materialize_fields(node, buffers = Buffers.new, &block) ⇒ Object

Zero-allocation streaming form — see the module docs. Requires a block: the reused buffers are only valid inside it.

Raises:

  • (ArgumentError)


93
94
95
96
97
98
99
100
101
102
103
# File 'lib/moxml/materializer.rb', line 93

def materialize_fields(node, buffers = Buffers.new, &block)
  raise ArgumentError, "materialize_fields requires a block" unless block

  adapter = node.context.config.adapter
  if adapter.bulk_materialize? &&
      adapter.materialize_fields(node.native, buffers, &block)
    return
  end

  walk_fields(node, 0, buffers, &block)
end

.walk_fields(node, depth, buffers, &block) ⇒ Object

Generic post-order walk over the wrapper tree. Works on every adapter; the fast bulk path exists where the engine offers one.



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
# File 'lib/moxml/materializer.rb', line 107

def walk_fields(node, depth, buffers, &block)
  case node
  when Element
    node.children.each { |child| walk_fields(child, depth + 1, buffers, &block) }
    fill_element_buffers(node, buffers)
    ns = node.namespace
    yield(:element, node.name, node.namespace_prefix, ns&.uri,
          buffers.namespaces, buffers.attributes, nil, depth)
  when Text
    yield(:text, nil, nil, nil, EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES,
          node.content, depth)
  when Cdata
    yield(:cdata, nil, nil, nil, EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES,
          node.content, depth)
  when Comment
    yield(:comment, nil, nil, nil, EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES,
          node.content, depth)
  when ProcessingInstruction
    yield(:processing_instruction, node.target, nil, nil,
          EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES, node.content, depth)
  when EntityReference
    yield(:entity_reference, node.name, nil, nil,
          EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES, "&#{node.name};", depth)
  end
end