Class: Moxml::Node
- Inherits:
-
Object
- Object
- Moxml::Node
- Includes:
- Enumerable, XmlUtils
- Defined in:
- lib/moxml/node.rb
Direct Known Subclasses
Attribute, Cdata, Comment, Declaration, Doctype, Document, Element, EntityReference, Namespace, ProcessingInstruction, Text
Constant Summary collapse
- TYPES =
%i[ element text cdata comment processing_instruction document declaration doctype namespace attribute unknown entity_reference ].freeze
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
writeonly
Internal: Set the parent node for cache invalidation tracking.
Class Method Summary collapse
- .adapter(context) ⇒ Object
-
.node_type_map ⇒ Object
Registry mapping node type symbols to wrapper classes.
- .wrap(node, context) ⇒ Object
Instance Method Summary collapse
- #==(other) ⇒ 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
-
#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.
- #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.
-
#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) ⇒ Node
constructor
A new instance of Node.
-
#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).
-
#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
-
#refresh_native!(new_native) ⇒ Object
Update native reference after identity-changing operations (e.g., LibXML doc.root= creates a new Ruby wrapper).
- #remove ⇒ Object
- #replace(node) ⇒ Object
-
#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
Constructor Details
#initialize(native, context) ⇒ Node
Returns a new instance of Node.
15 16 17 18 19 |
# File 'lib/moxml/node.rb', line 15 def initialize(native, context) @context = context @native = native @parent_node = nil end |
Instance Attribute Details
#context ⇒ Object (readonly)
Returns the value of attribute context.
13 14 15 |
# File 'lib/moxml/node.rb', line 13 def context @context end |
#native ⇒ Object (readonly)
Returns the value of attribute native.
13 14 15 |
# File 'lib/moxml/node.rb', line 13 def native @native end |
#parent_node=(value) ⇒ Object (writeonly)
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.
382 383 384 |
# File 'lib/moxml/node.rb', line 382 def parent_node=(value) @parent_node = value end |
Class Method Details
.adapter(context) ⇒ Object
390 391 392 |
# File 'lib/moxml/node.rb', line 390 def self.adapter(context) context.config.adapter end |
.node_type_map ⇒ Object
Registry mapping node type symbols to wrapper classes. Built lazily to avoid load-order issues with subclasses.
352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/moxml/node.rb', line 352 def self.node_type_map @node_type_map ||= { element: Element, text: Text, cdata: Cdata, comment: Comment, processing_instruction: ProcessingInstruction, document: Document, declaration: Declaration, doctype: Doctype, attribute: Attribute, entity_reference: EntityReference, }.freeze end |
.wrap(node, context) ⇒ Object
367 368 369 370 371 372 373 374 375 376 377 |
# File 'lib/moxml/node.rb', line 367 def self.wrap(node, context) return nil if node.nil? cached = context.wrapper_for(node) return cached if cached type = adapter(context).node_type(node) klass = node_type_map[type] || self klass.new(node, context).tap { |wrapper| context.register_wrapper(node, wrapper) } end |
Instance Method Details
#==(other) ⇒ Object
328 329 330 |
# File 'lib/moxml/node.rb', line 328 def ==(other) self.class == other.class && @native == other.native end |
#add_child(node) ⇒ Object
56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/moxml/node.rb', line 56 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=) refreshed = adapter.actual_native(node.native, @native) node.refresh_native!(refreshed) if refreshed && refreshed != node.native 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
76 77 78 79 80 81 |
# File 'lib/moxml/node.rb', line 76 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
69 70 71 72 73 74 |
# File 'lib/moxml/node.rb', line 69 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
320 321 322 |
# File 'lib/moxml/node.rb', line 320 def after(node) add_next_sibling(node) end |
#ancestors ⇒ NodeSet
Returns all ancestor nodes from the parent up to and including the document node.
218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/moxml/node.rb', line 218 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
134 135 136 137 |
# File 'lib/moxml/node.rb', line 134 def at_xpath(expression, namespaces = {}) Moxml::Node.wrap(adapter.at_xpath(@native, expression, namespaces), context) end |
#before(node) ⇒ Object
316 317 318 |
# File 'lib/moxml/node.rb', line 316 def before(node) add_previous_sibling(node) end |
#blank? ⇒ Boolean
324 325 326 |
# File 'lib/moxml/node.rb', line 324 def blank? text.strip.empty? end |
#children ⇒ Object
40 41 42 43 44 45 46 |
# File 'lib/moxml/node.rb', line 40 def children @children ||= begin natives = adapter.children(@native) natives = natives.map { adapter.patch_node(_1, @native) } if adapter.patches_children? NodeSet.new(natives, context, self) end 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)
175 176 177 |
# File 'lib/moxml/node.rb', line 175 def content "" end |
#descendants ⇒ NodeSet
Returns all descendant nodes (children, grandchildren, and so on), excluding the node itself.
236 237 238 239 240 |
# File 'lib/moxml/node.rb', line 236 def descendants natives = [] each_node { |node| natives << node.native } NodeSet.new(natives, context) end |
#document ⇒ Object
32 33 34 |
# File 'lib/moxml/node.rb', line 32 def document Document.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)
271 272 273 |
# File 'lib/moxml/node.rb', line 271 def dup Moxml::Node.wrap(adapter.duplicate_node(@native), context) end |
#each(&block) ⇒ Object
Yield direct children, enabling Enumerable on the node.
208 209 210 211 212 |
# File 'lib/moxml/node.rb', line 208 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
200 201 202 203 204 205 |
# File 'lib/moxml/node.rb', line 200 def each_node(&block) children.each do |child| yield child child.each_node(&block) end end |
#find(xpath_expression, namespaces = {}) ⇒ Object
Convenience find methods (aliases for xpath methods)
140 141 142 |
# File 'lib/moxml/node.rb', line 140 def find(xpath_expression, namespaces = {}) at_xpath(xpath_expression, namespaces) end |
#find_all(xpath_expression, namespaces = {}) ⇒ Object
144 145 146 |
# File 'lib/moxml/node.rb', line 144 def find_all(xpath_expression, namespaces = {}) xpath(xpath_expression, namespaces).to_a end |
#first_child ⇒ Object
Get first/last child
154 155 156 |
# File 'lib/moxml/node.rb', line 154 def first_child children.first end |
#following_siblings ⇒ NodeSet
Returns the siblings after this node, in document order.
245 246 247 248 249 250 251 252 253 254 |
# File 'lib/moxml/node.rb', line 245 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
149 150 151 |
# File 'lib/moxml/node.rb', line 149 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)
346 347 348 |
# File 'lib/moxml/node.rb', line 346 def identifier nil end |
#invalidate_namespace_cache! ⇒ Object
Namespace-scope caches live on Element; the base no-op lets tree mutations invalidate uniformly without type checks.
94 |
# File 'lib/moxml/node.rb', line 94 def invalidate_namespace_cache!; end |
#last_child ⇒ Object
158 159 160 |
# File 'lib/moxml/node.rb', line 158 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.
308 309 310 |
# File 'lib/moxml/node.rb', line 308 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.
130 131 132 |
# File 'lib/moxml/node.rb', line 130 def materialize(&block) Materializer.materialize(self, &block) end |
#namespace ⇒ Object
Returns the namespace of this node Only applicable to Element nodes, returns nil for others
181 182 183 184 185 186 |
# File 'lib/moxml/node.rb', line 181 def namespace return nil unless element? ns = adapter.namespace(@native) ns && Namespace.new(ns, context) end |
#namespaces ⇒ Object
Returns all namespace definitions on this node Only applicable to Element nodes, returns empty array for others
190 191 192 193 194 195 196 |
# File 'lib/moxml/node.rb', line 190 def namespaces return [] unless element? adapter.namespace_definitions(@native).map do |ns| Namespace.new(ns, context) end end |
#next_sibling ⇒ Object
48 49 50 |
# File 'lib/moxml/node.rb', line 48 def next_sibling Moxml::Node.wrap(adapter.next_sibling(@native), context) end |
#outer_xml ⇒ Object
312 313 314 |
# File 'lib/moxml/node.rb', line 312 def outer_xml to_xml end |
#parent ⇒ Object
36 37 38 |
# File 'lib/moxml/node.rb', line 36 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.
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/moxml/node.rb', line 284 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.
259 260 261 262 263 264 265 266 267 268 |
# File 'lib/moxml/node.rb', line 259 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
52 53 54 |
# File 'lib/moxml/node.rb', line 52 def previous_sibling Moxml::Node.wrap(adapter.previous_sibling(@native), context) end |
#refresh_native!(new_native) ⇒ Object
Update native reference after identity-changing operations (e.g., LibXML doc.root= creates a new Ruby wrapper)
23 24 25 26 27 28 29 30 |
# File 'lib/moxml/node.rb', line 23 def refresh_native!(new_native) unless new_native.equal?(@native) context.unregister_wrapper(@native) @native = new_native context.register_wrapper(new_native, self) end self end |
#remove ⇒ Object
83 84 85 86 87 88 89 90 |
# File 'lib/moxml/node.rb', line 83 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
96 97 98 99 100 101 102 |
# File 'lib/moxml/node.rb', line 96 def replace(node) node = prepare_node(node) invalidate_parent_children_cache! adapter.replace(@native, node.native) invalidate_children_cache! self end |
#text ⇒ Object
Returns the text content of this node Subclasses should override this method Element and Text have their own implementations
165 166 167 |
# File 'lib/moxml/node.rb', line 165 def text "" end |
#to_xml(options = {}) ⇒ Object
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/moxml/node.rb', line 104 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 = .merge() [:no_declaration] = !should_include_declaration?() 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 adapter.entity_bearing?(@native) result end |
#xpath(expression, namespaces = {}) ⇒ Object
120 121 122 123 124 125 |
# File 'lib/moxml/node.rb', line 120 def xpath(expression, namespaces = {}) result = adapter.xpath(@native, expression, namespaces) # Adapter contract: Array<native> | scalar. Scalars (count(), # string-length(), booleans) pass through unwrapped. result.is_a?(Array) ? NodeSet.new(result, context) : result end |