Class: Moxml::Node

Inherits:
Object
  • Object
show all
Includes:
Enumerable, XmlUtils
Defined in:
lib/moxml/node.rb

Constant Summary collapse

TYPES =
%i[
  element text cdata comment processing_instruction document
  declaration doctype namespace attribute unknown entity_reference
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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

#contextObject (readonly)

Returns the value of attribute context.



13
14
15
# File 'lib/moxml/node.rb', line 13

def context
  @context
end

#nativeObject (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_mapObject

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

#ancestorsNodeSet

Returns all ancestor nodes from the parent up to and including the document node.

Returns:

  • (NodeSet)

    ancestors ordered nearest-first



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

Returns:

  • (Boolean)


324
325
326
# File 'lib/moxml/node.rb', line 324

def blank?
  text.strip.empty?
end

#childrenObject



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

#contentObject

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

#descendantsNodeSet

Returns all descendant nodes (children, grandchildren, and so on), excluding the node itself.

Returns:

  • (NodeSet)

    descendants in document order



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

#documentObject



32
33
34
# File 'lib/moxml/node.rb', line 32

def document
  Document.wrap(adapter.document(@native), context)
end

#dupObject 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_childObject

Get first/last child



154
155
156
# File 'lib/moxml/node.rb', line 154

def first_child
  children.first
end

#following_siblingsNodeSet

Returns the siblings after this node, in document order.

Returns:



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

Returns:

  • (Boolean)


149
150
151
# File 'lib/moxml/node.rb', line 149

def has_children?
  !children.empty?
end

#identifierString?

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)

Returns:

  • (String, nil)

    the node's primary identifier or nil



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_childObject



158
159
160
# File 'lib/moxml/node.rb', line 158

def last_child
  children.last
end

#line_numberInteger?

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.

Returns:

  • (Integer, nil)


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

#namespaceObject

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

#namespacesObject

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_siblingObject



48
49
50
# File 'lib/moxml/node.rb', line 48

def next_sibling
  Moxml::Node.wrap(adapter.next_sibling(@native), context)
end

#outer_xmlObject



312
313
314
# File 'lib/moxml/node.rb', line 312

def outer_xml
  to_xml
end

#parentObject



36
37
38
# File 'lib/moxml/node.rb', line 36

def parent
  Moxml::Node.wrap(adapter.parent(@native), context)
end

#pathString

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.

Returns:

  • (String)

    XPath expression

Raises:



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_siblingsNodeSet

Returns the siblings before this node, in document order.

Returns:



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_siblingObject



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

#removeObject



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

#textObject

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(options = {})
  # 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
  serialize_options = default_options.merge(options)
  serialize_options[:no_declaration] = !should_include_declaration?(options)

  result = adapter.serialize(@native, serialize_options)
  result = apply_line_ending(result, serialize_options[: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