terminal-table 1.7.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- require 'unicode/display_width'
1
+ require 'unicode/display_width/no_string_ext'
2
2
 
3
3
  module Terminal
4
4
  class Table
@@ -7,15 +7,20 @@ module Terminal
7
7
  attr_reader :headings
8
8
 
9
9
  ##
10
- # Generates a ASCII table with the given _options_.
10
+ # Generates a ASCII/Unicode table with the given _options_.
11
11
 
12
12
  def initialize options = {}, &block
13
+ @elaborated = false
14
+ @headings = []
15
+ @rows = []
13
16
  @column_widths = []
14
17
  self.style = options.fetch :style, {}
15
18
  self.headings = options.fetch :headings, []
16
19
  self.rows = options.fetch :rows, []
17
20
  self.title = options.fetch :title, nil
18
21
  yield_or_eval(&block) if block
22
+
23
+ style.on_change(:width) { require_column_widths_recalc }
19
24
  end
20
25
 
21
26
  ##
@@ -25,6 +30,7 @@ module Terminal
25
30
  r = rows
26
31
  column(n).each_with_index do |col, i|
27
32
  cell = r[i][n]
33
+ next unless cell
28
34
  cell.alignment = alignment unless cell.alignment?
29
35
  end
30
36
  end
@@ -35,19 +41,19 @@ module Terminal
35
41
  def add_row array
36
42
  row = array == :separator ? Separator.new(self) : Row.new(self, array)
37
43
  @rows << row
38
- recalc_column_widths row
44
+ require_column_widths_recalc unless row.is_a?(Separator)
39
45
  end
40
46
  alias :<< :add_row
41
47
 
42
48
  ##
43
49
  # Add a separator.
44
50
 
45
- def add_separator
46
- self << :separator
51
+ def add_separator(border_type: :div)
52
+ @rows << Separator.new(self, border_type: border_type)
47
53
  end
48
54
 
49
55
  def cell_spacing
50
- cell_padding + style.border_y.length
56
+ cell_padding + style.border_y_width
51
57
  end
52
58
 
53
59
  def cell_padding
@@ -90,8 +96,7 @@ module Terminal
90
96
  # Return length of column _n_.
91
97
 
92
98
  def column_width n
93
- width = @column_widths[n] || 0
94
- width + additional_column_widths[n].to_i
99
+ column_widths[n] || 0
95
100
  end
96
101
  alias length_of_column column_width # for legacy support
97
102
 
@@ -99,7 +104,7 @@ module Terminal
99
104
  # Return total number of columns available.
100
105
 
101
106
  def number_of_columns
102
- headings_with_rows.map { |r| r.cells.size }.max
107
+ headings_with_rows.map { |r| r.number_of_columns }.max || 0
103
108
  end
104
109
 
105
110
  ##
@@ -109,34 +114,63 @@ module Terminal
109
114
  arrays = [arrays] unless arrays.first.is_a?(Array)
110
115
  @headings = arrays.map do |array|
111
116
  row = Row.new(self, array)
112
- recalc_column_widths row
117
+ require_column_widths_recalc
113
118
  row
114
119
  end
115
120
  end
116
121
 
117
122
  ##
118
- # Render the table.
123
+ # Elaborate rows to form an Array of Rows and Separators with adjacency properties added.
124
+ #
125
+ # This is separated from the String rendering so that certain features may be tweaked
126
+ # before the String is built.
119
127
 
120
- def render
121
- separator = Separator.new(self)
122
- buffer = [separator]
128
+ def elaborate_rows
129
+
130
+ buffer = style.border_top ? [Separator.new(self, border_type: :top, implicit: true)] : []
123
131
  unless @title.nil?
124
132
  buffer << Row.new(self, [title_cell_options])
125
- buffer << separator
133
+ buffer << Separator.new(self, implicit: true)
126
134
  end
127
135
  @headings.each do |row|
128
136
  unless row.cells.empty?
129
137
  buffer << row
130
- buffer << separator
138
+ buffer << Separator.new(self, border_type: :double, implicit: true)
131
139
  end
132
140
  end
133
141
  if style.all_separators
134
- buffer += @rows.product([separator]).flatten
142
+ @rows.each_with_index do |row, idx|
143
+ # last separator is bottom, others are :div
144
+ border_type = (idx == @rows.size - 1) ? :bot : :div
145
+ buffer << row
146
+ buffer << Separator.new(self, border_type: border_type, implicit: true)
147
+ end
135
148
  else
136
149
  buffer += @rows
137
- buffer << separator
150
+ buffer << Separator.new(self, border_type: :bot, implicit: true) if style.border_bottom
138
151
  end
139
- buffer.map { |r| style.margin_left + r.render.rstrip }.join("\n")
152
+
153
+ # After all implicit Separators are inserted we need to save off the
154
+ # adjacent rows so that we can decide what type of intersections to use
155
+ # based on column spans in the adjacent row(s).
156
+ buffer.each_with_index do |r, idx|
157
+ if r.is_a?(Separator)
158
+ prev_row = idx > 0 ? buffer[idx - 1] : nil
159
+ next_row = buffer.fetch(idx + 1, nil)
160
+ r.save_adjacent_rows(prev_row, next_row)
161
+ end
162
+ end
163
+
164
+ @elaborated = true
165
+ @rows = buffer
166
+ end
167
+
168
+ ##
169
+ # Render the table.
170
+
171
+ def render
172
+ elaborate_rows unless @elaborated
173
+ @rows.map { |r| style.margin_left + r.render.rstrip }.join("\n")
140
174
  end
141
175
  alias :to_s :render
142
176
 
@@ -162,7 +196,7 @@ module Terminal
162
196
 
163
197
  def title=(title)
164
198
  @title = title
165
- recalc_column_widths Row.new(self, [title_cell_options])
199
+ require_column_widths_recalc
166
200
  end
167
201
 
168
202
  ##
@@ -178,42 +212,134 @@ module Terminal
178
212
  private
179
213
 
180
214
  def columns_width
181
- @column_widths.inject(0) { |s, i| s + i + cell_spacing } + style.border_y.length
215
+ column_widths.inject(0) { |s, i| s + i + cell_spacing } + style.border_y_width
182
216
  end
183
217
 
184
- def additional_column_widths
185
- return [] if style.width.nil?
186
- spacing = style.width - columns_width
187
- if spacing < 0
188
- raise "Table width exceeds wanted width of #{style.width} characters."
189
- else
190
- per_col = spacing / number_of_columns
191
- arr = (1...number_of_columns).to_a.map { |i| per_col }
192
- other_cols = arr.inject(0) { |s, i| s + i }
193
- arr << spacing - other_cols
194
- arr
218
+ def recalc_column_widths
219
+ @require_column_widths_recalc = false
220
+ n_cols = number_of_columns
221
+ space_width = cell_spacing
222
+ return if n_cols == 0
223
+
224
+ # prepare rows
225
+ all_rows = headings_with_rows
226
+ all_rows << Row.new(self, [title_cell_options]) unless @title.nil?
227
+
228
+ # DP states, dp[colspan][index][split_offset] => column_width.
229
+ dp = []
230
+
231
+ # prepare initial value for DP.
232
+ all_rows.each do |row|
233
+ index = 0
234
+ row.cells.each do |cell|
235
+ cell_value = cell.value_for_column_width_recalc
236
+ cell_width = Unicode::DisplayWidth.of(cell_value.to_s)
237
+ colspan = cell.colspan
238
+
239
+ # find column width from each single cell.
240
+ dp[colspan] ||= []
241
+ dp[colspan][index] ||= [0] # add a fake cell with length 0.
242
+ dp[colspan][index][colspan] ||= 0 # initialize column length to 0.
243
+
244
+ # the last index `colspan` means width of the single column (split
245
+ # at end of each column), not a width made up of multiple columns.
246
+ single_column_length = [cell_width, dp[colspan][index][colspan]].max
247
+ dp[colspan][index][colspan] = single_column_length
248
+
249
+ index += colspan
250
+ end
195
251
  end
196
- end
197
252
 
198
- def recalc_column_widths row
199
- return if row.is_a? Separator
200
- i = 0
201
- row.cells.each do |cell|
202
- colspan = cell.colspan
203
- cell_value = cell.value_for_column_width_recalc
204
- colspan.downto(1) do |j|
205
- cell_length = Unicode::DisplayWidth.of(cell_value.to_s)
206
- if colspan > 1
207
- spacing_length = cell_spacing * (colspan - 1)
208
- length_in_columns = (cell_length - spacing_length)
209
- cell_length = (length_in_columns.to_f / colspan).ceil
253
+ # run DP.
254
+ (1..n_cols).each do |colspan|
255
+ dp[colspan] ||= []
256
+ (0..n_cols-colspan).each do |index|
257
+ dp[colspan][index] ||= [1]
258
+ (1...colspan).each do |offset|
259
+ # processed level became reverse map from width => [offset, ...].
260
+ left_colspan = offset
261
+ left_index = index
262
+ left_width = dp[left_colspan][left_index].keys.first
263
+
264
+ right_colspan = colspan - left_colspan
265
+ right_index = index + offset
266
+ right_width = dp[right_colspan][right_index].keys.first
267
+
268
+ dp[colspan][index][offset] = left_width + right_width + space_width
210
269
  end
211
- if @column_widths[i].to_i < cell_length
212
- @column_widths[i] = cell_length
270
+
271
+ # reverse map it for resolution (max width and short offset first).
272
+ rmap = {}
273
+ dp[colspan][index].each_with_index do |width, offset|
274
+ rmap[width] ||= []
275
+ rmap[width] << offset
276
+ end
277
+
278
+ # sort reversely and store it back.
279
+ dp[colspan][index] = Hash[rmap.sort.reverse]
280
+ end
281
+ end
282
+
283
+ resolve = lambda do |colspan, full_width, index = 0|
284
+ # stop if reaches the bottom level.
285
+ return @column_widths[index] = full_width if colspan == 1
286
+
287
+ # choose best split offset for partition, or second best result
288
+ # if first one is not dividable.
289
+ candidate_offsets = dp[colspan][index].collect(&:last).flatten
290
+ offset = candidate_offsets[0]
291
+ offset = candidate_offsets[1] if offset == colspan
292
+
293
+ # prepare for next round.
294
+ left_colspan = offset
295
+ left_index = index
296
+ left_width = dp[left_colspan][left_index].keys.first
297
+
298
+ right_colspan = colspan - left_colspan
299
+ right_index = index + offset
300
+ right_width = dp[right_colspan][right_index].keys.first
301
+
302
+ # calculate reference column width, give remaining spaces to left.
303
+ total_non_space_width = full_width - (colspan - 1) * space_width
304
+ ref_column_width = total_non_space_width / colspan
305
+ remainder = total_non_space_width % colspan
306
+ rem_left_width = [remainder, left_colspan].min
307
+ rem_right_width = remainder - rem_left_width
308
+ ref_left_width = ref_column_width * left_colspan +
309
+ (left_colspan - 1) * space_width + rem_left_width
310
+ ref_right_width = ref_column_width * right_colspan +
311
+ (right_colspan - 1) * space_width + rem_right_width
312
+
313
+ # at most one width can be greater than the reference width.
314
+ if left_width <= ref_left_width and right_width <= ref_right_width
315
+ # use refernce width (evenly partition).
316
+ left_width = ref_left_width
317
+ right_width = ref_right_width
318
+ else
319
+ # the wider one takes its value, shorter one takes the rest.
320
+ if left_width > ref_left_width
321
+ right_width = full_width - left_width - space_width
322
+ else
323
+ left_width = full_width - right_width - space_width
213
324
  end
214
- i = i + 1
215
325
  end
326
+
327
+ # run next round.
328
+ resolve.call(left_colspan, left_width, left_index)
329
+ resolve.call(right_colspan, right_width, right_index)
330
+ end
331
+
332
+ full_width = dp[n_cols][0].keys.first
333
+ unless style.width.nil?
334
+ new_width = style.width - space_width - style.border_y_width
335
+ if new_width < full_width
336
+ raise "Table width exceeds wanted width " +
337
+ "of #{style.width} characters."
338
+ end
339
+ full_width = new_width
216
340
  end
341
+
342
+ resolve.call(n_cols, full_width)
217
343
  end
218
344
 
219
345
  ##
@@ -235,5 +361,14 @@ module Terminal
235
361
  def title_cell_options
236
362
  {:value => @title, :alignment => :center, :colspan => number_of_columns}
237
363
  end
364
+
365
+ def require_column_widths_recalc
366
+ @require_column_widths_recalc = true
367
+ end
368
+
369
+ def column_widths
370
+ recalc_column_widths if @require_column_widths_recalc
371
+ @column_widths
372
+ end
238
373
  end
239
374
  end
@@ -0,0 +1,13 @@
1
+ module Terminal
2
+ class Table
3
+ module Util
4
+ # removes all ANSI escape sequences (e.g. color)
5
+ def ansi_escape(line)
6
+ line.to_s.gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '').
7
+ gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '').
8
+ gsub(/(\x03|\x1a)/, '')
9
+ end
10
+ module_function :ansi_escape
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  module Terminal
2
2
  class Table
3
- VERSION = '1.7.1'
3
+ VERSION = '3.0.0'
4
4
  end
5
5
  end
@@ -16,11 +16,11 @@ Gem::Specification.new do |spec|
16
16
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_development_dependency "bundler", "~> 1.10"
20
- spec.add_development_dependency "rake", "~> 10.0"
19
+ spec.add_development_dependency "bundler", "~> 2"
20
+ spec.add_development_dependency "rake", "~> 13.0"
21
21
  spec.add_development_dependency "rspec", ">= 3.0"
22
22
  spec.add_development_dependency "term-ansicolor"
23
23
  spec.add_development_dependency "pry"
24
24
 
25
- spec.add_runtime_dependency "unicode-display_width", "~> 1.1.1"
25
+ spec.add_runtime_dependency "unicode-display_width", ["~> 1.1", ">= 1.1.1"]
26
26
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terminal-table
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TJ Holowaychuk
8
8
  - Scott J. Goldman
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-09-07 00:00:00.000000000 Z
12
+ date: 2021-01-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -17,28 +17,28 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '1.10'
20
+ version: '2'
21
21
  type: :development
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: '1.10'
27
+ version: '2'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: rake
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: '10.0'
34
+ version: '13.0'
35
35
  type: :development
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: '10.0'
41
+ version: '13.0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: rspec
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,9 @@ dependencies:
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
88
  - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.1'
91
+ - - ">="
89
92
  - !ruby/object:Gem::Version
90
93
  version: 1.1.1
91
94
  type: :runtime
@@ -93,23 +96,34 @@ dependencies:
93
96
  version_requirements: !ruby/object:Gem::Requirement
94
97
  requirements:
95
98
  - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '1.1'
101
+ - - ">="
96
102
  - !ruby/object:Gem::Version
97
103
  version: 1.1.1
98
- description:
104
+ description:
99
105
  email:
100
106
  - tj@vision-media.ca
101
107
  executables: []
102
108
  extensions: []
103
109
  extra_rdoc_files: []
104
110
  files:
111
+ - ".github/workflows/ci.yml"
105
112
  - ".gitignore"
106
113
  - Gemfile
114
+ - Gemfile.lock
107
115
  - History.rdoc
116
+ - LICENSE.txt
108
117
  - Manifest
109
- - README.rdoc
118
+ - README.md
110
119
  - Rakefile
111
120
  - Todo.rdoc
112
121
  - examples/examples.rb
122
+ - examples/examples_unicode.rb
123
+ - examples/issue100.rb
124
+ - examples/issue111.rb
125
+ - examples/issue95.rb
126
+ - examples/strong_separator.rb
113
127
  - lib/terminal-table.rb
114
128
  - lib/terminal-table/cell.rb
115
129
  - lib/terminal-table/import.rb
@@ -118,13 +132,14 @@ files:
118
132
  - lib/terminal-table/style.rb
119
133
  - lib/terminal-table/table.rb
120
134
  - lib/terminal-table/table_helper.rb
135
+ - lib/terminal-table/util.rb
121
136
  - lib/terminal-table/version.rb
122
137
  - terminal-table.gemspec
123
138
  homepage: https://github.com/tj/terminal-table
124
139
  licenses:
125
140
  - MIT
126
141
  metadata: {}
127
- post_install_message:
142
+ post_install_message:
128
143
  rdoc_options: []
129
144
  require_paths:
130
145
  - lib
@@ -139,9 +154,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
154
  - !ruby/object:Gem::Version
140
155
  version: '0'
141
156
  requirements: []
142
- rubyforge_project:
143
- rubygems_version: 2.5.1
144
- signing_key:
157
+ rubygems_version: 3.1.4
158
+ signing_key:
145
159
  specification_version: 4
146
160
  summary: Simple, feature rich ascii table generation library
147
161
  test_files: []