arboretum 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/arboretum/doctree.rb +1566 -0
- data/lib/arboretum/scandent.rb +882 -0
- data/lib/arboretum/xml.rb +169 -0
- metadata +4 -1
@@ -0,0 +1,1566 @@
|
|
1
|
+
module Arboretum
|
2
|
+
module DocTree
|
3
|
+
module Counters
|
4
|
+
|
5
|
+
class Counter
|
6
|
+
attr_accessor :name, :incrementers, :resetters, :start_value, :gradient, :current_value
|
7
|
+
@@counters = Hash.new{|h, name| h[name] = Counter.new(name)}
|
8
|
+
|
9
|
+
def self.counters
|
10
|
+
@@counters
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(name)
|
14
|
+
name = name.to_sym if !name.is_a?(Symbol)
|
15
|
+
@name = name
|
16
|
+
@incrementers = []
|
17
|
+
@resetters = []
|
18
|
+
@start_value = 1
|
19
|
+
@gradient = 1
|
20
|
+
|
21
|
+
@current_value = start_value
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset
|
25
|
+
@current_value = start_value
|
26
|
+
end
|
27
|
+
|
28
|
+
def increment
|
29
|
+
@current_value += gradient
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Incrementer
|
34
|
+
attr_reader :name
|
35
|
+
attr_accessor :value
|
36
|
+
|
37
|
+
def initialize(name)
|
38
|
+
@name = name
|
39
|
+
@value = 0
|
40
|
+
Counter.counters[name].incrementers << self
|
41
|
+
end
|
42
|
+
|
43
|
+
def counter
|
44
|
+
Counter.counters[@name]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Resetter
|
49
|
+
attr_reader :name
|
50
|
+
|
51
|
+
def initialize(name)
|
52
|
+
@name = name
|
53
|
+
Counter.counters[name].resetters << self
|
54
|
+
end
|
55
|
+
|
56
|
+
def counter
|
57
|
+
Counter.counters[@name]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end # Counters
|
61
|
+
|
62
|
+
module Elements
|
63
|
+
require 'forwardable'
|
64
|
+
require 'securerandom'
|
65
|
+
require_relative 'scandent'
|
66
|
+
|
67
|
+
# Tree is a representation of a tree data structure consisting of elements
|
68
|
+
# A Tree holds only reference to the root Element of the tree
|
69
|
+
# A tree is useful for contextual operations on elements with the root as an ancestor
|
70
|
+
class Tree
|
71
|
+
include Enumerable
|
72
|
+
|
73
|
+
attr_accessor :root
|
74
|
+
|
75
|
+
def initialize(root=nil)
|
76
|
+
@root = root # Element
|
77
|
+
@listeners = [] # Array of GroupListeners
|
78
|
+
end
|
79
|
+
|
80
|
+
# Redefine the `each` method to iterate through all elements in the tree in depth-first order
|
81
|
+
def each(element=self.root)
|
82
|
+
yield element
|
83
|
+
element.children.each {|child| self.each(child) {|c| yield c}} unless element.nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
def each_with_level(element=self.root, level=0)
|
87
|
+
yield [element, level]
|
88
|
+
level += 1 unless element.is_a?(DocRootElement)
|
89
|
+
element.children.each {|child| self.each_with_level(child, level) {|c,l| yield [c, l]}} unless element.nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns an array with all elements in the subtree of the root in depth-first order
|
93
|
+
def get_DF_elements
|
94
|
+
Array.new.tap {|list| self.each {|element| list << element}}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Find any and all elements in the tree that match a given ScandentRule string
|
98
|
+
def scan(rule_string)
|
99
|
+
selected = []
|
100
|
+
rule = Arboretum::Scandent::Parser.parse_rule_string(rule_string, :PATH_LISTENER)
|
101
|
+
self.each do |element|
|
102
|
+
selected << element if rule.valid_on?(element)
|
103
|
+
yield element if rule.valid_on?(element) and block_given?
|
104
|
+
end
|
105
|
+
puts "--Warning: Rule #{rule_string} did not match any elements!--" if selected.empty?
|
106
|
+
ElementGroup.new(selected)
|
107
|
+
end
|
108
|
+
|
109
|
+
def listen(rule_string, &block)
|
110
|
+
listener = GroupListener.new(rule_string, block)
|
111
|
+
@listeners << listener
|
112
|
+
listener
|
113
|
+
end
|
114
|
+
|
115
|
+
def apply_listeners
|
116
|
+
self.each do |element|
|
117
|
+
@listeners.each do |listener|
|
118
|
+
if listener.rule.valid_on?(element)
|
119
|
+
listener << element
|
120
|
+
listener.exe_block.call(element) if !listener.exe_block.nil?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
@listeners.each do |listener|
|
125
|
+
puts "--Warning: Rule #{listener.rule} did not match any elements!--" if listener.empty?
|
126
|
+
end
|
127
|
+
@listeners = []
|
128
|
+
end
|
129
|
+
|
130
|
+
def apply_counters
|
131
|
+
self.each do |element|
|
132
|
+
element.resetters.each {|name, r| r.counter.reset}
|
133
|
+
element.incrementers.each {|name, i| i.value = i.counter.current_value; i.counter.increment}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s(root=true, pretty=true)
|
138
|
+
tree_string = (root ? "<<Tree: \n" : "")
|
139
|
+
if not self.root.nil?
|
140
|
+
tree_string << (root ? self.root.to_s + (pretty ? "\n" : "") : "")
|
141
|
+
self.root.children.each {|child| tree_string << self.r_to_s(child, 1, pretty)}
|
142
|
+
end
|
143
|
+
tree_string << (root ? ">>" : "")
|
144
|
+
end
|
145
|
+
|
146
|
+
def dump_markup(style=:pretty, type=:xml)
|
147
|
+
# The string containing all the markup
|
148
|
+
tree_string = ""
|
149
|
+
# Array of possible whitespace chars
|
150
|
+
whitespace_chars = [" ", "\t", "\n"]
|
151
|
+
# Whether there has been whitespace since the last text node
|
152
|
+
whitespace_trailing = true
|
153
|
+
# Whether the whitespace since the last text node has been honored
|
154
|
+
whitespace_honored = true
|
155
|
+
# To track which elements must be closed explicitly
|
156
|
+
open_elements = Array.new
|
157
|
+
if style.eql? :pretty
|
158
|
+
self.each_with_level do |element, level|
|
159
|
+
# Close elements that should close before the current element
|
160
|
+
until open_elements.empty? or level > open_elements.last[1]
|
161
|
+
# The element to be closed and its respective indentation level and whether it was a text-only element
|
162
|
+
closed_element, closed_level, text_only = open_elements.pop
|
163
|
+
closed_indent = " " * closed_level
|
164
|
+
|
165
|
+
# Whether the tail text of an element begins with whitespace e.g. `<div></div> I am tail text for the div` => true
|
166
|
+
tail_leading_space = (closed_element.sibling_next.is_a?(TextElement) and
|
167
|
+
whitespace_chars.include?(closed_element.sibling_next.text[0]))
|
168
|
+
# Whether element can break before the closing tag (to preserve whitespace):
|
169
|
+
# Element can break and still maintain whitespace if there has been whitespace since the
|
170
|
+
# last text element and the tail text of the current element
|
171
|
+
# Additionally, element can break if its instance variable :break_within is true
|
172
|
+
can_break_after = (whitespace_trailing or closed_element.break_within or tail_leading_space)
|
173
|
+
|
174
|
+
# Add a newline if it preserve whitespace and is not redundant and the element was not fit into a single line
|
175
|
+
tree_string << "\n" if can_break_after and !tree_string[-1].eql? "\n" and !text_only
|
176
|
+
# Add the indentation for this level if a newline occurred
|
177
|
+
tree_string << closed_indent if tree_string[-1].eql? "\n"
|
178
|
+
# If we added whitespace, then we have honored any whitespace in the document
|
179
|
+
whitespace_honored = true if whitespace_chars.include?(tree_string[-1])
|
180
|
+
# If we added whitespace, then the next element should be safe to break as well
|
181
|
+
whitespace_trailing = true if can_break_after
|
182
|
+
# Dump the closing tag of the element
|
183
|
+
tree_string << closed_element.dump_markup_close
|
184
|
+
end
|
185
|
+
# Determine the indentation level for the current element
|
186
|
+
indent = " " * level
|
187
|
+
|
188
|
+
# Handle element depending on its type
|
189
|
+
if element.is_a? TaggedElement
|
190
|
+
# Whether the element has any non-text children (text-only elements will be fit into a single line)
|
191
|
+
text_only = element.children.select{|c| !c.is_a?(TextElement)}.length == 0
|
192
|
+
# Whether element can break before the opening tag (to preserve whitespace):
|
193
|
+
# Element can break and still maintain whitespace if there has been whitespace since the
|
194
|
+
# last text element and the body text of the current element
|
195
|
+
# Additionally, element can break if its instance variable :break_within is true
|
196
|
+
can_break_before = (whitespace_trailing or element.break_within)
|
197
|
+
|
198
|
+
# Lookahead to determine if whitespace is in between element and next TextElement
|
199
|
+
# e.g. `<div> I am body text for the div <a>I am not</a></div>` => true
|
200
|
+
# Only takes place if the less expensive options didn't pan out
|
201
|
+
unless can_break_before
|
202
|
+
current = element
|
203
|
+
until current.nil? or current.is_a?(TextElement) or can_break_before
|
204
|
+
current = current.children.first
|
205
|
+
can_break_before = true if current.is_a?(TaggedElement) and current.break_within
|
206
|
+
end
|
207
|
+
can_break_before = true if current.is_a?(TextElement) and whitespace_chars.include?(current.text[0])
|
208
|
+
end
|
209
|
+
|
210
|
+
# Add a newline if it preserves whitespace and is not redundant
|
211
|
+
tree_string << "\n" if can_break_before and !tree_string[-1].eql?("\n")
|
212
|
+
# Add the indentation for this level if a newline occurs before the opening tag
|
213
|
+
tree_string << indent if tree_string[-1].eql? "\n"
|
214
|
+
# If we added whitespace, then we have honored any whitespace in the document
|
215
|
+
whitespace_honored = true if whitespace_chars.include?(tree_string[-1])
|
216
|
+
# If we added whitespace, then the next element should be safe to break as well
|
217
|
+
whitespace_trailing = true if can_break_before
|
218
|
+
# Dump the opening tag of the element
|
219
|
+
tree_string << element.dump_markup(type)
|
220
|
+
# Add another newline if it preserves whitespace and this elements has a non-text element
|
221
|
+
tree_string << "\n" if can_break_before and !text_only
|
222
|
+
# Mark the element for closing if it is paired
|
223
|
+
open_elements << [element, level, text_only] if element.paired?
|
224
|
+
elsif element.is_a? TextElement
|
225
|
+
# Whether this element has any non-text siblings (and will not be fit into a single line)
|
226
|
+
non_text_siblings = (element.preceding_siblings + element.following_siblings).select{|s| !s.is_a?(TextElement)}.length > 0
|
227
|
+
text_prev = element.sibling_prev.is_a? TextElement
|
228
|
+
text_next = element.sibling_next.is_a? TextElement
|
229
|
+
|
230
|
+
# The text of the element, to be modified before adding to the markup string
|
231
|
+
element_text = element.dump_markup(type)
|
232
|
+
|
233
|
+
element_trailing_space = whitespace_chars.include?(element_text[-1])
|
234
|
+
element_preceding_space = whitespace_chars.include?(element_text[0])
|
235
|
+
# Determine if the preceding space in the element is redundant or not needed
|
236
|
+
can_remove_preceding = (element_preceding_space and !text_prev and whitespace_trailing)
|
237
|
+
|
238
|
+
tree_string << "\n" if can_remove_preceding and !tree_string[-1].eql?("\n") and non_text_siblings
|
239
|
+
tree_string << indent if tree_string[-1].eql?("\n") and !element_text.strip.empty?
|
240
|
+
# If we added whitespace, then we have honored any whitespace in the document
|
241
|
+
whitespace_honored = true if whitespace_chars.include?(tree_string[-1])
|
242
|
+
|
243
|
+
# Tack on some whitespace or mark leading whitespace as not redundant if whitespace hasn't been honored yet
|
244
|
+
if whitespace_trailing and !whitespace_honored
|
245
|
+
if can_remove_preceding
|
246
|
+
can_remove_preceding = false
|
247
|
+
else
|
248
|
+
element_text = " " << element_text
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Strip redundant or unwanted whitespace
|
253
|
+
element_text[0] = "" if (can_remove_preceding and whitespace_chars.include?(tree_string[-1])) or
|
254
|
+
(can_remove_preceding and !non_text_siblings)
|
255
|
+
element_text[-1] = non_text_siblings ? "\n" : "" if element_trailing_space and !text_next and !element_text.strip.empty?
|
256
|
+
|
257
|
+
# Determine whether whitespace is trailing and if that trailing whitespace is honored by the end of the text node
|
258
|
+
whitespace_trailing = element_trailing_space
|
259
|
+
whitespace_honored = !(element_trailing_space and !whitespace_chars.include?(element_text[-1]))
|
260
|
+
|
261
|
+
tree_string << element_text unless tree_string[-1].eql?("\n") and element_text.strip.empty?
|
262
|
+
elsif element.is_a? DocRootElement
|
263
|
+
# Do nothing
|
264
|
+
else
|
265
|
+
# Just treat most elements like TaggedElements except for dumping a closing tag
|
266
|
+
#####
|
267
|
+
# Whether the element has any non-text children (text-only elements will be fit into a single line)
|
268
|
+
text_only = element.children.select{|c| !c.is_a?(TextElement)}.length == 0
|
269
|
+
# Whether element can break before the opening tag (to preserve whitespace):
|
270
|
+
# Element can break and still maintain whitespace if there has been whitespace since the
|
271
|
+
# last text element and the body text of the current element
|
272
|
+
# Additionally, element can break if its instance variable :break_within is true
|
273
|
+
can_break_before = (whitespace_trailing or element.break_within)
|
274
|
+
|
275
|
+
# Lookahead to determine if whitespace is in between element and next TextElement
|
276
|
+
# e.g. `<div> I am body text for the div <a>I am not</a></div>` => true
|
277
|
+
# Only takes place if the less expensive options didn't pan out
|
278
|
+
unless can_break_before
|
279
|
+
current = element
|
280
|
+
until current.nil? or current.is_a?(TextElement) or can_break_before
|
281
|
+
current = current.children.first
|
282
|
+
can_break_before = true if current.is_a?(TaggedElement) and current.break_within
|
283
|
+
end
|
284
|
+
can_break_before = true if current.is_a?(TextElement) and whitespace_chars.include?(current.text[0])
|
285
|
+
end
|
286
|
+
|
287
|
+
# Add a newline if it preserves whitespace and is not redundant
|
288
|
+
tree_string << "\n" if can_break_before and !tree_string[-1].eql?("\n")
|
289
|
+
# Add the indentation for this level if a newline occurs before the opening tag
|
290
|
+
tree_string << indent if tree_string[-1].eql? "\n"
|
291
|
+
# If we added whitespace, then we have honored any whitespace in the document
|
292
|
+
whitespace_honored = true if whitespace_chars.include?(tree_string[-1])
|
293
|
+
# If we added whitespace, then the next element should be safe to break as well
|
294
|
+
whitespace_trailing = true if can_break_before
|
295
|
+
# Dump the opening tag of the element
|
296
|
+
tree_string << element.dump_markup(type)
|
297
|
+
# Add another newline if it preserves whitespace and this elements has a non-text element
|
298
|
+
tree_string << "\n" if can_break_before and !text_only
|
299
|
+
end
|
300
|
+
end
|
301
|
+
# Close remaining
|
302
|
+
until open_elements.empty?
|
303
|
+
# The element to be closed and its respective indentation level and whether it was a text-only element
|
304
|
+
closed_element, closed_level, text_only = open_elements.pop
|
305
|
+
closed_indent = " " * closed_level
|
306
|
+
|
307
|
+
# Whether the tail text of an element begins with whitespace e.g. `<div></div> I am tail text for the div` => true
|
308
|
+
tail_leading_space = (closed_element.sibling_next.is_a?(TextElement) and
|
309
|
+
whitespace_chars.include?(closed_element.sibling_next.text[0]))
|
310
|
+
# Whether element can break before the closing tag (to preserve whitespace):
|
311
|
+
# Element can break and still maintain whitespace if there has been whitespace since the
|
312
|
+
# last text element and the tail text of the current element
|
313
|
+
# Additionally, element can break if its instance variable :break_within is true
|
314
|
+
can_break_after = (whitespace_trailing or closed_element.break_within or tail_leading_space)
|
315
|
+
|
316
|
+
# Add a newline if it preserve whitespace and is not redundant and the element was not fit into a single line
|
317
|
+
tree_string << "\n" if can_break_after and !tree_string[-1].eql? "\n" and !text_only
|
318
|
+
# Add the indentation for this level if a newline occurred
|
319
|
+
tree_string << closed_indent if tree_string[-1].eql? "\n"
|
320
|
+
# If we added whitespace, then we have honored any whitespace in the document
|
321
|
+
whitespace_honored = true if whitespace_chars.include?(tree_string[-1])
|
322
|
+
# If we added whitespace, then the next element should be safe to break as well
|
323
|
+
whitespace_trailing = true if can_break_after
|
324
|
+
# Dump the closing tag of the element
|
325
|
+
tree_string << closed_element.dump_markup_close
|
326
|
+
end
|
327
|
+
elsif style.eql? :compact
|
328
|
+
self.each_with_level do |element, level|
|
329
|
+
until open_elements.empty? or level > open_elements.last[1]
|
330
|
+
closed_element, closed_level = open_elements.pop
|
331
|
+
tree_string << " " if closed_element.break_within
|
332
|
+
whitespace_trailing = true if whitespace_chars.include?(tree_string[-1])
|
333
|
+
tree_string << closed_element.dump_markup_close
|
334
|
+
end
|
335
|
+
if element.is_a? TaggedElement
|
336
|
+
tree_string << element.dump_markup(type)
|
337
|
+
tree_string << " " if element.break_within
|
338
|
+
whitespace_trailing = true if whitespace_chars.include?(tree_string[-1])
|
339
|
+
open_elements << [element, level] if element.paired?
|
340
|
+
elsif element.is_a? TextElement
|
341
|
+
tree_string << element.dump_markup(type) unless whitespace_chars.include?(tree_string[-1]) and element.text.strip.empty?
|
342
|
+
whitespace_trailing = whitespace_chars.include?(tree_string[-1])
|
343
|
+
elsif element.is_a? DocRootElement
|
344
|
+
# Do nothing
|
345
|
+
else
|
346
|
+
tree_string << element.dump_markup(type)
|
347
|
+
tree_string << " " if element.break_within
|
348
|
+
whitespace_trailing = true if whitespace_chars.include?(tree_string[-1])
|
349
|
+
end
|
350
|
+
end
|
351
|
+
# Unknown print style
|
352
|
+
else
|
353
|
+
puts "Warning: Unknown print style. Using `:pretty`..."
|
354
|
+
tree_string = dump_markup(:pretty, type)
|
355
|
+
end
|
356
|
+
(whitespace_chars.include?(tree_string[0])) ? tree_string[1..-1] : tree_string
|
357
|
+
end
|
358
|
+
|
359
|
+
protected
|
360
|
+
def r_to_s(element, level, pretty)
|
361
|
+
element_string = (pretty ? " |"*level : "") + element.to_s + (pretty ? "\n" : "")
|
362
|
+
element.children.each do |child|
|
363
|
+
element_string << self.r_to_s(child, level + 1, pretty)
|
364
|
+
end
|
365
|
+
element_string
|
366
|
+
end
|
367
|
+
|
368
|
+
end
|
369
|
+
|
370
|
+
module Contextualized
|
371
|
+
# Swap locations of this ContextualGroup with another ContextualGroup
|
372
|
+
def swap(other)
|
373
|
+
puts "Warning: Can't swap with nil" if other.nil?
|
374
|
+
unless other.nil?
|
375
|
+
# Temp
|
376
|
+
other_parent = other.parent
|
377
|
+
other_index = other.index_in_parent
|
378
|
+
|
379
|
+
other.graft_onto(self.parent, self.index_in_parent)
|
380
|
+
self.graft_onto(other_parent, other_index)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
# Wrap this ContextualGroup in an Element, with the wrapping Element taking this ContextualGroup's original place in the tree
|
384
|
+
# If the wrapping Element already exitst, then this ContextualGroup is pushed to the given index in the other's
|
385
|
+
# children (but also idk why would you need to do that?)
|
386
|
+
def wrap(element, index=-1)
|
387
|
+
element = Element.create(element) if element.is_a?(Hash)
|
388
|
+
element.graft_onto(self.parent, self.index_in_parent)
|
389
|
+
self.graft_onto(element, index)
|
390
|
+
element
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
module Loggable
|
396
|
+
def log(*method_names)
|
397
|
+
method_names.each do |name|
|
398
|
+
method = instance_method(name)
|
399
|
+
define_method(name) do |*args, &block|
|
400
|
+
self.history << [name, args, caller]
|
401
|
+
method.bind(self).(*args, &block)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
def log_string
|
406
|
+
lines = ""
|
407
|
+
self.history.each {|h| lines << h.inspect << "\n"}
|
408
|
+
lines
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
# Generic element class
|
413
|
+
# Meant to be inherited rather than used directly
|
414
|
+
# All elements hold reference to a parent, children, and their left and right siblings
|
415
|
+
class Element
|
416
|
+
extend Forwardable
|
417
|
+
def_delegators :listing, :each
|
418
|
+
extend Loggable
|
419
|
+
include Loggable
|
420
|
+
include Enumerable
|
421
|
+
include Contextualized
|
422
|
+
|
423
|
+
protected
|
424
|
+
attr_writer :parent, :children, :sibling_prev, :sibling_next, :incrementers, :resetters, :library
|
425
|
+
|
426
|
+
public
|
427
|
+
attr_reader :parent, :children, :sibling_prev, :sibling_next, :incrementers, :resetters, :library
|
428
|
+
attr_accessor :break_within, :history
|
429
|
+
|
430
|
+
# Class method to stitch together two elements as siblings, ordered
|
431
|
+
# Will stitch values even if one or both is nil
|
432
|
+
# When used in isolation, can cause a mismatch in the tree (because parent is not updated)
|
433
|
+
def self.stitch!(prev_element, next_element)
|
434
|
+
prev_element.set_sibling_next!(next_element) unless prev_element.nil?
|
435
|
+
next_element.set_sibling_prev!(prev_element) unless next_element.nil?
|
436
|
+
end
|
437
|
+
|
438
|
+
# Create a custom element with a given namespace, tag, attributes, and text child
|
439
|
+
# If only text is given, a TextElement will be created rather than a tagged element
|
440
|
+
# If no tag is given, but attributes or namespace are given, a `div` element will be used by default
|
441
|
+
def self.create(namespace: nil, tag: nil, attrs: {}, text: nil)
|
442
|
+
tag = :div if tag.nil? and (!namespace.nil? or !attrs.empty? or text.nil?)
|
443
|
+
created_element = nil
|
444
|
+
if tag.nil?
|
445
|
+
raise TypeError.new("Text must be a String or TextElement") if !(text.is_a?(String) or text.is_a?(TextElement))
|
446
|
+
created_element = (text.is_a?(String)) ? TextElement.new(text) : text
|
447
|
+
else
|
448
|
+
tag = tag.to_sym if !tag.is_a?(Symbol)
|
449
|
+
raise TypeError.new("Tag must be a Symbol or String") if !tag.is_a?(Symbol)
|
450
|
+
namespace = namespace.to_sym if namespace.kind_of?(String) and !namespace.nil?
|
451
|
+
raise TypeError.new("Namespace must be a Symbol or String") if !namespace.is_a?(Symbol) and !namespace.nil?
|
452
|
+
sanitary_attrs = {}
|
453
|
+
attrs.each do |key, val|
|
454
|
+
key = key.to_sym if !key.is_a?(Symbol)
|
455
|
+
raise TypeError.new("Attribute name must be a Symbol or String") if !key.is_a?(Symbol)
|
456
|
+
val = val.split if val.is_a?(String)
|
457
|
+
# Ensure value is arrays of strings
|
458
|
+
raise TypeError.new("Attribute value must be a String or Array of Strings") if !val.is_a?(Array)
|
459
|
+
val.each{|sub_val| raise TypeError.new("Each attribute value in an array must be a String") if !sub_val.is_a?(String)}
|
460
|
+
sanitary_attrs[key] = val
|
461
|
+
end
|
462
|
+
created_element = TaggedElement.new(namespace, tag, sanitary_attrs)
|
463
|
+
if !text.nil?
|
464
|
+
raise TypeError.new("Text must be a string or TextElement") if !(text.is_a?(String) or text.is_a?(TextElement))
|
465
|
+
if text.is_a?(String)
|
466
|
+
text_child = TextElement.new(text)
|
467
|
+
created_element.append_child(text_child)
|
468
|
+
else
|
469
|
+
created_element.append_child(text)
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
created_element.break_within = true if !created_element.is_a?(TextElement)
|
474
|
+
yield created_element if block_given?
|
475
|
+
created_element
|
476
|
+
end
|
477
|
+
singleton_class.send(:alias_method, :make, :create)
|
478
|
+
singleton_class.send(:alias_method, :grow, :create)
|
479
|
+
|
480
|
+
def initialize
|
481
|
+
# Family of elements
|
482
|
+
@parent = nil # Element
|
483
|
+
@sibling_prev = nil # Element
|
484
|
+
@sibling_next = nil # Element
|
485
|
+
@children = [] # Array of Elements
|
486
|
+
|
487
|
+
# Properties, references, and counters
|
488
|
+
@break_within = false # Boolean
|
489
|
+
@incrementers = Hash.new # Hash with key: Symbol (name), value: Incrementer
|
490
|
+
@resetters = Hash.new # Hash with key: Symbol (name), value: Resetter
|
491
|
+
@library = Hash.new # Hash with key: Symbol, value: Element/ElementGroup
|
492
|
+
@history = Array.new # Array of arrays of form [method_called, arguments, callers]
|
493
|
+
end
|
494
|
+
|
495
|
+
# Add an element as this element's last child
|
496
|
+
def append_child(*elements)
|
497
|
+
placed = Array.new
|
498
|
+
elements.each do |element|
|
499
|
+
if !element.nil?
|
500
|
+
element = TextElement.new(element) if element.is_a?(String)
|
501
|
+
element = Element.create(element) if element.is_a?(Hash)
|
502
|
+
element.graft_last_onto(self)
|
503
|
+
element.listing.each do |member|
|
504
|
+
yield member if block_given?
|
505
|
+
placed << member
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
return nil if placed.empty?
|
510
|
+
return placed.first if placed.length == 1
|
511
|
+
return ElementGroup.new(placed)
|
512
|
+
end
|
513
|
+
alias_method :push, :append_child
|
514
|
+
alias_method :<<, :append_child
|
515
|
+
|
516
|
+
# Add an element as this element's first child
|
517
|
+
def prepend_child(*elements)
|
518
|
+
placed = Array.new
|
519
|
+
elements.each do |element|
|
520
|
+
if !element.nil?
|
521
|
+
element = TextElement.new(element) if element.is_a?(String)
|
522
|
+
element = Element.create(element) if element.is_a?(Hash)
|
523
|
+
element.graft_first_onto(self)
|
524
|
+
element.listing.each do |member|
|
525
|
+
yield member if block_given?
|
526
|
+
placed << member
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
return nil if placed.empty?
|
531
|
+
return placed.first if placed.length == 1
|
532
|
+
return ElementGroup.new(placed)
|
533
|
+
end
|
534
|
+
alias_method :place, :prepend_child
|
535
|
+
alias_method :unshift, :prepend_child
|
536
|
+
|
537
|
+
# Insert an element as a child at a specified index it this element's children
|
538
|
+
def insert_child(*elements)
|
539
|
+
placed = Array.new
|
540
|
+
elements.each do |element|
|
541
|
+
if !element.nil?
|
542
|
+
element = TextElement.new(element) if element.is_a?(String)
|
543
|
+
element = Element.create(element) if element.is_a?(Hash)
|
544
|
+
element.graft_onto(self, index)
|
545
|
+
element.listing.each do |member|
|
546
|
+
yield member if block_given?
|
547
|
+
placed << member
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
return nil if placed.empty?
|
552
|
+
return placed.first if placed.length == 1
|
553
|
+
return ElementGroup.new(placed)
|
554
|
+
end
|
555
|
+
|
556
|
+
def append_sibling(*elements)
|
557
|
+
placed = Array.new
|
558
|
+
elements.each do |element|
|
559
|
+
if !element.nil?
|
560
|
+
element = TextElement.new(element) if element.is_a?(String)
|
561
|
+
element = Element.create(element) if element.is_a?(Hash)
|
562
|
+
element.graft_onto(self.parent, self.index_in_parent+1)
|
563
|
+
element.listing.each do |member|
|
564
|
+
yield member if block_given?
|
565
|
+
placed << member
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
return nil if placed.empty?
|
570
|
+
return placed.first if placed.length == 1
|
571
|
+
return ElementGroup.new(placed)
|
572
|
+
end
|
573
|
+
|
574
|
+
def prepend_sibling(*elements)
|
575
|
+
placed = Array.new
|
576
|
+
elements.each do |element|
|
577
|
+
if !element.nil?
|
578
|
+
element = TextElement.new(element) if element.is_a?(String)
|
579
|
+
element = Element.create(element) if element.is_a?(Hash)
|
580
|
+
element.graft_onto(self.parent, self.index_in_parent)
|
581
|
+
element.listing.each do |member|
|
582
|
+
yield member if block_given?
|
583
|
+
placed << member
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
return nil if placed.empty?
|
588
|
+
return placed.first if placed.length == 1
|
589
|
+
return ElementGroup.new(placed)
|
590
|
+
end
|
591
|
+
|
592
|
+
def overwrite!(args)
|
593
|
+
replacement = Element.create(args)
|
594
|
+
puts "Warning: Overwriting this element will delete its children" if self.has_children? and !replacement.can_have_children?
|
595
|
+
replacement.graft_onto(self.parent, self.index_in_parent)
|
596
|
+
self.content.graft_onto(replacement)
|
597
|
+
replacement.break_within = self.break_within
|
598
|
+
replacement.counters = self.counters
|
599
|
+
replacement.resetters = self.resetters
|
600
|
+
replacement.library = self.library
|
601
|
+
self.detach
|
602
|
+
replacement
|
603
|
+
end
|
604
|
+
|
605
|
+
def index_in_parent
|
606
|
+
self.parent.children.index(self)
|
607
|
+
end
|
608
|
+
|
609
|
+
# Returns all text elements in this element's subtree
|
610
|
+
# Can be very expensive on large subtrees/documents, as it performs a full transversal of the subtree
|
611
|
+
def text_elements(text_children=[])
|
612
|
+
if self.is_a? TextElement
|
613
|
+
text_children << self
|
614
|
+
elsif self.is_a? TaggedElement or self.is_a? DocRootElement
|
615
|
+
@children.each {|child| child.text_elements(text_children)}
|
616
|
+
end
|
617
|
+
text_children
|
618
|
+
end
|
619
|
+
|
620
|
+
# Returns a string comprised of all text in this element's subtree
|
621
|
+
# Can be very expensive on large subtrees/documents, as it performs a full transversal of the subtree
|
622
|
+
def text_string(full_string='')
|
623
|
+
if self.is_a? TextElement
|
624
|
+
full_string << self.text
|
625
|
+
elsif self.is_a? TaggedElement or self.is_a? DocRootElement
|
626
|
+
full_string << ' ' if self.break_within
|
627
|
+
@children.each {|child| child.text_string(full_string)}
|
628
|
+
full_string << ' ' if self.break_within
|
629
|
+
end
|
630
|
+
full_string
|
631
|
+
end
|
632
|
+
|
633
|
+
# Special method to get an elements children as an AdjacentElementGroup
|
634
|
+
def content
|
635
|
+
group = AdjacentElementGroup.new
|
636
|
+
group.base(@children[0]) if not @children.empty?
|
637
|
+
group.fill
|
638
|
+
end
|
639
|
+
alias_method :contents, :content
|
640
|
+
|
641
|
+
# Add an counter incrementer to this element
|
642
|
+
def count(counter_name)
|
643
|
+
counter_name = counter_name.to_sym if !counter_name.is_a?(Symbol)
|
644
|
+
incrementers[counter_name] = Counters::Incrementer.new(counter_name)
|
645
|
+
end
|
646
|
+
|
647
|
+
# Get a CounterElement associated with the incrementer of this element of the given name
|
648
|
+
def counter(counter_name)
|
649
|
+
counter_name = counter_name.to_sym if !counter_name.is_a?(Symbol)
|
650
|
+
CounterElement.new(incrementers[counter_name])
|
651
|
+
end
|
652
|
+
|
653
|
+
# Add a counter resetter to this element
|
654
|
+
def reset(counter_name)
|
655
|
+
counter_name = counter_name.to_sym if !counter_name.is_a?(Symbol)
|
656
|
+
resetters[counter_name] = Counters::Resetter.new(counter_name)
|
657
|
+
end
|
658
|
+
|
659
|
+
# Add an element to a category in this element's library
|
660
|
+
def lib_add(other, category)
|
661
|
+
raise TypeError.new("Cannot create a record for a non-element") if !other.is_a?(Element)
|
662
|
+
if @library.has_key?(category)
|
663
|
+
record = @library[category]
|
664
|
+
if record.is_a?(Element)
|
665
|
+
@library[category] = ElementGroup.new([record, other])
|
666
|
+
else record.is_a?(ElementGroup)
|
667
|
+
record << other
|
668
|
+
end
|
669
|
+
else
|
670
|
+
@library[category] = other
|
671
|
+
end
|
672
|
+
|
673
|
+
end
|
674
|
+
alias_method :allude, :lib_add
|
675
|
+
alias_method :cite, :lib_add
|
676
|
+
|
677
|
+
# Get record from the given category in this element's library
|
678
|
+
def lib_get(category)
|
679
|
+
@library[category]
|
680
|
+
end
|
681
|
+
alias_method :[], :lib_get
|
682
|
+
alias_method :record, :lib_get
|
683
|
+
|
684
|
+
# Provides a listing/array containing the element
|
685
|
+
def listing
|
686
|
+
[self]
|
687
|
+
end
|
688
|
+
|
689
|
+
# General element deep copy method
|
690
|
+
def copy
|
691
|
+
element_copy = Element.new
|
692
|
+
element_copy.set_children!(@children.map {|child| child.copy})
|
693
|
+
element_copy
|
694
|
+
end
|
695
|
+
|
696
|
+
# Detach from current parent/siblings
|
697
|
+
def detach
|
698
|
+
Element.stitch!(@sibling_prev, @sibling_next)
|
699
|
+
@parent.children.delete(self) unless @parent.nil?
|
700
|
+
@parent, @sibling_prev, @sibling_next = nil
|
701
|
+
end
|
702
|
+
alias_method :prune, :detach
|
703
|
+
alias_method :delete, :detach
|
704
|
+
|
705
|
+
# Graft onto another element of the tree at any index of its children
|
706
|
+
# By default, it will graft as the last element of the other element's children
|
707
|
+
def graft_onto(graft_parent, index=-1)
|
708
|
+
# If index to small or large, graft to edges of graft_parent children
|
709
|
+
if index.abs > graft_parent.children.length
|
710
|
+
index = graft_parent.children.length * (index > 1 ? 1 : 0)
|
711
|
+
end
|
712
|
+
if index == graft_parent.children.length or index == -1
|
713
|
+
self.graft_last_onto(graft_parent)
|
714
|
+
elsif index == 0
|
715
|
+
self.graft_first_onto(graft_parent)
|
716
|
+
else
|
717
|
+
# Detach from current context
|
718
|
+
self.detach
|
719
|
+
|
720
|
+
# Update context
|
721
|
+
@parent = graft_parent
|
722
|
+
|
723
|
+
previous_child = graft_parent.children[index-1]
|
724
|
+
Element.stitch!(previous_child, self)
|
725
|
+
|
726
|
+
next_child = graft_parent.children[index]
|
727
|
+
Element.stitch!(self, next_child)
|
728
|
+
|
729
|
+
# Graft group at index
|
730
|
+
graft_parent.children.insert(index, self)
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
# Graft onto another element of the tree as the first child
|
735
|
+
def graft_first_onto(graft_parent)
|
736
|
+
# Detach from current context
|
737
|
+
self.detach
|
738
|
+
|
739
|
+
# Update context
|
740
|
+
@parent = graft_parent
|
741
|
+
|
742
|
+
next_child = graft_parent.children[0]
|
743
|
+
Element.stitch!(nil, self)
|
744
|
+
Element.stitch!(self, next_child)
|
745
|
+
|
746
|
+
# Insert graft group at the beginning of parent children
|
747
|
+
graft_parent.children.insert(0, self)
|
748
|
+
end
|
749
|
+
|
750
|
+
# Graft onto another element of the tree as the last child
|
751
|
+
def graft_last_onto(graft_parent)
|
752
|
+
# Detach from current context
|
753
|
+
self.detach
|
754
|
+
|
755
|
+
# Update context
|
756
|
+
@parent = graft_parent
|
757
|
+
|
758
|
+
previous_child = graft_parent.children[-1]
|
759
|
+
Element.stitch!(previous_child, self)
|
760
|
+
Element.stitch!(self, nil)
|
761
|
+
|
762
|
+
# Push graft group onto parent children
|
763
|
+
graft_parent.children.push(self)
|
764
|
+
end
|
765
|
+
|
766
|
+
# Unwrap the children of this Element, deleting it, and it's children taking its original place in the tree
|
767
|
+
def unwrap_children
|
768
|
+
unwrapped_elements = self.content
|
769
|
+
unwrapped_elements.graft_onto(self.parent, self.index_in_parent)
|
770
|
+
self.detach
|
771
|
+
unwrapped_elements
|
772
|
+
end
|
773
|
+
|
774
|
+
# Get list of all of this element's preceding siblings
|
775
|
+
def preceding_siblings
|
776
|
+
sibling_list = []
|
777
|
+
current = self
|
778
|
+
while not current.sibling_prev.nil?
|
779
|
+
sibling_list << current.sibling_prev
|
780
|
+
current = current.sibling_prev
|
781
|
+
end
|
782
|
+
sibling_list = sibling_list.reverse
|
783
|
+
sibling_list.each {|sibling| yield sibling if block_given?}
|
784
|
+
ElementGroup.new(sibling_list)
|
785
|
+
end
|
786
|
+
|
787
|
+
# Get list of all of this element's preceding siblings in reversed depth-first order
|
788
|
+
# Used for slightly speedier and practical searching
|
789
|
+
def preceding_siblings_reverse
|
790
|
+
sibling_list = []
|
791
|
+
current = self
|
792
|
+
while not current.sibling_prev.nil?
|
793
|
+
yield current.sibling_prev if block_given?
|
794
|
+
sibling_list << current.sibling_prev
|
795
|
+
current = current.sibling_prev
|
796
|
+
end
|
797
|
+
ElementGroup.new(sibling_list)
|
798
|
+
end
|
799
|
+
|
800
|
+
# Get list of all of this element's following siblings in depth-first order
|
801
|
+
def following_siblings
|
802
|
+
sibling_list = []
|
803
|
+
current = self
|
804
|
+
while not current.sibling_next.nil?
|
805
|
+
yield current.sibling_next if block_given?
|
806
|
+
sibling_list << current.sibling_next
|
807
|
+
current = current.sibling_next
|
808
|
+
end
|
809
|
+
ElementGroup.new(sibling_list)
|
810
|
+
end
|
811
|
+
|
812
|
+
# Get list of all this element's ancesotrs in depth-first order
|
813
|
+
def ancestors
|
814
|
+
parent_list = []
|
815
|
+
current = self
|
816
|
+
while not current.parent.nil?
|
817
|
+
parent_list << current.parent
|
818
|
+
current = current.parent
|
819
|
+
end
|
820
|
+
parent_list = parent_list.reverse
|
821
|
+
parent_list.each {|parent| yield parent if block_given?}
|
822
|
+
ElementGroup.new(parent_list)
|
823
|
+
end
|
824
|
+
|
825
|
+
# Get list of all of this element's ancestors in reversed depth-first order
|
826
|
+
# Used for slightly speedier and practical searching
|
827
|
+
def ancestors_reverse
|
828
|
+
parent_list = []
|
829
|
+
current = self
|
830
|
+
while not current.parent.nil?
|
831
|
+
yield current.parent if block_given?
|
832
|
+
parent_list << current.parent
|
833
|
+
current = current.parent
|
834
|
+
end
|
835
|
+
ElementGroup.new(parent_list)
|
836
|
+
end
|
837
|
+
|
838
|
+
# Get list of all this element's descendants in depth-first order
|
839
|
+
def descendants(child_list=[])
|
840
|
+
self.children.each do |child|
|
841
|
+
yield child if block_given?
|
842
|
+
child_list << child
|
843
|
+
child.descendants(child_list)
|
844
|
+
end
|
845
|
+
ElementGroup.new(child_list)
|
846
|
+
end
|
847
|
+
|
848
|
+
# Finds elements in relation to this one that fit a ScandentRule string
|
849
|
+
def find(rule_string)
|
850
|
+
rule = Arboretum::Scandent::Parser.parse_rule_string(rule_string, :PATH_LOCATOR)
|
851
|
+
selected = rule.locate(self) {|found_element| yield found_element if block_given?}
|
852
|
+
puts "--Warning: Rule #{rule} did not match any elements!--" if selected.empty?
|
853
|
+
ElementGroup.new(selected)
|
854
|
+
end
|
855
|
+
alias_method :locate, :find
|
856
|
+
|
857
|
+
# Finds up to `n` elements in relation to this one that fit a ScandentRule string
|
858
|
+
def find_first_n(rule_string, limit)
|
859
|
+
if limit.zero?
|
860
|
+
puts "--Warning: Rule #{rule} was given limit '0'. Returning nil...--" if selected.empty?
|
861
|
+
return nil
|
862
|
+
end
|
863
|
+
selected = []
|
864
|
+
rule = Arboretum::Scandent::Parser.parse_rule_string(rule_string, :PATH_LOCATOR)
|
865
|
+
rule.locate(self) do |found_element|
|
866
|
+
return ElementGroup.new(selected) if selected.length >= limit
|
867
|
+
yield found_element if block_given?
|
868
|
+
selected << found_element
|
869
|
+
end
|
870
|
+
puts "--Warning: Rule #{rule} did not match any elements!--" if selected.empty?
|
871
|
+
ElementGroup.new(selected)
|
872
|
+
end
|
873
|
+
alias_method :locate_first_n, :find_first_n
|
874
|
+
|
875
|
+
# Find the first element in relation to this one that fits a ScandentRule string
|
876
|
+
def find_first(rule_string)
|
877
|
+
self.find_first_n(rule_string, 1) {|found_element| yield found_element if block_given?}.first
|
878
|
+
end
|
879
|
+
alias_method :locate_first, :find_first
|
880
|
+
|
881
|
+
def matches_rule?(rule_string)
|
882
|
+
rule = Arboretum::Scandent::Parser.parse_rule_string(rule_string, :PATH_LISTENER)
|
883
|
+
rule.valid_on?(self)
|
884
|
+
end
|
885
|
+
|
886
|
+
def has_children?
|
887
|
+
!@children.empty?
|
888
|
+
end
|
889
|
+
|
890
|
+
def can_have_children?
|
891
|
+
false
|
892
|
+
end
|
893
|
+
|
894
|
+
# Does nothing but set sibling_next of this element
|
895
|
+
# If used in isolation, will cause a sibling mismatch in the tree
|
896
|
+
def set_sibling_next!(other)
|
897
|
+
@sibling_next = other
|
898
|
+
self
|
899
|
+
end
|
900
|
+
|
901
|
+
# Does nothing but set sibling_prev of this element
|
902
|
+
# If used in isolation, will cause a sibling mismatch in the tree
|
903
|
+
def set_sibling_prev!(other)
|
904
|
+
@sibling_prev = other
|
905
|
+
self
|
906
|
+
end
|
907
|
+
|
908
|
+
# Does nothing but set parent of this element
|
909
|
+
# If used in isolation, will cause a parent <=> child mismatch in the tree
|
910
|
+
def set_parent!(other)
|
911
|
+
@parent = other
|
912
|
+
self
|
913
|
+
end
|
914
|
+
|
915
|
+
# Does nothing but set children of this element
|
916
|
+
# If used in isolation, will cause a parent <=> child mismatch in the tree
|
917
|
+
# Should only be used for when set operations must be performed on element children
|
918
|
+
def set_children!(other_arr)
|
919
|
+
@children = other_arr
|
920
|
+
self
|
921
|
+
end
|
922
|
+
|
923
|
+
def to_tree
|
924
|
+
Tree.new(self)
|
925
|
+
end
|
926
|
+
|
927
|
+
def to_s
|
928
|
+
"<Generic_Element>"
|
929
|
+
end
|
930
|
+
|
931
|
+
# Temporary fix to prevent inspect from exploding due to child references
|
932
|
+
# Ideally will provide detailed state information rather than string output
|
933
|
+
def inspect
|
934
|
+
self.to_s
|
935
|
+
end
|
936
|
+
|
937
|
+
end
|
938
|
+
|
939
|
+
# Special type of element to represent the root element of a document
|
940
|
+
# A DocRootElement has no output but simply acts as a wrapper for the top level elements of an imported document
|
941
|
+
class DocRootElement < Element
|
942
|
+
def initialize
|
943
|
+
super()
|
944
|
+
end
|
945
|
+
|
946
|
+
def can_have_children?
|
947
|
+
true
|
948
|
+
end
|
949
|
+
|
950
|
+
# DocRootElement deep copy method
|
951
|
+
def copy
|
952
|
+
element_copy = DocRootElement.new
|
953
|
+
element_copy.set_children!(@children.map {|child| child.copy})
|
954
|
+
element_copy
|
955
|
+
end
|
956
|
+
|
957
|
+
def to_s
|
958
|
+
"<Root_Element>"
|
959
|
+
end
|
960
|
+
|
961
|
+
end
|
962
|
+
|
963
|
+
# A tagged element of a doctree
|
964
|
+
# Only TaggedElements have tags (duh!) and attributes, and TaggedElements hold no direct reference to text
|
965
|
+
# Ex: Both <p></p> or <br /> are considered tagged elements with no children
|
966
|
+
class TaggedElement < Element
|
967
|
+
@@unpaired_tags = [:'DOCTYPE', :'!DOCTYPE', :'area', :'base', :'br', :'col', :'command', :'embed', :'hr', :'img',
|
968
|
+
:'input', :'keygen', :'link', :'meta', :'param', :'source', :'track', :'wbr']
|
969
|
+
@@need_valid_xml = true
|
970
|
+
|
971
|
+
attr_accessor :namespace, :tag, :attrs
|
972
|
+
|
973
|
+
def initialize(namespace=nil, tag=nil, attrs={})
|
974
|
+
super()
|
975
|
+
|
976
|
+
# Element tag and attributes
|
977
|
+
@namespace = namespace # Symbol
|
978
|
+
@tag = tag # Symbol
|
979
|
+
@attrs = attrs # Hash with key: Symbol, String: Array of Strings
|
980
|
+
end
|
981
|
+
|
982
|
+
def self.paired?(tag)
|
983
|
+
!@@unpaired_tags.include?(tag)
|
984
|
+
end
|
985
|
+
|
986
|
+
# TaggedElement deep copy method
|
987
|
+
def copy
|
988
|
+
element_copy = TaggedElement.new(@namespace, @tag, @attrs.clone)
|
989
|
+
element_copy.set_children!(@children.map {|child| child.copy})
|
990
|
+
element_copy
|
991
|
+
end
|
992
|
+
|
993
|
+
# Returns the id of this element, or an auto-assigned one if none exists
|
994
|
+
def ref
|
995
|
+
if !self.has_attr?(:id) or self.attr_value_str(:id).nil?
|
996
|
+
auto_id = "auto_#{SecureRandom.uuid}"
|
997
|
+
self.attrs[:id] = [auto_id]
|
998
|
+
auto_id
|
999
|
+
else
|
1000
|
+
self.attrs[:id].join.gsub(' ', '')
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
def href
|
1005
|
+
"#" << self.ref
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
# Returns the string value for an attribute of the given name
|
1009
|
+
def attr_value_str(attr_name)
|
1010
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1011
|
+
return nil if !self.has_attr?(attr_name)
|
1012
|
+
return self.attrs[attr_name].join(' ')
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def del_attr(attr_name)
|
1016
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1017
|
+
self.attrs.delete(attr_name)
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
def set_attr_value(attr_name, attr_value)
|
1021
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1022
|
+
attr_value = attr_value.split if attr_value.is_a?(String)
|
1023
|
+
|
1024
|
+
# Ensure value is arrays of strings
|
1025
|
+
raise TypeError.new("Attribute value must be a String or Array of Strings") if !attr_value.is_a?(Array)
|
1026
|
+
attr_value.each{|sub_val| raise TypeError.new("Each attribute value in an array must be a String") if !sub_val.is_a?(String)}
|
1027
|
+
|
1028
|
+
self.attrs[attr_name] = attr_value
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
def del_attr_value(attr_name, attr_value)
|
1032
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1033
|
+
attr_value = attr_value.split if attr_value.is_a?(String)
|
1034
|
+
|
1035
|
+
# Ensure value is arrays of strings
|
1036
|
+
raise TypeError.new("Attribute value must be a String or Array of Strings") if !attr_value.is_a?(Array)
|
1037
|
+
attr_value.each{|sub_val| raise TypeError.new("Each attribute value in an array must be a String") if !sub_val.is_a?(String)}
|
1038
|
+
|
1039
|
+
self.attrs[attr_name].delete_if {|sub_val| attr_value.include?(sub_val)}
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
def add_attr_value(attr_name, attr_value)
|
1043
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1044
|
+
attr_value = attr_value.split if attr_value.is_a?(String)
|
1045
|
+
|
1046
|
+
# Ensure value is arrays of strings
|
1047
|
+
raise TypeError.new("Attribute value must be a String or Array of Strings") if !attr_value.is_a?(Array)
|
1048
|
+
attr_value.each{|sub_val| raise TypeError.new("Each attribute value in an array must be a String") if !sub_val.is_a?(String)}
|
1049
|
+
if self.has_attr?(attr_name)
|
1050
|
+
self.attrs[attr_name] += attr_value
|
1051
|
+
else
|
1052
|
+
self.attrs[attr_name] = attr_value
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def set_tag(new_tag)
|
1057
|
+
new_tag = new_tag.to_sym if !new_tag.is_a?(Symbol)
|
1058
|
+
@tag = new_tag
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
def set_namespace(new_ns)
|
1062
|
+
new_ns = new_tag.to_sym if !new_tag.is_a?(Symbol)
|
1063
|
+
@namespace = new_ns
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
def del_namespace
|
1067
|
+
@namespace = nil
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
# Returns whether or not the element is a paired element
|
1071
|
+
def paired?
|
1072
|
+
not @@unpaired_tags.include? self.tag
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
def has_attr?(attr_name)
|
1076
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1077
|
+
self.attrs.has_key?(attr_name)
|
1078
|
+
end
|
1079
|
+
|
1080
|
+
def contains_attr_val?(attr_name, attr_value)
|
1081
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1082
|
+
self.has_attr?(attr_name) and self.attrs[attr_name].include?(attr_value)
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
def equals_attr_val?(attr_name, attr_value)
|
1086
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1087
|
+
self.has_attr?(attr_name) and (self.attrs[attr_name]-attr_value).empty?
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
def matches_attr_val?(attr_name, attr_regex)
|
1091
|
+
attr_name = attr_name.to_sym if !attr_name.is_a?(Symbol)
|
1092
|
+
self.has_attr?(attr_name) and !self.attrs[attr_name].grep(attr_regex).empty?
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
def namespaced_tag
|
1096
|
+
self.namespace.nil? ? "#{self.tag}" : "#{self.namespace}:#{self.tag}"
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
def can_have_children?
|
1100
|
+
true
|
1101
|
+
end
|
1102
|
+
|
1103
|
+
def dump_markup(type=:xml)
|
1104
|
+
element_string = "<#{self.namespaced_tag}"
|
1105
|
+
self.attrs.each do |key, values|
|
1106
|
+
element_string << " #{key}"
|
1107
|
+
element_string << "=\""
|
1108
|
+
values.each do |v|
|
1109
|
+
element_string << "#{v.gsub('&','&').gsub('<', '<').gsub('>','>')} "
|
1110
|
+
end
|
1111
|
+
if not values.empty?
|
1112
|
+
element_string = element_string[0..-2]
|
1113
|
+
end
|
1114
|
+
element_string << "\""
|
1115
|
+
end
|
1116
|
+
# Close the tag if document must have valid xml
|
1117
|
+
if self.paired? or type == :html
|
1118
|
+
element_string << ">"
|
1119
|
+
else
|
1120
|
+
element_string << " />"
|
1121
|
+
end
|
1122
|
+
element_string
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
def dump_markup_close
|
1126
|
+
"</#{self.namespaced_tag}>"
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
def to_s
|
1130
|
+
element_string = "<#{self.namespaced_tag}"
|
1131
|
+
self.attrs.each do |key, values|
|
1132
|
+
element_string << " #{key}"
|
1133
|
+
element_string << "=\""
|
1134
|
+
values.each do |v|
|
1135
|
+
element_string << "#{v} "
|
1136
|
+
end
|
1137
|
+
if not values.empty?
|
1138
|
+
element_string = element_string[0..-2]
|
1139
|
+
end
|
1140
|
+
element_string << "\""
|
1141
|
+
end
|
1142
|
+
# Close the tag if document must have xml/xhtml
|
1143
|
+
if self.paired? or not @@need_valid_xml
|
1144
|
+
element_string << ">"
|
1145
|
+
else
|
1146
|
+
element_string << " />"
|
1147
|
+
end
|
1148
|
+
element_string
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
def to_s_close
|
1152
|
+
"</#{self.namespaced_tag}>"
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
# A text element of a doctree.
|
1158
|
+
# TextElements have no tags nor attributes and are only meant to represent document content
|
1159
|
+
# Ex: <p>Hello World</p> is a tagged element 'p' with a single text element child with text "Hello World"
|
1160
|
+
class TextElement < Element
|
1161
|
+
attr_writer :text
|
1162
|
+
|
1163
|
+
def initialize(text='')
|
1164
|
+
super()
|
1165
|
+
|
1166
|
+
# Element text
|
1167
|
+
@text = text # String
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
# TextElement deep copy method
|
1171
|
+
def copy
|
1172
|
+
TextElement.new(@text)
|
1173
|
+
end
|
1174
|
+
|
1175
|
+
def text
|
1176
|
+
@text
|
1177
|
+
end
|
1178
|
+
|
1179
|
+
def dump_markup(type=:xml)
|
1180
|
+
self.text.gsub('&','&').gsub('<', '<').gsub('>','>')
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
def to_s
|
1184
|
+
self.text.inspect
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
# A comment element of a doctree.
|
1190
|
+
# CommentElements have no tags nor attributes nor contribute to document content, but are preserved in the tree
|
1191
|
+
# Ex: <!-- This is an example comment -->
|
1192
|
+
class CommentElement < Element
|
1193
|
+
attr_accessor :text
|
1194
|
+
|
1195
|
+
def initialize(text='')
|
1196
|
+
super()
|
1197
|
+
|
1198
|
+
# Comment text
|
1199
|
+
@text = text # String
|
1200
|
+
end
|
1201
|
+
|
1202
|
+
# CommentElement deep copy method
|
1203
|
+
def copy
|
1204
|
+
CommentElement.new(@text)
|
1205
|
+
end
|
1206
|
+
|
1207
|
+
def dump_markup(type=:xml)
|
1208
|
+
"<!--#{self.text.gsub('&','&').gsub('<', '<').gsub('>','>')}-->"
|
1209
|
+
end
|
1210
|
+
|
1211
|
+
def to_s
|
1212
|
+
"<!--#{self.text}-->"
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
# An XML Processing Instruction element in the doctree
|
1218
|
+
# PIElements have no tags nor attributes nor contribute to document content, but are preserved in the tree
|
1219
|
+
# Ex: <?...?> represents a processing instruction i.e. <?PITarget PIContent?>
|
1220
|
+
class PIElement < Element
|
1221
|
+
attr_accessor :text
|
1222
|
+
|
1223
|
+
def initialize(text='')
|
1224
|
+
super()
|
1225
|
+
|
1226
|
+
# Element text
|
1227
|
+
@text = text # String
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
# PIElement deep copy method
|
1231
|
+
def copy
|
1232
|
+
PIElement.new(@text)
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
def dump_markup(type=:xml)
|
1236
|
+
"<?#{self.text.gsub('&','&').gsub('<', '<').gsub('>','>')}?>"
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
def to_s
|
1240
|
+
"<?#{self.text}?>"
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
# A TextElement that holds a reference to an incrementer within the tree
|
1246
|
+
# CounterElements have no tags nor attributes and are only meant to represent document content
|
1247
|
+
class CounterElement < TextElement
|
1248
|
+
def initialize(incrementer)
|
1249
|
+
super()
|
1250
|
+
@incrementer = incrementer
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
# CounterElement deep copy method
|
1254
|
+
def copy
|
1255
|
+
CounterElement.new(@incrementer)
|
1256
|
+
end
|
1257
|
+
|
1258
|
+
# The text representation of this element is the value of the referenced incrementere
|
1259
|
+
def text
|
1260
|
+
@incrementer.value.to_s
|
1261
|
+
end
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
# An arbitrary group of elements
|
1265
|
+
# ElementGroups can be placed and moved to a single location like standard elements
|
1266
|
+
class ElementGroup
|
1267
|
+
extend Forwardable
|
1268
|
+
def_delegators :@group, :[], :each, :push, :<<, :length, :empty?, :first, :last
|
1269
|
+
include Enumerable
|
1270
|
+
|
1271
|
+
attr_accessor :group
|
1272
|
+
|
1273
|
+
def initialize(elements=[])
|
1274
|
+
@group = (elements.nil? or elements.empty?) ? [] : elements
|
1275
|
+
end
|
1276
|
+
|
1277
|
+
# Provides a listing/array containing the elements of the group
|
1278
|
+
def listing
|
1279
|
+
@group
|
1280
|
+
end
|
1281
|
+
|
1282
|
+
def +(other)
|
1283
|
+
ElementGroup.new(self.group + other.group)
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
def adjacent?
|
1287
|
+
return true if @group.length <= 1
|
1288
|
+
@group.each_cons(2) do |current, following|
|
1289
|
+
return false if !current.sibling_next.eql? following or !following.sibling_prev.eql? current
|
1290
|
+
end
|
1291
|
+
@group.first.sibling_prev.nil? and @group.last.sibling_next.nil?
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
def to_adjacent
|
1295
|
+
adj_group = AdjacentElementGroup.new
|
1296
|
+
adj_group.base(@group[0])
|
1297
|
+
adj_group.fill
|
1298
|
+
end
|
1299
|
+
|
1300
|
+
# Deep copy for ElementGroup
|
1301
|
+
def copy
|
1302
|
+
ElementGroup.new(@group.map {|member| member.copy})
|
1303
|
+
end
|
1304
|
+
|
1305
|
+
# Detach from current parent/siblings
|
1306
|
+
def detach
|
1307
|
+
@group.each do |member|
|
1308
|
+
member.detach
|
1309
|
+
end
|
1310
|
+
end
|
1311
|
+
alias_method :prune, :detach
|
1312
|
+
alias_method :delete, :detach
|
1313
|
+
|
1314
|
+
# Graft onto another element of the tree at any index of its children
|
1315
|
+
# By default, it will graft as the last element of the other element's children
|
1316
|
+
def graft_onto(graft_parent, index=-1)
|
1317
|
+
# If index to small or large, graft to edges of graft_parent children
|
1318
|
+
if index.abs > graft_parent.children.length
|
1319
|
+
index = graft_parent.children.length * (index > 1 ? 1 : 0)
|
1320
|
+
end
|
1321
|
+
if index == graft_parent.children.length or index == -1
|
1322
|
+
self.graft_last_onto(graft_parent)
|
1323
|
+
elsif index == 0
|
1324
|
+
self.graft_first_onto(graft_parent)
|
1325
|
+
else
|
1326
|
+
# Detach from current context
|
1327
|
+
self.detach
|
1328
|
+
|
1329
|
+
# Update context
|
1330
|
+
previous_child = graft_parent.children[index-1]
|
1331
|
+
next_child = graft_parent.children[index]
|
1332
|
+
@group.each do |member|
|
1333
|
+
member.set_parent!(graft_parent)
|
1334
|
+
|
1335
|
+
Element.stitch!(previous_child, member)
|
1336
|
+
previous_child = member
|
1337
|
+
end
|
1338
|
+
Element.stitch!(@group[-1], next_child)
|
1339
|
+
|
1340
|
+
# Graft group at index
|
1341
|
+
graft_parent.children.insert(index, *@group)
|
1342
|
+
end
|
1343
|
+
end
|
1344
|
+
|
1345
|
+
# Graft onto another element of the tree as the last child
|
1346
|
+
def graft_last_onto(graft_parent)
|
1347
|
+
# Detach from current context
|
1348
|
+
self.detach
|
1349
|
+
|
1350
|
+
# Update context
|
1351
|
+
previous_child = graft_parent.children[-1]
|
1352
|
+
@group.each do |member|
|
1353
|
+
member.set_parent!(graft_parent)
|
1354
|
+
|
1355
|
+
Element.stitch!(previous_child, member)
|
1356
|
+
previous_child = member
|
1357
|
+
end
|
1358
|
+
Element.stitch!(@group[-1], nil)
|
1359
|
+
|
1360
|
+
# Push graft group onto parent children
|
1361
|
+
graft_parent.children.push(*@group)
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
# Graft onto another element of the tree as the first child
|
1365
|
+
def graft_first_onto(graft_parent)
|
1366
|
+
# Detach from current context
|
1367
|
+
self.detach
|
1368
|
+
|
1369
|
+
# Update context
|
1370
|
+
previous_child = nil
|
1371
|
+
next_child = graft_parent.children[0]
|
1372
|
+
@group.each do |member|
|
1373
|
+
member.set_parent!(graft_parent)
|
1374
|
+
|
1375
|
+
Element.stitch!(previous_child, member)
|
1376
|
+
previous_child = member
|
1377
|
+
end
|
1378
|
+
Element.stitch!(@group[-1], next_child)
|
1379
|
+
|
1380
|
+
# Graft group at index
|
1381
|
+
graft_parent.children.insert(0, *@group)
|
1382
|
+
end
|
1383
|
+
|
1384
|
+
end
|
1385
|
+
|
1386
|
+
class GroupListener < ElementGroup
|
1387
|
+
attr_accessor :rule, :exe_block
|
1388
|
+
|
1389
|
+
def initialize(rule_string, exe_block)
|
1390
|
+
super()
|
1391
|
+
@rule = Arboretum::Scandent::Parser.parse_rule_string(rule_string, :PATH_LISTENER)
|
1392
|
+
@exe_block = exe_block
|
1393
|
+
end
|
1394
|
+
end
|
1395
|
+
|
1396
|
+
# A group of adjacent/sibling elements
|
1397
|
+
# Must strictly be connected to one another continuously (in order)
|
1398
|
+
# AdjacentElementGroups have all functionality of ElementGroups
|
1399
|
+
# They can also wrapped and swapped with other Elements/AdjacentElementGroups
|
1400
|
+
class AdjacentElementGroup < ElementGroup
|
1401
|
+
extend Forwardable
|
1402
|
+
def_delegators :@group, :[], :each, :length, :empty?, :first, :last
|
1403
|
+
undef :<<, :push
|
1404
|
+
alias_method :to_adjacent, :itself
|
1405
|
+
include Enumerable
|
1406
|
+
include Contextualized
|
1407
|
+
|
1408
|
+
attr_accessor :group
|
1409
|
+
|
1410
|
+
def initialize(element=nil)
|
1411
|
+
@group = element.nil? ? [] : [element]
|
1412
|
+
end
|
1413
|
+
|
1414
|
+
# Assign starting element of the AdjacentElementGroup, if there is not already one
|
1415
|
+
def base(element)
|
1416
|
+
raise IndexError.new("Base element already selected") if not @group.empty?
|
1417
|
+
raise TypeError.new("Base element cannot be nil") if element.nil?
|
1418
|
+
@group << element
|
1419
|
+
self
|
1420
|
+
end
|
1421
|
+
|
1422
|
+
def +(other)
|
1423
|
+
ElementGroup.new(self.group + other.group)
|
1424
|
+
end
|
1425
|
+
|
1426
|
+
def adjacent?
|
1427
|
+
true
|
1428
|
+
end
|
1429
|
+
|
1430
|
+
# Deep copy for AdjacentElementGroup
|
1431
|
+
def copy
|
1432
|
+
group_copy = @group.map {|member| member.copy}
|
1433
|
+
Element.stitch!(nil, group_copy.first)
|
1434
|
+
group_copy.each_cons(2) {|current, following| Element.stitch!(current,following)}
|
1435
|
+
Element.stitch(group_copy.last, nil)
|
1436
|
+
adj_group_copy = AdjacentElementGroup.new
|
1437
|
+
adj_group_copy.base(group_copy.first)
|
1438
|
+
adj_group_copy.fill
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
# Extend group as much as it can be extended
|
1442
|
+
def fill
|
1443
|
+
if not @group.empty?
|
1444
|
+
extend_prev until @group.first.sibling_prev.nil?
|
1445
|
+
extend_next until @group.last.sibling_next.nil?
|
1446
|
+
end
|
1447
|
+
self
|
1448
|
+
end
|
1449
|
+
|
1450
|
+
def index_in_parent
|
1451
|
+
self.parent.children.index(@group.first)
|
1452
|
+
end
|
1453
|
+
|
1454
|
+
def sibling_prev
|
1455
|
+
@group.first.sibling_prev
|
1456
|
+
end
|
1457
|
+
|
1458
|
+
def sibling_next
|
1459
|
+
@group.last.sibling_next
|
1460
|
+
end
|
1461
|
+
|
1462
|
+
def parent
|
1463
|
+
@group.first.parent
|
1464
|
+
end
|
1465
|
+
|
1466
|
+
# Extend the adjacent group to the last element's next sibling
|
1467
|
+
def extend_next
|
1468
|
+
@group << @group.last.sibling_next unless @group.last.sibling_next.nil?
|
1469
|
+
self
|
1470
|
+
end
|
1471
|
+
# Extend the adjacent group to the first element's previous sibling
|
1472
|
+
def extend_prev
|
1473
|
+
@group.unshift(@group.first.sibling_prev) unless @group.first.sibling_prev.nil?
|
1474
|
+
self
|
1475
|
+
end
|
1476
|
+
|
1477
|
+
# Provides a listing/array containing the elements of the group
|
1478
|
+
def listing
|
1479
|
+
@group
|
1480
|
+
end
|
1481
|
+
|
1482
|
+
# Detach from current parent/siblings, but maintain internal references between siblings
|
1483
|
+
def detach
|
1484
|
+
next_sibling = @group[-1].nil? ? nil : @group[-1].sibling_next
|
1485
|
+
prev_sibling = @group[0].nil? ? nil : @group[0].sibling_prev
|
1486
|
+
Element.stitch!(prev_sibling, next_sibling)
|
1487
|
+
@group[0].parent.set_children!(@group[0].parent.children - @group) unless (@group[0].nil? or @group[0].parent.nil?)
|
1488
|
+
|
1489
|
+
@group.each {|member| member.set_parent!(nil)}
|
1490
|
+
@group.first.set_sibling_prev!(nil)
|
1491
|
+
@group.last.set_sibling_next!(nil)
|
1492
|
+
end
|
1493
|
+
alias_method :prune, :detach
|
1494
|
+
alias_method :delete, :detach
|
1495
|
+
|
1496
|
+
# Graft onto another element of the tree at any index of its children
|
1497
|
+
# By default, it will graft as the last element of the other element's children
|
1498
|
+
def graft_onto(graft_parent, index=-1)
|
1499
|
+
# If index to small or large, graft to edges of graft_parent children
|
1500
|
+
if index.abs > graft_parent.children.length
|
1501
|
+
index = graft_parent.children.length * (index > 1 ? 1 : 0)
|
1502
|
+
end
|
1503
|
+
if index == graft_parent.children.length or index == -1
|
1504
|
+
self.graft_last_onto(graft_parent)
|
1505
|
+
elsif index == 0
|
1506
|
+
self.graft_first_onto(graft_parent)
|
1507
|
+
else
|
1508
|
+
# Detach from current context
|
1509
|
+
self.detach
|
1510
|
+
|
1511
|
+
# Update context
|
1512
|
+
@group.each do |member|
|
1513
|
+
member.set_parent!(graft_parent)
|
1514
|
+
end
|
1515
|
+
|
1516
|
+
previous_child = graft_parent.children[index-1]
|
1517
|
+
next_child = graft_parent.children[index]
|
1518
|
+
Element.stitch!(previous_child, @group[0])
|
1519
|
+
Element.stitch!(@group[-1], next_child)
|
1520
|
+
|
1521
|
+
# Graft group at index
|
1522
|
+
graft_parent.children.insert(index, *@group)
|
1523
|
+
end
|
1524
|
+
end
|
1525
|
+
|
1526
|
+
# Graft onto another element of the tree as the first child
|
1527
|
+
def graft_first_onto(graft_parent)
|
1528
|
+
# Detach from current context
|
1529
|
+
self.detach
|
1530
|
+
|
1531
|
+
# Update context
|
1532
|
+
@group.each do |member|
|
1533
|
+
member.set_parent!(graft_parent)
|
1534
|
+
end
|
1535
|
+
|
1536
|
+
next_child = graft_parent.children[0]
|
1537
|
+
Element.stitch!(nil, @group[0])
|
1538
|
+
Element.stitch!(@group[-1], next_child)
|
1539
|
+
|
1540
|
+
# Insert graft group at the beginning of parent children
|
1541
|
+
graft_parent.children.insert(0, *@group)
|
1542
|
+
end
|
1543
|
+
|
1544
|
+
# Graft onto another element of the tree as the last child
|
1545
|
+
def graft_last_onto(graft_parent)
|
1546
|
+
# Detach from current context
|
1547
|
+
self.detach
|
1548
|
+
|
1549
|
+
# Update context
|
1550
|
+
@group.each do |member|
|
1551
|
+
member.set_parent!(graft_parent)
|
1552
|
+
end
|
1553
|
+
|
1554
|
+
previous_child = graft_parent.children[-1]
|
1555
|
+
Element.stitch!(previous_child, @group[0])
|
1556
|
+
Element.stitch!(@group[-1], nil)
|
1557
|
+
|
1558
|
+
# Push graft group onto parent children
|
1559
|
+
graft_parent.children.push(*@group)
|
1560
|
+
end
|
1561
|
+
|
1562
|
+
end
|
1563
|
+
|
1564
|
+
end # Elements
|
1565
|
+
end # DocTree
|
1566
|
+
end # Arboretum
|