summaryrefslogtreecommitdiff
path: root/lib/highline/list_renderer.rb
blob: be1aa9593324cd1b90ecf2e48693bb44470306fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
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
# coding: utf-8

require 'highline/template_renderer'
require 'highline/wrapper'
require 'highline/list'

#
# This class is a utility for quickly and easily laying out lists
# to be used by HighLine.
#
class HighLine::ListRenderer
  # Items list
  # @return [Array]
  attr_reader :items

  # @return [Symbol] the current mode the List is being rendered
  # @see #initialize for more details see mode parameter of #initialize
  attr_reader :mode

  # Changes the behaviour of some modes. Example, in :inline mode
  # the option is treated as the 'end separator' (defaults to " or ")
  # @return option parameter that changes the behaviour of some modes.
  attr_reader :option

  # @return [HighLine] context
  attr_reader :highline

  # The only required parameters are _items_ and _highline_.
  # @param items [Array] the Array of items to list
  # @param mode [Symbol] controls how that list is formed
  # @param option has different effects, depending on the _mode_.
  # @param highline [HighLine] a HighLine instance to direct the output to.
  #
  # Recognized modes are:
  #
  # <tt>:columns_across</tt>::         _items_ will be placed in columns,
  #                                    flowing from left to right.  If given,
  #                                    _option_ is the number of columns to be
  #                                    used.  When absent, columns will be
  #                                    determined based on _wrap_at_ or a
  #                                    default of 80 characters.
  # <tt>:columns_down</tt>::           Identical to <tt>:columns_across</tt>,
  #                                    save flow goes down.
  # <tt>:uneven_columns_across</tt>::  Like <tt>:columns_across</tt> but each
  #                                    column is sized independently.
  # <tt>:uneven_columns_down</tt>::    Like <tt>:columns_down</tt> but each
  #                                    column is sized independently.
  # <tt>:inline</tt>::                 All _items_ are placed on a single line.
  #                                    The last two _items_ are separated by
  #                                    _option_ or a default of " or ".  All
  #                                    other _items_ are separated by ", ".
  # <tt>:rows</tt>::                   The default mode.  Each of the _items_ is
  #                                    placed on its own line.  The _option_
  #                                    parameter is ignored in this mode.
  #
  # Each member of the _items_ Array is passed through ERb and thus can contain
  # their own expansions.  Color escape expansions do not contribute to the
  # final field width.

  def initialize(items, mode = :rows, option = nil, highline)
    @highline = highline
    @mode     = mode
    @option   = option
    @items    = render_list_items(items)
  end

  # Render the list using the appropriate mode and options.
  # @return [String] rendered list as String
  def render
    return "" if items.empty?

    case mode
    when :inline
      list_inline_mode
    when :columns_across
      list_columns_across_mode
    when :columns_down
      list_columns_down_mode
    when :uneven_columns_across
      list_uneven_columns_mode
    when :uneven_columns_down
      list_uneven_columns_down_mode
    else
      list_default_mode
    end
  end

  private

  def render_list_items(items)
    items.to_ary.map do |item|
      item = String(item)
      template = ERB.new(item, nil, "%")
      template_renderer =
        HighLine::TemplateRenderer.new(template, self, highline)
      template_renderer.render
    end
  end

  def list_default_mode
    items.map { |i| "#{i}\n" }.join
  end

  def list_inline_mode
    end_separator = option || " or "

    if items.size == 1
      items.first
    else
      items[0..-2].join(", ") + "#{end_separator}#{items.last}"
    end
  end

  def list_columns_across_mode
    HighLine::List.new(right_padded_items, cols: col_count).to_s
  end

  def list_columns_down_mode
    HighLine::List.new(right_padded_items, cols: col_count, col_down: true).to_s
  end

  def list_uneven_columns_mode(list = nil)
    list ||= HighLine::List.new(items)

    col_max = option || items.size
    col_max.downto(1) do |column_count|
      list.cols = column_count
      widths = get_col_widths(list)

      if column_count == 1 || # last guess
         inside_line_size_limit?(widths) || # good guess
         option # defined by user
        return pad_uneven_rows(list, widths)
      end
    end
  end

  def list_uneven_columns_down_mode
    list = HighLine::List.new(items, col_down: true)
    list_uneven_columns_mode(list)
  end

  def pad_uneven_rows(list, widths)
    right_padded_list = Array(list).map do |row|
      right_pad_row(row.compact, widths)
    end
    stringfy_list(right_padded_list)
  end

  def stringfy_list(list)
    list.map { |row| row_to_s(row) }.join
  end

  def row_to_s(row)
    row.compact.join(row_join_string) + "\n"
  end

  def right_pad_row(row, widths)
    row.zip(widths).map do |field, width|
      right_pad_field(field, width)
    end
  end

  def right_pad_field(field, width)
    field = String(field) # nil protection
    pad_size = width - actual_length(field)
    field + (pad_char * pad_size)
  end

  def get_col_widths(lines)
    lines = transpose(lines)
    get_segment_widths(lines)
  end

  def get_row_widths(lines)
    get_segment_widths(lines)
  end

  def get_segment_widths(lines)
    lines.map do |col|
      actual_lengths_for(col).max
    end
  end

  def actual_lengths_for(line)
    line.map do |item|
      actual_length(item)
    end
  end

  def transpose(lines)
    lines = Array(lines)
    first_line = lines.shift
    first_line.zip(*lines)
  end

  def inside_line_size_limit?(widths)
    line_size = widths.reduce(0) { |sum, n| sum + n + row_join_str_size }
    line_size <= line_size_limit + row_join_str_size
  end

  def actual_length(text)
    HighLine::Wrapper.actual_length text
  end

  def items_max_length
    @items_max_length ||= max_length(items)
  end

  def max_length(items)
    items.map { |item| actual_length(item) }.max
  end

  def line_size_limit
    @line_size_limit ||= (highline.wrap_at || 80)
  end

  def row_join_string
    @row_join_string ||= "  "
  end

  attr_writer :row_join_string

  def row_join_str_size
    row_join_string.size
  end

  def col_count_calculate
    (line_size_limit + row_join_str_size) /
      (items_max_length + row_join_str_size)
  end

  def col_count
    option || col_count_calculate
  end

  def right_padded_items
    items.map do |item|
      right_pad_field(item, items_max_length)
    end
  end

  def pad_char
    " "
  end

  def row_count
    (items.count / col_count.to_f).ceil
  end
end