Class: Itonoko::XML::Node

Inherits:
Object
  • Object
show all
Defined in:
lib/itonoko/xml/node.rb

Constant Summary collapse

ELEMENT_NODE =
1
ATTRIBUTE_NODE =
2
TEXT_NODE =
3
CDATA_SECTION_NODE =
4
PROCESSING_INSTRUCTION_NODE =
7
COMMENT_NODE =
8
DOCUMENT_NODE =
9
DOCUMENT_TYPE_NODE =
10
DOCUMENT_FRAGMENT_NODE =
11
EMPTY_ATTRS =

Shared frozen constants — leaf nodes use these to avoid per-node allocation.

{}.freeze
EMPTY_CHILDREN =
[].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node_type, node_name, document = nil) ⇒ Node

Returns a new instance of Node.



25
26
27
28
29
30
31
32
# File 'lib/itonoko/xml/node.rb', line 25

def initialize(node_type, node_name, document = nil)
  @node_type  = node_type
  @node_name  = node_name
  @document   = document
  @parent     = nil
  @children   = []
  @attributes = EMPTY_ATTRS  # upgraded to mutable hash on first write
end

Instance Attribute Details

#childrenObject (readonly)

Returns the value of attribute children.



23
24
25
# File 'lib/itonoko/xml/node.rb', line 23

def children
  @children
end

#documentObject

Returns the value of attribute document.



22
23
24
# File 'lib/itonoko/xml/node.rb', line 22

def document
  @document
end

#node_nameObject (readonly)

Returns the value of attribute node_name.



23
24
25
# File 'lib/itonoko/xml/node.rb', line 23

def node_name
  @node_name
end

#node_typeObject (readonly)

Returns the value of attribute node_type.



23
24
25
# File 'lib/itonoko/xml/node.rb', line 23

def node_type
  @node_type
end

#parentObject

Returns the value of attribute parent.



22
23
24
# File 'lib/itonoko/xml/node.rb', line 22

def parent
  @parent
end

Instance Method Details

#==(other) ⇒ Object



340
341
342
# File 'lib/itonoko/xml/node.rb', line 340

def ==(other)
  equal?(other)
end

#[](attr_name) ⇒ Object



90
91
92
# File 'lib/itonoko/xml/node.rb', line 90

def [](attr_name)
  @attributes[attr_name.to_s]
end

#[]=(attr_name, value) ⇒ Object



94
95
96
97
98
99
100
# File 'lib/itonoko/xml/node.rb', line 94

def []=(attr_name, value)
  if @attributes.frozen?
    @attributes = { attr_name.to_s => value.to_s }
  else
    @attributes[attr_name.to_s] = value.to_s
  end
end

#_collect_text(buf) ⇒ Object

Override in subclasses for leaf nodes.



152
153
154
# File 'lib/itonoko/xml/node.rb', line 152

def _collect_text(buf)
  @children.each { |c| c._collect_text(buf) }
end

#add_child(node_or_markup) ⇒ Object Also known as: <<

── tree manipulation ─────────────────────────────────────────



172
173
174
175
176
177
178
179
180
181
# File 'lib/itonoko/xml/node.rb', line 172

def add_child(node_or_markup)
  nodes = coerce_nodes(node_or_markup)
  nodes.each do |node|
    node.parent&.children&.delete(node)
    node.parent   = self
    node.document = document
    @children << node
  end
  nodes.length == 1 ? nodes.first : NodeSet.new(document, nodes)
end

#add_next_sibling(node_or_markup) ⇒ Object Also known as: after



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/itonoko/xml/node.rb', line 195

def add_next_sibling(node_or_markup)
  raise "no parent" unless parent
  nodes = coerce_nodes(node_or_markup)
  idx   = parent.children.index(self) + 1
  nodes.each_with_index do |node, i|
    node.parent&.children&.delete(node)
    node.parent   = parent
    node.document = document
    parent.children.insert(idx + i, node)
  end
  nodes.length == 1 ? nodes.first : NodeSet.new(document, nodes)
end

#add_previous_sibling(node_or_markup) ⇒ Object Also known as: before



209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/itonoko/xml/node.rb', line 209

def add_previous_sibling(node_or_markup)
  raise "no parent" unless parent
  nodes = coerce_nodes(node_or_markup)
  idx   = parent.children.index(self)
  nodes.each_with_index do |node, i|
    node.parent&.children&.delete(node)
    node.parent   = parent
    node.document = document
    parent.children.insert(idx + i, node)
  end
  nodes.length == 1 ? nodes.first : NodeSet.new(document, nodes)
end

#ancestors(selector = nil) ⇒ Object



65
66
67
68
69
70
71
72
73
74
# File 'lib/itonoko/xml/node.rb', line 65

def ancestors(selector = nil)
  result = []
  node = parent
  while node
    result << node if node.node_type != DOCUMENT_NODE
    node = node.parent
  end
  list = NodeSet.new(document, result)
  selector ? list.select { |n| n.matches_css?(selector) } : list
end

#append_child(node) ⇒ Object

── fast path for parsers (skips parent-removal + coercion) ───



163
164
165
166
167
168
# File 'lib/itonoko/xml/node.rb', line 163

def append_child(node)
  node.parent   = self
  node.document = @document
  @children << node
  node
end

#at(*queries) ⇒ Object



276
277
278
# File 'lib/itonoko/xml/node.rb', line 276

def at(*queries)
  search(*queries).first
end

#at_css(selector) ⇒ Object



264
265
266
# File 'lib/itonoko/xml/node.rb', line 264

def at_css(selector)
  css(selector).first
end

#at_xpath(expr, namespaces = {}) ⇒ Object



268
269
270
# File 'lib/itonoko/xml/node.rb', line 268

def at_xpath(expr, namespaces = {})
  xpath(expr, namespaces).first
end

#attribute(name) ⇒ Object



123
124
125
126
127
# File 'lib/itonoko/xml/node.rb', line 123

def attribute(name)
  val = @attributes[name.to_s]
  return nil unless val
  Attr.new(name.to_s, val, document)
end

#attribute_nodesObject



136
137
138
# File 'lib/itonoko/xml/node.rb', line 136

def attribute_nodes
  @attributes.map { |k, v| Attr.new(k, v, document) }
end

#attributesObject



129
130
131
132
133
134
# File 'lib/itonoko/xml/node.rb', line 129

def attributes
  @attributes.each_with_object({}) do |(k, v), h|
    a = Attr.new(k, v, document)
    h[k] = a
  end
end

#cdata_node?Boolean

Returns:

  • (Boolean)


356
357
358
# File 'lib/itonoko/xml/node.rb', line 356

def cdata_node?
  node_type == CDATA_SECTION_NODE
end

#childObject



80
81
82
# File 'lib/itonoko/xml/node.rb', line 80

def child
  children.first
end

#comment?Boolean

Returns:

  • (Boolean)


352
353
354
# File 'lib/itonoko/xml/node.rb', line 352

def comment?
  node_type == COMMENT_NODE
end

#css(selector) ⇒ Object

── search ────────────────────────────────────────────────────



254
255
256
257
# File 'lib/itonoko/xml/node.rb', line 254

def css(selector)
  require_relative "../css/matcher"
  CSS::Matcher.match(self, selector)
end

#descriptionObject



368
369
370
# File 'lib/itonoko/xml/node.rb', line 368

def description
  node_name
end

#document?Boolean

Returns:

  • (Boolean)


364
365
366
# File 'lib/itonoko/xml/node.rb', line 364

def document?
  node_type == DOCUMENT_NODE
end

#element?Boolean

Returns:

  • (Boolean)


344
345
346
# File 'lib/itonoko/xml/node.rb', line 344

def element?
  node_type == ELEMENT_NODE
end

#element_childrenObject



76
77
78
# File 'lib/itonoko/xml/node.rb', line 76

def element_children
  NodeSet.new(document, children.select { |c| c.node_type == ELEMENT_NODE })
end

#fragment?Boolean

Returns:

  • (Boolean)


360
361
362
# File 'lib/itonoko/xml/node.rb', line 360

def fragment?
  node_type == DOCUMENT_FRAGMENT_NODE
end

#get_attribute(name) ⇒ Object



102
103
104
# File 'lib/itonoko/xml/node.rb', line 102

def get_attribute(name)
  @attributes[name.to_s]
end

#has_attribute?(name) ⇒ Boolean

Returns:

  • (Boolean)


115
116
117
# File 'lib/itonoko/xml/node.rb', line 115

def has_attribute?(name)
  @attributes.key?(name.to_s)
end

#inner_htmlObject

String concat instead of map+join — one less intermediate Array.



287
288
289
290
291
# File 'lib/itonoko/xml/node.rb', line 287

def inner_html
  buf = +""
  @children.each { |c| buf << c.to_html }
  buf
end

#inner_html=(markup) ⇒ Object



244
245
246
247
248
249
250
# File 'lib/itonoko/xml/node.rb', line 244

def inner_html=(markup)
  @children = []
  frag = document.is_a?(HTML::Document) ?
         HTML::DocumentFragment.parse(markup, document) :
         XML::DocumentFragment.parse(markup, document)
  frag.children.each { |c| add_child(c) }
end

#inspectObject



336
337
338
# File 'lib/itonoko/xml/node.rb', line 336

def inspect
  "#<#{self.class} name=#{node_name.inspect} children=#{children.length}>"
end

#keysObject



119
120
121
# File 'lib/itonoko/xml/node.rb', line 119

def keys
  @attributes.keys
end

#matches?(selector) ⇒ Boolean

Returns:

  • (Boolean)


280
281
282
# File 'lib/itonoko/xml/node.rb', line 280

def matches?(selector)
  CSS::Matcher.matches_selector?(self, selector)
end

#nameObject

── attributes ────────────────────────────────────────────────



86
87
88
# File 'lib/itonoko/xml/node.rb', line 86

def name
  node_name
end

#next_elementObject



53
54
55
56
57
# File 'lib/itonoko/xml/node.rb', line 53

def next_element
  sib = next_sibling
  sib = sib.next_sibling while sib && sib.node_type != ELEMENT_NODE
  sib
end

#next_siblingObject



41
42
43
44
45
# File 'lib/itonoko/xml/node.rb', line 41

def next_sibling
  return nil unless parent
  idx = parent.children.index(self)
  idx && parent.children[idx + 1]
end

#prepend_child(node_or_markup) ⇒ Object



184
185
186
187
188
189
190
191
192
193
# File 'lib/itonoko/xml/node.rb', line 184

def prepend_child(node_or_markup)
  nodes = coerce_nodes(node_or_markup)
  nodes.reverse_each do |node|
    node.parent&.children&.delete(node)
    node.parent   = self
    node.document = document
    @children.unshift(node)
  end
  nodes.length == 1 ? nodes.first : NodeSet.new(document, nodes)
end

#previous_elementObject



59
60
61
62
63
# File 'lib/itonoko/xml/node.rb', line 59

def previous_element
  sib = previous_sibling
  sib = sib.previous_sibling while sib && sib.node_type != ELEMENT_NODE
  sib
end

#previous_siblingObject



47
48
49
50
51
# File 'lib/itonoko/xml/node.rb', line 47

def previous_sibling
  return nil unless parent
  idx = parent.children.index(self)
  idx && idx > 0 ? parent.children[idx - 1] : nil
end

#removeObject Also known as: unlink



223
224
225
226
227
# File 'lib/itonoko/xml/node.rb', line 223

def remove
  parent&.children&.delete(self)
  @parent = nil
  self
end

#remove_attribute(name) ⇒ Object



110
111
112
113
# File 'lib/itonoko/xml/node.rb', line 110

def remove_attribute(name)
  return if @attributes.frozen?
  @attributes.delete(name.to_s)
end

#replace(node_or_markup) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/itonoko/xml/node.rb', line 230

def replace(node_or_markup)
  raise "no parent" unless parent
  nodes = coerce_nodes(node_or_markup)
  idx   = parent.children.index(self)
  parent.children.delete_at(idx)
  nodes.reverse_each do |node|
    node.parent   = parent
    node.document = document
    parent.children.insert(idx, node)
  end
  @parent = nil
  nodes.length == 1 ? nodes.first : NodeSet.new(document, nodes)
end

#rootObject

── navigation ────────────────────────────────────────────────



36
37
38
39
# File 'lib/itonoko/xml/node.rb', line 36

def root
  return self if parent.nil? || parent.node_type == DOCUMENT_NODE
  parent.root
end

#search(*queries) ⇒ Object



272
273
274
# File 'lib/itonoko/xml/node.rb', line 272

def search(*queries)
  NodeSet.new(document, queries.flat_map { |q| css(q).to_a })
end

#set_attribute(name, value) ⇒ Object



106
107
108
# File 'lib/itonoko/xml/node.rb', line 106

def set_attribute(name, value)
  self[name] = value
end

#textObject Also known as: content, inner_text

Accumulator-based text extraction — avoids intermediate arrays and join.



143
144
145
146
147
# File 'lib/itonoko/xml/node.rb', line 143

def text
  buf = +""
  _collect_text(buf)
  buf
end

#text=(str) ⇒ Object Also known as: content=



156
157
158
# File 'lib/itonoko/xml/node.rb', line 156

def text=(str)
  @children = [Text.new(str.to_s, document).tap { |t| t.parent = self }]
end

#text?Boolean

Returns:

  • (Boolean)


348
349
350
# File 'lib/itonoko/xml/node.rb', line 348

def text?
  node_type == TEXT_NODE
end

#to_htmlObject



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/itonoko/xml/node.rb', line 293

def to_html
  html_mode = document.nil? || document.is_a?(HTML::Document)
  case node_type
  when ELEMENT_NODE
    serialize_element(html_mode)
  when TEXT_NODE, CDATA_SECTION_NODE
    escape_text(node_name)
  when COMMENT_NODE
    "<!--#{node_name}-->"
  when PROCESSING_INSTRUCTION_NODE
    "<?#{node_name}?>"
  when DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE
    inner_html
  else
    ""
  end
end

#to_sObject



332
333
334
# File 'lib/itonoko/xml/node.rb', line 332

def to_s
  to_html
end

#to_xml(options = {}) ⇒ Object



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/itonoko/xml/node.rb', line 311

def to_xml(options = {})
  case node_type
  when ELEMENT_NODE
    serialize_element(false)
  when TEXT_NODE
    escape_text(node_name)
  when CDATA_SECTION_NODE
    "<![CDATA[#{node_name}]]>"
  when COMMENT_NODE
    "<!--#{node_name}-->"
  when PROCESSING_INSTRUCTION_NODE
    "<?#{node_name}?>"
  when DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE
    buf = +""
    @children.each { |c| buf << c.to_xml(options) }
    buf
  else
    ""
  end
end

#xpath(expr, namespaces = {}) ⇒ Object



259
260
261
262
# File 'lib/itonoko/xml/node.rb', line 259

def xpath(expr, namespaces = {})
  require_relative "../xpath/evaluator"
  XPath::Evaluator.new(self, namespaces).evaluate(expr)
end