Class: Trivet::Node

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Querier
Defined in:
lib/trivet.rb

Overview

Objects of this class represent a single node in a hierarchy.

Constant Summary collapse

INDEX_WITHIN =

Values for positioning a node within its parent. These values are used internally only.

%w{first last}
INDEX_WITHOUT =

Values for positioning a node before or after a sibling. These values are used internally only.

%w{before after}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Querier

#query_first

Constructor Details

#initialize(pod = nil) ⇒ Node

Creates a new Trivet::Node object. The first param can be a parent node, the id of the new node, or nil.



100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/trivet.rb', line 100

def initialize(pod=nil)
	@id = nil
	@parent = nil
	@children = Trivet::Childset.new(self)
	@misc = {}
	
	# if parent object send
	if pod.is_a?(Trivet::Node) or pod.is_a?(Trivet::Document)
		self.parent = pod
	elsif pod.is_a?(String)
		self.id = pod
	end
end

Instance Attribute Details

#childrenObject (readonly)

A Trivet::ChildSet object containing the children. This property can generally be treated like an array, but it has a few other features as well.



128
129
130
# File 'lib/trivet.rb', line 128

def children
  @children
end

#idObject

Returns the id of the node, or nil if it does not have an id.



134
135
136
# File 'lib/trivet.rb', line 134

def id
  @id
end

#miscObject (readonly)

A hash of any miscellaneous information you want to attach to the node.



131
132
133
# File 'lib/trivet.rb', line 131

def misc
  @misc
end

#parentObject

Returns the parent object of the node, or nil if there is no parent.



123
124
125
# File 'lib/trivet.rb', line 123

def parent
  @parent
end

Class Method Details

.no_childrenObject

This method provides a concise way to override Trivet::Node#allow_child? so that it always returns false. Just add this to your class:

self.no_children()


942
943
944
945
946
# File 'lib/trivet.rb', line 942

def self.no_children()
	self.define_method('allow_child?') do |child|
		return false
	end
end

Instance Method Details

#add(*objs) ⇒ Object

A shortcut for adding children of any arbitrary class. Takes zero or more params, each of which is an object.



388
389
390
391
392
# File 'lib/trivet.rb', line 388

def add(*objs)
	objs.each do |obj|
		@children.push obj
	end
end

#allow_child?(child) ⇒ Boolean

This method is called when a node or other object is added to a node. By default, always returns true. Override this method to create custom rules.

Returns:

  • (Boolean)


925
926
927
# File 'lib/trivet.rb', line 925

def allow_child?(child)
	return true
end

#ancestorsObject

Returns an array of the node’s ancestors.



631
632
633
634
635
636
637
# File 'lib/trivet.rb', line 631

def ancestors
	if @parent.is_a?(Trivet::Node)
		return @parent.heritage()
	else
		return []
	end
end

#child_class(*opts) ⇒ Object

Returns the class to use for new nodes in Trivet::Node.node(). By default, this method returns the same class as the calling node. Override this method to create different rules for the class to use.



572
573
574
# File 'lib/trivet.rb', line 572

def child_class(*opts)
	return self.class
end

#depthObject

Returns the depth of the node. The root note returns 0, its children return 1, etc.



995
996
997
# File 'lib/trivet.rb', line 995

def depth
	return ancestors.length
end

#documentObject

Returns the Trivet::Document object that holds the tree. Returns nil if there is no document.



958
959
960
961
# File 'lib/trivet.rb', line 958

def document()
	# $tm.hrm
	return root.parent
end

#have_object?(object) ⇒ Object

Returns true if the node has the given object as a child. This method is not quite the same as include?. This method returns true only if children contains the actual object given, not just an object that matches ‘==`. So, for example, the following use of have_object? would return false, even though it cohtains a string identical to the string in @children.

node.add 'whatever'
node.include? 'whatever'     # true
node.have_object? 'whatever' # false


# File 'lib/trivet.rb', line 76

#heritageObject

Returns an array of the node’s ancestors plus the node itself.



648
649
650
651
652
653
654
# File 'lib/trivet.rb', line 648

def heritage
	if @parent.is_a?(Trivet::Node)
		return @parent.heritage() + [self]
	else
		return [self]
	end
end

#indexObject

Returns the index of the node within the parent. Returns nil if the node has no parent.



431
432
433
434
435
436
437
# File 'lib/trivet.rb', line 431

def index
	if @parent
		return @parent.children.find_index(self)
	else
		return nil
	end
end

#match?(qobj) ⇒ Boolean

This method is called for each node in a query. If this method return true then the node is yielded/returned in the query. By default, this method always returns true. See Trivet::Node#query() for more details.

Returns:

  • (Boolean)


801
802
803
# File 'lib/trivet.rb', line 801

def match?(qobj)
	return true
end

#move_child(child, tgt) ⇒ Object

Moves the given object from this node to the given node. Note that the object being moved doesn’t actually have to be a child of this node, but it will be removed if it is.



1069
1070
1071
1072
1073
# File 'lib/trivet.rb', line 1069

def move_child(child, tgt)
	# $tm.hrm
	@children.remove_object child
	tgt.add child
end

#next_siblingObject

Returns the node’s next sibling.



1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
# File 'lib/trivet.rb', line 1037

def next_sibling
	# $tm.hrm
	found_self = false
	
	# if parent, search through siblings
	if parent
		parent.children.each do |sib|
			if sib == self
				found_self = true
			elsif found_self
				return sib
			end
		end
	end
	
	# no next sibling
	return nil
end

#node(opts = {}) ⇒ Object

Create a node and positions the new node either within the calling node, before it, after it, or replaces it. By default, the new node is positioned as the last child node of the caller.

In its simplest use, node() creates a new node, yields it if given a block, and returns it. You can build structures by calling node() within node blocks. If a single string is given as a param, that string is used for the ‘id` for the new node.

food = Trivet::Node.new()
food.id = 'food'

food.node('spices') do |spices|
    spices.node 'paprika'

    spices.node('pepper') do |pepper|
        pepper.node 'java'
        pepper.node 'matico'
        pepper.node 'cubeb'
    end
end

option: index

The index option indicates where the new node should be positioned. This option can have one of the following values:

  • first: The new node becomes the first child of the caller object.

  • last: The new node becomes the last child of the caller object. This is the default behavior.

  • before: The new node is placed before the caller object.

  • after: The new node is placed after the caller object.

  • replace: The node node replaces the caller object and the caller is removed from the tree.

  • [integer]: The new node is placed at the index of the given integer.

option: use

If the ‘use` option is sent, then that object is used as the new node.



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/trivet.rb', line 318

def node(opts={})
	# normalize opts
	if opts.is_a?(String)
		opts = {'id'=>opts}
	elsif opts.is_a?(Trivet::Node)
		opts = {'use'=>opts}
	end
	
	# $tm.hrm
	idx = opts['index'] || 'last'
	
	# create child object
	new_node = opts['use'] || child_class(opts).new()
	
	# id if sent
	if opts['id']
		new_node.id = opts['id']
	end
	
	# add to this node
	if INDEX_WITHIN.include?(idx) or idx.is_a?(Integer)
		new_node.set_parent self, 'index'=>idx
	
	# add to parent
	elsif INDEX_WITHOUT.include?(idx)
		if @parent
			if idx == 'before'
				new_node.set_parent @parent, 'index'=>self.index
			elsif idx == 'after'
				new_node.set_parent @parent, 'index'=>self.index + 1
			else
				raise 'unrecognized-without-index: ' + idx.to_s
			end
		else
			raise 'cannot-set-before-or-after-if-no-parent'
		end
	
	# replace
	elsif idx == 'replace'
		new_node.set_parent @parent, 'index'=>self.index
		
		@children.to_a.each do |child|
			child.parent = new_node
		end
		
		unlink()
	else
		raise 'node-unknown-index: ' + idx.to_s
	end
	
	# yield if necessary
	if block_given?
		yield new_node
	end
	
	# return
	return new_node
end

#node_by_id(qid) ⇒ Object

Searches for a node by its id.



897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
# File 'lib/trivet.rb', line 897

def node_by_id(qid)
	if @id == qid
		return self
	end
	
	# check children
	@children.each do |child|
		if child.is_a?(Trivet::Node)
			if node = child.node_by_id(qid)
				return node
			end
		end
	end
	
	# didn't find the node
	return nil
end

#previous_siblingObject

Returns the node’s previous sibling



1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
# File 'lib/trivet.rb', line 1008

def previous_sibling
	# $tm.hrm
	latest = nil
	
	# if parent, search through sibling
	if parent
		parent.children.each do |sib|
			if sib == self
				return latest
			else
				latest = sib
			end
		end
	end
	
	# no parent
	return nil
end

#query(qobj, opts = {}) ⇒ Object

Runs a query on the tree, yielding and returning nodes that match the given query. This method is really only useful if you subclass Trivet::Node and override Trivet::Node.match?(). By default, match? always returns true. The query param can be any kind of object you want it to be. That param will be passed to match? for each node. If match? returns true then the query yields/returns that node.

In these examples, we’ll assume a tree like this:

food
   spices
      paprika
      pepper
         java
         matico
         cubeb
   fruit
      red
         cherry
         apple

For example, you could override match? so that it returns true if a given string is within a node’s id.

class MyNode < Trivet::Node
    def match?(qobj)
        if @id
            return @id.match(/#{qobj}/mu)
        else
            return false
        end
    end
end

You could then query for nodes that have ‘o’ in their id:

food = MyNode.new('food')

# ... add a bunch of child nodes

food.query('o') do |node|
    puts node.id
end

That query returns one node, ‘matico’

option: self

By default, the query does not include the node itself, only its descendents. To include the node itself, use the ‘self’ option.

food.query('o', 'self'=>true) do |node|
    puts node.id
end

That query will return ‘food’ and ‘matico’.

option: recurse

The recurse option indicates how far into the tree the query should recurse. The default is Trivet::ALWAYS, which means to recurse all the way down.

Trivet::UNTIL_MATCH means to not recurse into nodes that match the query. So if a node matches, its children are not included in the query.

Trivet::STOP_ON_FIRST means that the query is stopped completely after the first match is found. Using this option means that the query always returns either zero or one matches.



757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
# File 'lib/trivet.rb', line 757

def query(qobj, opts={})
	ctl = opts['recurse'] || Trivet::ALWAYS
	rv = []
	
	# traverse
	self.traverse(opts) do |node, recurse|
		if node.match?(qobj)
			# add to return array
			rv.push node
			
			if ctl == Trivet::UNTIL_MATCH
				recurse.prune
			elsif ctl == Trivet::STOP_ON_FIRST
				recurse.stop
			end
		end
	end
	
	# yield
	if block_given?
		rv.each do |node|
			yield node
		end
	end
	
	# return
	return rv
end

#remove_object(object) ⇒ Object

Removes the given object from children. Only the exact object is removed. If the object is a string, strings that are identical but not that actual object will not be removed.



91
# File 'lib/trivet.rb', line 91

delegate %w(remove_object have_object?) => :@children

#rootObject

Returns the root node of the tree. Returns self if the node has no parent.



586
587
588
589
590
591
592
# File 'lib/trivet.rb', line 586

def root
	if @parent
		return @parent.root
	else
		return self
	end
end

#set_parent(new_parent, opts = {}) ⇒ Object

Sets a new parent for the node. The new parent can be either a Trivet::Node object or a Trivet::Document object.

For example, we’ll start with the standard tree we’ve been using:

food
   spices
      paprika
      pepper
         java
         matico
         cubeb
   fruit
      red
         cherry
         apple

Now we’ll set fruit’s parent to the pepper node:

fruit = food.node_by_id('fruit')
pepper = food.node_by_id('pepper')
fruit.set_parent pepper

That moves the fruit node and all its descendents into pepper:

food
   spices
      paprika
      pepper
         java
         matico
         cubeb
         fruit
            red
               cherry
               apple

option: index

The index option indicates which position within the parent the node should be moved to. This option has no meaning if the new parent is a Trivet::Document object.

Set index to ‘first’ to move it to the first position:

fruit = food.node_by_id('fruit')
pepper = food.node_by_id('pepper')
fruit.set_parent pepper, 'index'=>'first'

Set index to an integer to move the node to that index within the parent.

fruit = food.node_by_id('fruit')
pepper = food.node_by_id('pepper')
fruit.set_parent pepper, 'index'=>1

nil

If the parent param is nil then the object is unlinked from its parent.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/trivet.rb', line 217

def set_parent(new_parent, opts={})
	# $tm.hrm @id
	opts = {'recurse'=>true}.merge(opts)
	index = opts['index'] || 'last'
	
	# add to new_parent
	if new_parent
		# add to another node
		if new_parent.is_a?(Trivet::Node)
			new_parent.trace(self)
			unlink()
			@parent = new_parent
			
			# add to parent's children
			if opts['recurse']
				if index == 'last'
					@parent.children.push self, 'recurse'=>false
				elsif index == 'first'
					@parent.children.unshift self, 'recurse'=>false
				elsif index.is_a?(Integer)
					@parent.children.insert index, self, 'recurse'=>false
				else
					raise 'set-parent-unknown-index: ' + index.to_s
				end
			end
		
		# new parent is a document
		elsif new_parent.is_a?(Trivet::Document)
			unlink()
			@parent = new_parent
			
			# set as document's root
			if opts['recurse']
				new_parent.set_root self, 'recurse'=>false
			end
		
		# else raise exception
		else
			raise 'unknown-class-for-parent: ' + new_parent.class.to_s
		end
	
	# unlink node because it does not have a parent anymore.
	else
		unlink()
	end
end

#tabObject


tab



884
885
886
# File 'lib/trivet.rb', line 884

def tab
	return '   '
end

#to_debugObject

Useful to outputting a string that represents the node for the purpose of debugging. By default, outputs to_s. This method is used in to_tree, so it’s usually a good idea to return a single-line string.



821
822
823
# File 'lib/trivet.rb', line 821

def to_debug
	return to_s
end

#to_sObject

Returns the id if there is one. Otherwise returns Object#to_s.



814
815
816
# File 'lib/trivet.rb', line 814

def to_s
	return @id || super()
end

#to_tree(opts = {}) ⇒ Object

This method is for development and debugging. Returns a string consisting of the node and all its descending arranged as an indented tree. Each node’s Trivet::Node#to_s method is used to display the node. Descendents that are not Trivet::Node objects are not displayed.



838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
# File 'lib/trivet.rb', line 838

def to_tree(opts={})
	opts = {'depth'=>0}.merge(opts)
	rv = tab() * opts['depth'] + self.to_debug
	
	# indent
	if opts['indent']
		rv = opts['indent'] + rv
	end
	
	# send_opts
	send_opts = opts.clone
	send_opts['depth'] += 1
	
	# recurse
	@children.each do |child|
		# recurse into child node
		if child.is_a?(Trivet::Node)
			rv += "\n" + child.to_tree(send_opts)
			
		# elsif if there's a custom child_to_tree method
		elsif respond_to?('child_to_tree')
			send_opts['tab'] = tab()
			add = child_to_tree(child, send_opts)
			
			if add and add.match(/./mu)
				rv += "\n" + add
			end
		
		# String
		elsif child.is_a?(String)
			rv += "\n" + (tab() * send_opts['depth'] ) + child.lx.collapse
			
		end
	end
	
	# return
	return rv
end

#trace(new_node) ⇒ Object

Checks if a node is about to become nested within itself. This method is used by Trivet::Node.set_parent to prevent circular references. Generally you don’t need to call this method.



667
668
669
670
671
672
673
674
675
676
# File 'lib/trivet.rb', line 667

def trace(new_node)
	if new_node == self
		raise 'circular-reference'
	end
	
	# trace to parent
	if @parent and not(@parent.is_a?(Trivet::Document))
		@parent.trace(new_node)
	end
end

#traverse(opts = {}, &block) ⇒ Object

Traverses the tree starting with the children of the node. Each node in the tree is yielded to the block if there is one. Returns and array of descendents.

food.traverse() do |node|
    puts ('  ' * node.depth) +  node.id
end

That gives us output similar to that of the to_tree() method.

spices
  paprika
  pepper
    java
    matico
    cubeb
fruit
  red
    cherry
    apple

By default, the node on which you call this method itself is not traversed. You can include that node with the ‘self’ option:

food.traverse('self'=>true) do |node|
    puts ('  ' * node.depth) +  node.id
end

The first parameter for the yield block is the node, as in the examples above. The second param is a Trivet::TraverseControl object which can be used to control the recursion.

Prune recursion

You can indicate that the traversal should not recurse into a node’s children with Trivet::TraverseControl.prune. For example, the following code doesn’t traverse into the spices node:

food.traverse('self'=>true) do |node, ctl|
    puts ('  ' * node.depth) +  node.id

    if node.id == 'spices'
        ctl.prune
    end
end

Giving us this output:

food
  spices
  fruit
    red
      cherry
      apple

Stop recursion

You can stop the recursion by calling Trivet::TraverseControl.stop. For example, suppose you want to stop the traversal completely when you get to the “java” node. You could do that like this:

food.traverse('self'=>true) do |node, ctl|
    puts ('  ' * node.depth) +  node.id

    if node.id == 'java'
        ctl.stop
    end
end

That gives us output like this:

food
  spices
    paprika
    pepper
      java


522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/trivet.rb', line 522

def traverse(opts={}, &block)
	ctrl = opts['ctrl'] || Trivet::TraverseControl.new()
	rv = []
	
	# yield self
	if opts['self']
		# yield
		if block_given?
			yield self, ctrl
		end
		
		# add to return array
		rv.push self
		
		# return if stopped
		ctrl.stopped and return rv
	end
	
	# if pruned, don't recurse into children, but continue to next sibling node
	if ctrl.pruned
		ctrl.pruned = false
	
	# recurse into children
	else
		# puts "--- #{self}"
		
		@children.to_a.each do |child|
			if child.is_a?(Trivet::Node)
				rv += child.traverse('self'=>true, 'ctrl'=>ctrl, &block)
				ctrl.stopped and return rv
				ctrl.pruned = false
			end
		end
	end
	
	# return
	return rv
end

Removes the node from the tree.



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# File 'lib/trivet.rb', line 603

def unlink(opts={})
	# $tm.hrm
	opts = {'recurse'=>true}.merge(opts)
	
	# remove from parent
	if @parent and opts['recurse']
		if @parent.is_a?(Trivet::Document)
			@parent.set_root nil, 'recurse'=>false
		elsif @parent.is_a?(Trivet::Node)
			@parent.remove_object self
		else
			raise 'unlink-unknown-parent-class: ' + @parent.class.to_
		end
	end
	
	# set parent to nil
	@parent = nil
end

#unwrapObject

Moves the child nodes into the node’s parent and deletes self.



972
973
974
975
976
977
978
979
980
981
982
983
# File 'lib/trivet.rb', line 972

def unwrap()
	# $tm.hrm
	pchildren = @parent.children
	
	# move children
	@children.to_a.each do |child|
		pchildren.insert self.index, child
	end
	
	# unlink self
	unlink()
end