tabulo 2.3.3 → 2.6.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.
@@ -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