terminal-table 1.7.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []