tabulo 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ackrc +10 -0
- data/.gitignore +12 -0
- data/.rdoc_options +22 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +249 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1353 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/_config.yml +1 -0
- data/assets/social_media_preview/table.png +0 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/tabulo.rb +10 -0
- data/lib/tabulo/border.rb +164 -0
- data/lib/tabulo/cell.rb +155 -0
- data/lib/tabulo/cell_data.rb +14 -0
- data/lib/tabulo/column.rb +114 -0
- data/lib/tabulo/deprecation.rb +33 -0
- data/lib/tabulo/exceptions.rb +12 -0
- data/lib/tabulo/row.rb +51 -0
- data/lib/tabulo/table.rb +763 -0
- data/lib/tabulo/util.rb +45 -0
- data/lib/tabulo/version.rb +3 -0
- data/tabulo.gemspec +43 -0
- metadata +227 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module Tabulo
|
2
|
+
|
3
|
+
# @!visibility private
|
4
|
+
module Deprecation
|
5
|
+
|
6
|
+
# @!visibility private
|
7
|
+
def self.skipping_warnings
|
8
|
+
@skipping_warnings ||= false
|
9
|
+
end
|
10
|
+
|
11
|
+
# @!visibility private
|
12
|
+
def self.skipping_warnings=(v)
|
13
|
+
@skipping_warnings = v
|
14
|
+
end
|
15
|
+
|
16
|
+
# @!visibility private
|
17
|
+
def self.without_warnings
|
18
|
+
original = skipping_warnings
|
19
|
+
self.skipping_warnings = true
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
self.skipping_warnings = original
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!visibility private
|
26
|
+
def self.warn(deprecated, replacement, stack_level = 1)
|
27
|
+
return if skipping_warnings
|
28
|
+
|
29
|
+
kaller = Kernel.caller[stack_level]
|
30
|
+
Kernel.warn "#{kaller}: [DEPRECATION] #{deprecated} is deprecated. Please use #{replacement} instead."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Tabulo
|
2
|
+
|
3
|
+
# Error indicating that the label of a column is invalid.
|
4
|
+
class InvalidColumnLabelError < StandardError; end
|
5
|
+
|
6
|
+
# Error indicating that an attempt was made to use an invalid truncation indicator for
|
7
|
+
# the table.
|
8
|
+
class InvalidTruncationIndicatorError < StandardError; end
|
9
|
+
|
10
|
+
# Error indicating the table border configuration is invalid.
|
11
|
+
class InvalidBorderError < StandardError; end
|
12
|
+
end
|
data/lib/tabulo/row.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Tabulo
|
2
|
+
|
3
|
+
class Row
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# @return the element of the {Table}'s underlying enumerable to which this {Row} corresponds
|
7
|
+
attr_reader :source
|
8
|
+
|
9
|
+
# @!visibility private
|
10
|
+
def initialize(table, source, divider:, header:, index:)
|
11
|
+
@table = table
|
12
|
+
@source = source
|
13
|
+
@divider = divider
|
14
|
+
@header = header
|
15
|
+
@index = index
|
16
|
+
end
|
17
|
+
|
18
|
+
# Calls the given block once for each {Cell} in the {Row}, passing that {Cell} as parameter.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# table = Tabulo::Table.new([1, 10], columns: %i(itself even?))
|
22
|
+
# row = table.first
|
23
|
+
# row.each do |cell|
|
24
|
+
# puts cell.value # => 1, => false
|
25
|
+
# end
|
26
|
+
def each
|
27
|
+
@table.column_registry.each_with_index do |(_, column), column_index|
|
28
|
+
yield column.body_cell(@source, row_index: @index, column_index: column_index)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return a String being an "ASCII" graphical representation of the {Row}, including
|
33
|
+
# any column headers or row divider that appear just above it in the {Table} (depending on where
|
34
|
+
# this Row is in the {Table}, and how the {Table} was configured with respect to header frequency
|
35
|
+
# and divider frequency).
|
36
|
+
def to_s
|
37
|
+
if @table.column_registry.any?
|
38
|
+
@table.formatted_body_row(@source, divider: @divider, header: @header, index: @index)
|
39
|
+
else
|
40
|
+
""
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return a Hash representation of the {Row}, with column labels acting as keys and the {Cell}s the values.
|
45
|
+
def 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
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/tabulo/table.rb
ADDED
@@ -0,0 +1,763 @@
|
|
1
|
+
require "tty-screen"
|
2
|
+
require "unicode/display_width"
|
3
|
+
|
4
|
+
module Tabulo
|
5
|
+
|
6
|
+
# Represents a table primarily intended for "pretty-printing" in a fixed-width font.
|
7
|
+
#
|
8
|
+
# A Table is also an Enumerable, of which each element is a {Row}.
|
9
|
+
class Table
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
# @!visibility public
|
13
|
+
DEFAULT_BORDER = :ascii
|
14
|
+
|
15
|
+
# @!visibility public
|
16
|
+
DEFAULT_COLUMN_WIDTH = 12
|
17
|
+
|
18
|
+
# @!visibility public
|
19
|
+
DEFAULT_COLUMN_PADDING = 1
|
20
|
+
|
21
|
+
# @!visibility public
|
22
|
+
DEFAULT_TRUNCATION_INDICATOR = "~"
|
23
|
+
|
24
|
+
# @!visibility private
|
25
|
+
PADDING_CHARACTER = " "
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
attr_reader :column_registry
|
29
|
+
|
30
|
+
# @return [Enumerable] the underlying enumerable from which the table derives its data
|
31
|
+
attr_accessor :sources
|
32
|
+
|
33
|
+
# @param [Enumerable] sources the underlying Enumerable from which the table will derive its data
|
34
|
+
# @param [Array[Symbol]] columns Specifies the initial columns. The Symbols provided must
|
35
|
+
# be unique. Each element of the Array will be used to create a column whose content is
|
36
|
+
# created by calling the corresponding method on each element of sources. Note
|
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
|
39
|
+
# (i.e. non-header) content within columns in this Table. Can be overridden for individual columns
|
40
|
+
# using the <tt>align_body</tt> option passed to {#add_column}. If passed <tt>:auto</tt>,
|
41
|
+
# alignment is determined by cell content, with numbers aligned right, booleans
|
42
|
+
# center-aligned, and other values left-aligned.
|
43
|
+
# @param [:left, :right, :center] align_header (:center) Determines the alignment of header text
|
44
|
+
# for columns in this Table. Can be overridden for individual columns using the
|
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.
|
48
|
+
# @param [:ascii, :markdown, :modern, :blank, nil] border (nil) Determines the characters used
|
49
|
+
# for the Table border, including both the characters around the outside of table, and the lines drawn
|
50
|
+
# within the table to separate columns from each other and the header row from the Table body.
|
51
|
+
# If <tt>nil</tt>, then the value of {DEFAULT_BORDER} will be used.
|
52
|
+
# Possible values are:
|
53
|
+
# - `:ascii` Uses ASCII characters only
|
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.
|
58
|
+
# - `:modern` Uses non-ASCII Unicode characters to render a border with smooth continuous lines
|
59
|
+
# - `:blank` No border characters are rendered
|
60
|
+
# - `:reduced_ascii` Like `:ascii`, but without left or right borders, and with internal vertical
|
61
|
+
# borders and intersection characters consisting of whitespace only
|
62
|
+
# - `:reduced_modern` Like `:modern`, but without left or right borders, and with internal vertical
|
63
|
+
# borders and intersection characters consisting of whitespace only
|
64
|
+
# - `:classic` Like `:ascii`, but does not have a horizontal line at the bottom of the
|
65
|
+
# table. This reproduces the default behaviour in `tabulo` v1.
|
66
|
+
# @param [nil, #to_proc] border_styler (nil) A lambda or other callable object taking
|
67
|
+
# a single parameter, representing a section of the table's borders (which for this purpose
|
68
|
+
# include any horizontal and vertical lines inside the table), and returning a string.
|
69
|
+
# If passed <tt>nil</tt>, then no additional styling will be applied to borders. If passed a
|
70
|
+
# callable, then that callable will be called for each border section, with the
|
71
|
+
# resulting string rendered in place of that border. The extra width of the string returned by the
|
72
|
+
# <tt>border_styler</tt> is not taken into consideration by the internal table rendering calculations
|
73
|
+
# Thus it can be used to apply ANSI escape codes to border characters, to colour the borders
|
74
|
+
# for example, without breaking the table formatting.
|
75
|
+
# @param [nil, Integer, Array] column_padding (1) Determines the amount of blank space with which to pad
|
76
|
+
# either side of each column. If passed an Integer, then the given amount of padding is
|
77
|
+
# applied to each side of each column. If passed a two-element Array, then the first element of the
|
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. This setting can be overridden for
|
80
|
+
# individual columns using the `padding` option of {#add_column}.
|
81
|
+
# @param [Integer, nil] column_width The default column width for columns in this
|
82
|
+
# table, not excluding padding. If <tt>nil</tt>, then {DEFAULT_COLUMN_WIDTH} will be used.
|
83
|
+
# @param [nil, #to_proc] formatter (:to_s.to_proc) The default formatter for columns in this
|
84
|
+
# table. See `formatter` option of {#add_column} for details.
|
85
|
+
# @param [:start, nil, Integer] header_frequency (:start) Controls the display of column headers.
|
86
|
+
# If passed <tt>:start</tt>, headers will be shown at the top of the table only. If passed <tt>nil</tt>,
|
87
|
+
# headers will not be shown. If passed an Integer N (> 0), headers will be shown at the top of the table,
|
88
|
+
# then repeated every N rows.
|
89
|
+
# @param [nil, #to_proc] header_styler (nil) The default header styler for columns in this
|
90
|
+
# table. See `header_styler` option of {#add_column} for details.
|
91
|
+
# @param [nil, Integer] row_divider_frequency (nil) Controls the display of horizontal row dividers within
|
92
|
+
# the table body. If passed <tt>nil</tt>, dividers will not be shown. If passed an Integer N (> 0),
|
93
|
+
# dividers will be shown after every N rows. The characters used to form the dividers are
|
94
|
+
# determined by the `border` option, and are the same as those used to form the bottom edge of the
|
95
|
+
# header row.
|
96
|
+
# @param [nil, #to_proc] styler (nil) The default styler for columns in this table. See `styler`
|
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
|
+
#
|
123
|
+
# @param [nil, String] truncation_indicator Determines the character used to indicate that a
|
124
|
+
# cell's content has been truncated. If omitted or passed <tt>nil</tt>,
|
125
|
+
# defaults to {DEFAULT_TRUNCATION_INDICATOR}. If passed something other than <tt>nil</tt> or
|
126
|
+
# a single-character String, raises {InvalidTruncationIndicatorError}.
|
127
|
+
# @param [nil, Integer] wrap_body_cells_to Controls wrapping behaviour for table cells (excluding
|
128
|
+
# headers), if their content is longer than the column's fixed width. If passed <tt>nil</tt>, content will
|
129
|
+
# be wrapped for as many rows as required to accommodate it. If passed an Integer N (> 0), content will be
|
130
|
+
# wrapped up to N rows and then truncated thereafter.
|
131
|
+
# headers), if their content is longer than the column's fixed width. If passed <tt>nil</tt>, content will
|
132
|
+
# be wrapped for as many rows as required to accommodate it. If passed an Integer N (> 0), content will be
|
133
|
+
# wrapped up to N rows and then truncated thereafter.
|
134
|
+
# @param [nil, Integer] wrap_header_cells_to Controls wrapping behaviour for header
|
135
|
+
# cells if the content thereof is longer than the column's fixed width. If passed <tt>nil</tt> (default),
|
136
|
+
# content will be wrapped for as many rows as required to accommodate it. If passed an Integer N (> 0),
|
137
|
+
# content will be wrapped up to N rows and then truncated thereafter.
|
138
|
+
# @return [Table] a new {Table}
|
139
|
+
# @raise [InvalidColumnLabelError] if non-unique Symbols are provided to columns.
|
140
|
+
# @raise [InvalidBorderError] if invalid option passed to `border` parameter.
|
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,
|
143
|
+
header_frequency: :start, header_styler: nil, row_divider_frequency: nil, styler: nil,
|
144
|
+
title: nil, title_styler: nil, truncation_indicator: nil, wrap_body_cells_to: nil,
|
145
|
+
wrap_header_cells_to: nil)
|
146
|
+
|
147
|
+
@sources = sources
|
148
|
+
|
149
|
+
@align_body = align_body
|
150
|
+
@align_header = align_header
|
151
|
+
@align_title = align_title
|
152
|
+
@border = (border || DEFAULT_BORDER)
|
153
|
+
@border_styler = border_styler
|
154
|
+
@border_instance = Border.from(@border, @border_styler)
|
155
|
+
@column_padding = (column_padding || DEFAULT_COLUMN_PADDING)
|
156
|
+
|
157
|
+
@left_column_padding, @right_column_padding =
|
158
|
+
(Array === @column_padding ? @column_padding : [@column_padding, @column_padding])
|
159
|
+
|
160
|
+
@column_width = (column_width || DEFAULT_COLUMN_WIDTH)
|
161
|
+
@formatter = formatter
|
162
|
+
@header_frequency = header_frequency
|
163
|
+
@header_styler = header_styler
|
164
|
+
@row_divider_frequency = row_divider_frequency
|
165
|
+
@styler = styler
|
166
|
+
@title = title
|
167
|
+
@title_styler = title_styler
|
168
|
+
@truncation_indicator = validate_character(truncation_indicator,
|
169
|
+
DEFAULT_TRUNCATION_INDICATOR, InvalidTruncationIndicatorError, "truncation indicator")
|
170
|
+
@wrap_body_cells_to = wrap_body_cells_to
|
171
|
+
@wrap_header_cells_to = wrap_header_cells_to
|
172
|
+
|
173
|
+
@column_registry = { }
|
174
|
+
columns.each { |item| add_column(item) }
|
175
|
+
|
176
|
+
yield self if block_given?
|
177
|
+
end
|
178
|
+
|
179
|
+
# Adds a column to the Table.
|
180
|
+
#
|
181
|
+
# @param [Symbol, String, Integer] label A unique identifier for this column, which by
|
182
|
+
# default will also be used as the column header text (see also the header param). If the
|
183
|
+
# extractor argument is not also provided, then the label argument should correspond to
|
184
|
+
# a method to be called on each item in the table sources to provide the content
|
185
|
+
# for this column. If a String is passed as the label, then it will be converted to
|
186
|
+
# a Symbol for the purpose of serving as this label.
|
187
|
+
# @param [:left, :center, :right, :auto, nil] align_body (nil) Specifies how the cell body contents
|
188
|
+
# should be aligned. If <tt>nil</tt> is passed, then the alignment is determined
|
189
|
+
# by the Table-level setting passed to the <tt>align_body</tt> option on Table initialization
|
190
|
+
# (which itself defaults to <tt>:auto</tt>). Otherwise this option determines the alignment of
|
191
|
+
# this column. If <tt>:auto</tt> is passed, the alignment is determined by the type of the cell
|
192
|
+
# value, with numbers aligned right, booleans center-aligned, and other values left-aligned.
|
193
|
+
# Note header text alignment is configured separately using the :align_header param.
|
194
|
+
# @param [:left, :center, :right, nil] align_header (nil) Specifies how the header text
|
195
|
+
# should be aligned. If <tt>nil</tt> is passed, then the alignment is determined
|
196
|
+
# by the Table-level setting passed to the <tt>align_header</tt> (which itself defaults
|
197
|
+
# to <tt>:center</tt>). Otherwise, this option determines the alignment of the header
|
198
|
+
# content for this column.
|
199
|
+
# @param [Symbol, String, Integer, nil] before (nil) The label of the column before (i.e. to
|
200
|
+
# the left of) which the new column should inserted. If <tt>nil</tt> is passed, it will be
|
201
|
+
# inserted after all other columns. If there is no column with the given label, then an
|
202
|
+
# {InvalidColumnLabelError} will be raised. A non-Integer labelled column can be identified
|
203
|
+
# in either String or Symbol form for this purpose.
|
204
|
+
# @param [#to_proc] formatter (nil) A lambda or other callable object that
|
205
|
+
# will be passed the calculated value of each cell to determine how it should be displayed. This
|
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.
|
222
|
+
# @param [nil, #to_s] header (nil) Text to be displayed in the column header. If passed nil,
|
223
|
+
# the column's label will also be used as its header text.
|
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
|
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.
|
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
|
+
#
|
289
|
+
# Note that if the content of a cell is truncated, then the whatever styling is applied by the
|
290
|
+
# <tt>styler</tt> to the cell content will also be applied to the truncation indicator character.
|
291
|
+
# @param [Integer] width (nil) Specifies the width of the column, excluding padding. If
|
292
|
+
# nil, then the column will take the width provided by the `column_width` param
|
293
|
+
# with which the Table was initialized.
|
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..
|
304
|
+
# @raise [InvalidColumnLabelError] if label has already been used for another column in this
|
305
|
+
# Table. (This is case-sensitive, but is insensitive to whether a String or Symbol is passed
|
306
|
+
# to the label parameter.)
|
307
|
+
def add_column(label, align_body: nil, align_header: nil, before: nil, formatter: nil,
|
308
|
+
header: nil, header_styler: nil, padding: nil, styler: nil, width: nil, &extractor)
|
309
|
+
|
310
|
+
column_label = normalize_column_label(label)
|
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
|
+
|
319
|
+
if column_registry.include?(column_label)
|
320
|
+
raise InvalidColumnLabelError, "Column label already used in this table."
|
321
|
+
end
|
322
|
+
|
323
|
+
column = Column.new(
|
324
|
+
align_body: align_body || @align_body,
|
325
|
+
align_header: align_header || @align_header,
|
326
|
+
extractor: extractor || label.to_proc,
|
327
|
+
formatter: formatter || @formatter,
|
328
|
+
header: (header || label).to_s,
|
329
|
+
header_styler: header_styler || @header_styler,
|
330
|
+
index: column_registry.count,
|
331
|
+
left_padding: left_padding,
|
332
|
+
padding_character: PADDING_CHARACTER,
|
333
|
+
right_padding: right_padding,
|
334
|
+
styler: styler || @styler,
|
335
|
+
truncation_indicator: @truncation_indicator,
|
336
|
+
width: width || @column_width,
|
337
|
+
)
|
338
|
+
|
339
|
+
if before == nil
|
340
|
+
add_column_final(column, column_label)
|
341
|
+
else
|
342
|
+
add_column_before(column, column_label, before)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Removes the column identifed by the passed label.
|
347
|
+
#
|
348
|
+
# @example
|
349
|
+
# table = Table.new(1..10, :itself, :even?, :odd?)
|
350
|
+
# table.add_column(:even2, header: "even?") { |n| n.even? }
|
351
|
+
# table.remove_column(:even2)
|
352
|
+
# table.remove_column(:odd?)
|
353
|
+
#
|
354
|
+
# @param [Symbol, String, Integer] label The unique identifier for the column to be removed,
|
355
|
+
# corresponding to the label that was passed as the first parameter to {#add_column} (or was
|
356
|
+
# used in the table initializer) when the column was originally added. For columns that were
|
357
|
+
# originally added with a String or Symbol label, either a String or Symbol form of that label
|
358
|
+
# can be passed to {#remove_column}, indifferently. For example, if the label passed to
|
359
|
+
# {#add_column} had been `"height"`, then that column could be removed by passing either
|
360
|
+
# `"height"` or `:height` to {#remove_column}. (However, if an Integer was originally passed
|
361
|
+
# as the label to {#add_column}, then only that same Integer, as an Integer, can be passed to
|
362
|
+
# {#remove_column} to remove that column.)
|
363
|
+
# @return [true, false] If the label identifies a column in the table, then the column will be
|
364
|
+
# removed and true will be returned; otherwise no column will be removed, and false will be returned.
|
365
|
+
def remove_column(label)
|
366
|
+
!!column_registry.delete(Integer === label ? label : label.to_sym)
|
367
|
+
end
|
368
|
+
|
369
|
+
# @return [String] a graphical "ASCII" representation of the Table, suitable for
|
370
|
+
# display in a fixed-width font.
|
371
|
+
def to_s
|
372
|
+
if column_registry.any?
|
373
|
+
bottom_edge = horizontal_rule(:bottom)
|
374
|
+
rows = map(&:to_s)
|
375
|
+
bottom_edge.empty? ? Util.join_lines(rows) : Util.join_lines(rows + [bottom_edge])
|
376
|
+
else
|
377
|
+
""
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Calls the given block once for each {Row} in the Table, passing that {Row} as parameter.
|
382
|
+
#
|
383
|
+
# @example
|
384
|
+
# table.each do |row|
|
385
|
+
# puts row
|
386
|
+
# end
|
387
|
+
#
|
388
|
+
# Note that when printed, the first row will visually include the headers (assuming these
|
389
|
+
# were not disabled when the Table was initialized).
|
390
|
+
def each
|
391
|
+
@sources.each_with_index do |source, index|
|
392
|
+
header =
|
393
|
+
if (index == 0) && @header_frequency
|
394
|
+
:top
|
395
|
+
elsif (Integer === @header_frequency) && Util.divides?(@header_frequency, index)
|
396
|
+
:middle
|
397
|
+
end
|
398
|
+
|
399
|
+
show_divider = @row_divider_frequency && (index != 0) && Util.divides?(@row_divider_frequency, index)
|
400
|
+
|
401
|
+
yield Row.new(self, source, header: header, divider: show_divider, index: index)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
# @return [String] a graphical representation of the Table column headers formatted with fixed
|
406
|
+
# width plain text.
|
407
|
+
def formatted_header
|
408
|
+
cells = get_columns.map(&:header_cell)
|
409
|
+
format_row(cells, @wrap_header_cells_to)
|
410
|
+
end
|
411
|
+
|
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.
|
420
|
+
# @return [String] an "ASCII" graphical representation of a horizontal
|
421
|
+
# dividing line.
|
422
|
+
# @example Print a horizontal divider between each pair of rows, and again at the bottom:
|
423
|
+
#
|
424
|
+
# table.each_with_index do |row, i|
|
425
|
+
# puts table.horizontal_rule(:middle) unless i == 0
|
426
|
+
# puts row
|
427
|
+
# end
|
428
|
+
# puts table.horizontal_rule(:bottom)
|
429
|
+
#
|
430
|
+
# It may be that `:top`, `:middle` and `:bottom` all look the same. Whether
|
431
|
+
# this is the case depends on the characters used for the table border.
|
432
|
+
def horizontal_rule(position = :bottom)
|
433
|
+
column_widths = get_columns.map { |column| column.width + column.total_padding }
|
434
|
+
@border_instance.horizontal_rule(column_widths, position)
|
435
|
+
end
|
436
|
+
|
437
|
+
# Resets all the column widths so that each column is *just* wide enough to accommodate
|
438
|
+
# its header text as well as the formatted content of each its cells for the entire
|
439
|
+
# collection, together with a single character of padding on either side of the column,
|
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.
|
444
|
+
#
|
445
|
+
# Note that calling this method will cause the entire source Enumerable to
|
446
|
+
# be traversed and all the column extractors and formatters to be applied in order
|
447
|
+
# to calculate the required widths.
|
448
|
+
#
|
449
|
+
# Note also that this method causes column widths to be fixed as appropriate to the
|
450
|
+
# formatted cell contents given the state of the source Enumerable at the point it
|
451
|
+
# is called. If the source Enumerable changes between that point, and the point when
|
452
|
+
# the Table is printed, then columns will *not* be resized yet again on printing.
|
453
|
+
#
|
454
|
+
# @param [nil, Numeric] max_table_width (:auto) With no args, or if passed <tt>:auto</tt>,
|
455
|
+
# stops the total table width (including padding and borders) from expanding beyond the
|
456
|
+
# bounds of the terminal screen.
|
457
|
+
# If passed <tt>nil</tt>, the table width will not be capped.
|
458
|
+
# Width is deducted from columns if required to achieve this, with one character progressively
|
459
|
+
# deducted from the width of the widest column until the target is reached. When the
|
460
|
+
# table is printed, wrapping or truncation will then occur in these columns as required
|
461
|
+
# (depending on how they were configured).
|
462
|
+
# Note that regardless of the value passed to max_table_width, the table will always be left wide
|
463
|
+
# enough to accommodate at least 1 character's width of content, 1 character of left padding and
|
464
|
+
# 1 character of right padding in each column, together with border characters (1 on each side
|
465
|
+
# of the table and 1 between adjacent columns). I.e. there is a certain width below width the
|
466
|
+
# Table will refuse to shrink itself.
|
467
|
+
# @return [Table] the Table itself
|
468
|
+
def pack(max_table_width: :auto)
|
469
|
+
get_columns.each { |column| column.width = Util.wrapped_width(column.header) }
|
470
|
+
|
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)
|
475
|
+
column.width = Util.max(column.width, cell_width)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
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
|
+
)
|
490
|
+
end
|
491
|
+
|
492
|
+
self
|
493
|
+
end
|
494
|
+
|
495
|
+
# Creates a new {Table} from the current Table, transposed, that is rotated 90 degrees,
|
496
|
+
# relative to the current Table, so that the header names of the current Table form the
|
497
|
+
# content of left-most column of the new Table, and each column thereafter corresponds to one of the
|
498
|
+
# elements of the current Table's <tt>sources</tt>, with the header of that column being the String
|
499
|
+
# value of that element.
|
500
|
+
#
|
501
|
+
# @example
|
502
|
+
# puts Tabulo::Table.new(-1..1, :even?, :odd?, :abs).transpose
|
503
|
+
# # => +-------+--------------+--------------+--------------+
|
504
|
+
# # | | -1 | 0 | 1 |
|
505
|
+
# # +-------+--------------+--------------+--------------+
|
506
|
+
# # | even? | false | true | false |
|
507
|
+
# # | odd? | true | false | true |
|
508
|
+
# # | abs | 1 | 0 | 1 |
|
509
|
+
#
|
510
|
+
# @param [Hash] opts Options for configuring the new, transposed {Table}.
|
511
|
+
# The following options are the same as the keyword params for the {#initialize} method for
|
512
|
+
# {Table}: <tt>column_width</tt>, <tt>column_padding</tt>, <tt>formatter</tt>,
|
513
|
+
# <tt>header_frequency</tt>, <tt>row_divider_frequency</tt>, <tt>wrap_header_cells_to</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>.
|
517
|
+
# These are applied in the same way as documented for {#initialize}, when
|
518
|
+
# creating the new, transposed Table. Any options not specified explicitly in the call to {#transpose}
|
519
|
+
# will inherit their values from the original {Table} (with the exception of settings
|
520
|
+
# for the left-most column, containing the field names, which are determined as described
|
521
|
+
# below). In addition, the following options also apply to {#transpose}:
|
522
|
+
# @option opts [nil, Integer] :field_names_width Determines the width of the left-most column of the
|
523
|
+
# new Table, which contains the names of "fields" (corresponding to the original Table's
|
524
|
+
# column headings). If this is not provided, then by default this column will be made just
|
525
|
+
# wide enough to accommodate its contents.
|
526
|
+
# @option opts [String] :field_names_header ("") By default the left-most column will have a
|
527
|
+
# blank header; but this can be overridden by passing a String to this option.
|
528
|
+
# @option opts [:left, :center, :right] :field_names_header_alignment (:right) Specifies how the
|
529
|
+
# header text of the left-most column (if it has header text) should be aligned.
|
530
|
+
# @option opts [:left, :center, :right] :field_names_body_alignment (:right) Specifies how the
|
531
|
+
# body text of the left-most column should be aligned.
|
532
|
+
# @option opts [#to_proc] :headers (:to_s.to_proc) A lambda or other callable object that
|
533
|
+
# will be passed in turn each of the elements of the current Table's <tt>sources</tt>
|
534
|
+
# Enumerable, to determine the text to be displayed in the header of each column of the
|
535
|
+
# new Table (other than the left-most column's header, which is determined as described
|
536
|
+
# above).
|
537
|
+
# @return [Table] a new {Table}
|
538
|
+
# @raise [InvalidBorderError] if invalid argument passed to `border` parameter.
|
539
|
+
def transpose(opts = {})
|
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|
|
543
|
+
[sym, instance_variable_get("@#{sym}")]
|
544
|
+
end.to_h
|
545
|
+
|
546
|
+
initializer_opts = default_opts.merge(Util.slice_hash(opts, *default_opts.keys))
|
547
|
+
default_extra_opts = { field_names_body_alignment: :right, field_names_header: "",
|
548
|
+
field_names_header_alignment: :right, field_names_width: nil, headers: :to_s.to_proc }
|
549
|
+
extra_opts = default_extra_opts.merge(Util.slice_hash(opts, *default_extra_opts.keys))
|
550
|
+
|
551
|
+
# The underlying enumerable for the new table, is the columns of the original table.
|
552
|
+
fields = column_registry.values
|
553
|
+
|
554
|
+
Table.new(fields, **initializer_opts) do |t|
|
555
|
+
|
556
|
+
# Left hand column of new table, containing field names
|
557
|
+
width_opt = extra_opts[:field_names_width]
|
558
|
+
field_names_width = (width_opt.nil? ? fields.map { |f| f.header.length }.max : width_opt)
|
559
|
+
|
560
|
+
t.add_column(:dummy, align_body: extra_opts[:field_names_body_alignment],
|
561
|
+
align_header: extra_opts[:field_names_header_alignment], header: extra_opts[:field_names_header],
|
562
|
+
width: field_names_width, &:header)
|
563
|
+
|
564
|
+
# Add a column to the new table for each of the original table's sources
|
565
|
+
sources.each_with_index do |source, i|
|
566
|
+
t.add_column(i, header: extra_opts[:headers].call(source)) do |original_column|
|
567
|
+
original_column.body_cell_value(source, row_index: i, column_index: original_column.index)
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# @!visibility private
|
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) }
|
576
|
+
inner = format_row(cells, @wrap_body_cells_to)
|
577
|
+
|
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])
|
583
|
+
elsif header
|
584
|
+
Util.condense_lines([horizontal_rule(:middle), formatted_header, horizontal_rule(:middle), inner])
|
585
|
+
elsif divider
|
586
|
+
Util.condense_lines([horizontal_rule(:middle), inner])
|
587
|
+
else
|
588
|
+
inner
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
private
|
593
|
+
|
594
|
+
# @!visibility private
|
595
|
+
def get_columns
|
596
|
+
column_registry.values
|
597
|
+
end
|
598
|
+
|
599
|
+
# @!visibility private
|
600
|
+
def add_column_before(column, label, before)
|
601
|
+
old_column_entries = @column_registry.to_a
|
602
|
+
new_column_entries = []
|
603
|
+
|
604
|
+
old_column_entries.each do |entry|
|
605
|
+
new_column_entries << [label, column] if entry[0] == before
|
606
|
+
new_column_entries << entry
|
607
|
+
end
|
608
|
+
|
609
|
+
found = (new_column_entries.size == old_column_entries.size + 1)
|
610
|
+
raise InvalidColumnLabelError, "There is no column with label #{before}" unless found
|
611
|
+
|
612
|
+
@column_registry = new_column_entries.to_h
|
613
|
+
end
|
614
|
+
|
615
|
+
# @!visibility private
|
616
|
+
def add_column_final(column, label)
|
617
|
+
@column_registry[label] = column
|
618
|
+
end
|
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
|
+
|
669
|
+
# @!visibility private
|
670
|
+
def normalize_column_label(label)
|
671
|
+
case label
|
672
|
+
when Integer, Symbol
|
673
|
+
label
|
674
|
+
when String
|
675
|
+
label.to_sym
|
676
|
+
end
|
677
|
+
end
|
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
|
+
|
697
|
+
# @!visibility private
|
698
|
+
def shrink_to(max_table_width)
|
699
|
+
columns = get_columns
|
700
|
+
num_columns = columns.count
|
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 }
|
703
|
+
total_borders = num_columns + 1
|
704
|
+
unadjusted_table_width = total_columns_padded_width + total_borders
|
705
|
+
|
706
|
+
# Ensure max table width is at least wide enough to accommodate table borders and padding
|
707
|
+
# and one character of content.
|
708
|
+
min_table_width = total_padding + total_borders + column_registry.count
|
709
|
+
max_table_width = Util.max(min_table_width, max_table_width)
|
710
|
+
required_reduction = Util.max(unadjusted_table_width - max_table_width, 0)
|
711
|
+
|
712
|
+
required_reduction.times do
|
713
|
+
widest_column = columns.inject(columns.first) do |widest, column|
|
714
|
+
column.width >= widest.width ? column : widest
|
715
|
+
end
|
716
|
+
|
717
|
+
widest_column.width -= 1
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
# @!visibility private
|
722
|
+
#
|
723
|
+
# Formats a single header row or body row as a String.
|
724
|
+
#
|
725
|
+
# @param [String[][]] cells an Array of Array-of-Strings, each of which represents a
|
726
|
+
# "stack" of "subcells". Each such stack represents the wrapped content of a given
|
727
|
+
# "cell" in this row, from the top down, one String for each "line".
|
728
|
+
# Each String includes the spaces, if any, on either side required for the
|
729
|
+
# "internal padding" of the cell to carry out the cell content alignment -- but
|
730
|
+
# does not include the single character of padding around each column.
|
731
|
+
# @param [Integer] wrap_cells_to the number of "lines" of wrapped content to allow
|
732
|
+
# before truncating.
|
733
|
+
# @return [String] the entire formatted row including all padding and borders.
|
734
|
+
def format_row(cells, wrap_cells_to)
|
735
|
+
max_cell_height = cells.map(&:height).max
|
736
|
+
row_height = ([wrap_cells_to, max_cell_height].compact.min || 1)
|
737
|
+
subcell_stacks = cells.map do |cell|
|
738
|
+
cell.padded_truncated_subcells(row_height)
|
739
|
+
end
|
740
|
+
subrows = subcell_stacks.transpose.map do |subrow_components|
|
741
|
+
@border_instance.join_cell_contents(subrow_components)
|
742
|
+
end
|
743
|
+
|
744
|
+
Util.join_lines(subrows)
|
745
|
+
end
|
746
|
+
|
747
|
+
# @!visibility private
|
748
|
+
def validate_character(character, default, exception_class, message_fragment)
|
749
|
+
case (c = (character || default))
|
750
|
+
when nil
|
751
|
+
; # do nothing
|
752
|
+
when String
|
753
|
+
if Unicode::DisplayWidth.of(c) != 1
|
754
|
+
raise exception_class, "#{message_fragment} is neither nil nor a single-character String"
|
755
|
+
end
|
756
|
+
else
|
757
|
+
raise exception_class, "#{message_fragment} is neither nil nor a single-character String"
|
758
|
+
end
|
759
|
+
c
|
760
|
+
end
|
761
|
+
|
762
|
+
end
|
763
|
+
end
|