Chrononaut-hirb 0.2.1
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.
- 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
|