hsume2-hirb 0.6.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemspec +21 -0
- data/CHANGELOG.rdoc +144 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +194 -0
- data/Rakefile +35 -0
- data/lib/bond/completions/hirb.rb +15 -0
- data/lib/hirb/console.rb +43 -0
- data/lib/hirb/dynamic_view.rb +113 -0
- data/lib/hirb/formatter.rb +126 -0
- data/lib/hirb/helpers/auto_table.rb +24 -0
- data/lib/hirb/helpers/object_table.rb +14 -0
- data/lib/hirb/helpers/parent_child_tree.rb +24 -0
- data/lib/hirb/helpers/tab_table.rb +24 -0
- data/lib/hirb/helpers/table/filters.rb +10 -0
- data/lib/hirb/helpers/table/resizer.rb +82 -0
- data/lib/hirb/helpers/table.rb +349 -0
- data/lib/hirb/helpers/tree.rb +181 -0
- data/lib/hirb/helpers/unicode_table.rb +15 -0
- data/lib/hirb/helpers/vertical_table.rb +37 -0
- data/lib/hirb/helpers.rb +18 -0
- data/lib/hirb/import_object.rb +10 -0
- data/lib/hirb/menu.rb +238 -0
- data/lib/hirb/pager.rb +105 -0
- data/lib/hirb/string.rb +44 -0
- data/lib/hirb/util.rb +96 -0
- data/lib/hirb/version.rb +3 -0
- data/lib/hirb/view.rb +270 -0
- data/lib/hirb/views/couch_db.rb +11 -0
- data/lib/hirb/views/misc_db.rb +15 -0
- data/lib/hirb/views/mongo_db.rb +14 -0
- data/lib/hirb/views/orm.rb +11 -0
- data/lib/hirb/views/rails.rb +19 -0
- data/lib/hirb/views.rb +8 -0
- data/lib/hirb.rb +82 -0
- data/lib/ripl/hirb.rb +15 -0
- data/test/auto_table_test.rb +30 -0
- data/test/console_test.rb +27 -0
- data/test/deps.rip +4 -0
- data/test/dynamic_view_test.rb +94 -0
- data/test/formatter_test.rb +176 -0
- data/test/hirb_test.rb +39 -0
- data/test/import_test.rb +9 -0
- data/test/menu_test.rb +255 -0
- data/test/object_table_test.rb +79 -0
- data/test/pager_test.rb +162 -0
- data/test/resizer_test.rb +62 -0
- data/test/table_test.rb +630 -0
- data/test/test_helper.rb +61 -0
- data/test/tree_test.rb +184 -0
- data/test/util_test.rb +59 -0
- data/test/view_test.rb +165 -0
- data/test/views_test.rb +13 -0
- metadata +184 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
module Hirb
|
2
|
+
# A Formatter object formats an output object (using Formatter.format_output) into a string based on the views defined
|
3
|
+
# for its class and/or ancestry.
|
4
|
+
class Formatter
|
5
|
+
class<<self
|
6
|
+
# This config is used by Formatter.format_output to lazily load dynamic views defined with Hirb::DynamicView.
|
7
|
+
# This hash has the same format as Formatter.config.
|
8
|
+
attr_accessor :dynamic_config
|
9
|
+
|
10
|
+
# Array of classes whose objects respond to :to_a and allow the first
|
11
|
+
# element of the converted array to determine the output class.
|
12
|
+
attr_accessor :to_a_classes
|
13
|
+
end
|
14
|
+
self.dynamic_config = {}
|
15
|
+
self.to_a_classes = %w{Array Set ActiveRecord::Relation}
|
16
|
+
|
17
|
+
def initialize(additional_config={}) #:nodoc:
|
18
|
+
@klass_config = {}
|
19
|
+
@config = additional_config || {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# A hash of Ruby class strings mapped to view hashes. A view hash must have at least a :method, :output_method
|
23
|
+
# or :class option for a view to be applied to an output. A view hash has the following keys:
|
24
|
+
# [*:method*] Specifies a global (Kernel) method to do the formatting.
|
25
|
+
# [*:class*] Specifies a class to do the formatting, using its render() class method. If a symbol it's converted to a corresponding
|
26
|
+
# Hirb::Helpers::* class if it exists.
|
27
|
+
# [*:output_method*] Specifies a method or proc to call on output before passing it to a helper. If the output is an array, it's applied
|
28
|
+
# to every element in the array.
|
29
|
+
# [*:options*] Options to pass the helper method or class.
|
30
|
+
# [*:ancestor*] Boolean which when true causes subclasses of the output class to inherit its config. This doesn't effect the current
|
31
|
+
# output class. Defaults to false. This is used by ActiveRecord classes.
|
32
|
+
#
|
33
|
+
# Examples:
|
34
|
+
# {'WWW::Delicious::Element'=>{:class=>'Hirb::Helpers::ObjectTable', :ancestor=>true, :options=>{:max_width=>180}}}
|
35
|
+
# {'Date'=>{:class=>:auto_table, :ancestor=>true}}
|
36
|
+
# {'Hash'=>{:method=>:puts}}
|
37
|
+
def config
|
38
|
+
@config
|
39
|
+
end
|
40
|
+
|
41
|
+
# Adds the view for the given class and view hash config. See Formatter.config for valid keys for view hash.
|
42
|
+
def add_view(klass, view_config)
|
43
|
+
@klass_config.delete(klass)
|
44
|
+
@config[klass.to_s] = view_config
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
# This method looks for an output object's view in Formatter.config and then Formatter.dynamic_config.
|
49
|
+
# If a view is found, a stringified view is returned based on the object. If no view is found, nil is returned. The options this
|
50
|
+
# class takes are a view hash as described in Formatter.config. These options will be merged with any existing helper
|
51
|
+
# config hash an output class has in Formatter.config. Any block given is passed along to a helper class.
|
52
|
+
def format_output(output, options={}, &block)
|
53
|
+
output_class = determine_output_class(output)
|
54
|
+
options = parse_console_options(options) if options.delete(:console)
|
55
|
+
options = Util.recursive_hash_merge(klass_config(output_class), options)
|
56
|
+
_format_output(output, options, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
#:stopdoc:
|
60
|
+
def to_a_classes
|
61
|
+
@to_a_classes ||= self.class.to_a_classes.map {|e| Util.any_const_get(e) }.compact
|
62
|
+
end
|
63
|
+
|
64
|
+
def _format_output(output, options, &block)
|
65
|
+
output = options[:output_method] ? (output.is_a?(Array) ?
|
66
|
+
output.map {|e| call_output_method(options[:output_method], e) } :
|
67
|
+
call_output_method(options[:output_method], output) ) : output
|
68
|
+
args = [output]
|
69
|
+
args << options[:options] if options[:options] && !options[:options].empty?
|
70
|
+
if options[:method]
|
71
|
+
send(options[:method],*args)
|
72
|
+
elsif options[:class] && (helper_class = Helpers.helper_class(options[:class]))
|
73
|
+
helper_class.render(*args, &block)
|
74
|
+
elsif options[:output_method]
|
75
|
+
output
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse_console_options(options) #:nodoc:
|
80
|
+
real_options = [:method, :class, :output_method].inject({}) do |h, e|
|
81
|
+
h[e] = options.delete(e) if options[e]; h
|
82
|
+
end
|
83
|
+
real_options.merge! :options=>options
|
84
|
+
end
|
85
|
+
|
86
|
+
def determine_output_class(output)
|
87
|
+
output.respond_to?(:to_a) && to_a_classes.any? {|e| output.is_a?(e) } ?
|
88
|
+
Array(output)[0].class : output.class
|
89
|
+
end
|
90
|
+
|
91
|
+
def call_output_method(output_method, output)
|
92
|
+
output_method.is_a?(Proc) ? output_method.call(output) : output.send(output_method)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Internal view options built from user-defined ones. Options are built by recursively merging options from oldest
|
96
|
+
# ancestors to the most recent ones.
|
97
|
+
def klass_config(output_class)
|
98
|
+
@klass_config[output_class] ||= build_klass_config(output_class)
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_klass_config(output_class)
|
102
|
+
output_ancestors = output_class.ancestors.map {|e| e.to_s}.reverse
|
103
|
+
output_ancestors.pop
|
104
|
+
hash = output_ancestors.inject({}) {|h, klass|
|
105
|
+
add_klass_config_if_true(h, klass) {|c,klass| c[klass] && c[klass][:ancestor] }
|
106
|
+
}
|
107
|
+
add_klass_config_if_true(hash, output_class.to_s) {|c,klass| c[klass] }
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_klass_config_if_true(hash, klass)
|
111
|
+
if yield(@config, klass)
|
112
|
+
Util.recursive_hash_merge hash, @config[klass]
|
113
|
+
elsif yield(self.class.dynamic_config, klass)
|
114
|
+
@config[klass] = self.class.dynamic_config[klass].dup # copy to local
|
115
|
+
Util.recursive_hash_merge hash, self.class.dynamic_config[klass]
|
116
|
+
else
|
117
|
+
hash
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def reset_klass_config
|
122
|
+
@klass_config = {}
|
123
|
+
end
|
124
|
+
#:startdoc:
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# This helper wraps around the other table helpers i.e. Hirb::Helpers::Table while
|
2
|
+
# providing default helper options via Hirb::DynamicView. Using these default options, this
|
3
|
+
# helper supports views for the following modules/classes:
|
4
|
+
# ActiveRecord::Base, CouchFoo::Base, CouchPotato::Persistence, CouchRest::ExtendedDocument,
|
5
|
+
# DBI::Row, DataMapper::Resource, Friendly::Document, MongoMapper::Document, MongoMapper::EmbeddedDocument,
|
6
|
+
# Mongoid::Document, Ripple::Document, Sequel::Model.
|
7
|
+
class Hirb::Helpers::AutoTable < Hirb::Helpers::Table
|
8
|
+
extend Hirb::DynamicView
|
9
|
+
|
10
|
+
# Takes same options as Hirb::Helpers::Table.render except as noted below.
|
11
|
+
#
|
12
|
+
# ==== Options:
|
13
|
+
# [:table_class] Explicit table class to use for rendering. Defaults to
|
14
|
+
# Hirb::Helpers::ObjectTable if output is not an Array or Hash. Otherwise
|
15
|
+
# defaults to Hirb::Helpers::Table.
|
16
|
+
def self.render(output, options={})
|
17
|
+
output = Array(output)
|
18
|
+
(defaults = dynamic_options(output[0])) && (options = defaults.merge(options))
|
19
|
+
klass = options.delete(:table_class) || (
|
20
|
+
!(output[0].is_a?(Hash) || output[0].is_a?(Array)) ?
|
21
|
+
Hirb::Helpers::ObjectTable : Hirb::Helpers::Table)
|
22
|
+
klass.render(output, options)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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
|
+
item_hashes = options[:fields].empty? ? [] : Array(rows).inject([]) {|t,item|
|
10
|
+
t << options[:fields].inject({}) {|h,f| h[f] = item.__send__(f); h}
|
11
|
+
}
|
12
|
+
super(item_hashes, options)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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 or proc 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 or proc 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
|
+
@value_method = value_method.is_a?(Proc) ? value_method : lambda {|n| n.send(value_method) }
|
12
|
+
children_method = options[:children_method] || :children
|
13
|
+
@children_method = children_method.is_a?(Proc) ? children_method : lambda {|n| n.send(children_method)}
|
14
|
+
@nodes = []
|
15
|
+
build_node(root_node, 0)
|
16
|
+
super(@nodes, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_node(node, level) #:nodoc:
|
20
|
+
@nodes << {:value=>@value_method.call(node), :level=>level}
|
21
|
+
@children_method.call(node).each {|e| build_node(e, level + 1)}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Hirb::Helpers::TabTable < Hirb::Helpers::Table
|
2
|
+
DELIM = "\t"
|
3
|
+
|
4
|
+
# Renders a tab-delimited table
|
5
|
+
def self.render(rows, options={})
|
6
|
+
new(rows, {:description => false}.merge(options)).render
|
7
|
+
end
|
8
|
+
|
9
|
+
def render_header
|
10
|
+
@headers ? render_table_header : []
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_table_header
|
14
|
+
[ format_values(@headers).join(DELIM) ]
|
15
|
+
end
|
16
|
+
|
17
|
+
def render_rows
|
18
|
+
@rows.map { |row| format_values(row).join(DELIM) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_footer
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class Hirb::Helpers::Table
|
2
|
+
# Contains filter methods used by :filters option. To define a custom filter, simply open this module and create a method
|
3
|
+
# that take one argument, the value you will be filtering.
|
4
|
+
module Filters
|
5
|
+
extend self
|
6
|
+
def comma_join(arr) #:nodoc:
|
7
|
+
arr.join(', ')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class Hirb::Helpers::Table
|
2
|
+
# Resizes a table's fields to the table's max width.
|
3
|
+
class Resizer
|
4
|
+
# Modifies field_lengths to fit within width. Also enforces a table's max_fields.
|
5
|
+
def self.resize!(table)
|
6
|
+
obj = new(table)
|
7
|
+
obj.resize
|
8
|
+
obj.field_lengths
|
9
|
+
end
|
10
|
+
|
11
|
+
#:stopdoc:
|
12
|
+
attr_reader :field_lengths
|
13
|
+
def initialize(table)
|
14
|
+
@table, @width, @field_size = table, table.actual_width, table.fields.size
|
15
|
+
@field_lengths = table.field_lengths
|
16
|
+
@original_field_lengths = @field_lengths.dup
|
17
|
+
end
|
18
|
+
|
19
|
+
def resize
|
20
|
+
adjust_long_fields || default_restrict_field_lengths
|
21
|
+
@table.enforce_field_constraints
|
22
|
+
add_extra_width
|
23
|
+
end
|
24
|
+
|
25
|
+
# Simple algorithm which allows smaller fields to be displayed while
|
26
|
+
# restricting longer fields to an average_long_field
|
27
|
+
def adjust_long_fields
|
28
|
+
while (total_length = sum(@field_lengths.values)) > @width
|
29
|
+
average_field = total_length / @field_size.to_f
|
30
|
+
long_lengths = @field_lengths.values.select {|e| e > average_field }
|
31
|
+
return false if long_lengths.empty?
|
32
|
+
|
33
|
+
# adjusts average long field by ratio with @width
|
34
|
+
average_long_field = sum(long_lengths)/long_lengths.size * @width/total_length
|
35
|
+
@field_lengths.each {|f,length|
|
36
|
+
@field_lengths[f] = average_long_field if length > average_long_field
|
37
|
+
}
|
38
|
+
end
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
# Produces a field_lengths which meets the @width requirement
|
43
|
+
def default_restrict_field_lengths
|
44
|
+
original_total_length = sum @original_field_lengths.values
|
45
|
+
# set fields by their relative weight to original length
|
46
|
+
new_lengths = @original_field_lengths.inject({}) {|t,(k,v)|
|
47
|
+
t[k] = (v / original_total_length.to_f * @width).to_i; t }
|
48
|
+
|
49
|
+
# set all fields the same if relative doesn't work
|
50
|
+
unless new_lengths.values.all? {|e| e > MIN_FIELD_LENGTH} && (sum(new_lengths.values) <= @width)
|
51
|
+
new_lengths = @field_lengths.inject({}) {|t,(k,v)| t[k] = @width / @field_size; t }
|
52
|
+
end
|
53
|
+
@field_lengths.each {|k,v| @field_lengths[k] = new_lengths[k] }
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_extra_width
|
57
|
+
added_width = 0
|
58
|
+
extra_width = @width - sum(@field_lengths.values)
|
59
|
+
unmaxed_fields = @field_lengths.keys.select {|f| !remaining_width(f).zero? }
|
60
|
+
# order can affect which one gets the remainder so let's keep it consistent
|
61
|
+
unmaxed_fields = unmaxed_fields.sort_by {|e| e.to_s}
|
62
|
+
|
63
|
+
unmaxed_fields.each_with_index do |f, i|
|
64
|
+
extra_per_field = (extra_width - added_width) / (unmaxed_fields.size - i)
|
65
|
+
add_to_field = remaining_width(f) < extra_per_field ? remaining_width(f) : extra_per_field
|
66
|
+
added_width += add_to_field
|
67
|
+
@field_lengths[f] += add_to_field
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def remaining_width(field)
|
72
|
+
(@remaining_width ||= {})[field] ||= begin
|
73
|
+
(@table.max_fields[field] || @original_field_lengths[field]) - @field_lengths[field]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def sum(arr)
|
78
|
+
arr.inject {|t,e| t += e } || 0
|
79
|
+
end
|
80
|
+
#:startdoc:
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'hirb/helpers/table/filters'
|
3
|
+
require 'hirb/helpers/table/resizer'
|
4
|
+
|
5
|
+
module Hirb
|
6
|
+
# Base Table class from which other table classes inherit.
|
7
|
+
# By default, a table is constrained to a default width but this can be adjusted
|
8
|
+
# via the max_width option or Hirb::View.width.
|
9
|
+
# Rows can be an array of arrays or an array of hashes.
|
10
|
+
#
|
11
|
+
# An array of arrays ie [[1,2], [2,3]], would render:
|
12
|
+
# +---+---+
|
13
|
+
# | 0 | 1 |
|
14
|
+
# +---+---+
|
15
|
+
# | 1 | 2 |
|
16
|
+
# | 2 | 3 |
|
17
|
+
# +---+---+
|
18
|
+
#
|
19
|
+
# By default, the fields/columns are the numerical indices of the array.
|
20
|
+
#
|
21
|
+
# An array of hashes ie [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], would render:
|
22
|
+
# +-----+--------+
|
23
|
+
# | age | weight |
|
24
|
+
# +-----+--------+
|
25
|
+
# | 10 | 100 |
|
26
|
+
# | 80 | 500 |
|
27
|
+
# +-----+--------+
|
28
|
+
#
|
29
|
+
# By default, the fields/columns are the keys of the first hash.
|
30
|
+
#
|
31
|
+
# === Custom Callbacks
|
32
|
+
# Callback methods can be defined to add your own options that modify rows right before they are rendered.
|
33
|
+
# Here's an example that allows for searching with a :query option:
|
34
|
+
# module Query
|
35
|
+
# # Searches fields given a query hash
|
36
|
+
# def query_callback(rows, options)
|
37
|
+
# return rows unless options[:query]
|
38
|
+
# options[:query].map {|field,query|
|
39
|
+
# rows.select {|e| e[field].to_s =~ /#{query}/i }
|
40
|
+
# }.flatten.uniq
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# Hirb::Helpers::Table.send :include, Query
|
44
|
+
#
|
45
|
+
# >> puts Hirb::Helpers::Table.render [{:name=>'batman'}, {:name=>'robin'}], :query=>{:name=>'rob'}
|
46
|
+
# +-------+
|
47
|
+
# | name |
|
48
|
+
# +-------+
|
49
|
+
# | robin |
|
50
|
+
# +-------+
|
51
|
+
# 1 row in set
|
52
|
+
#
|
53
|
+
# Callback methods:
|
54
|
+
# * must be defined in Helpers::Table and end in '_callback'.
|
55
|
+
# * should expect rows and a hash of render options. Rows will be an array of hashes.
|
56
|
+
# * are expected to return an array of hashes.
|
57
|
+
# * are invoked in alphabetical order.
|
58
|
+
# For a thorough example, see {Boson::Pipe}[http://github.com/cldwalker/boson/blob/master/lib/boson/pipe.rb].
|
59
|
+
#--
|
60
|
+
# derived from http://gist.github.com/72234
|
61
|
+
class Helpers::Table
|
62
|
+
BORDER_LENGTH = 3 # " | " and "-+-" are the borders
|
63
|
+
MIN_FIELD_LENGTH = 3
|
64
|
+
class TooManyFieldsForWidthError < StandardError; end
|
65
|
+
|
66
|
+
CHARS = {
|
67
|
+
:top => {:left => '+', :center => '+', :right => '+', :horizontal => '-',
|
68
|
+
:vertical => {:outside => '|', :inside => '|'} },
|
69
|
+
:middle => {:left => '+', :center => '+', :right => '+', :horizontal => '-'},
|
70
|
+
:bottom => {:left => '+', :center => '+', :right => '+', :horizontal => '-',
|
71
|
+
:vertical => {:outside => '|', :inside => '|'} }
|
72
|
+
}
|
73
|
+
|
74
|
+
class << self
|
75
|
+
|
76
|
+
# Main method which returns a formatted table.
|
77
|
+
# ==== Options:
|
78
|
+
# [*:fields*] An array which overrides the default fields and can be used to indicate field order.
|
79
|
+
# [*:headers*] A hash of fields and their header names. Fields that aren't specified here default to their name.
|
80
|
+
# When set to false, headers are hidden. Can also be an array but only for array rows.
|
81
|
+
# [*:max_fields*] A hash of fields and their maximum allowed lengths. Maximum length can also be a percentage of the total width
|
82
|
+
# (decimal less than one). When a field exceeds it's maximum then it's
|
83
|
+
# truncated and has a ... appended to it. Fields that aren't specified have no maximum.
|
84
|
+
# [*:max_width*] The maximum allowed width of all fields put together including field borders. Only valid when :resize is true.
|
85
|
+
# Default is Hirb::View.width.
|
86
|
+
# [*:resize*] Resizes table to display all columns in allowed :max_width. Default is true. Setting this false will display the full
|
87
|
+
# length of each field.
|
88
|
+
# [*:number*] When set to true, numbers rows by adding a :hirb_number column as the first column. Default is false.
|
89
|
+
# [*:change_fields*] A hash to change old field names to new field names. This can also be an array of new names but only for array rows.
|
90
|
+
# This is useful when wanting to change auto-generated keys to more user-friendly names i.e. for array rows.
|
91
|
+
# [*:grep_fields*] A regexp that selects which fields to display. By default this is not set and applied.
|
92
|
+
# [*:filters*] A hash of fields and their filters, applied to every row in a field. A filter can be a proc, an instance method
|
93
|
+
# applied to the field value or a Filters method. Also see the filter_classes attribute below.
|
94
|
+
# [*:header_filter*] A filter, like one in :filters, that is applied to all headers after the :headers option.
|
95
|
+
# [*:filter_any*] When set to true, any cell defaults to being filtered by its class in :filter_classes.
|
96
|
+
# Default Hirb::Helpers::Table.filter_any().
|
97
|
+
# [*:filter_classes*] Hash which maps classes to filters. Default is Hirb::Helpers::Table.filter_classes().
|
98
|
+
# [*:vertical*] When set to true, renders a vertical table using Hirb::Helpers::VerticalTable. Default is false.
|
99
|
+
# [*:unicode*] When set to true, renders a unicode table using Hirb::Helpers::UnicodeTable. Default is false.
|
100
|
+
# [*:tab*] When set to true, renders a tab-delimited table using Hirb::Helpers::TabTable. Default is false.
|
101
|
+
# [*:all_fields*] When set to true, renders fields in all rows. Valid only in rows that are hashes. Default is false.
|
102
|
+
# [*:description*] When set to true, renders row count description at bottom. Default is true.
|
103
|
+
# [*:escape_special_chars*] When set to true, escapes special characters \n,\t,\r so they don't disrupt tables. Default is false for
|
104
|
+
# vertical tables and true for anything else.
|
105
|
+
# Examples:
|
106
|
+
# Hirb::Helpers::Table.render [[1,2], [2,3]]
|
107
|
+
# Hirb::Helpers::Table.render [[1,2], [2,3]], :max_fields=>{0=>10}, :header_filter=>:capitalize
|
108
|
+
# Hirb::Helpers::Table.render [['a',1], ['b',2]], :change_fields=>%w{letters numbers}, :max_fields=>{'numbers'=>0.4}
|
109
|
+
# Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}]
|
110
|
+
# Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :headers=>{:weight=>"Weight(lbs)"}
|
111
|
+
# Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :filters=>{:age=>[:to_f]}
|
112
|
+
def render(rows, options={})
|
113
|
+
options[:vertical] ? Helpers::VerticalTable.render(rows, options) :
|
114
|
+
options[:unicode] ? Helpers::UnicodeTable.render(rows, options) :
|
115
|
+
options[:tab] ? Helpers::TabTable.render(rows, options) :
|
116
|
+
new(rows, options).render
|
117
|
+
rescue TooManyFieldsForWidthError
|
118
|
+
$stderr.puts "", "** Hirb Warning: Too many fields for the current width. Configure your width " +
|
119
|
+
"and/or fields to avoid this error. Defaulting to a vertical table. **"
|
120
|
+
Helpers::VerticalTable.render(rows, options)
|
121
|
+
end
|
122
|
+
|
123
|
+
# A hash which maps a cell value's class to a filter. This serves to set a default filter per field if all of its
|
124
|
+
# values are a class in this hash. By default, Array values are comma joined and Hashes are inspected.
|
125
|
+
# See the :filter_any option to apply this filter per value.
|
126
|
+
attr_accessor :filter_classes
|
127
|
+
# Boolean which sets the default for :filter_any option.
|
128
|
+
attr_accessor :filter_any
|
129
|
+
# Holds last table object created
|
130
|
+
attr_accessor :last_table
|
131
|
+
end
|
132
|
+
self.filter_classes = { Array=>:comma_join, Hash=>:inspect }
|
133
|
+
|
134
|
+
def chars
|
135
|
+
self.class.const_get(:CHARS)
|
136
|
+
end
|
137
|
+
|
138
|
+
#:stopdoc:
|
139
|
+
attr_accessor :width, :max_fields, :field_lengths, :fields
|
140
|
+
def initialize(rows, options={})
|
141
|
+
raise ArgumentError, "Table must be an array of hashes or array of arrays" unless rows.is_a?(Array) &&
|
142
|
+
(rows[0].is_a?(Hash) or rows[0].is_a?(Array) or rows.empty?)
|
143
|
+
@options = {:description=>true, :filters=>{}, :change_fields=>{}, :escape_special_chars=>true,
|
144
|
+
:filter_any=>Helpers::Table.filter_any, :resize=>true}.merge(options)
|
145
|
+
@fields = set_fields(rows)
|
146
|
+
@fields = @fields.select {|e| e.to_s[@options[:grep_fields]] } if @options[:grep_fields]
|
147
|
+
@rows = set_rows(rows)
|
148
|
+
@headers = set_headers
|
149
|
+
if @options[:number]
|
150
|
+
@headers[:hirb_number] ||= "number"
|
151
|
+
@fields.unshift :hirb_number
|
152
|
+
end
|
153
|
+
Helpers::Table.last_table = self
|
154
|
+
end
|
155
|
+
|
156
|
+
def set_fields(rows)
|
157
|
+
@options[:change_fields] = array_to_indices_hash(@options[:change_fields]) if @options[:change_fields].is_a?(Array)
|
158
|
+
return @options[:fields].dup if @options[:fields]
|
159
|
+
|
160
|
+
fields = if rows[0].is_a?(Hash)
|
161
|
+
keys = @options[:all_fields] ? rows.map {|e| e.keys}.flatten.uniq : rows[0].keys
|
162
|
+
keys.sort {|a,b| a.to_s <=> b.to_s}
|
163
|
+
else
|
164
|
+
rows[0].is_a?(Array) ? (0..rows[0].length - 1).to_a : []
|
165
|
+
end
|
166
|
+
|
167
|
+
@options[:change_fields].each do |oldf, newf|
|
168
|
+
(index = fields.index(oldf)) && fields[index] = newf
|
169
|
+
end
|
170
|
+
fields
|
171
|
+
end
|
172
|
+
|
173
|
+
def set_rows(rows)
|
174
|
+
rows = Array(rows)
|
175
|
+
if rows[0].is_a?(Array)
|
176
|
+
rows = rows.inject([]) {|new_rows, row|
|
177
|
+
new_rows << array_to_indices_hash(row)
|
178
|
+
}
|
179
|
+
end
|
180
|
+
@options[:change_fields].each do |oldf, newf|
|
181
|
+
rows.each {|e| e[newf] = e.delete(oldf) if e.key?(oldf) }
|
182
|
+
end
|
183
|
+
rows = filter_values(rows)
|
184
|
+
rows.each_with_index {|e,i| e[:hirb_number] = (i + 1).to_s} if @options[:number]
|
185
|
+
deleted_callbacks = Array(@options[:delete_callbacks]).map {|e| "#{e}_callback" }
|
186
|
+
(methods.grep(/_callback$/).map {|e| e.to_s} - deleted_callbacks).sort.each do |meth|
|
187
|
+
rows = send(meth, rows, @options.dup)
|
188
|
+
end
|
189
|
+
validate_values(rows)
|
190
|
+
rows
|
191
|
+
end
|
192
|
+
|
193
|
+
def set_headers
|
194
|
+
headers = @fields.inject({}) {|h,e| h[e] = e.to_s; h}
|
195
|
+
if @options.has_key?(:headers)
|
196
|
+
headers = @options[:headers].is_a?(Hash) ? headers.merge(@options[:headers]) :
|
197
|
+
(@options[:headers].is_a?(Array) ? array_to_indices_hash(@options[:headers]) : @options[:headers])
|
198
|
+
end
|
199
|
+
if @options[:header_filter]
|
200
|
+
headers.each {|k,v|
|
201
|
+
headers[k] = call_filter(@options[:header_filter], v)
|
202
|
+
}
|
203
|
+
end
|
204
|
+
headers
|
205
|
+
end
|
206
|
+
|
207
|
+
def render
|
208
|
+
body = []
|
209
|
+
unless @rows.length == 0
|
210
|
+
setup_field_lengths
|
211
|
+
body += render_header
|
212
|
+
body += render_rows
|
213
|
+
body += render_footer
|
214
|
+
end
|
215
|
+
body << render_table_description if @options[:description]
|
216
|
+
body.join("\n")
|
217
|
+
end
|
218
|
+
|
219
|
+
def render_header
|
220
|
+
@headers ? render_table_header : [render_border(:top)]
|
221
|
+
end
|
222
|
+
|
223
|
+
def render_footer
|
224
|
+
[render_border(:bottom)]
|
225
|
+
end
|
226
|
+
|
227
|
+
def render_table_header
|
228
|
+
title_row = chars[:top][:vertical][:outside] + ' ' +
|
229
|
+
format_values(@headers).join(' ' + chars[:top][:vertical][:inside] +' ') +
|
230
|
+
' ' + chars[:top][:vertical][:outside]
|
231
|
+
[render_border(:top), title_row, render_border(:middle)]
|
232
|
+
end
|
233
|
+
|
234
|
+
def render_border(which)
|
235
|
+
chars[which][:left] + chars[which][:horizontal] +
|
236
|
+
@fields.map {|f| chars[which][:horizontal] * @field_lengths[f] }.
|
237
|
+
join(chars[which][:horizontal] + chars[which][:center] + chars[which][:horizontal]) +
|
238
|
+
chars[which][:horizontal] + chars[which][:right]
|
239
|
+
end
|
240
|
+
|
241
|
+
def format_values(values)
|
242
|
+
@fields.map {|field| format_cell(values[field], @field_lengths[field]) }
|
243
|
+
end
|
244
|
+
|
245
|
+
def format_cell(value, cell_width)
|
246
|
+
text = String.size(value) > cell_width ?
|
247
|
+
(
|
248
|
+
(cell_width < 5) ? String.slice(value, 0, cell_width) : String.slice(value, 0, cell_width - 3) + '...'
|
249
|
+
) : value
|
250
|
+
String.ljust(text, cell_width)
|
251
|
+
end
|
252
|
+
|
253
|
+
def render_rows
|
254
|
+
@rows.map do |row|
|
255
|
+
chars[:bottom][:vertical][:outside] + ' ' +
|
256
|
+
format_values(row).join(' ' + chars[:bottom][:vertical][:inside] + ' ') +
|
257
|
+
' ' + chars[:bottom][:vertical][:outside]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def render_table_description
|
262
|
+
(@rows.length == 0) ? "0 rows in set" :
|
263
|
+
"#{@rows.length} #{@rows.length == 1 ? 'row' : 'rows'} in set"
|
264
|
+
end
|
265
|
+
|
266
|
+
def setup_field_lengths
|
267
|
+
@field_lengths = default_field_lengths
|
268
|
+
if @options[:resize]
|
269
|
+
raise TooManyFieldsForWidthError if @fields.size > self.actual_width.to_f / MIN_FIELD_LENGTH
|
270
|
+
Resizer.resize!(self)
|
271
|
+
else
|
272
|
+
enforce_field_constraints
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def enforce_field_constraints
|
277
|
+
max_fields.each {|k,max| @field_lengths[k] = max if @field_lengths[k].to_i > max }
|
278
|
+
end
|
279
|
+
|
280
|
+
def max_fields
|
281
|
+
@max_fields ||= (@options[:max_fields] ||= {}).each {|k,v|
|
282
|
+
@options[:max_fields][k] = (actual_width * v.to_f.abs).floor if v.to_f.abs < 1
|
283
|
+
}
|
284
|
+
end
|
285
|
+
|
286
|
+
def actual_width
|
287
|
+
@actual_width ||= self.width - (@fields.size * BORDER_LENGTH + 1)
|
288
|
+
end
|
289
|
+
|
290
|
+
def width
|
291
|
+
@width ||= @options[:max_width] || View.width
|
292
|
+
end
|
293
|
+
|
294
|
+
# find max length for each field; start with the headers
|
295
|
+
def default_field_lengths
|
296
|
+
field_lengths = @headers ? @headers.inject({}) {|h,(k,v)| h[k] = String.size(v); h} :
|
297
|
+
@fields.inject({}) {|h,e| h[e] = 1; h }
|
298
|
+
@rows.each do |row|
|
299
|
+
@fields.each do |field|
|
300
|
+
len = String.size(row[field])
|
301
|
+
field_lengths[field] = len if len > field_lengths[field].to_i
|
302
|
+
end
|
303
|
+
end
|
304
|
+
field_lengths
|
305
|
+
end
|
306
|
+
|
307
|
+
def set_filter_defaults(rows)
|
308
|
+
@filter_classes.each do |klass, filter|
|
309
|
+
@fields.each {|field|
|
310
|
+
if rows.all? {|r| r[field].class == klass }
|
311
|
+
@options[:filters][field] ||= filter
|
312
|
+
end
|
313
|
+
}
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def filter_values(rows)
|
318
|
+
@filter_classes = Helpers::Table.filter_classes.merge @options[:filter_classes] || {}
|
319
|
+
set_filter_defaults(rows) unless @options[:filter_any]
|
320
|
+
rows.map {|row|
|
321
|
+
@fields.inject({}) {|new_row,f|
|
322
|
+
(filter = @options[:filters][f]) || (@options[:filter_any] && (filter = @filter_classes[row[f].class]))
|
323
|
+
new_row[f] = filter ? call_filter(filter, row[f]) : row[f]
|
324
|
+
new_row
|
325
|
+
}
|
326
|
+
}
|
327
|
+
end
|
328
|
+
|
329
|
+
def call_filter(filter, val)
|
330
|
+
filter.is_a?(Proc) ? filter.call(val) :
|
331
|
+
val.respond_to?(Array(filter)[0]) ? val.send(*filter) : Filters.send(filter, val)
|
332
|
+
end
|
333
|
+
|
334
|
+
def validate_values(rows)
|
335
|
+
rows.each {|row|
|
336
|
+
@fields.each {|f|
|
337
|
+
row[f] = row[f].to_s || ''
|
338
|
+
row[f] = row[f].gsub(/(\t|\r|\n)/) {|e| e.dump.gsub('"','') } if @options[:escape_special_chars]
|
339
|
+
}
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
# Converts an array to a hash mapping a numerical index to its array value.
|
344
|
+
def array_to_indices_hash(array)
|
345
|
+
array.inject({}) {|hash,e| hash[hash.size] = e; hash }
|
346
|
+
end
|
347
|
+
#:startdoc:
|
348
|
+
end
|
349
|
+
end
|