tabulo 2.5.0 → 2.6.0

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.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.5.0
1
+ 2.6.0
@@ -3,107 +3,62 @@ module Tabulo
3
3
  # @!visibility private
4
4
  class Border
5
5
 
6
+ Style = Struct.new(
7
+ :corner_top_left, :corner_top_right, :corner_bottom_right, :corner_bottom_left,
8
+ :edge_top, :edge_right, :edge_bottom, :edge_left,
9
+ :tee_top, :tee_right, :tee_bottom, :tee_left,
10
+ :divider_vertical, :divider_horizontal, :intersection)
11
+
6
12
  STYLES = {
7
- ascii: {
8
- corner_top_left: "+",
9
- corner_top_right: "+",
10
- corner_bottom_right: "+",
11
- corner_bottom_left: "+",
12
- edge_top: "-",
13
- edge_right: "|",
14
- edge_bottom: "-",
15
- edge_left: "|",
16
- tee_top: "+",
17
- tee_right: "+",
18
- tee_bottom: "+",
19
- tee_left: "+",
20
- divider_vertical: "|",
21
- divider_horizontal: "-",
22
- intersection: "+",
23
- },
24
- classic: {
25
- corner_top_left: "+",
26
- corner_top_right: "+",
27
- edge_top: "-",
28
- edge_right: "|",
29
- edge_left: "|",
30
- tee_top: "+",
31
- tee_right: "+",
32
- tee_left: "+",
33
- divider_vertical: "|",
34
- divider_horizontal: "-",
35
- intersection: "+",
36
- },
37
- reduced_ascii: {
38
- corner_top_left: "",
39
- corner_top_right: "",
40
- corner_bottom_right: "",
41
- corner_bottom_left: "",
42
- edge_top: "-",
43
- edge_right: "",
44
- edge_bottom: "-",
45
- edge_left: "",
46
- tee_top: " ",
47
- tee_right: "",
48
- tee_bottom: " ",
49
- tee_left: "",
50
- divider_vertical: " ",
51
- divider_horizontal: "-",
52
- intersection: " ",
53
- },
54
- reduced_modern: {
55
- corner_top_left: "",
56
- corner_top_right: "",
57
- corner_bottom_right: "",
58
- corner_bottom_left: "",
59
- edge_top: "─",
60
- edge_right: "",
61
- edge_bottom: "─",
62
- edge_left: "",
63
- tee_top: " ",
64
- tee_right: "",
65
- tee_bottom: " ",
66
- tee_left: "",
67
- divider_vertical: " ",
68
- divider_horizontal: "─",
69
- intersection: " ",
70
- },
71
- markdown: {
72
- corner_top_left: "",
73
- corner_top_right: "",
74
- corner_bottom_right: "",
75
- corner_bottom_left: "",
76
- edge_top: "",
77
- edge_right: "|",
78
- edge_bottom: "",
79
- edge_left: "|",
80
- tee_top: "",
81
- tee_right: "|",
82
- tee_bottom: "",
83
- tee_left: "|",
84
- divider_vertical: "|",
85
- divider_horizontal: "-",
86
- intersection: "|",
87
- },
88
- modern: {
89
- corner_top_left: "┌",
90
- corner_top_right: "┐",
91
- corner_bottom_right: "┘",
92
- corner_bottom_left: "└",
93
- edge_top: "─",
94
- edge_right: "│",
95
- edge_bottom: "─",
96
- edge_left: "│",
97
- tee_top: "┬",
98
- tee_right: "┤",
99
- tee_bottom: "┴",
100
- tee_left: "├",
101
- divider_vertical: "│",
102
- divider_horizontal: "─",
103
- intersection: "┼",
104
- },
105
- blank: {
106
- },
13
+ ascii:
14
+ Style.new(
15
+ "+", "+", "+", "+",
16
+ "-", "|", "-", "|",
17
+ "+", "+", "+", "+",
18
+ "|", "-", "+",
19
+ ),
20
+ classic:
21
+ Style.new(
22
+ "+", "+", "", "",
23
+ "-", "|", "", "|",
24
+ "+", "+", "", "+",
25
+ "|", "-", "+",
26
+ ),
27
+ reduced_ascii:
28
+ Style.new(
29
+ "", "", "", "",
30
+ "-", "", "-", "",
31
+ " ", "", " ", "",
32
+ " ", "-", " ",
33
+ ),
34
+ reduced_modern:
35
+ Style.new(
36
+ "", "", "", "",
37
+ "─", "", "─", "",
38
+ " ", "", " ", "",
39
+ " ", "─", " ",
40
+ ),
41
+ markdown:
42
+ Style.new(
43
+ "", "", "", "",
44
+ "", "|", "", "|",
45
+ "", "|", "", "|",
46
+ "|", "-", "|",
47
+ ),
48
+ modern:
49
+ Style.new(
50
+ "┌", "", "┘", "└",
51
+ "─", "", "─", "│",
52
+ "┬", "┤", "┴", "├",
53
+ "│", "", "┼",
54
+ ),
55
+ blank:
56
+ Style.new(
57
+ "", "", "", "",
58
+ "", "", "", "",
59
+ "", "", "", "",
60
+ "", "", "",
61
+ ),
107
62
  }
108
63
 
109
64
  # @!visibility private
@@ -147,7 +102,7 @@ module Tabulo
147
102
 
148
103
  def self.options(kind)
149
104
  opts = STYLES[kind]
150
- return opts if opts
105
+ return opts.to_h if opts
151
106
  raise InvalidBorderError
152
107
  end
153
108
 
@@ -13,7 +13,9 @@ module Tabulo
13
13
  alignment:,
14
14
  cell_data:,
15
15
  formatter:,
16
+ left_padding:,
16
17
  padding_character:,
18
+ right_padding:,
17
19
  styler:,
18
20
  truncation_indicator:,
19
21
  value:,
@@ -21,8 +23,10 @@ module Tabulo
21
23
 
22
24
  @alignment = alignment
23
25
  @cell_data = cell_data
24
- @padding_character = padding_character
25
26
  @formatter = formatter
27
+ @left_padding = left_padding
28
+ @padding_character = padding_character
29
+ @right_padding = right_padding
26
30
  @styler = styler
27
31
  @truncation_indicator = truncation_indicator
28
32
  @value = value
@@ -35,12 +39,12 @@ module Tabulo
35
39
  end
36
40
 
37
41
  # @!visibility private
38
- def padded_truncated_subcells(target_height, padding_amount_left, padding_amount_right)
39
- total_padding_amount = padding_amount_left + padding_amount_right
42
+ def padded_truncated_subcells(target_height)
43
+ total_padding_amount = @left_padding + @right_padding
40
44
  truncated = (height > target_height)
41
45
  (0...target_height).map do |subcell_index|
42
46
  append_truncator = (truncated && (total_padding_amount != 0) && (subcell_index + 1 == target_height))
43
- padded_subcell(subcell_index, padding_amount_left, padding_amount_right, append_truncator)
47
+ padded_subcell(subcell_index, append_truncator)
44
48
  end
45
49
  end
46
50
 
@@ -60,8 +64,11 @@ module Tabulo
60
64
  end
61
65
  end
62
66
 
63
- def apply_styler(content)
64
- if @styler.arity == 3
67
+ def apply_styler(content, line_index)
68
+ case @styler.arity
69
+ when 4
70
+ @styler.call(@value, content, @cell_data, line_index)
71
+ when 3
65
72
  @styler.call(@value, content, @cell_data)
66
73
  else
67
74
  @styler.call(@value, content)
@@ -72,13 +79,13 @@ module Tabulo
72
79
  @subcells ||= calculate_subcells
73
80
  end
74
81
 
75
- def padded_subcell(subcell_index, padding_amount_left, padding_amount_right, append_truncator)
76
- lpad = @padding_character * padding_amount_left
82
+ def padded_subcell(subcell_index, append_truncator)
83
+ lpad = @padding_character * @left_padding
77
84
  rpad =
78
85
  if append_truncator
79
- styled_truncation_indicator + padding(padding_amount_right - 1)
86
+ styled_truncation_indicator(subcell_index) + padding(@right_padding - 1)
80
87
  else
81
- padding(padding_amount_right)
88
+ padding(@right_padding)
82
89
  end
83
90
  inner = subcell_index < height ? subcells[subcell_index] : padding(@width)
84
91
  "#{lpad}#{inner}#{rpad}"
@@ -88,31 +95,35 @@ module Tabulo
88
95
  @padding_character * amount
89
96
  end
90
97
 
91
- def styled_truncation_indicator
92
- apply_styler(@truncation_indicator)
98
+ def styled_truncation_indicator(line_index)
99
+ apply_styler(@truncation_indicator, line_index)
93
100
  end
94
101
 
95
102
  def calculate_subcells
103
+ line_index = 0
96
104
  formatted_content.split($/, -1).flat_map do |substr|
97
105
  subsubcells, subsubcell, subsubcell_width = [], String.new(""), 0
98
106
 
99
107
  substr.scan(/\X/).each do |grapheme_cluster|
100
108
  grapheme_cluster_width = Unicode::DisplayWidth.of(grapheme_cluster)
101
109
  if subsubcell_width + grapheme_cluster_width > @width
102
- subsubcells << style_and_align_cell_content(subsubcell)
110
+ subsubcells << style_and_align_cell_content(subsubcell, line_index)
103
111
  subsubcell_width = 0
104
112
  subsubcell.clear
113
+ line_index += 1
105
114
  end
106
115
 
107
116
  subsubcell << grapheme_cluster
108
117
  subsubcell_width += grapheme_cluster_width
109
118
  end
110
119
 
111
- subsubcells << style_and_align_cell_content(subsubcell)
120
+ subsubcells << style_and_align_cell_content(subsubcell, line_index)
121
+ line_index += 1
122
+ subsubcells
112
123
  end
113
124
  end
114
125
 
115
- def style_and_align_cell_content(content)
126
+ def style_and_align_cell_content(content, line_index)
116
127
  padding = Util.max(@width - Unicode::DisplayWidth.of(content), 0)
117
128
  left_padding, right_padding =
118
129
  case real_alignment
@@ -125,7 +136,7 @@ module Tabulo
125
136
  [padding, 0]
126
137
  end
127
138
 
128
- "#{' ' * left_padding}#{apply_styler(content)}#{' ' * right_padding}"
139
+ "#{' ' * left_padding}#{apply_styler(content, line_index)}#{' ' * right_padding}"
129
140
  end
130
141
 
131
142
  def real_alignment
@@ -6,6 +6,8 @@ module Tabulo
6
6
  attr_accessor :width
7
7
  attr_reader :header
8
8
  attr_reader :index
9
+ attr_reader :left_padding
10
+ attr_reader :right_padding
9
11
 
10
12
  def initialize(
11
13
  align_body:,
@@ -15,7 +17,9 @@ module Tabulo
15
17
  header:,
16
18
  header_styler:,
17
19
  index:,
20
+ left_padding:,
18
21
  padding_character:,
22
+ right_padding:,
19
23
  styler:,
20
24
  truncation_indicator:,
21
25
  width:)
@@ -26,12 +30,19 @@ module Tabulo
26
30
  @formatter = formatter
27
31
  @header = header
28
32
  @index = index
33
+ @left_padding = left_padding
34
+ @right_padding = right_padding
29
35
 
30
36
  @header_styler =
31
- if header_styler && (header_styler.arity == 2)
32
- -> (_, str, cell_data) { header_styler.call(str, cell_data.column_index) }
33
- elsif header_styler
34
- -> (_, str) { header_styler.call(str) }
37
+ if header_styler
38
+ case header_styler.arity
39
+ when 3
40
+ -> (_, str, cell_data, line_index) { header_styler.call(str, cell_data.column_index, line_index) }
41
+ when 2
42
+ -> (_, str, cell_data) { header_styler.call(str, cell_data.column_index) }
43
+ else
44
+ -> (_, str) { header_styler.call(str) }
45
+ end
35
46
  else
36
47
  -> (_, str) { str }
37
48
  end
@@ -43,14 +54,16 @@ module Tabulo
43
54
  end
44
55
 
45
56
  def header_cell
46
- if @header_styler.arity == 3
57
+ if @header_styler.arity >= 3
47
58
  cell_data = CellData.new(nil, nil, @index)
48
59
  end
49
60
  Cell.new(
50
61
  alignment: @align_header,
51
62
  cell_data: cell_data,
52
63
  formatter: -> (s) { s },
64
+ left_padding: @left_padding,
53
65
  padding_character: @padding_character,
66
+ right_padding: @right_padding,
54
67
  styler: @header_styler,
55
68
  truncation_indicator: @truncation_indicator,
56
69
  value: @header,
@@ -66,7 +79,9 @@ module Tabulo
66
79
  alignment: @align_body,
67
80
  cell_data: cell_data,
68
81
  formatter: @formatter,
82
+ left_padding: @left_padding,
69
83
  padding_character: @padding_character,
84
+ right_padding: @right_padding,
70
85
  styler: @styler,
71
86
  truncation_indicator: @truncation_indicator,
72
87
  value: body_cell_value(source, row_index: row_index, column_index: column_index),
@@ -82,6 +97,14 @@ module Tabulo
82
97
  end
83
98
  end
84
99
 
100
+ def padded_width
101
+ width + total_padding
102
+ end
103
+
104
+ def total_padding
105
+ @left_padding + @right_padding
106
+ end
107
+
85
108
  private
86
109
 
87
110
  def body_cell_data_required?
@@ -76,7 +76,8 @@ module Tabulo
76
76
  # either side of each column. If passed an Integer, then the given amount of padding is
77
77
  # applied to each side of each column. If passed a two-element Array, then the first element of the
78
78
  # Array indicates the amount of padding to apply to the left of each column, and the second
79
- # 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}.
80
81
  # @param [Integer, nil] column_width The default column width for columns in this
81
82
  # table, not excluding padding. If <tt>nil</tt>, then {DEFAULT_COLUMN_WIDTH} will be used.
82
83
  # @param [nil, #to_proc] formatter (:to_s.to_proc) The default formatter for columns in this
@@ -99,14 +100,26 @@ module Tabulo
99
100
  # will cause it to cease being valid Markdown when rendered, since Markdown engines do not generally
100
101
  # support adding a caption element (i.e. title) to tables.
101
102
  # @param [nil, #to_proc] title_styler (nil) A lambda or other callable object that will
102
- # determine the colors or other styling applied to the table title. If the table doesn't have
103
- # a title, this parameter has no effect. If passed a single-parameter callable, then that
104
- # callable will be called for each line of content within the title, and the resulting string
105
- # rendered in place of that line.
106
- # The extra width of the string returned by the <tt>title_styler</tt> is not taken into
107
- # consideration by the internal table width calculations involved in rendering the
108
- # table. Thus it can be used to apply ANSI escape codes to title content, to color the
109
- # title for example, without breaking the table formatting.
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
+ #
110
123
  # @param [nil, String] truncation_indicator Determines the character used to indicate that a
111
124
  # cell's content has been truncated. If omitted or passed <tt>nil</tt>,
112
125
  # defaults to {DEFAULT_TRUNCATION_INDICATOR}. If passed something other than <tt>nil</tt> or
@@ -142,7 +155,7 @@ module Tabulo
142
155
  @column_padding = (column_padding || DEFAULT_COLUMN_PADDING)
143
156
 
144
157
  @left_column_padding, @right_column_padding =
145
- Array === @column_padding ? @column_padding : [@column_padding, @column_padding]
158
+ (Array === @column_padding ? @column_padding : [@column_padding, @column_padding])
146
159
 
147
160
  @column_width = (column_width || DEFAULT_COLUMN_WIDTH)
148
161
  @formatter = formatter
@@ -210,7 +223,7 @@ module Tabulo
210
223
  # the column's label will also be used as its header text.
211
224
  # @param [nil, #to_proc] header_styler (nil) A lambda or other callable object that will
212
225
  # determine the colors or other styling applied to the header content. Can be passed
213
- # <tt>nil</tt>, or can be passed a callable that takes either 1 or 2 parameters:
226
+ # <tt>nil</tt>, or can be passed a callable that takes 1, 2 or 3 parameters:
214
227
  # * If passed <tt>nil</tt>, then no additional styling will be applied to the cell content
215
228
  # (other than what was already applied by the <tt>formatter</tt>).
216
229
  # * If passed a callable, then that callable will be called for each line of content within
@@ -227,9 +240,22 @@ module Tabulo
227
240
  # second parameter is an Integer representing the positional index of this header's {Column},
228
241
  # with the leftmost column having index 0, the next having index 1 etc.. This can be
229
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.
230
249
  #
231
250
  # Note that if the header content is truncated, then any <tt>header_styler</tt> will be applied to the
232
251
  # truncation indicator character as well as to the truncated content.
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.
233
259
  # @param [nil, #to_proc] styler (nil) A lambda or other callable object that will determine
234
260
  # the colors or other styling applied to the formatted value of the cell. Can be passed
235
261
  # <tt>nil</tt>, or can be passed a callable that takes either 2 or 3 parameters:
@@ -253,6 +279,12 @@ module Tabulo
253
279
  # {CellData#row_index} attribute can be inspected to determine whether the {Cell} is an
254
280
  # odd- or even-numbered {Row}, to arrange for different styling to be applied to
255
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.
256
288
  #
257
289
  # Note that if the content of a cell is truncated, then the whatever styling is applied by the
258
290
  # <tt>styler</tt> to the cell content will also be applied to the truncation indicator character.
@@ -273,10 +305,17 @@ module Tabulo
273
305
  # Table. (This is case-sensitive, but is insensitive to whether a String or Symbol is passed
274
306
  # to the label parameter.)
275
307
  def add_column(label, align_body: nil, align_header: nil, before: nil, formatter: nil,
276
- header: nil, header_styler: nil, styler: nil, width: nil, &extractor)
308
+ header: nil, header_styler: nil, padding: nil, styler: nil, width: nil, &extractor)
277
309
 
278
310
  column_label = normalize_column_label(label)
279
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
+
280
319
  if column_registry.include?(column_label)
281
320
  raise InvalidColumnLabelError, "Column label already used in this table."
282
321
  end
@@ -289,7 +328,9 @@ module Tabulo
289
328
  header: (header || label).to_s,
290
329
  header_styler: header_styler || @header_styler,
291
330
  index: column_registry.count,
331
+ left_padding: left_padding,
292
332
  padding_character: PADDING_CHARACTER,
333
+ right_padding: right_padding,
293
334
  styler: styler || @styler,
294
335
  truncation_indicator: @truncation_indicator,
295
336
  width: width || @column_width,
@@ -331,7 +372,7 @@ module Tabulo
331
372
  if column_registry.any?
332
373
  bottom_edge = horizontal_rule(:bottom)
333
374
  rows = map(&:to_s)
334
- 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])
335
376
  else
336
377
  ""
337
378
  end
@@ -389,7 +430,7 @@ module Tabulo
389
430
  # It may be that `:top`, `:middle` and `:bottom` all look the same. Whether
390
431
  # this is the case depends on the characters used for the table border.
391
432
  def horizontal_rule(position = :bottom)
392
- column_widths = get_columns.map { |column| column.width + total_column_padding }
433
+ column_widths = get_columns.map { |column| column.width + column.total_padding }
393
434
  @border_instance.horizontal_rule(column_widths, position)
394
435
  end
395
436
 
@@ -425,22 +466,27 @@ module Tabulo
425
466
  # Table will refuse to shrink itself.
426
467
  # @return [Table] the Table itself
427
468
  def pack(max_table_width: :auto)
428
- get_columns.each { |column| column.width = wrapped_width(column.header) }
469
+ get_columns.each { |column| column.width = Util.wrapped_width(column.header) }
429
470
 
430
471
  @sources.each_with_index do |source, row_index|
431
472
  get_columns.each_with_index do |column, column_index|
432
473
  cell = column.body_cell(source, row_index: row_index, column_index: column_index)
433
- cell_width = wrapped_width(cell.formatted_content)
474
+ cell_width = Util.wrapped_width(cell.formatted_content)
434
475
  column.width = Util.max(column.width, cell_width)
435
476
  end
436
477
  end
437
478
 
438
- if max_table_width
439
- shrink_to(max_table_width == :auto ? TTY::Screen.width : max_table_width)
440
- end
479
+ shrink_to(max_table_width == :auto ? TTY::Screen.width : max_table_width) if max_table_width
441
480
 
442
481
  if @title
443
- expand_to(Unicode::DisplayWidth.of(@title) + total_column_padding + (@border == :blank ? 0 : 2))
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
+ )
444
490
  end
445
491
 
446
492
  self
@@ -530,33 +576,14 @@ module Tabulo
530
576
  inner = format_row(cells, @wrap_body_cells_to)
531
577
 
532
578
  if @title && header == :top
533
- join_lines([
534
- horizontal_rule(:title_top),
535
- formatted_title,
536
- horizontal_rule(:title_bottom),
537
- formatted_header,
538
- horizontal_rule(:middle),
539
- inner
540
- ].reject(&:empty?))
579
+ Util.condense_lines([horizontal_rule(:title_top), formatted_title, horizontal_rule(:title_bottom),
580
+ formatted_header, horizontal_rule(:middle), inner])
541
581
  elsif header == :top
542
- join_lines([
543
- horizontal_rule(:top),
544
- formatted_header,
545
- horizontal_rule(:middle),
546
- inner
547
- ].reject(&:empty?))
582
+ Util.condense_lines([horizontal_rule(:top), formatted_header, horizontal_rule(:middle), inner])
548
583
  elsif header
549
- join_lines([
550
- horizontal_rule(:middle),
551
- formatted_header,
552
- horizontal_rule(:middle),
553
- inner
554
- ].reject(&:empty?))
584
+ Util.condense_lines([horizontal_rule(:middle), formatted_header, horizontal_rule(:middle), inner])
555
585
  elsif divider
556
- join_lines([
557
- horizontal_rule(:middle),
558
- inner
559
- ].reject(&:empty?))
586
+ Util.condense_lines([horizontal_rule(:middle), inner])
560
587
  else
561
588
  inner
562
589
  end
@@ -593,23 +620,34 @@ module Tabulo
593
620
  # @visibility private
594
621
  def formatted_title
595
622
  columns = get_columns
596
- num_fudged_columns = columns.count - 1
597
- basic_width = columns.inject(0) { |total_width, column| total_width + column.width }
623
+
598
624
  extra_for_internal_dividers = (@border == :blank ? 0 : 1)
599
- extra_for_internal_padding = @left_column_padding + @right_column_padding
600
- extra_total = num_fudged_columns * (extra_for_internal_dividers + extra_for_internal_padding)
601
- title_cell_width = basic_width + extra_total
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
+
602
632
  styler =
603
633
  if @title_styler
604
- -> (v, s) { @title_styler.call(s) }
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
605
640
  else
606
- -> (v, s) { s }
641
+ -> (_val, str) { str }
607
642
  end
643
+
608
644
  title_cell = Cell.new(
609
645
  alignment: @align_title,
610
646
  cell_data: nil,
611
647
  formatter: -> (s) { s },
648
+ left_padding: columns.first.left_padding,
612
649
  padding_character: PADDING_CHARACTER,
650
+ right_padding: columns.last.right_padding,
613
651
  styler: styler,
614
652
  truncation_indicator: @truncation_indicator,
615
653
  value: @title,
@@ -619,13 +657,13 @@ module Tabulo
619
657
  max_cell_height = cells.map(&:height).max
620
658
  row_height = ([nil, max_cell_height].compact.min || 1)
621
659
  subcell_stacks = cells.map do |cell|
622
- cell.padded_truncated_subcells(row_height, @left_column_padding, @right_column_padding)
660
+ cell.padded_truncated_subcells(row_height)
623
661
  end
624
662
  subrows = subcell_stacks.transpose.map do |subrow_components|
625
663
  @border_instance.join_cell_contents(subrow_components)
626
664
  end
627
665
 
628
- join_lines(subrows)
666
+ Util.join_lines(subrows)
629
667
  end
630
668
 
631
669
  # @!visibility private
@@ -642,10 +680,9 @@ module Tabulo
642
680
  def expand_to(min_table_width)
643
681
  columns = get_columns
644
682
  num_columns = columns.count
645
- total_columns_width = columns.inject(0) { |sum, column| sum + column.width }
646
- total_padding = num_columns * total_column_padding
683
+ total_columns_padded_width = columns.inject(0) { |sum, column| sum + column.padded_width }
647
684
  total_borders = num_columns + 1
648
- unadjusted_table_width = total_columns_width + total_padding + total_borders
685
+ unadjusted_table_width = total_columns_padded_width + total_borders
649
686
  required_increase = Util.max(min_table_width - unadjusted_table_width, 0)
650
687
 
651
688
  required_increase.times do
@@ -661,10 +698,10 @@ module Tabulo
661
698
  def shrink_to(max_table_width)
662
699
  columns = get_columns
663
700
  num_columns = columns.count
664
- total_columns_width = columns.inject(0) { |sum, column| sum + column.width }
665
- 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 }
666
703
  total_borders = num_columns + 1
667
- unadjusted_table_width = total_columns_width + total_padding + total_borders
704
+ unadjusted_table_width = total_columns_padded_width + total_borders
668
705
 
669
706
  # Ensure max table width is at least wide enough to accommodate table borders and padding
670
707
  # and one character of content.
@@ -681,11 +718,6 @@ module Tabulo
681
718
  end
682
719
  end
683
720
 
684
- # @!visibility private
685
- def total_column_padding
686
- @left_column_padding + @right_column_padding
687
- end
688
-
689
721
  # @!visibility private
690
722
  #
691
723
  # Formats a single header row or body row as a String.
@@ -703,18 +735,13 @@ module Tabulo
703
735
  max_cell_height = cells.map(&:height).max
704
736
  row_height = ([wrap_cells_to, max_cell_height].compact.min || 1)
705
737
  subcell_stacks = cells.map do |cell|
706
- cell.padded_truncated_subcells(row_height, @left_column_padding, @right_column_padding)
738
+ cell.padded_truncated_subcells(row_height)
707
739
  end
708
740
  subrows = subcell_stacks.transpose.map do |subrow_components|
709
741
  @border_instance.join_cell_contents(subrow_components)
710
742
  end
711
743
 
712
- join_lines(subrows)
713
- end
714
-
715
- # @!visibility private
716
- def join_lines(lines)
717
- lines.join($/) # join strings with cross-platform newline
744
+ Util.join_lines(subrows)
718
745
  end
719
746
 
720
747
  # @!visibility private
@@ -732,13 +759,5 @@ module Tabulo
732
759
  c
733
760
  end
734
761
 
735
- # @!visibility private
736
- # @return [Integer] the length of the longest segment of str when split by newlines
737
- def wrapped_width(str)
738
- segments = str.split($/)
739
- segments.inject(1) do |longest_length_so_far, segment|
740
- Util.max(longest_length_so_far, Unicode::DisplayWidth.of(segment))
741
- end
742
- end
743
762
  end
744
763
  end