tabulo 2.4.1 → 2.6.3

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.4.1
1
+ 2.6.3
data/lib/tabulo/border.rb CHANGED
@@ -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
@@ -115,6 +70,10 @@ module Tabulo
115
70
  def horizontal_rule(column_widths, position = :bottom)
116
71
  left, center, right, segment =
117
72
  case position
73
+ when :title_top
74
+ [@corner_top_left, @edge_top, @corner_top_right, @edge_top]
75
+ when :title_bottom
76
+ [@tee_left, @tee_top, @tee_right, @edge_top]
118
77
  when :top
119
78
  [@corner_top_left, @tee_top, @corner_top_right, @edge_top]
120
79
  when :middle
@@ -123,6 +82,11 @@ module Tabulo
123
82
  [@corner_bottom_left, @tee_bottom, @corner_bottom_right, @edge_bottom]
124
83
  end
125
84
  segments = column_widths.map { |width| segment * width }
85
+
86
+ # Prevent weird bottom edge of title if segments empty but right/left not empty, as in
87
+ # Markdown border.
88
+ left = right = "" if segments.all?(&:empty?)
89
+
126
90
  style("#{left}#{segments.join(center)}#{right}")
127
91
  end
128
92
 
@@ -138,7 +102,7 @@ module Tabulo
138
102
 
139
103
  def self.options(kind)
140
104
  opts = STYLES[kind]
141
- return opts if opts
105
+ return opts.to_h if opts
142
106
  raise InvalidBorderError
143
107
  end
144
108
 
data/lib/tabulo/cell.rb CHANGED
@@ -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
96
- formatted_content.split($/, -1).flat_map do |substr|
103
+ line_index = 0
104
+ formatted_content.split(Util::NEWLINE, -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
data/lib/tabulo/column.rb CHANGED
@@ -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?
data/lib/tabulo/table.rb CHANGED
@@ -35,21 +35,26 @@ module Tabulo
35
35
  # be unique. Each element of the Array will be used to create a column whose content is
36
36
  # created by calling the corresponding method on each element of sources. Note
37
37
  # the {#add_column} method is a much more flexible way to set up columns on the table.
38
- # @param [:left, :right, :center, :auto] align_body (:auto) Determines the alignment of body cell
38
+ # @param [:left, :right, :center, :auto] align_body Determines the alignment of body cell
39
39
  # (i.e. non-header) content within columns in this Table. Can be overridden for individual columns
40
40
  # using the <tt>align_body</tt> option passed to {#add_column}. If passed <tt>:auto</tt>,
41
41
  # alignment is determined by cell content, with numbers aligned right, booleans
42
42
  # center-aligned, and other values left-aligned.
43
- # @param [:left, :right, :center] align_header (:center) Determines the alignment of header text
43
+ # @param [:left, :right, :center] align_header 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 [:ascii, :markdown, :modern, :blank, nil] border (nil) Determines the characters used
46
+ # @param [:left, :right, :center] align_header Determines the alignment of the table
47
+ # title, if present.
48
+ # @param [:ascii, :markdown, :modern, :blank, nil] border 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
@@ -58,7 +63,7 @@ module Tabulo
58
63
  # borders and intersection characters consisting of whitespace only
59
64
  # - `:classic` Like `:ascii`, but does not have a horizontal line at the bottom of the
60
65
  # table. This reproduces the default behaviour in `tabulo` v1.
61
- # @param [nil, #to_proc] border_styler (nil) A lambda or other callable object taking
66
+ # @param [nil, #to_proc] border_styler A lambda or other callable object taking
62
67
  # a single parameter, representing a section of the table's borders (which for this purpose
63
68
  # include any horizontal and vertical lines inside the table), and returning a string.
64
69
  # If passed <tt>nil</tt>, then no additional styling will be applied to borders. If passed a
@@ -67,28 +72,54 @@ module Tabulo
67
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
- # @param [nil, Integer, Array] column_padding (1) Determines the amount of blank space with which to pad
75
+ # @param [nil, Integer, Array] column_padding 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
- # @param [nil, #to_proc] formatter (:to_s.to_proc) The default formatter for columns in this
83
+ # @param [nil, #to_proc] formatter The default formatter for columns in this
78
84
  # table. See `formatter` option of {#add_column} for details.
79
85
  # @param [:start, nil, Integer] header_frequency (:start) Controls the display of column headers.
80
86
  # If passed <tt>:start</tt>, headers will be shown at the top of the table only. If passed <tt>nil</tt>,
81
87
  # headers will not be shown. If passed an Integer N (> 0), headers will be shown at the top of the table,
82
88
  # then repeated every N rows.
83
- # @param [nil, #to_proc] header_styler (nil) The default header styler for columns in this
89
+ # @param [nil, #to_proc] header_styler The default header styler for columns in this
84
90
  # table. See `header_styler` option of {#add_column} for details.
85
- # @param [nil, Integer] row_divider_frequency (nil) Controls the display of horizontal row dividers within
91
+ # @param [nil, Integer] row_divider_frequency Controls the display of horizontal row dividers within
86
92
  # the table body. If passed <tt>nil</tt>, dividers will not be shown. If passed an Integer N (> 0),
87
93
  # dividers will be shown after every N rows. The characters used to form the dividers are
88
94
  # determined by the `border` option, and are the same as those used to form the bottom edge of the
89
95
  # header row.
90
- # @param [nil, #to_proc] styler (nil) The default styler for columns in this table. See `styler`
96
+ # @param [nil, #to_proc] styler The default styler for columns in this table. See `styler`
91
97
  # option of {#add_column} for details.
98
+ # @param [nil, String] title 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 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
@@ -149,24 +184,24 @@ module Tabulo
149
184
  # a method to be called on each item in the table sources to provide the content
150
185
  # for this column. If a String is passed as the label, then it will be converted to
151
186
  # a Symbol for the purpose of serving as this label.
152
- # @param [:left, :center, :right, :auto, nil] align_body (nil) Specifies how the cell body contents
187
+ # @param [:left, :center, :right, :auto, nil] align_body Specifies how the cell body contents
153
188
  # should be aligned. If <tt>nil</tt> is passed, then the alignment is determined
154
189
  # by the Table-level setting passed to the <tt>align_body</tt> option on Table initialization
155
190
  # (which itself defaults to <tt>:auto</tt>). Otherwise this option determines the alignment of
156
191
  # this column. If <tt>:auto</tt> is passed, the alignment is determined by the type of the cell
157
192
  # value, with numbers aligned right, booleans center-aligned, and other values left-aligned.
158
193
  # Note header text alignment is configured separately using the :align_header param.
159
- # @param [:left, :center, :right, nil] align_header (nil) Specifies how the header text
194
+ # @param [:left, :center, :right, nil] align_header Specifies how the header text
160
195
  # should be aligned. If <tt>nil</tt> is passed, then the alignment is determined
161
196
  # by the Table-level setting passed to the <tt>align_header</tt> (which itself defaults
162
197
  # to <tt>:center</tt>). Otherwise, this option determines the alignment of the header
163
198
  # content for this column.
164
- # @param [Symbol, String, Integer, nil] before (nil) The label of the column before (i.e. to
199
+ # @param [Symbol, String, Integer, nil] before The label of the column before (i.e. to
165
200
  # the left of) which the new column should inserted. If <tt>nil</tt> is passed, it will be
166
201
  # inserted after all other columns. If there is no column with the given label, then an
167
202
  # {InvalidColumnLabelError} will be raised. A non-Integer labelled column can be identified
168
203
  # in either String or Symbol form for this purpose.
169
- # @param [#to_proc] formatter (nil) A lambda or other callable object that
204
+ # @param [#to_proc] formatter 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
206
  # is distinct from the extractor and the styler (see below).
172
207
  # For example, if the extractor for this column generates a Date, then the formatter might format
@@ -184,11 +219,11 @@ module Tabulo
184
219
  # whether the {Cell} is an odd- or even-numbered {Row}, to arrange for different formatting
185
220
  # to be applied to alternating rows.
186
221
  # See the documentation for {CellData} for more.
187
- # @param [nil, #to_s] header (nil) Text to be displayed in the column header. If passed nil,
222
+ # @param [nil, #to_s] header Text to be displayed in the column header. If passed nil,
188
223
  # the column's label will also be used as its header text.
189
224
  # @param [nil, #to_proc] header_styler (nil) A lambda or other callable object that will
190
225
  # determine the colors or other styling applied to the header content. Can be passed
191
- # <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:
192
227
  # * If passed <tt>nil</tt>, then no additional styling will be applied to the cell content
193
228
  # (other than what was already applied by the <tt>formatter</tt>).
194
229
  # * If passed a callable, then that callable will be called for each line of content within
@@ -205,10 +240,23 @@ module Tabulo
205
240
  # second parameter is an Integer representing the positional index of this header's {Column},
206
241
  # with the leftmost column having index 0, the next having index 1 etc.. This can be
207
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.
208
249
  #
209
250
  # Note that if the header content is truncated, then any <tt>header_styler</tt> will be applied to the
210
251
  # truncation indicator character as well as to the truncated content.
211
- # @param [nil, #to_proc] styler (nil) A lambda or other callable object that will determine
252
+ # @param [nil, Integer, Array] padding 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 A lambda or other callable object that will determine
212
260
  # the colors or other styling applied to the formatted value of the cell. Can be passed
213
261
  # <tt>nil</tt>, or can be passed a callable that takes either 2 or 3 parameters:
214
262
  # * If passed <tt>nil</tt>, then no additional styling will be applied to the cell content
@@ -231,10 +279,16 @@ module Tabulo
231
279
  # {CellData#row_index} attribute can be inspected to determine whether the {Cell} is an
232
280
  # odd- or even-numbered {Row}, to arrange for different styling to be applied to
233
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.
234
288
  #
235
289
  # Note that if the content of a cell is truncated, then the whatever styling is applied by the
236
290
  # <tt>styler</tt> to the cell content will also be applied to the truncation indicator character.
237
- # @param [Integer] width (nil) Specifies the width of the column, excluding padding. If
291
+ # @param [Integer] width Specifies the width of the column, excluding padding. If
238
292
  # nil, then the column will take the width provided by the `column_width` param
239
293
  # with which the Table was initialized.
240
294
  # @param [#to_proc] extractor A block or other callable that will be passed each of the {Table}
@@ -251,10 +305,17 @@ module Tabulo
251
305
  # Table. (This is case-sensitive, but is insensitive to whether a String or Symbol is passed
252
306
  # to the label parameter.)
253
307
  def add_column(label, align_body: nil, align_header: nil, before: nil, formatter: nil,
254
- header: nil, header_styler: nil, styler: nil, width: nil, &extractor)
308
+ header: nil, header_styler: nil, padding: nil, styler: nil, width: nil, &extractor)
255
309
 
256
310
  column_label = normalize_column_label(label)
257
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
+
258
319
  if column_registry.include?(column_label)
259
320
  raise InvalidColumnLabelError, "Column label already used in this table."
260
321
  end
@@ -267,7 +328,9 @@ module Tabulo
267
328
  header: (header || label).to_s,
268
329
  header_styler: header_styler || @header_styler,
269
330
  index: column_registry.count,
331
+ left_padding: left_padding,
270
332
  padding_character: PADDING_CHARACTER,
333
+ right_padding: right_padding,
271
334
  styler: styler || @styler,
272
335
  truncation_indicator: @truncation_indicator,
273
336
  width: width || @column_width,
@@ -309,7 +372,7 @@ module Tabulo
309
372
  if column_registry.any?
310
373
  bottom_edge = horizontal_rule(:bottom)
311
374
  rows = map(&:to_s)
312
- 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])
313
376
  else
314
377
  ""
315
378
  end
@@ -339,7 +402,8 @@ module Tabulo
339
402
  end
340
403
  end
341
404
 
342
- # @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.
343
407
  def formatted_header
344
408
  cells = get_columns.map(&:header_cell)
345
409
  format_row(cells, @wrap_header_cells_to)
@@ -348,9 +412,11 @@ module Tabulo
348
412
  # Produce a horizontal dividing line suitable for printing at the top, bottom or middle
349
413
  # of the table.
350
414
  #
351
- # @param [:top, :middle, :bottom] position (:bottom) Specifies the position
352
- # for which the resulting horizontal dividing line is intended to be printed.
353
- # This determines the border characters that are used to construct the line.
415
+ # @param [:top, :middle, :bottom, :title_top, :title_bottom] position
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.
354
420
  # @return [String] an "ASCII" graphical representation of a horizontal
355
421
  # dividing line.
356
422
  # @example Print a horizontal divider between each pair of rows, and again at the bottom:
@@ -364,14 +430,17 @@ module Tabulo
364
430
  # It may be that `:top`, `:middle` and `:bottom` all look the same. Whether
365
431
  # this is the case depends on the characters used for the table border.
366
432
  def horizontal_rule(position = :bottom)
367
- column_widths = get_columns.map { |column| column.width + total_column_padding }
433
+ column_widths = get_columns.map { |column| column.width + column.total_padding }
368
434
  @border_instance.horizontal_rule(column_widths, position)
369
435
  end
370
436
 
371
- # 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
372
438
  # its header text as well as the formatted content of each its cells for the entire
373
439
  # collection, together with a single character of padding on either side of the column,
374
- # 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.
375
444
  #
376
445
  # Note that calling this method will cause the entire source Enumerable to
377
446
  # be traversed and all the column extractors and formatters to be applied in order
@@ -382,7 +451,7 @@ module Tabulo
382
451
  # is called. If the source Enumerable changes between that point, and the point when
383
452
  # the Table is printed, then columns will *not* be resized yet again on printing.
384
453
  #
385
- # @param [nil, Numeric] max_table_width (:auto) With no args, or if passed <tt>:auto</tt>,
454
+ # @param [nil, Numeric] max_table_width With no args, or if passed <tt>:auto</tt>,
386
455
  # stops the total table width (including padding and borders) from expanding beyond the
387
456
  # bounds of the terminal screen.
388
457
  # If passed <tt>nil</tt>, the table width will not be capped.
@@ -397,18 +466,27 @@ module Tabulo
397
466
  # Table will refuse to shrink itself.
398
467
  # @return [Table] the Table itself
399
468
  def pack(max_table_width: :auto)
400
- get_columns.each { |column| column.width = wrapped_width(column.header) }
469
+ get_columns.each { |column| column.width = Util.wrapped_width(column.header) }
401
470
 
402
471
  @sources.each_with_index do |source, row_index|
403
472
  get_columns.each_with_index do |column, column_index|
404
473
  cell = column.body_cell(source, row_index: row_index, column_index: column_index)
405
- cell_width = wrapped_width(cell.formatted_content)
474
+ cell_width = Util.wrapped_width(cell.formatted_content)
406
475
  column.width = Util.max(column.width, cell_width)
407
476
  end
408
477
  end
409
478
 
410
- if max_table_width
411
- 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
+ )
412
490
  end
413
491
 
414
492
  self
@@ -433,8 +511,9 @@ module Tabulo
433
511
  # The following options are the same as the keyword params for the {#initialize} method for
434
512
  # {Table}: <tt>column_width</tt>, <tt>column_padding</tt>, <tt>formatter</tt>,
435
513
  # <tt>header_frequency</tt>, <tt>row_divider_frequency</tt>, <tt>wrap_header_cells_to</tt>,
436
- # <tt>wrap_body_cells_to</tt>, <tt>border</tt>, <tt>border_styler</tt>, <tt>truncation_indicator</tt>,
437
- # <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>.
438
517
  # These are applied in the same way as documented for {#initialize}, when
439
518
  # creating the new, transposed Table. Any options not specified explicitly in the call to {#transpose}
440
519
  # will inherit their values from the original {Table} (with the exception of settings
@@ -444,13 +523,13 @@ module Tabulo
444
523
  # new Table, which contains the names of "fields" (corresponding to the original Table's
445
524
  # column headings). If this is not provided, then by default this column will be made just
446
525
  # wide enough to accommodate its contents.
447
- # @option opts [String] :field_names_header ("") By default the left-most column will have a
526
+ # @option opts [String] :field_names_header By default the left-most column will have a
448
527
  # blank header; but this can be overridden by passing a String to this option.
449
- # @option opts [:left, :center, :right] :field_names_header_alignment (:right) Specifies how the
528
+ # @option opts [:left, :center, :right] :field_names_header_alignment Specifies how the
450
529
  # header text of the left-most column (if it has header text) should be aligned.
451
- # @option opts [:left, :center, :right] :field_names_body_alignment (:right) Specifies how the
530
+ # @option opts [:left, :center, :right] :field_names_body_alignment Specifies how the
452
531
  # body text of the left-most column should be aligned.
453
- # @option opts [#to_proc] :headers (:to_s.to_proc) A lambda or other callable object that
532
+ # @option opts [#to_proc] :headers A lambda or other callable object that
454
533
  # will be passed in turn each of the elements of the current Table's <tt>sources</tt>
455
534
  # Enumerable, to determine the text to be displayed in the header of each column of the
456
535
  # new Table (other than the left-most column's header, which is determined as described
@@ -458,9 +537,9 @@ module Tabulo
458
537
  # @return [Table] a new {Table}
459
538
  # @raise [InvalidBorderError] if invalid argument passed to `border` parameter.
460
539
  def transpose(opts = {})
461
- default_opts = [:align_body, :align_header, :border, :border_styler, :column_padding, :column_width,
462
- :formatter, :header_frequency, :row_divider_frequency, :truncation_indicator, :wrap_body_cells_to,
463
- :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|
464
543
  [sym, instance_variable_get("@#{sym}")]
465
544
  end.to_h
466
545
 
@@ -496,25 +575,15 @@ module Tabulo
496
575
  cells = get_columns.map.with_index { |c, i| c.body_cell(source, row_index: index, column_index: i) }
497
576
  inner = format_row(cells, @wrap_body_cells_to)
498
577
 
499
- if header == :top
500
- join_lines([
501
- horizontal_rule(:top),
502
- formatted_header,
503
- horizontal_rule(:middle),
504
- inner
505
- ].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])
506
583
  elsif header
507
- join_lines([
508
- horizontal_rule(:middle),
509
- formatted_header,
510
- horizontal_rule(:middle),
511
- inner
512
- ].reject(&:empty?))
584
+ Util.condense_lines([horizontal_rule(:middle), formatted_header, horizontal_rule(:middle), inner])
513
585
  elsif divider
514
- join_lines([
515
- horizontal_rule(:middle),
516
- inner
517
- ].reject(&:empty?))
586
+ Util.condense_lines([horizontal_rule(:middle), inner])
518
587
  else
519
588
  inner
520
589
  end
@@ -548,6 +617,55 @@ module Tabulo
548
617
  @column_registry[label] = column
549
618
  end
550
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
+
551
669
  # @!visibility private
552
670
  def normalize_column_label(label)
553
671
  case label
@@ -558,14 +676,32 @@ module Tabulo
558
676
  end
559
677
  end
560
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
+
561
697
  # @!visibility private
562
698
  def shrink_to(max_table_width)
563
699
  columns = get_columns
564
700
  num_columns = columns.count
565
- total_columns_width = columns.inject(0) { |sum, column| sum + column.width }
566
- 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 }
567
703
  total_borders = num_columns + 1
568
- unadjusted_table_width = total_columns_width + total_padding + total_borders
704
+ unadjusted_table_width = total_columns_padded_width + total_borders
569
705
 
570
706
  # Ensure max table width is at least wide enough to accommodate table borders and padding
571
707
  # and one character of content.
@@ -582,11 +718,6 @@ module Tabulo
582
718
  end
583
719
  end
584
720
 
585
- # @!visibility private
586
- def total_column_padding
587
- @left_column_padding + @right_column_padding
588
- end
589
-
590
721
  # @!visibility private
591
722
  #
592
723
  # Formats a single header row or body row as a String.
@@ -604,18 +735,13 @@ module Tabulo
604
735
  max_cell_height = cells.map(&:height).max
605
736
  row_height = ([wrap_cells_to, max_cell_height].compact.min || 1)
606
737
  subcell_stacks = cells.map do |cell|
607
- cell.padded_truncated_subcells(row_height, @left_column_padding, @right_column_padding)
738
+ cell.padded_truncated_subcells(row_height)
608
739
  end
609
740
  subrows = subcell_stacks.transpose.map do |subrow_components|
610
741
  @border_instance.join_cell_contents(subrow_components)
611
742
  end
612
743
 
613
- join_lines(subrows)
614
- end
615
-
616
- # @!visibility private
617
- def join_lines(lines)
618
- lines.join($/) # join strings with cross-platform newline
744
+ Util.join_lines(subrows)
619
745
  end
620
746
 
621
747
  # @!visibility private
@@ -633,13 +759,5 @@ module Tabulo
633
759
  c
634
760
  end
635
761
 
636
- # @!visibility private
637
- # @return [Integer] the length of the longest segment of str when split by newlines
638
- def wrapped_width(str)
639
- segments = str.split($/)
640
- segments.inject(1) do |longest_length_so_far, segment|
641
- Util.max(longest_length_so_far, Unicode::DisplayWidth.of(segment))
642
- end
643
- end
644
762
  end
645
763
  end