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.
466 467 468 |
# File 'lib/moxml/node.rb', line 466 def parent_node @parent_node end |
Instance Method Details
#==(other) ⇒ Object
429 430 431 432 433 434 435 |
# File 'lib/moxml/node.rb', line 429 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
468 469 470 471 472 |
# File 'lib/moxml/node.rb', line 468 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
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/moxml/node.rb', line 91 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
114 115 116 117 118 119 |
# File 'lib/moxml/node.rb', line 114 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
107 108 109 110 111 112 |
# File 'lib/moxml/node.rb', line 107 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
421 422 423 |
# File 'lib/moxml/node.rb', line 421 def after(node) add_next_sibling(node) end |
#ancestors ⇒ NodeSet
Returns all ancestor nodes from the parent up to and including the document node.
299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/moxml/node.rb', line 299 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
202 203 204 205 |
# File 'lib/moxml/node.rb', line 202 def at_xpath(expression, namespaces = {}) Moxml::Node.wrap(adapter.at_xpath(@native, expression, namespaces), context) end |
#before(node) ⇒ Object
417 418 419 |
# File 'lib/moxml/node.rb', line 417 def before(node) add_previous_sibling(node) end |
#blank? ⇒ Boolean
425 426 427 |
# File 'lib/moxml/node.rb', line 425 def blank? text.strip.empty? end |
#children ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/moxml/node.rb', line 65 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.
51 52 53 54 55 |
# File 'lib/moxml/node.rb', line 51 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)
243 244 245 |
# File 'lib/moxml/node.rb', line 243 def content "" end |
#descendants ⇒ NodeSet
Returns all descendant nodes (children, grandchildren, and so on), excluding the node itself.
317 318 319 320 321 |
# File 'lib/moxml/node.rb', line 317 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.
409 410 411 |
# File 'lib/moxml/node.rb', line 409 def digest(drop_ws_text: false) adapter.digest(@native, drop_ws_text: drop_ws_text) end |
#document ⇒ Object
57 58 59 |
# File 'lib/moxml/node.rb', line 57 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)
352 353 354 |
# File 'lib/moxml/node.rb', line 352 def dup Moxml::Node.wrap(adapter.duplicate_node(@native), context) end |
#each(&block) ⇒ Object
Yield direct children, enabling Enumerable on the node.
289 290 291 292 293 |
# File 'lib/moxml/node.rb', line 289 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
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/moxml/node.rb', line 268 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.
169 170 171 172 173 174 175 176 177 |
# File 'lib/moxml/node.rb', line 169 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)
208 209 210 |
# File 'lib/moxml/node.rb', line 208 def find(xpath_expression, namespaces = {}) at_xpath(xpath_expression, namespaces) end |
#find_all(xpath_expression, namespaces = {}) ⇒ Object
212 213 214 |
# File 'lib/moxml/node.rb', line 212 def find_all(xpath_expression, namespaces = {}) xpath(xpath_expression, namespaces).to_a end |
#first_child ⇒ Object
Get first/last child
222 223 224 |
# File 'lib/moxml/node.rb', line 222 def first_child children.first end |
#following_siblings ⇒ NodeSet
Returns the siblings after this node, in document order.
326 327 328 329 330 331 332 333 334 335 |
# File 'lib/moxml/node.rb', line 326 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
217 218 219 |
# File 'lib/moxml/node.rb', line 217 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)
459 460 461 |
# File 'lib/moxml/node.rb', line 459 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.
132 |
# File 'lib/moxml/node.rb', line 132 def invalidate_namespace_cache!; end |
#last_child ⇒ Object
226 227 228 |
# File 'lib/moxml/node.rb', line 226 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.
389 390 391 |
# File 'lib/moxml/node.rb', line 389 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.
190 191 192 |
# File 'lib/moxml/node.rb', line 190 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.
196 197 198 199 200 |
# File 'lib/moxml/node.rb', line 196 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
249 250 251 252 253 254 |
# File 'lib/moxml/node.rb', line 249 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
258 259 260 261 262 263 264 |
# File 'lib/moxml/node.rb', line 258 def namespaces return [] unless element? adapter.namespace_definitions(@native).map do |ns| Wrappers::Namespace.new(ns, context) end end |
#next_sibling ⇒ Object
83 84 85 |
# File 'lib/moxml/node.rb', line 83 def next_sibling Moxml::Node.wrap(adapter.next_sibling(@native), context) end |
#outer_xml ⇒ Object
413 414 415 |
# File 'lib/moxml/node.rb', line 413 def outer_xml to_xml end |
#parent ⇒ Object
61 62 63 |
# File 'lib/moxml/node.rb', line 61 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.
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/moxml/node.rb', line 365 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.
340 341 342 343 344 345 346 347 348 349 |
# File 'lib/moxml/node.rb', line 340 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
87 88 89 |
# File 'lib/moxml/node.rb', line 87 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 |
# File 'lib/moxml/node.rb', line 39 def refresh_native!(new_native) unless new_native.equal?(@native) context.unregister_wrapper(@native) @native = new_native context.register_wrapper(new_native, self) clear_native_memo! end self end |
#remove ⇒ Object
121 122 123 124 125 126 127 128 |
# File 'lib/moxml/node.rb', line 121 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
134 135 136 137 138 139 140 |
# File 'lib/moxml/node.rb', line 134 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.
397 398 399 |
# File 'lib/moxml/node.rb', line 397 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
233 234 235 |
# File 'lib/moxml/node.rb', line 233 def text "" end |
#to_xml(options = {}) ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/moxml/node.rb', line 142 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
179 180 181 182 183 184 185 |
# File 'lib/moxml/node.rb', line 179 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 |