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.
@@ -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('&','&amp;').gsub('<', '&lt;').gsub('>','&gt;')} "
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('&','&amp;').gsub('<', '&lt;').gsub('>','&gt;')
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('&','&amp;').gsub('<', '&lt;').gsub('>','&gt;')}-->"
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('&','&amp;').gsub('<', '&lt;').gsub('>','&gt;')}?>"
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