summaryrefslogtreecommitdiff
path: root/lib/highline/list_renderer.rb
blob: 5446ea52c6713c78884e5fc2e472943be0fdeb09 (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
251
252
253
254
255
256
257
258
259
260
261
# coding: utf-8

require "highline/template_renderer"
require "highline/wrapper"
require "highline/list"

class HighLine
  #
  # This class is a utility for quickly and easily laying out lists
  # to be used by HighLine.
  #
  class 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 = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
          ERB.new(item, trim_mode: "%")
        else
          ERB.new(item, nil, "%")
        end
        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
end