tabulo 2.3.3 → 2.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,11 +7,12 @@ module Tabulo
7
7
  attr_reader :source
8
8
 
9
9
  # @!visibility private
10
- def initialize(table, source, divider: false, header: :top)
10
+ def initialize(table, source, divider:, header:, index:)
11
11
  @table = table
12
12
  @source = source
13
13
  @divider = divider
14
14
  @header = header
15
+ @index = index
15
16
  end
16
17
 
17
18
  # Calls the given block once for each {Cell} in the {Row}, passing that {Cell} as parameter.
@@ -23,8 +24,8 @@ module Tabulo
23
24
  # puts cell.value # => 1, => false
24
25
  # end
25
26
  def each
26
- @table.column_registry.each do |_, column|
27
- yield column.body_cell(@source)
27
+ @table.column_registry.each_with_index do |(_, column), column_index|
28
+ yield column.body_cell(@source, row_index: @index, column_index: column_index)
28
29
  end
29
30
  end
30
31
 
@@ -34,7 +35,7 @@ module Tabulo
34
35
  # and divider frequency).
35
36
  def to_s
36
37
  if @table.column_registry.any?
37
- @table.formatted_body_row(@source, header: @header, divider: @divider)
38
+ @table.formatted_body_row(@source, divider: @divider, header: @header, index: @index)
38
39
  else
39
40
  ""
40
41
  end
@@ -42,7 +43,9 @@ module Tabulo
42
43
 
43
44
  # @return a Hash representation of the {Row}, with column labels acting as keys and the {Cell}s the values.
44
45
  def to_h
45
- @table.column_registry.map { |label, column| [label, column.body_cell(@source)] }.to_h
46
+ @table.column_registry.map.with_index do |(label, column), column_index|
47
+ [label, column.body_cell(@source, row_index: @index, column_index: column_index)]
48
+ end.to_h
46
49
  end
47
50
  end
48
51
  end
@@ -43,13 +43,18 @@ module Tabulo
43
43
  # @param [:left, :right, :center] align_header (:center) Determines the alignment of header text
44
44
  # for columns in this Table. Can be overridden for individual columns using the
45
45
  # <tt>align_header</tt> option passed to {#add_column}
46
+ # @param [:left, :right, :center] align_header (:center) Determines the alignment of the table
47
+ # title, if present.
46
48
  # @param [:ascii, :markdown, :modern, :blank, nil] border (nil) Determines the characters used
47
49
  # for the Table border, including both the characters around the outside of table, and the lines drawn
48
50
  # within the table to separate columns from each other and the header row from the Table body.
49
51
  # If <tt>nil</tt>, then the value of {DEFAULT_BORDER} will be used.
50
52
  # Possible values are:
51
53
  # - `:ascii` Uses ASCII characters only
52
- # - `:markdown` Produces a GitHub-flavoured Markdown table
54
+ # - `:markdown` Produces a GitHub-flavoured Markdown table. Note: Using the `title`
55
+ # option in combination with this border type will cause the rendered
56
+ # table not to be valid Markdown, since Markdown engines do not generally
57
+ # support adding a caption element (i.e. title) to tables.
53
58
  # - `:modern` Uses non-ASCII Unicode characters to render a border with smooth continuous lines
54
59
  # - `:blank` No border characters are rendered
55
60
  # - `:reduced_ascii` Like `:ascii`, but without left or right borders, and with internal vertical
@@ -64,14 +69,15 @@ module Tabulo
64
69
  # If passed <tt>nil</tt>, then no additional styling will be applied to borders. If passed a
65
70
  # callable, then that callable will be called for each border section, with the
66
71
  # resulting string rendered in place of that border. The extra width of the string returned by the
67
- # {border_styler} is not taken into consideration by the internal table rendering calculations
72
+ # <tt>border_styler</tt> is not taken into consideration by the internal table rendering calculations
68
73
  # Thus it can be used to apply ANSI escape codes to border characters, to colour the borders
69
74
  # for example, without breaking the table formatting.
70
75
  # @param [nil, Integer, Array] column_padding (1) Determines the amount of blank space with which to pad
71
76
  # either side of each column. If passed an Integer, then the given amount of padding is
72
77
  # applied to each side of each column. If passed a two-element Array, then the first element of the
73
78
  # Array indicates the amount of padding to apply to the left of each column, and the second
74
- # element indicates the amount to apply to the right.
79
+ # element indicates the amount to apply to the right. This setting can be overridden for
80
+ # individual columns using the `padding` option of {#add_column}.
75
81
  # @param [Integer, nil] column_width The default column width for columns in this
76
82
  # table, not excluding padding. If <tt>nil</tt>, then {DEFAULT_COLUMN_WIDTH} will be used.
77
83
  # @param [nil, #to_proc] formatter (:to_s.to_proc) The default formatter for columns in this
@@ -89,6 +95,31 @@ module Tabulo
89
95
  # header row.
90
96
  # @param [nil, #to_proc] styler (nil) The default styler for columns in this table. See `styler`
91
97
  # option of {#add_column} for details.
98
+ # @param [nil, String] title (nil) If passed a String, will arrange for a title to be shown at the top
99
+ # of the table. Note: If the `border` option is set to `:markdown`, adding a title to the table
100
+ # will cause it to cease being valid Markdown when rendered, since Markdown engines do not generally
101
+ # support adding a caption element (i.e. title) to tables.
102
+ # @param [nil, #to_proc] title_styler (nil) A lambda or other callable object that will
103
+ # determine the colors or other styling applied to the table title. Can be passed
104
+ # <tt>nil</tt>, or can be passed a callable that takes either 1 or 2 parametes:
105
+ # * If passed <tt>nil</tt>, then no additional styling will be applied to the title.
106
+ # * If passed a callable, then that callable will be called for each line of
107
+ # the title, and the resulting string rendered in place of that line.
108
+ # The extra width of the string returned by the <tt>title_styler</tt> is not taken into
109
+ # consideration by the internal table and cell width calculations involved in rendering the
110
+ # table. Thus it can be used to apply ANSI escape codes to title content, to color the
111
+ # content for example, without breaking the table formatting.
112
+ # * If the passed callable takes 1 parameter, then the first parameter is a string
113
+ # representing a single line within the title. For example, if the title
114
+ # is wrapped over three lines, then the <tt>title_styler</tt> will be called
115
+ # three times, once for each line of content.
116
+ # * If the passed callable takes 2 parameters, then the first parameter is as above, and the
117
+ # second parameter is an Integer representing the index of the line within the
118
+ # title that is currently being styled. For example, if the title is wrapped over 3
119
+ # lines, then the callable will be called first with a line index of 0, to style the first line,
120
+ # then with a line index of 1, to style the second line, and finally with a line index of 2, for
121
+ # the third and final wrapped line of the cell.
122
+ #
92
123
  # @param [nil, String] truncation_indicator Determines the character used to indicate that a
93
124
  # cell's content has been truncated. If omitted or passed <tt>nil</tt>,
94
125
  # defaults to {DEFAULT_TRUNCATION_INDICATOR}. If passed something other than <tt>nil</tt> or
@@ -107,22 +138,24 @@ module Tabulo
107
138
  # @return [Table] a new {Table}
108
139
  # @raise [InvalidColumnLabelError] if non-unique Symbols are provided to columns.
109
140
  # @raise [InvalidBorderError] if invalid option passed to `border` parameter.
110
- def initialize(sources, *columns, align_body: :auto, align_header: :center, border: nil,
111
- border_styler: nil, column_padding: nil, column_width: nil, formatter: :to_s.to_proc,
141
+ def initialize(sources, *columns, align_body: :auto, align_header: :center, align_title: :center,
142
+ border: nil, border_styler: nil, column_padding: nil, column_width: nil, formatter: :to_s.to_proc,
112
143
  header_frequency: :start, header_styler: nil, row_divider_frequency: nil, styler: nil,
113
- truncation_indicator: nil, wrap_body_cells_to: nil, wrap_header_cells_to: nil)
144
+ title: nil, title_styler: nil, truncation_indicator: nil, wrap_body_cells_to: nil,
145
+ wrap_header_cells_to: nil)
114
146
 
115
147
  @sources = sources
116
148
 
117
149
  @align_body = align_body
118
150
  @align_header = align_header
151
+ @align_title = align_title
119
152
  @border = (border || DEFAULT_BORDER)
120
153
  @border_styler = border_styler
121
154
  @border_instance = Border.from(@border, @border_styler)
122
155
  @column_padding = (column_padding || DEFAULT_COLUMN_PADDING)
123
156
 
124
157
  @left_column_padding, @right_column_padding =
125
- Array === @column_padding ? @column_padding : [@column_padding, @column_padding]
158
+ (Array === @column_padding ? @column_padding : [@column_padding, @column_padding])
126
159
 
127
160
  @column_width = (column_width || DEFAULT_COLUMN_WIDTH)
128
161
  @formatter = formatter
@@ -130,6 +163,8 @@ module Tabulo
130
163
  @header_styler = header_styler
131
164
  @row_divider_frequency = row_divider_frequency
132
165
  @styler = styler
166
+ @title = title
167
+ @title_styler = title_styler
133
168
  @truncation_indicator = validate_character(truncation_indicator,
134
169
  DEFAULT_TRUNCATION_INDICATOR, InvalidTruncationIndicatorError, "truncation indicator")
135
170
  @wrap_body_cells_to = wrap_body_cells_to
@@ -168,56 +203,119 @@ module Tabulo
168
203
  # in either String or Symbol form for this purpose.
169
204
  # @param [#to_proc] formatter (nil) A lambda or other callable object that
170
205
  # will be passed the calculated value of each cell to determine how it should be displayed. This
171
- # is distinct from the extractor (see below). For example, if the extractor for this column
172
- # generates a Date, then the formatter might format that Date in a particular way.
173
- # If no formatter is provided, then the callable that was passed to the `formatter` option
174
- # of the table itself on its creation (see {#initialize}) (which itself defaults to
175
- # `:to_s.to_proc`), will be used as the formatter for the column.
206
+ # is distinct from the extractor and the styler (see below).
207
+ # For example, if the extractor for this column generates a Date, then the formatter might format
208
+ # that Date in a particular way.
209
+ # * If <tt>nil</tt> is provided, then the callable that was passed to the `formatter` option
210
+ # of the table itself on its creation (see {#initialize}) (which itself defaults to
211
+ # `:to_s.to_proc`), will be used as the formatter for the column.
212
+ # * If a 1-parameter callable is passed, then this callable will be called with the calculated
213
+ # value of the cell; it should then return a String, and this String will be displayed as
214
+ # the formatted value of the cell.
215
+ # * If a 2-parameter callable is passed, then the first parameter represents the calculated
216
+ # value of the cell, and the second parameter is a {CellData} instance, containing
217
+ # additional information about the cell that may be relevant to what formatting should
218
+ # be applied. For example, the {CellData#row_index} attribute can be inspected to determine
219
+ # whether the {Cell} is an odd- or even-numbered {Row}, to arrange for different formatting
220
+ # to be applied to alternating rows.
221
+ # See the documentation for {CellData} for more.
176
222
  # @param [nil, #to_s] header (nil) Text to be displayed in the column header. If passed nil,
177
223
  # the column's label will also be used as its header text.
178
- # @param [nil, #to_proc] header_styler (nil) A lambda or other callable object taking
179
- # a single parameter, representing a single line of within the header content for
180
- # this column. For example, if the header cell content is wrapped over three lines, then
181
- # the {header_styler} will be called once for each line. If passed <tt>nil</tt>, then
182
- # no additional styling will be applied to the header cell content. If passed a callable,
183
- # then that callable will be called for each line of content within the header cell, and the
184
- # resulting string rendered in place of that line. The extra width of the string returned by the
185
- # {header_styler} is not taken into consideration by the internal table and
186
- # cell width calculations involved in rendering the table. Thus it can be used to apply
187
- # ANSI escape codes to header cell content, to colour the cell content for example, without
188
- # breaking the table formatting.
189
- # Note that if the header content is truncated, then any {header_styler} will be applied to the
224
+ # @param [nil, #to_proc] header_styler (nil) A lambda or other callable object that will
225
+ # determine the colors or other styling applied to the header content. Can be passed
226
+ # <tt>nil</tt>, or can be passed a callable that takes 1, 2 or 3 parameters:
227
+ # * If passed <tt>nil</tt>, then no additional styling will be applied to the cell content
228
+ # (other than what was already applied by the <tt>formatter</tt>).
229
+ # * If passed a callable, then that callable will be called for each line of content within
230
+ # the header cell, and the resulting string rendered in place of that line.
231
+ # The extra width of the string returned by the <tt>header_styler</tt> is not taken into
232
+ # consideration by the internal table and cell width calculations involved in rendering the
233
+ # table. Thus it can be used to apply ANSI escape codes to header cell content, to color the
234
+ # cell content for example, without breaking the table formatting.
235
+ # * If the passed callable takes 1 parameter, then the first parameter is a string
236
+ # representing a single formatted line within the header cell. For example, if the header
237
+ # cell content is wrapped over three lines, then the <tt>header_styler</tt> will be called
238
+ # three times for that header cell, once for each line of content.
239
+ # * If the passed callable takes 2 parameters, then the first parameter is as above, and the
240
+ # second parameter is an Integer representing the positional index of this header's {Column},
241
+ # with the leftmost column having index 0, the next having index 1 etc.. This can be
242
+ # used, for example, to apply different styles to alternating {Column}s.
243
+ # * If the passed callable takes 3 parameters, then the first and second parameters are as above,
244
+ # and the third parameter is an Integer representing the index of the line within the
245
+ # header cell that is currently being styled. For example, if the cell content is wrapped over 3
246
+ # lines, then the callable will be called first with a line index of 0, to style the first line,
247
+ # then with a line index of 1, to style the second line, and finally with a line index of 2, for
248
+ # the third and final wrapped line of the cell.
249
+ #
250
+ # Note that if the header content is truncated, then any <tt>header_styler</tt> will be applied to the
190
251
  # truncation indicator character as well as to the truncated content.
191
- # @param [nil, #to_proc] styler (nil) A lambda or other callable object that will be passed
192
- # two arguments: the calculated value of the cell (prior to the {formatter} being applied);
193
- # and a string representing a single formatted line within the cell. For example, if the
194
- # cell content is wrapped over three lines, then for that cell, the {styler} will be called
195
- # three times, once for each line of content within the cell. If passed <tt>nil</tt>, then
196
- # no additional styling will be applied to the cell content (other than what was already
197
- # applied by the {formatter}). If passed a callable, then that callable will be called for
198
- # each line of content within the cell, and the resulting string rendered in place of that
199
- # line. The {styler} option differs from the {formatter} option in that the extra width of the
200
- # string returned by {styler} is not taken into consideration by the internal table and
201
- # cell width calculations involved in rendering the table. Thus it can be used to apply
202
- # ANSI escape codes to cell content, to colour the cell content for example, without
203
- # breaking the table formatting.
252
+ # @param [nil, Integer, Array] padding (nil) Determines the amount of blank space with which to
253
+ # pad either side of the column. If passed nil, then the `column_padding` setting of the
254
+ # {Table} will determine the column's padding. (See {#initialize}.) Otherwise, this option
255
+ # overrides, for this column, the `column_padding` that was set at the table level: if passed an Integer,
256
+ # then the given amount of padding is applied to either side of the column; or if passed a two-element Array,
257
+ # then the first element of the Array indicates the amount of padding to apply to the left of the column,
258
+ # and the second element indicates the amount to apply to the right.
259
+ # @param [nil, #to_proc] styler (nil) A lambda or other callable object that will determine
260
+ # the colors or other styling applied to the formatted value of the cell. Can be passed
261
+ # <tt>nil</tt>, or can be passed a callable that takes either 2 or 3 parameters:
262
+ # * If passed <tt>nil</tt>, then no additional styling will be applied to the cell content
263
+ # (other than what was already applied by the <tt>formatter</tt>).
264
+ # * If passed a callable, then that callable will be called for each line of content within
265
+ # the cell, and the resulting string rendered in place of that line.
266
+ # The <tt>styler</tt> option differs from the <tt>formatter</tt> option in that the extra width of the
267
+ # string returned by <tt>styler</tt> is not taken into consideration by the internal table and
268
+ # cell width calculations involved in rendering the table. Thus it can be used to apply
269
+ # ANSI escape codes to cell content, to color the cell content for example, without
270
+ # breaking the table formatting.
271
+ # * If the passed callable takes 2 parameters, then the first parameter is the calculated
272
+ # value of the cell (prior to the <tt>formatter</tt> being applied); and the second parameter is
273
+ # a string representing a single formatted line within the cell. For example, if the cell
274
+ # content is wrapped over three lines, then for that cell, the <tt>styler</tt> will be called
275
+ # three times, once for each line of content within the cell.
276
+ # * If the passed callable takes 3 parameters, then the first two parameters are as above,
277
+ # and the third parameter is a {CellData} instance, containing additional information
278
+ # about the cell that may be relevant to what styles should be applied. For example, the
279
+ # {CellData#row_index} attribute can be inspected to determine whether the {Cell} is an
280
+ # odd- or even-numbered {Row}, to arrange for different styling to be applied to
281
+ # alternating rows. See the documentation for {CellData} for more.
282
+ # * If the passed callable takes 4 parameters, then the first three parameters are as above,
283
+ # and the fourth parameter is an Integer representing the index of the line within the
284
+ # cell that is currently being styled. For example, if the cell content is wrapped over 3
285
+ # lines, then the callable will be called first with a line index of 0, to style the first
286
+ # line, then with a line index of 1, to style the second line, and finally with a line
287
+ # index of 2, to style the third and final wrapped line of the cell.
288
+ #
204
289
  # Note that if the content of a cell is truncated, then the whatever styling is applied by the
205
- # {styler} to the cell content will also be applied to the truncation indicator character.
290
+ # <tt>styler</tt> to the cell content will also be applied to the truncation indicator character.
206
291
  # @param [Integer] width (nil) Specifies the width of the column, excluding padding. If
207
292
  # nil, then the column will take the width provided by the `column_width` param
208
293
  # with which the Table was initialized.
209
- # @param [#to_proc] extractor A block or other callable
210
- # that will be passed each of the Table sources to determine the value in each cell of this
211
- # column. If this is not provided, then the column label will be treated as a method to be
212
- # called on each source item to determine each cell's value.
294
+ # @param [#to_proc] extractor A block or other callable that will be passed each of the {Table}
295
+ # sources to determine the value in each cell of this column.
296
+ # * If this is not provided, then the column label will be treated as a method to be called on
297
+ # each source item to determine each cell's value.
298
+ # * If provided a single-parameter callable, then this callable will be passed each of the
299
+ # {Table} sources to determine the cell value for each row in this column.
300
+ # * If provided a 2-parameter callable, then for each of the {Table} sources, this callable
301
+ # will be passed the source, and the row index, to determine the cell value for that row.
302
+ # For this purpose, the first body row (not counting the header row) has an index of 0,
303
+ # the next an index of 1, etc..
213
304
  # @raise [InvalidColumnLabelError] if label has already been used for another column in this
214
305
  # Table. (This is case-sensitive, but is insensitive to whether a String or Symbol is passed
215
306
  # to the label parameter.)
216
307
  def add_column(label, align_body: nil, align_header: nil, before: nil, formatter: nil,
217
- header: nil, header_styler: nil, styler: nil, width: nil, &extractor)
308
+ header: nil, header_styler: nil, padding: nil, styler: nil, width: nil, &extractor)
218
309
 
219
310
  column_label = normalize_column_label(label)
220
311
 
312
+ left_padding, right_padding =
313
+ if padding
314
+ Array === padding ? padding : [padding, padding]
315
+ else
316
+ [@left_column_padding, @right_column_padding]
317
+ end
318
+
221
319
  if column_registry.include?(column_label)
222
320
  raise InvalidColumnLabelError, "Column label already used in this table."
223
321
  end
@@ -229,7 +327,10 @@ module Tabulo
229
327
  formatter: formatter || @formatter,
230
328
  header: (header || label).to_s,
231
329
  header_styler: header_styler || @header_styler,
330
+ index: column_registry.count,
331
+ left_padding: left_padding,
232
332
  padding_character: PADDING_CHARACTER,
333
+ right_padding: right_padding,
233
334
  styler: styler || @styler,
234
335
  truncation_indicator: @truncation_indicator,
235
336
  width: width || @column_width,
@@ -271,7 +372,7 @@ module Tabulo
271
372
  if column_registry.any?
272
373
  bottom_edge = horizontal_rule(:bottom)
273
374
  rows = map(&:to_s)
274
- bottom_edge.empty? ? join_lines(rows) : join_lines(rows + [bottom_edge])
375
+ bottom_edge.empty? ? Util.join_lines(rows) : Util.join_lines(rows + [bottom_edge])
275
376
  else
276
377
  ""
277
378
  end
@@ -297,24 +398,28 @@ module Tabulo
297
398
 
298
399
  show_divider = @row_divider_frequency && (index != 0) && Util.divides?(@row_divider_frequency, index)
299
400
 
300
- yield Row.new(self, source, header: header, divider: show_divider)
401
+ yield Row.new(self, source, header: header, divider: show_divider, index: index)
301
402
  end
302
403
  end
303
404
 
304
- # @return [String] an "ASCII" graphical representation of the Table column headers.
405
+ # @return [String] a graphical representation of the Table column headers formatted with fixed
406
+ # width plain text.
305
407
  def formatted_header
306
408
  cells = get_columns.map(&:header_cell)
307
409
  format_row(cells, @wrap_header_cells_to)
308
410
  end
309
411
 
310
- # @param [:top, :middle, :bottom] align_body (:bottom) Specifies the position
311
- # for which the resulting horizontal dividing line is intended to be printed.
312
- # This determines the border characters that are used to construct the line.
412
+ # Produce a horizontal dividing line suitable for printing at the top, bottom or middle
413
+ # of the table.
414
+ #
415
+ # @param [:top, :middle, :bottom, :title_top, :title_bottom] position (:bottom)
416
+ # Specifies the position for which the resulting horizontal dividing line is intended to
417
+ # be printed. This determines the border characters that are used to construct the line.
418
+ # The `:title_top` and `:title_bottom` options are used internally for adding borders
419
+ # above and below the table title text.
313
420
  # @return [String] an "ASCII" graphical representation of a horizontal
314
- # dividing line suitable for printing at the top, bottom or middle of the
315
- # table.
316
- # @example Print a horizontal divider between each pair of rows, and again
317
- # at the bottom:
421
+ # dividing line.
422
+ # @example Print a horizontal divider between each pair of rows, and again at the bottom:
318
423
  #
319
424
  # table.each_with_index do |row, i|
320
425
  # puts table.horizontal_rule(:middle) unless i == 0
@@ -325,14 +430,17 @@ module Tabulo
325
430
  # It may be that `:top`, `:middle` and `:bottom` all look the same. Whether
326
431
  # this is the case depends on the characters used for the table border.
327
432
  def horizontal_rule(position = :bottom)
328
- column_widths = get_columns.map { |column| column.width + total_column_padding }
433
+ column_widths = get_columns.map { |column| column.width + column.total_padding }
329
434
  @border_instance.horizontal_rule(column_widths, position)
330
435
  end
331
436
 
332
- # Reset all the column widths so that each column is *just* wide enough to accommodate
437
+ # Resets all the column widths so that each column is *just* wide enough to accommodate
333
438
  # its header text as well as the formatted content of each its cells for the entire
334
439
  # collection, together with a single character of padding on either side of the column,
335
- # without any wrapping.
440
+ # without any wrapping. In addition, if the table has a title but is not wide enough to
441
+ # accommodate (without wrapping) the title text (with a character of padding either side),
442
+ # widens the columns roughly evenly until the table as a whole is just wide enough to
443
+ # accommodate the title text.
336
444
  #
337
445
  # Note that calling this method will cause the entire source Enumerable to
338
446
  # be traversed and all the column extractors and formatters to be applied in order
@@ -358,17 +466,27 @@ module Tabulo
358
466
  # Table will refuse to shrink itself.
359
467
  # @return [Table] the Table itself
360
468
  def pack(max_table_width: :auto)
361
- get_columns.each { |column| column.width = wrapped_width(column.header) }
469
+ get_columns.each { |column| column.width = Util.wrapped_width(column.header) }
362
470
 
363
- @sources.each do |source|
364
- get_columns.each do |column|
365
- cell_width = wrapped_width(column.body_cell(source).formatted_content)
471
+ @sources.each_with_index do |source, row_index|
472
+ get_columns.each_with_index do |column, column_index|
473
+ cell = column.body_cell(source, row_index: row_index, column_index: column_index)
474
+ cell_width = Util.wrapped_width(cell.formatted_content)
366
475
  column.width = Util.max(column.width, cell_width)
367
476
  end
368
477
  end
369
478
 
370
- if max_table_width
371
- shrink_to(max_table_width == :auto ? TTY::Screen.width : max_table_width)
479
+ shrink_to(max_table_width == :auto ? TTY::Screen.width : max_table_width) if max_table_width
480
+
481
+ if @title
482
+ border_edge_width = (@border == :blank ? 0 : 2)
483
+ columns = get_columns
484
+ expand_to(
485
+ Unicode::DisplayWidth.of(@title) +
486
+ columns.first.left_padding +
487
+ columns.last.right_padding +
488
+ border_edge_width
489
+ )
372
490
  end
373
491
 
374
492
  self
@@ -393,8 +511,9 @@ module Tabulo
393
511
  # The following options are the same as the keyword params for the {#initialize} method for
394
512
  # {Table}: <tt>column_width</tt>, <tt>column_padding</tt>, <tt>formatter</tt>,
395
513
  # <tt>header_frequency</tt>, <tt>row_divider_frequency</tt>, <tt>wrap_header_cells_to</tt>,
396
- # <tt>wrap_body_cells_to</tt>, <tt>border</tt>, <tt>border_styler</tt>, <tt>truncation_indicator</tt>,
397
- # <tt>align_header</tt>, <tt>align_body</tt>.
514
+ # <tt>wrap_body_cells_to</tt>, <tt>border</tt>, <tt>border_styler</tt>, <tt>title</tt>,
515
+ # <tt>title_styler</tt>, <tt>truncation_indicator</tt>, <tt>align_header</tt>, <tt>align_body</tt>,
516
+ # <tt>align_title</tt>.
398
517
  # These are applied in the same way as documented for {#initialize}, when
399
518
  # creating the new, transposed Table. Any options not specified explicitly in the call to {#transpose}
400
519
  # will inherit their values from the original {Table} (with the exception of settings
@@ -418,9 +537,9 @@ module Tabulo
418
537
  # @return [Table] a new {Table}
419
538
  # @raise [InvalidBorderError] if invalid argument passed to `border` parameter.
420
539
  def transpose(opts = {})
421
- default_opts = [:align_body, :align_header, :border, :border_styler, :column_padding, :column_width,
422
- :formatter, :header_frequency, :row_divider_frequency, :truncation_indicator, :wrap_body_cells_to,
423
- :wrap_header_cells_to].map do |sym|
540
+ default_opts = [:align_body, :align_header, :align_title, :border, :border_styler, :column_padding,
541
+ :column_width, :formatter, :header_frequency, :row_divider_frequency, :title, :title_styler,
542
+ :truncation_indicator, :wrap_body_cells_to, :wrap_header_cells_to].map do |sym|
424
543
  [sym, instance_variable_get("@#{sym}")]
425
544
  end.to_h
426
545
 
@@ -445,36 +564,26 @@ module Tabulo
445
564
  # Add a column to the new table for each of the original table's sources
446
565
  sources.each_with_index do |source, i|
447
566
  t.add_column(i, header: extra_opts[:headers].call(source)) do |original_column|
448
- original_column.body_cell_value(source)
567
+ original_column.body_cell_value(source, row_index: i, column_index: original_column.index)
449
568
  end
450
569
  end
451
570
  end
452
571
  end
453
572
 
454
573
  # @!visibility private
455
- def formatted_body_row(source, header:, divider:)
456
- cells = get_columns.map { |column| column.body_cell(source) }
574
+ def formatted_body_row(source, header:, divider:, index:)
575
+ cells = get_columns.map.with_index { |c, i| c.body_cell(source, row_index: index, column_index: i) }
457
576
  inner = format_row(cells, @wrap_body_cells_to)
458
577
 
459
- if header == :top
460
- join_lines([
461
- horizontal_rule(:top),
462
- formatted_header,
463
- horizontal_rule(:middle),
464
- inner
465
- ].reject(&:empty?))
578
+ if @title && header == :top
579
+ Util.condense_lines([horizontal_rule(:title_top), formatted_title, horizontal_rule(:title_bottom),
580
+ formatted_header, horizontal_rule(:middle), inner])
581
+ elsif header == :top
582
+ Util.condense_lines([horizontal_rule(:top), formatted_header, horizontal_rule(:middle), inner])
466
583
  elsif header
467
- join_lines([
468
- horizontal_rule(:middle),
469
- formatted_header,
470
- horizontal_rule(:middle),
471
- inner
472
- ].reject(&:empty?))
584
+ Util.condense_lines([horizontal_rule(:middle), formatted_header, horizontal_rule(:middle), inner])
473
585
  elsif divider
474
- join_lines([
475
- horizontal_rule(:middle),
476
- inner
477
- ].reject(&:empty?))
586
+ Util.condense_lines([horizontal_rule(:middle), inner])
478
587
  else
479
588
  inner
480
589
  end
@@ -508,6 +617,55 @@ module Tabulo
508
617
  @column_registry[label] = column
509
618
  end
510
619
 
620
+ # @visibility private
621
+ def formatted_title
622
+ columns = get_columns
623
+
624
+ extra_for_internal_dividers = (@border == :blank ? 0 : 1)
625
+
626
+ title_cell_width = columns.inject(0) do |total_width, column|
627
+ total_width + column.padded_width + extra_for_internal_dividers
628
+ end
629
+
630
+ title_cell_width -= (columns.first.left_padding + columns.last.right_padding + extra_for_internal_dividers)
631
+
632
+ styler =
633
+ if @title_styler
634
+ case @title_styler.arity
635
+ when 1
636
+ -> (_val, str) { @title_styler.call(str) }
637
+ when 2
638
+ -> (_val, str, _cell_data, line_index) { @title_styler.call(str, line_index) }
639
+ end
640
+ else
641
+ -> (_val, str) { str }
642
+ end
643
+
644
+ title_cell = Cell.new(
645
+ alignment: @align_title,
646
+ cell_data: nil,
647
+ formatter: -> (s) { s },
648
+ left_padding: columns.first.left_padding,
649
+ padding_character: PADDING_CHARACTER,
650
+ right_padding: columns.last.right_padding,
651
+ styler: styler,
652
+ truncation_indicator: @truncation_indicator,
653
+ value: @title,
654
+ width: title_cell_width
655
+ )
656
+ cells = [title_cell]
657
+ max_cell_height = cells.map(&:height).max
658
+ row_height = ([nil, max_cell_height].compact.min || 1)
659
+ subcell_stacks = cells.map do |cell|
660
+ cell.padded_truncated_subcells(row_height)
661
+ end
662
+ subrows = subcell_stacks.transpose.map do |subrow_components|
663
+ @border_instance.join_cell_contents(subrow_components)
664
+ end
665
+
666
+ Util.join_lines(subrows)
667
+ end
668
+
511
669
  # @!visibility private
512
670
  def normalize_column_label(label)
513
671
  case label
@@ -518,14 +676,32 @@ module Tabulo
518
676
  end
519
677
  end
520
678
 
679
+ # @!visibility private
680
+ def expand_to(min_table_width)
681
+ columns = get_columns
682
+ num_columns = columns.count
683
+ total_columns_padded_width = columns.inject(0) { |sum, column| sum + column.padded_width }
684
+ total_borders = num_columns + 1
685
+ unadjusted_table_width = total_columns_padded_width + total_borders
686
+ required_increase = Util.max(min_table_width - unadjusted_table_width, 0)
687
+
688
+ required_increase.times do
689
+ narrowest_column = columns.inject(columns.first) do |narrowest, column|
690
+ column.width <= narrowest.width ? column : narrowest
691
+ end
692
+
693
+ narrowest_column.width += 1
694
+ end
695
+ end
696
+
521
697
  # @!visibility private
522
698
  def shrink_to(max_table_width)
523
699
  columns = get_columns
524
700
  num_columns = columns.count
525
- total_columns_width = columns.inject(0) { |sum, column| sum + column.width }
526
- total_padding = num_columns * total_column_padding
701
+ total_columns_padded_width = columns.inject(0) { |sum, column| sum + column.padded_width }
702
+ total_padding = columns.inject(0) { |sum, column| sum + column.total_padding }
527
703
  total_borders = num_columns + 1
528
- unadjusted_table_width = total_columns_width + total_padding + total_borders
704
+ unadjusted_table_width = total_columns_padded_width + total_borders
529
705
 
530
706
  # Ensure max table width is at least wide enough to accommodate table borders and padding
531
707
  # and one character of content.
@@ -542,11 +718,6 @@ module Tabulo
542
718
  end
543
719
  end
544
720
 
545
- # @!visibility private
546
- def total_column_padding
547
- @left_column_padding + @right_column_padding
548
- end
549
-
550
721
  # @!visibility private
551
722
  #
552
723
  # Formats a single header row or body row as a String.
@@ -564,18 +735,13 @@ module Tabulo
564
735
  max_cell_height = cells.map(&:height).max
565
736
  row_height = ([wrap_cells_to, max_cell_height].compact.min || 1)
566
737
  subcell_stacks = cells.map do |cell|
567
- cell.padded_truncated_subcells(row_height, @left_column_padding, @right_column_padding)
738
+ cell.padded_truncated_subcells(row_height)
568
739
  end
569
740
  subrows = subcell_stacks.transpose.map do |subrow_components|
570
741
  @border_instance.join_cell_contents(subrow_components)
571
742
  end
572
743
 
573
- join_lines(subrows)
574
- end
575
-
576
- # @!visibility private
577
- def join_lines(lines)
578
- lines.join($/) # join strings with cross-platform newline
744
+ Util.join_lines(subrows)
579
745
  end
580
746
 
581
747
  # @!visibility private
@@ -593,13 +759,5 @@ module Tabulo
593
759
  c
594
760
  end
595
761
 
596
- # @!visibility private
597
- # @return [Integer] the length of the longest segment of str when split by newlines
598
- def wrapped_width(str)
599
- segments = str.split($/)
600
- segments.inject(1) do |longest_length_so_far, segment|
601
- Util.max(longest_length_so_far, Unicode::DisplayWidth.of(segment))
602
- end
603
- end
604
762
  end
605
763
  end