Module: Moxml::NodeBehavior
Overview
Instance behavior for Moxml::Node, extracted so the leptris adapter can extend natives with it in place (issue #230); the class remains the consumer-facing contract and the wrap factory.
Constant Summary collapse
- TYPES =
%i[ element text cdata comment processing_instruction document declaration doctype namespace attribute unknown entity_reference ].freeze
Constants included from XmlUtils
Instance Attribute Summary collapse
-
#context ⇒ Object
readonly
Returns the value of attribute context.
-
#native ⇒ Object
readonly
Returns the value of attribute native.
-
#parent_node ⇒ Object
Internal: Set the parent node for cache invalidation tracking.
Instance Method Summary collapse
- #==(other) ⇒ Object
- #adapter ⇒ Object
- #add_child(node) ⇒ Object
- #add_next_sibling(node) ⇒ Object
- #add_previous_sibling(node) ⇒ Object
- #after(node) ⇒ Object
-
#ancestors ⇒ NodeSet
Returns all ancestor nodes from the parent up to and including the document node.
- #at_xpath(expression, namespaces = {}) ⇒ Object
- #before(node) ⇒ Object
- #blank? ⇒ Boolean
- #children ⇒ Object
-
#clear_native_memo! ⇒ Object
Wrapper-local memos derived from the native (name, attribute reads) die with the native they were computed from.
-
#content ⇒ Object
Returns the content/value of this node as a string.
-
#descendants ⇒ NodeSet
Returns all descendant nodes (children, grandchildren, and so on), excluding the node itself.
-
#digest(drop_ws_text: false) ⇒ Integer?
Content-defined Merkle digest of this subtree (issue #173, companion to leptris#869): a u64 Integer where the backend computes one, nil everywhere else.
- #document ⇒ Object
-
#dup ⇒ Object
(also: #clone)
Deep copy of the node (both dup and clone create deep copies for XML nodes).
-
#each(&block) ⇒ Object
Yield direct children, enabling Enumerable on the node.
-
#each_node(&block) ⇒ Object
Recursively yield all descendant nodes Used by XPath descendant-or-self and descendant axes.
-
#entity_bearing? ⇒ Boolean
Memoized against the adapter's serialize generation — the entity-marker flag flips at parse and entity-reference mint, both adapter-level, and the generation bump is the invalidation signal.
-
#find(xpath_expression, namespaces = {}) ⇒ Object
Convenience find methods (aliases for xpath methods).
- #find_all(xpath_expression, namespaces = {}) ⇒ Object
-
#first_child ⇒ Object
Get first/last child.
-
#following_siblings ⇒ NodeSet
Returns the siblings after this node, in document order.
-
#has_children? ⇒ Boolean
Check if node has any children.
-
#identifier ⇒ String?
Returns the primary identifier for this node type For Element: the tag name For Attribute: the attribute name For ProcessingInstruction: the target For content nodes (Text, Comment, Cdata, Declaration): nil (no identifier) For Doctype: nil (not fully implemented across adapters).
-
#initialize(native, context, adapter = nil, node_type = nil) ⇒ Object
adapter/node_type are primed by Node.wrap, which resolves both before choosing the wrapper class — a fresh wrapper would otherwise pay the context hop and the type probe again on its first access.
-
#invalidate_namespace_cache! ⇒ Object
Namespace-scope caches live on Element; the base no-op lets tree mutations invalidate uniformly without type checks.
- #last_child ⇒ Object
-
#line_number ⇒ Integer?
Returns the 1-based line number where this node appears in the source XML, or nil when the underlying adapter does not track source positions.
-
#materialize(&block) ⇒ Object
Flattened post-order records for this subtree without allocating wrappers — see Moxml::Materializer (issue #132).
-
#materialize_fields(&block) ⇒ Object
Zero-allocation streaming form — flat reused buffers valid only inside the block (issue #143).
-
#namespace ⇒ Object
Returns the namespace of this node Only applicable to Element nodes, returns nil for others.
-
#namespaces ⇒ Object
Returns all namespace definitions on this node Only applicable to Element nodes, returns empty array for others.
- #next_sibling ⇒ Object
- #outer_xml ⇒ Object
- #parent ⇒ Object
-
#path ⇒ String
Returns an XPath expression that uniquely locates this node within its document.
-
#preceding_siblings ⇒ NodeSet
Returns the siblings before this node, in document order.
- #previous_sibling ⇒ Object
-
#prime_contract(native, context, adapter = nil, node_type = nil) ⇒ Object
Update native reference after identity-changing operations (e.g., LibXML doc.root= creates a new Ruby wrapper) Contract priming shared by the constructor and the extend-in-place mint (#230): an extended native IS its own @native.
- #refresh_native!(new_native) ⇒ Object
- #remove ⇒ Object
- #replace(node) ⇒ Object
-
#source_position ⇒ Object
col_start, col_end where the engine exposes source positions (leptris 1.9.181+); nil elsewhere.
-
#text ⇒ Object
Returns the text content of this node Subclasses should override this method Element and Text have their own implementations.
- #to_xml(options = {}) ⇒ Object
- #xpath(expression, namespaces = {}) ⇒ Object
Methods included from XmlUtils
#encode_entities, #normalize_xml_value, #validate_comment_content, #validate_declaration_encoding, #validate_declaration_standalone, #validate_declaration_version, #validate_element_name, #validate_entity_reference_name, #validate_pi_target, #validate_prefix, #validate_uri
Instance Attribute Details
#context ⇒ Object (readonly)
Returns the value of attribute context.
16 17 18 |
# File 'lib/moxml/node.rb', line 16 def context @context end |
#native ⇒ Object (readonly)
Returns the value of attribute native.
16 17 18 |
# File 'lib/moxml/node.rb', line 16 def native @native end |
#parent_node ⇒ Object
Internal: Set the parent node for cache invalidation tracking. Called by NodeSet, Document, Element when establishing parent-child relationships. Public to allow cross-class usage within Moxml internals.
477 478 479 |
# File 'lib/moxml/node.rb', line 477 def parent_node @parent_node end |
Instance Method Details
#==(other) ⇒ Object
440 441 442 443 444 445 446 |
# File 'lib/moxml/node.rb', line 440 def ==(other) # Native equality goes through the adapter: engines with a # native read layer hand out two wrapper classes over one C # node, and raw native == is false across that seam. other.is_a?(Moxml::Node) && adapter.same_node?(@native, other.native) end |
#adapter ⇒ Object
479 480 481 482 483 |
# File 'lib/moxml/node.rb', line 479 def adapter # A context's adapter object is fixed for its lifetime; the # chain deref ran on every node access. @adapter ||= context.config.adapter end |
#add_child(node) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/moxml/node.rb', line 102 def add_child(node) node = prepare_node(node) adapter.add_child(@native, node.native) # Refresh native in case adapter changed identity (e.g., LibXML # doc.root=); stable-identity adapters skip the round trip. unless adapter.native_identity_stable? refreshed = adapter.actual_native(node.native, @native) node.refresh_native!(refreshed) if refreshed && refreshed != node.native end node.parent_node = self # The adopted subtree's in-scope namespaces changed node.invalidate_namespace_cache! invalidate_children_cache! self end |
#add_next_sibling(node) ⇒ Object
125 126 127 128 129 130 |
# File 'lib/moxml/node.rb', line 125 def add_next_sibling(node) node = prepare_node(node) adapter.add_next_sibling(@native, node.native) invalidate_parent_children_cache! self end |
#add_previous_sibling(node) ⇒ Object
118 119 120 121 122 123 |
# File 'lib/moxml/node.rb', line 118 def add_previous_sibling(node) node = prepare_node(node) adapter.add_previous_sibling(@native, node.native) invalidate_parent_children_cache! self end |
#after(node) ⇒ Object
432 433 434 |
# File 'lib/moxml/node.rb', line 432 def after(node) add_next_sibling(node) end |
#ancestors ⇒ NodeSet
Returns all ancestor nodes from the parent up to and including the document node.
310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/moxml/node.rb', line 310 def ancestors return NodeSet.new([], context) if document? natives = [] current = parent while current natives << current.native break if current.document? current = current.parent end NodeSet.new(natives, context) end |
#at_xpath(expression, namespaces = {}) ⇒ Object
213 214 215 216 |
# File 'lib/moxml/node.rb', line 213 def at_xpath(expression, namespaces = {}) Moxml::Node.wrap(adapter.at_xpath(@native, expression, namespaces), context) end |
#before(node) ⇒ Object
428 429 430 |
# File 'lib/moxml/node.rb', line 428 def before(node) add_previous_sibling(node) end |
#blank? ⇒ Boolean
436 437 438 |
# File 'lib/moxml/node.rb', line 436 def blank? text.strip.empty? end |
#children ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/moxml/node.rb', line 76 def children @children ||= begin # The wrapper's entity memo decides the marker split; the # adapter would otherwise re-derive it per call (a C parent # climb on the native layer). The kwarg rides along only # for adapters that accept it — downstream overrides with # the pre-0.5.36 one-argument signature raise ArgumentError # otherwise (issue #218). natives = if adapter.children_accepts_entity_flag? adapter.children(@native, entity_bearing: entity_bearing?) else adapter.children(@native) end natives = natives.map { adapter.patch_node(_1, @native) } if adapter.patches_children? NodeSet.new(natives, context, self) end end |
#clear_native_memo! ⇒ Object
Wrapper-local memos derived from the native (name, attribute reads) die with the native they were computed from.
62 63 64 65 66 |
# File 'lib/moxml/node.rb', line 62 def clear_native_memo! @name = nil @attribute_cache = nil @entity_bearing_gen = nil end |
#content ⇒ Object
Returns the content/value of this node as a string. Each subclass overrides this with type-specific semantics:
- Text, Comment, Cdata: raw text content
- ProcessingInstruction: instruction content
- Attribute: attribute value
- Element: delegates to text (descendant text concatenation)
254 255 256 |
# File 'lib/moxml/node.rb', line 254 def content "" end |
#descendants ⇒ NodeSet
Returns all descendant nodes (children, grandchildren, and so on), excluding the node itself.
328 329 330 331 332 |
# File 'lib/moxml/node.rb', line 328 def descendants natives = [] each_node { |node| natives << node.native } NodeSet.new(natives, context) end |
#digest(drop_ws_text: false) ⇒ Integer?
Content-defined Merkle digest of this subtree (issue #173,
companion to leptris#869): a u64 Integer where the backend
computes one, nil everywhere else. Consumers gate on nil and
fall back to walking. Equal digests imply subtree equivalence
under the flag set; unequal digests imply nothing (descend).
drop_ws_text skips whitespace-only text nodes.
420 421 422 |
# File 'lib/moxml/node.rb', line 420 def digest(drop_ws_text: false) adapter.digest(@native, drop_ws_text: drop_ws_text) end |
#document ⇒ Object
68 69 70 |
# File 'lib/moxml/node.rb', line 68 def document Moxml::Node.wrap(adapter.document(@native), context) end |
#dup ⇒ Object Also known as: clone
Deep copy of the node (both dup and clone create deep copies for XML nodes)
363 364 365 |
# File 'lib/moxml/node.rb', line 363 def dup Moxml::Node.wrap(adapter.duplicate_node(@native), context) end |
#each(&block) ⇒ Object
Yield direct children, enabling Enumerable on the node.
300 301 302 303 304 |
# File 'lib/moxml/node.rb', line 300 def each(&block) return to_enum(:each) unless block children.each(&block) end |
#each_node(&block) ⇒ Object
Recursively yield all descendant nodes Used by XPath descendant-or-self and descendant axes
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/moxml/node.rb', line 279 def each_node(&block) unless block # Eager materialization: the leptris C-side walk rb_yields # from the C callback and segfaults across fiber-suspended # enumerator frames, so the no-block form never enters it. nodes = [] each_node { |node| nodes << node } return nodes.each end # Adapters with a C-side subtree walk take it in one # dispatch; the recursive children walk stays the fallback. return if adapter.walk_descendants(@native, context, &block) children.each do |child| yield child child.each_node(&block) end end |
#entity_bearing? ⇒ Boolean
Memoized against the adapter's serialize generation — the entity-marker flag flips at parse and entity-reference mint, both adapter-level, and the generation bump is the invalidation signal. Adapters with static answers (base class) never bump. The adapter resolves the owning document itself (doc_for is a single C read on the native layer); wrappers no longer climb.
180 181 182 183 184 185 186 187 188 |
# File 'lib/moxml/node.rb', line 180 def entity_bearing? gen = adapter.serialize_generation if @entity_bearing_gen == gen @entity_bearing else @entity_bearing_gen = gen @entity_bearing = adapter.entity_bearing?(@native) end end |
#find(xpath_expression, namespaces = {}) ⇒ Object
Convenience find methods (aliases for xpath methods)
219 220 221 |
# File 'lib/moxml/node.rb', line 219 def find(xpath_expression, namespaces = {}) at_xpath(xpath_expression, namespaces) end |
#find_all(xpath_expression, namespaces = {}) ⇒ Object
223 224 225 |
# File 'lib/moxml/node.rb', line 223 def find_all(xpath_expression, namespaces = {}) xpath(xpath_expression, namespaces).to_a end |
#first_child ⇒ Object
Get first/last child
233 234 235 |
# File 'lib/moxml/node.rb', line 233 def first_child children.first end |
#following_siblings ⇒ NodeSet
Returns the siblings after this node, in document order.
337 338 339 340 341 342 343 344 345 346 |
# File 'lib/moxml/node.rb', line 337 def following_siblings parent = self.parent return NodeSet.new([], context) unless parent siblings = parent.children.to_a index = siblings.index { |child| child.native.equal?(@native) } return NodeSet.new([], context) if index.nil? NodeSet.new(siblings[(index + 1)..].map(&:native), context) end |
#has_children? ⇒ Boolean
Check if node has any children
228 229 230 |
# File 'lib/moxml/node.rb', line 228 def has_children? !children.empty? end |
#identifier ⇒ String?
Returns the primary identifier for this node type For Element: the tag name For Attribute: the attribute name For ProcessingInstruction: the target For content nodes (Text, Comment, Cdata, Declaration): nil (no identifier) For Doctype: nil (not fully implemented across adapters)
470 471 472 |
# File 'lib/moxml/node.rb', line 470 def identifier nil end |
#initialize(native, context, adapter = nil, node_type = nil) ⇒ Object
adapter/node_type are primed by Node.wrap, which resolves both before choosing the wrapper class — a fresh wrapper would otherwise pay the context hop and the type probe again on its first access.
22 23 24 |
# File 'lib/moxml/node.rb', line 22 def initialize(native, context, adapter = nil, node_type = nil) prime_contract(native, context, adapter, node_type) end |
#invalidate_namespace_cache! ⇒ Object
Namespace-scope caches live on Element; the base no-op lets tree mutations invalidate uniformly without type checks.
143 |
# File 'lib/moxml/node.rb', line 143 def invalidate_namespace_cache!; end |
#last_child ⇒ Object
237 238 239 |
# File 'lib/moxml/node.rb', line 237 def last_child children.last end |
#line_number ⇒ Integer?
Returns the 1-based line number where this node appears in the source XML, or nil when the underlying adapter does not track source positions.
400 401 402 |
# File 'lib/moxml/node.rb', line 400 def line_number adapter.line_number(@native) end |
#materialize(&block) ⇒ Object
Flattened post-order records for this subtree without allocating wrappers — see Moxml::Materializer (issue #132). Returns an Enumerator when no block is given.
201 202 203 |
# File 'lib/moxml/node.rb', line 201 def materialize(&block) Materializer.materialize(self, &block) end |
#materialize_fields(&block) ⇒ Object
Zero-allocation streaming form — flat reused buffers valid only inside the block (issue #143). See Moxml::Materializer.
207 208 209 210 211 |
# File 'lib/moxml/node.rb', line 207 def materialize_fields(&block) raise ArgumentError, "materialize_fields requires a block" unless block Materializer.materialize_fields(self, &block) end |
#namespace ⇒ Object
Returns the namespace of this node Only applicable to Element nodes, returns nil for others
260 261 262 263 264 265 |
# File 'lib/moxml/node.rb', line 260 def namespace return nil unless element? ns = adapter.namespace(@native) ns && Wrappers::Namespace.new(ns, context) end |
#namespaces ⇒ Object
Returns all namespace definitions on this node Only applicable to Element nodes, returns empty array for others
269 270 271 272 273 274 275 |
# File 'lib/moxml/node.rb', line 269 def namespaces return [] unless element? adapter.namespace_definitions(@native).map do |ns| Wrappers::Namespace.new(ns, context) end end |
#next_sibling ⇒ Object
94 95 96 |
# File 'lib/moxml/node.rb', line 94 def next_sibling Moxml::Node.wrap(adapter.next_sibling(@native), context) end |
#outer_xml ⇒ Object
424 425 426 |
# File 'lib/moxml/node.rb', line 424 def outer_xml to_xml end |
#parent ⇒ Object
72 73 74 |
# File 'lib/moxml/node.rb', line 72 def parent Moxml::Node.wrap(adapter.parent(@native), context) end |
#path ⇒ String
Returns an XPath expression that uniquely locates this node within its document. Positional predicates are emitted only when sibling elements share the same qualified name, keeping paths minimal.
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/moxml/node.rb', line 376 def path return "/" if document? unless element? raise Moxml::NotImplementedError.new( "path is only supported for element and document nodes", feature: "path", ) end segments = [] current = self while current && !current.document? segments.unshift(path_segment_for(current)) current = current.parent end "/#{segments.join('/')}" end |
#preceding_siblings ⇒ NodeSet
Returns the siblings before this node, in document order.
351 352 353 354 355 356 357 358 359 360 |
# File 'lib/moxml/node.rb', line 351 def preceding_siblings parent = self.parent return NodeSet.new([], context) unless parent siblings = parent.children.to_a index = siblings.index { |child| child.native.equal?(@native) } return NodeSet.new([], context) if index.nil? NodeSet.new(siblings[0...index].map(&:native), context) end |
#previous_sibling ⇒ Object
98 99 100 |
# File 'lib/moxml/node.rb', line 98 def previous_sibling Moxml::Node.wrap(adapter.previous_sibling(@native), context) end |
#prime_contract(native, context, adapter = nil, node_type = nil) ⇒ Object
Update native reference after identity-changing operations (e.g., LibXML doc.root= creates a new Ruby wrapper) Contract priming shared by the constructor and the extend-in-place mint (#230): an extended native IS its own @native.
30 31 32 33 34 35 36 37 |
# File 'lib/moxml/node.rb', line 30 def prime_contract(native, context, adapter = nil, node_type = nil) @context = context @native = native @parent_node = nil @adapter = adapter @node_type_cached = node_type self end |
#refresh_native!(new_native) ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/moxml/node.rb', line 39 def refresh_native!(new_native) unless new_native.equal?(@native) # Cache-stable binding nodes carry the wrapper in an ivar # (see Node.wrap_with) — re-point it there; other natives # ride the context map. if @native.instance_variable_defined?(:@moxml_wrapper) @native.instance_variable_set(:@moxml_wrapper, nil) else context.unregister_wrapper(@native) end @native = new_native if new_native.instance_variable_defined?(:@c_address) new_native.instance_variable_set(:@moxml_wrapper, self) else context.register_wrapper(new_native, self) end clear_native_memo! end self end |
#remove ⇒ Object
132 133 134 135 136 137 138 139 |
# File 'lib/moxml/node.rb', line 132 def remove invalidate_parent_children_cache! adapter.remove(@native) invalidate_children_cache! # The detached subtree left its declaring ancestors behind invalidate_namespace_cache! self end |
#replace(node) ⇒ Object
145 146 147 148 149 150 151 |
# File 'lib/moxml/node.rb', line 145 def replace(node) node = prepare_node(node) invalidate_parent_children_cache! adapter.replace(@native, node.native) invalidate_children_cache! self end |
#source_position ⇒ Object
col_start, col_end where the engine exposes source positions (leptris 1.9.181+); nil elsewhere. Created nodes answer zeros upstream — distinguishable from nil by callers that care.
408 409 410 |
# File 'lib/moxml/node.rb', line 408 def source_position adapter.source_position(@native) end |
#text ⇒ Object
Returns the text content of this node Subclasses should override this method Element and Text have their own implementations
244 245 246 |
# File 'lib/moxml/node.rb', line 244 def text "" end |
#to_xml(options = {}) ⇒ Object
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/moxml/node.rb', line 153 def to_xml( = {}) # Determine if we should include XML declaration # For Document nodes: check native then wrapper, unless explicitly overridden # For other nodes: default to no declaration unless explicitly set = if .empty? && !is_a?(Document) context. else merged = context..merge() merged[:no_declaration] = !should_include_declaration?() merged end result = adapter.serialize(@native, ) result = apply_line_ending(result, [:line_ending]) # Restore entity markers to named entity references; skipped # when the adapter knows the document carries no markers. result = adapter.restore_entities(result) if entity_bearing? result end |
#xpath(expression, namespaces = {}) ⇒ Object
190 191 192 193 194 195 196 |
# File 'lib/moxml/node.rb', line 190 def xpath(expression, namespaces = {}) result = adapter.xpath(@native, expression, namespaces) # Adapter contract: Array<native> | LazyNodeSet | scalar. # Scalars (count(), string-length(), booleans) pass through # unwrapped; the set forms wrap lazily. result.is_a?(Array) || result.is_a?(LazyNodeSet) ? NodeSet.new(result, context) : result end |