Chrononaut-hirb 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +19 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +164 -0
- data/Rakefile +50 -0
- data/VERSION.yml +4 -0
- data/lib/hirb.rb +56 -0
- data/lib/hirb/console.rb +43 -0
- data/lib/hirb/formatter.rb +199 -0
- data/lib/hirb/hash_struct.rb +17 -0
- data/lib/hirb/helpers.rb +7 -0
- data/lib/hirb/helpers/active_record_table.rb +16 -0
- data/lib/hirb/helpers/auto_table.rb +15 -0
- data/lib/hirb/helpers/object_table.rb +15 -0
- data/lib/hirb/helpers/parent_child_tree.rb +22 -0
- data/lib/hirb/helpers/table.rb +243 -0
- data/lib/hirb/helpers/tree.rb +177 -0
- data/lib/hirb/import_object.rb +10 -0
- data/lib/hirb/menu.rb +47 -0
- data/lib/hirb/pager.rb +94 -0
- data/lib/hirb/util.rb +80 -0
- data/lib/hirb/view.rb +177 -0
- data/lib/hirb/views/activerecord_base.rb +9 -0
- data/test/console_test.rb +12 -0
- data/test/formatter_test.rb +172 -0
- data/test/hirb_test.rb +23 -0
- data/test/import_test.rb +9 -0
- data/test/menu_test.rb +94 -0
- data/test/pager_test.rb +164 -0
- data/test/table_test.rb +374 -0
- data/test/test_helper.rb +47 -0
- data/test/tree_test.rb +167 -0
- data/test/util_test.rb +56 -0
- data/test/view_test.rb +116 -0
- metadata +96 -0
data/lib/hirb/helpers.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Hirb::Helpers::ActiveRecordTable < Hirb::Helpers::ObjectTable
|
2
|
+
# Rows are Rails' ActiveRecord::Base objects.
|
3
|
+
# Takes same options as Hirb::Helpers::Table.render except as noted below.
|
4
|
+
#
|
5
|
+
# Options:
|
6
|
+
# :fields- Can be any attribute, column or not. If not given, this defaults to the database table's columns.
|
7
|
+
def self.render(rows, options={})
|
8
|
+
rows = [rows] unless rows.is_a?(Array)
|
9
|
+
options[:fields] ||=
|
10
|
+
begin
|
11
|
+
fields = rows.first.class.column_names
|
12
|
+
fields.map {|e| e.to_sym }
|
13
|
+
end
|
14
|
+
super(rows, options)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Detects the table class the output should use and delegates rendering to it.
|
2
|
+
class Hirb::Helpers::AutoTable
|
3
|
+
# Same options as Hirb::Helpers::Table.render.
|
4
|
+
def self.render(output, options={})
|
5
|
+
output = output.to_a if !output.is_a?(Array) && output.respond_to?(:to_a)
|
6
|
+
klass = if ((output.is_a?(Array) && output[0].is_a?(ActiveRecord::Base)) or output.is_a?(ActiveRecord::Base) rescue false)
|
7
|
+
Hirb::Helpers::ActiveRecordTable
|
8
|
+
elsif (output.is_a?(Array) && !(output[0].is_a?(Hash) || output[0].is_a?(Array)))
|
9
|
+
Hirb::Helpers::ObjectTable
|
10
|
+
else
|
11
|
+
Hirb::Helpers::Table
|
12
|
+
end
|
13
|
+
klass.render(output, options)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Hirb::Helpers::ObjectTable < Hirb::Helpers::Table
|
2
|
+
# Rows are any ruby objects. Takes same options as Hirb::Helpers::Table.render except as noted below.
|
3
|
+
#
|
4
|
+
# Options:
|
5
|
+
# :fields- Methods of the object to represent as columns. Defaults to [:to_s].
|
6
|
+
def self.render(rows, options ={})
|
7
|
+
options[:fields] ||= [:to_s]
|
8
|
+
options[:headers] = {:to_s=>'value'} if options[:fields] == [:to_s]
|
9
|
+
rows = [rows] unless rows.is_a?(Array)
|
10
|
+
item_hashes = rows.inject([]) {|t,item|
|
11
|
+
t << options[:fields].inject({}) {|h,f| h[f] = item.send(f); h}
|
12
|
+
}
|
13
|
+
super(item_hashes, options)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Hirb::Helpers::ParentChildTree < Hirb::Helpers::Tree
|
2
|
+
class <<self
|
3
|
+
# Starting with the given node, this builds a tree by recursively calling a children method.
|
4
|
+
# Takes same options as Hirb::Helper::Table.render with some additional ones below.
|
5
|
+
# ==== Options:
|
6
|
+
# [:value_method] Method to call to display as a node's value. If not given, uses :name if node
|
7
|
+
# responds to :name or defaults to :object_id.
|
8
|
+
# [:children_method] Method to call to obtain a node's children. Default is :children.
|
9
|
+
def render(root_node, options={})
|
10
|
+
@value_method = options[:value_method] || (root_node.respond_to?(:name) ? :name : :object_id)
|
11
|
+
@children_method = options[:children_method] || :children
|
12
|
+
@nodes = []
|
13
|
+
build_node(root_node, 0)
|
14
|
+
super(@nodes, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_node(node, level) #:nodoc:
|
18
|
+
@nodes << {:value=>node.send(@value_method), :level=>level}
|
19
|
+
node.send(@children_method).each {|e| build_node(e, level + 1)}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
# Base Table class from which other table classes inherit.
|
2
|
+
# By default, a table is constrained to a default width but this can be adjusted
|
3
|
+
# via the max_width option or Hirb::View.width.
|
4
|
+
# Rows can be an array of arrays or an array of hashes.
|
5
|
+
#
|
6
|
+
# An array of arrays ie [[1,2], [2,3]], would render:
|
7
|
+
# +---+---+
|
8
|
+
# | 0 | 1 |
|
9
|
+
# +---+---+
|
10
|
+
# | 1 | 2 |
|
11
|
+
# | 2 | 3 |
|
12
|
+
# +---+---+
|
13
|
+
#
|
14
|
+
# By default, the fields/columns are the numerical indices of the array.
|
15
|
+
#
|
16
|
+
# An array of hashes ie [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], would render:
|
17
|
+
# +-----+--------+
|
18
|
+
# | age | weight |
|
19
|
+
# +-----+--------+
|
20
|
+
# | 10 | 100 |
|
21
|
+
# | 80 | 500 |
|
22
|
+
# +-----+--------+
|
23
|
+
#
|
24
|
+
# By default, the fields/columns are the keys of the first hash.
|
25
|
+
#--
|
26
|
+
# derived from http://gist.github.com/72234
|
27
|
+
class Hirb::Helpers::Table
|
28
|
+
BORDER_LENGTH = 3 # " | " and "-+-" are the borders
|
29
|
+
class TooManyFieldsForWidthError < StandardError; end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
|
33
|
+
# Main method which returns a formatted table.
|
34
|
+
# ==== Options:
|
35
|
+
# [:fields] An array which overrides the default fields and can be used to indicate field order.
|
36
|
+
# [:headers] A hash of fields and their header names. Fields that aren't specified here default to their name.
|
37
|
+
# This option can also be an array but only for array rows.
|
38
|
+
# [:field_lengths] A hash of fields and their maximum allowed lengths. If a field exceeds it's maximum
|
39
|
+
# length than it's truncated and has a ... appended to it. Fields that aren't specified here have no maximum allowed
|
40
|
+
# length.
|
41
|
+
# [:max_width] The maximum allowed width of all fields put together. This option is enforced except when the field_lengths option is set.
|
42
|
+
# This doesn't count field borders as part of the total.
|
43
|
+
# [:number] When set to true, numbers rows by adding a :hirb_number column as the first column. Default is false.
|
44
|
+
# [:filters] A hash of fields and the filters that each row in the field must run through. The filter converts the cell's value by applying
|
45
|
+
# a given proc or an array containing a method and optional arguments to it.
|
46
|
+
# Examples:
|
47
|
+
# Hirb::Helpers::Table.render [[1,2], [2,3]]
|
48
|
+
# Hirb::Helpers::Table.render [[1,2], [2,3]], :field_lengths=>{0=>10}
|
49
|
+
# Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}]
|
50
|
+
# Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :headers=>{:weight=>"Weight(lbs)"}
|
51
|
+
# Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :filters=>{:age=>[:to_f]}
|
52
|
+
def render(rows, options={})
|
53
|
+
new(rows,options).render
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
#:stopdoc:
|
58
|
+
def initialize(rows, options={})
|
59
|
+
@options = options
|
60
|
+
@options[:filters] ||= {}
|
61
|
+
@vertical = options[:vertical] || (Hirb::View.config[:vertical] if Hirb::View.config)
|
62
|
+
@fields = @options[:fields] ? @options[:fields].dup : ((rows[0].is_a?(Hash)) ? rows[0].keys.sort {|a,b| a.to_s <=> b.to_s} :
|
63
|
+
rows[0].is_a?(Array) ? (0..rows[0].length - 1).to_a : [])
|
64
|
+
if @vertical
|
65
|
+
@longest = @fields.map.sort_by { |i| i.to_s.length }.last.to_s.length
|
66
|
+
end
|
67
|
+
@rows = setup_rows(rows)
|
68
|
+
@headers = @fields.inject({}) {|h,e| h[e] = e.to_s; h}
|
69
|
+
if @options.has_key?(:headers)
|
70
|
+
@headers = @options[:headers].is_a?(Hash) ? @headers.merge(@options[:headers]) :
|
71
|
+
(@options[:headers].is_a?(Array) ? array_to_indices_hash(@options[:headers]) : @options[:headers])
|
72
|
+
end
|
73
|
+
if @options[:number]
|
74
|
+
@headers[:hirb_number] = "number"
|
75
|
+
@fields.unshift :hirb_number
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def setup_rows(rows)
|
80
|
+
rows ||= []
|
81
|
+
rows = [rows] unless rows.is_a?(Array)
|
82
|
+
if rows[0].is_a?(Array)
|
83
|
+
rows = rows.inject([]) {|new_rows, row|
|
84
|
+
new_rows << array_to_indices_hash(row)
|
85
|
+
}
|
86
|
+
end
|
87
|
+
rows = filter_values(rows)
|
88
|
+
rows.each_with_index {|e,i| e[:hirb_number] = (i + 1).to_s} if @options[:number] or @vertical
|
89
|
+
validate_values(rows)
|
90
|
+
rows
|
91
|
+
end
|
92
|
+
|
93
|
+
def render
|
94
|
+
body = []
|
95
|
+
unless @rows.length == 0
|
96
|
+
setup_field_lengths
|
97
|
+
body += @headers ? render_header : [render_border] unless @vertical
|
98
|
+
body += render_rows
|
99
|
+
body << render_border unless @vertical
|
100
|
+
end
|
101
|
+
body << render_table_description
|
102
|
+
body.join("\n")
|
103
|
+
end
|
104
|
+
|
105
|
+
def render_header
|
106
|
+
title_row = '| ' + @fields.map {|f|
|
107
|
+
format_cell(@headers[f], @field_lengths[f])
|
108
|
+
}.join(' | ') + ' |'
|
109
|
+
[render_border, title_row, render_border]
|
110
|
+
end
|
111
|
+
|
112
|
+
def render_border
|
113
|
+
'+-' + @fields.map {|f| '-' * @field_lengths[f] }.join('-+-') + '-+'
|
114
|
+
end
|
115
|
+
|
116
|
+
def format_cell(value, cell_width)
|
117
|
+
text = value.length > cell_width ?
|
118
|
+
(
|
119
|
+
(cell_width < 5) ? value.slice(0,cell_width) : value.slice(0, cell_width - 3) + '...'
|
120
|
+
) : value
|
121
|
+
sprintf("%-#{cell_width}s", text)
|
122
|
+
end
|
123
|
+
|
124
|
+
def render_rows
|
125
|
+
i = 0
|
126
|
+
@rows.map do |row|
|
127
|
+
if @vertical
|
128
|
+
stars = "*" * [(@longest + (@longest / 2)), 3].max
|
129
|
+
row = "#{stars} #{@rows[i][:hirb_number]}. row #{stars}\n" +
|
130
|
+
@fields.map {|f|
|
131
|
+
"#{f.to_s.rjust(@longest)}: #{row[f]}"
|
132
|
+
}.join("\n")
|
133
|
+
else
|
134
|
+
row = '| ' + @fields.map {|f|
|
135
|
+
format_cell(row[f], @field_lengths[f])
|
136
|
+
}.join(' | ') + ' |'
|
137
|
+
end
|
138
|
+
i += 1
|
139
|
+
row
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def render_table_description
|
144
|
+
(@rows.length == 0) ? "0 rows in set" :
|
145
|
+
"#{@rows.length} #{@rows.length == 1 ? 'row' : 'rows'} in set"
|
146
|
+
end
|
147
|
+
|
148
|
+
def setup_field_lengths
|
149
|
+
@field_lengths = default_field_lengths
|
150
|
+
if @options[:field_lengths]
|
151
|
+
@field_lengths.merge!(@options[:field_lengths])
|
152
|
+
else
|
153
|
+
table_max_width = @options.has_key?(:max_width) ? @options[:max_width] : Hirb::View.width
|
154
|
+
restrict_field_lengths(@field_lengths, table_max_width) if table_max_width
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def restrict_field_lengths(field_lengths, max_width)
|
159
|
+
max_width -= @fields.size * BORDER_LENGTH + 1
|
160
|
+
original_field_lengths = field_lengths.dup
|
161
|
+
@min_field_length = BORDER_LENGTH
|
162
|
+
adjust_long_fields(field_lengths, max_width)
|
163
|
+
rescue TooManyFieldsForWidthError
|
164
|
+
raise
|
165
|
+
rescue
|
166
|
+
default_restrict_field_lengths(field_lengths, original_field_lengths, max_width)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Simple algorithm which given a max width, allows smaller fields to be displayed while
|
170
|
+
# restricting longer fields at an average_long_field_length.
|
171
|
+
def adjust_long_fields(field_lengths, max_width)
|
172
|
+
total_length = field_lengths.values.inject {|t,n| t += n}
|
173
|
+
while total_length > max_width
|
174
|
+
raise TooManyFieldsForWidthError if @fields.size > max_width.to_f / @min_field_length
|
175
|
+
average_field_length = total_length / @fields.size.to_f
|
176
|
+
long_lengths = field_lengths.values.select {|e| e > average_field_length}
|
177
|
+
if long_lengths.empty?
|
178
|
+
raise "Algorithm didn't work, resort to default"
|
179
|
+
else
|
180
|
+
total_long_field_length = (long_lengths.inject {|t,n| t += n}) * max_width/total_length
|
181
|
+
average_long_field_length = total_long_field_length / long_lengths.size
|
182
|
+
field_lengths.each {|f,length|
|
183
|
+
field_lengths[f] = average_long_field_length if length > average_long_field_length
|
184
|
+
}
|
185
|
+
end
|
186
|
+
total_length = field_lengths.values.inject {|t,n| t += n}
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Produces a field_lengths which meets the max_width requirement
|
191
|
+
def default_restrict_field_lengths(field_lengths, original_field_lengths, max_width)
|
192
|
+
original_total_length = original_field_lengths.values.inject {|t,n| t += n}
|
193
|
+
relative_lengths = original_field_lengths.values.map {|v| (v / original_total_length.to_f * max_width).to_i }
|
194
|
+
# set fields by their relative weight to original length
|
195
|
+
if relative_lengths.all? {|e| e > @min_field_length} && (relative_lengths.inject {|a,e| a += e} <= max_width)
|
196
|
+
original_field_lengths.each {|k,v| field_lengths[k] = (v / original_total_length.to_f * max_width).to_i }
|
197
|
+
else
|
198
|
+
# set all fields the same if nothing else works
|
199
|
+
field_lengths.each {|k,v| field_lengths[k] = max_width / @fields.size}
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# find max length for each field; start with the headers
|
204
|
+
def default_field_lengths
|
205
|
+
field_lengths = @headers ? @headers.inject({}) {|h,(k,v)| h[k] = v.length; h} : {}
|
206
|
+
@rows.each do |row|
|
207
|
+
@fields.each do |field|
|
208
|
+
len = row[field].length
|
209
|
+
field_lengths[field] = len if len > field_lengths[field].to_i
|
210
|
+
end
|
211
|
+
end
|
212
|
+
field_lengths
|
213
|
+
end
|
214
|
+
|
215
|
+
def filter_values(rows)
|
216
|
+
rows.map {|row|
|
217
|
+
new_row = {}
|
218
|
+
@fields.each {|f|
|
219
|
+
if @options[:filters][f]
|
220
|
+
new_row[f] = @options[:filters][f].is_a?(Proc) ? @options[:filters][f].call(row[f]) :
|
221
|
+
row[f].send(*@options[:filters][f])
|
222
|
+
else
|
223
|
+
new_row[f] = row[f]
|
224
|
+
end
|
225
|
+
}
|
226
|
+
new_row
|
227
|
+
}
|
228
|
+
end
|
229
|
+
|
230
|
+
def validate_values(rows)
|
231
|
+
rows.each {|row|
|
232
|
+
@fields.each {|f|
|
233
|
+
row[f] = row[f].to_s || ''
|
234
|
+
}
|
235
|
+
}
|
236
|
+
end
|
237
|
+
|
238
|
+
# Converts an array to a hash mapping a numerical index to its array value.
|
239
|
+
def array_to_indices_hash(array)
|
240
|
+
array.inject({}) {|hash,e| hash[hash.size] = e; hash }
|
241
|
+
end
|
242
|
+
#:startdoc:
|
243
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# Base tree class which given an array of nodes produces different types of trees.
|
2
|
+
# The types of trees currently are:
|
3
|
+
# * basic:
|
4
|
+
# 0
|
5
|
+
# 1
|
6
|
+
# 2
|
7
|
+
# 3
|
8
|
+
# 4
|
9
|
+
#
|
10
|
+
# * directory:
|
11
|
+
# 0
|
12
|
+
# |-- 1
|
13
|
+
# | |-- 2
|
14
|
+
# | `-- 3
|
15
|
+
# `-- 4
|
16
|
+
#
|
17
|
+
# * number:
|
18
|
+
# 1. 0
|
19
|
+
# 1. 1
|
20
|
+
# 1. 2
|
21
|
+
# 2. 3
|
22
|
+
# 2. 4
|
23
|
+
#
|
24
|
+
# Tree nodes can be given as an array of arrays or an array of hashes.
|
25
|
+
# To render the above basic tree with an array of hashes:
|
26
|
+
# Hirb::Helpers::Tree.render([{:value=>0, :level=>0}, {:value=>1, :level=>1}, {:value=>2, :level=>2},
|
27
|
+
# {:value=>3, :level=>2}, {:value=>4, :level=>1}])
|
28
|
+
# Note from the hash keys that :level refers to the depth of the tree while :value refers to the text displayed
|
29
|
+
# for a node.
|
30
|
+
#
|
31
|
+
# To render the above basic tree with an array of arrays:
|
32
|
+
# Hirb::Helpers::Tree.render([[0,0], [1,1], [2,2], [2,3], [1,4]])
|
33
|
+
# Note that the each array pair consists of the level and the value for the node.
|
34
|
+
class Hirb::Helpers::Tree
|
35
|
+
class ParentlessNodeError < StandardError; end
|
36
|
+
|
37
|
+
class <<self
|
38
|
+
# Main method which renders a tree.
|
39
|
+
# ==== Options:
|
40
|
+
# [:type] Type of tree. Either :basic, :directory or :number. Default is :basic.
|
41
|
+
# [:validate] Boolean to validate tree. Checks to see if all nodes have parents. Raises ParentlessNodeError if
|
42
|
+
# an invalid node is found. Default is false.
|
43
|
+
# [:indent] Number of spaces to indent between levels for basic + number trees. Default is 4.
|
44
|
+
# [:limit] Limits the level or depth of a tree that is displayed. Root node is level 0.
|
45
|
+
# [:description] Displays brief description about tree ie how many nodes it has.
|
46
|
+
# Examples:
|
47
|
+
# Hirb::Helpers::Tree.render([[0, 'root'], [1, 'child']], :type=>:directory)
|
48
|
+
def render(nodes, options={})
|
49
|
+
new(nodes, options).render
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# :stopdoc:
|
54
|
+
attr_accessor :nodes
|
55
|
+
|
56
|
+
def initialize(input_nodes, options={})
|
57
|
+
@options = options
|
58
|
+
@type = options[:type] || :basic
|
59
|
+
if input_nodes[0].is_a?(Array)
|
60
|
+
@nodes = input_nodes.map {|e| Node.new(:level=>e[0], :value=>e[1]) }
|
61
|
+
else
|
62
|
+
@nodes = input_nodes.map {|e| Node.new(e)}
|
63
|
+
end
|
64
|
+
@nodes.each_with_index {|e,i| e.merge!(:tree=>self, :index=>i)}
|
65
|
+
@nodes.each {|e| e[:value] = e[:value].to_s }
|
66
|
+
validate_nodes if options[:validate]
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def render
|
71
|
+
body = render_tree
|
72
|
+
body += render_description if @options[:description]
|
73
|
+
body
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_description
|
77
|
+
"\n\n#{@nodes.length} #{@nodes.length == 1 ? 'node' : 'nodes'} in tree"
|
78
|
+
end
|
79
|
+
|
80
|
+
def render_tree
|
81
|
+
@indent = ' ' * (@options[:indent] || 4 )
|
82
|
+
@nodes = @nodes.select {|e| e[:level] <= @options[:limit] } if @options[:limit]
|
83
|
+
case @type.to_s
|
84
|
+
when 'directory'
|
85
|
+
render_directory
|
86
|
+
when 'number'
|
87
|
+
render_number
|
88
|
+
else
|
89
|
+
render_basic
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def render_directory
|
94
|
+
mark_last_nodes_per_level
|
95
|
+
new_nodes = []
|
96
|
+
@nodes.each_with_index {|e, i|
|
97
|
+
value = ''
|
98
|
+
unless e.root?
|
99
|
+
value << e.render_parent_characters
|
100
|
+
value << (e[:last_node] ? "`-- " : "|-- ")
|
101
|
+
end
|
102
|
+
value << e[:value]
|
103
|
+
new_nodes << value
|
104
|
+
}
|
105
|
+
new_nodes.join("\n")
|
106
|
+
end
|
107
|
+
|
108
|
+
def render_number
|
109
|
+
counter = {}
|
110
|
+
@nodes.each {|e|
|
111
|
+
parent_level_key = "#{(e.parent ||{})[:index]}.#{e[:level]}"
|
112
|
+
counter[parent_level_key] ||= 0
|
113
|
+
counter[parent_level_key] += 1
|
114
|
+
e[:pre_value] = "#{counter[parent_level_key]}. "
|
115
|
+
}
|
116
|
+
@nodes.map {|e| @indent * e[:level] + e[:pre_value] + e[:value]}.join("\n")
|
117
|
+
end
|
118
|
+
|
119
|
+
def render_basic
|
120
|
+
@nodes.map {|e| @indent * e[:level] + e[:value]}.join("\n")
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_nodes
|
124
|
+
@nodes.each do |e|
|
125
|
+
raise ParentlessNodeError if (e[:level] > e.previous[:level]) && (e[:level] - e.previous[:level]) > 1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# walks tree accumulating last nodes per unique parent+level
|
130
|
+
def mark_last_nodes_per_level
|
131
|
+
@nodes.each {|e| e.delete(:last_node)}
|
132
|
+
last_node_hash = @nodes.inject({}) {|h,e|
|
133
|
+
h["#{(e.parent ||{})[:index]}.#{e[:level]}"] = e; h
|
134
|
+
}
|
135
|
+
last_node_hash.values.uniq.each {|e| e[:last_node] = true}
|
136
|
+
end
|
137
|
+
#:startdoc:
|
138
|
+
class Node < ::Hash #:nodoc:
|
139
|
+
class MissingLevelError < StandardError; end
|
140
|
+
class MissingValueError < StandardError; end
|
141
|
+
|
142
|
+
def initialize(hash)
|
143
|
+
super
|
144
|
+
raise MissingLevelError unless hash.has_key?(:level)
|
145
|
+
raise MissingValueError unless hash.has_key?(:value)
|
146
|
+
replace(hash)
|
147
|
+
end
|
148
|
+
|
149
|
+
def parent
|
150
|
+
self[:tree].nodes.slice(0 .. self[:index]).reverse.detect {|e| e[:level] < self[:level]}
|
151
|
+
end
|
152
|
+
|
153
|
+
def next
|
154
|
+
self[:tree].nodes[self[:index] + 1]
|
155
|
+
end
|
156
|
+
|
157
|
+
def previous
|
158
|
+
self[:tree].nodes[self[:index] - 1]
|
159
|
+
end
|
160
|
+
|
161
|
+
def root?; self[:level] == 0; end
|
162
|
+
|
163
|
+
# refers to characters which connect parent nodes
|
164
|
+
def render_parent_characters
|
165
|
+
parent_chars = []
|
166
|
+
get_parents_character(parent_chars)
|
167
|
+
parent_chars.reverse.map {|level| level + ' ' * 3 }.to_s
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_parents_character(parent_chars)
|
171
|
+
if self.parent
|
172
|
+
parent_chars << (self.parent[:last_node] ? ' ' : '|') unless self.parent.root?
|
173
|
+
self.parent.get_parents_character(parent_chars)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|