terminal-table 1.7.2 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,171 @@
1
+ # coding: utf-8
2
+ require 'forwardable'
3
+
1
4
  module Terminal
2
5
  class Table
6
+
7
+ class Border
8
+
9
+ attr_accessor :data, :top, :bottom, :left, :right
10
+ def initialize
11
+ @top, @bottom, @left, @right = true, true, true, true
12
+ end
13
+ def []=(key, val)
14
+ @data[key] = val
15
+ end
16
+ def [](key)
17
+ @data[key]
18
+ end
19
+ def initialize_dup(other)
20
+ super
21
+ @data = other.data.dup
22
+ end
23
+ def remove_verticals
24
+ self.class.const_get("VERTICALS").each { |key| @data[key] = "" }
25
+ self.class.const_get("INTERSECTIONS").each { |key| @data[key] = "" }
26
+ end
27
+ def remove_horizontals
28
+ self.class.const_get("HORIZONTALS").each { |key| @data[key] = "" }
29
+ end
30
+
31
+ # If @left, return the edge else empty-string.
32
+ def maybeleft(key) ; @left ? @data[key] : '' ; end
33
+
34
+ # If @right, return the edge else empty-string.
35
+ def mayberight(key) ; @right ? @data[key] : '' ; end
36
+
37
+ end
38
+
39
+ class AsciiBorder < Border
40
+ HORIZONTALS = %i[x]
41
+ VERTICALS = %i[y]
42
+ INTERSECTIONS = %i[i]
43
+
44
+ def initialize
45
+ super
46
+ @data = { x: "-", y: "|", i: "+" }
47
+ end
48
+
49
+ # Get vertical border elements
50
+ # @return [Array] 3-element list of [left, center, right]
51
+ def vertical
52
+ [maybeleft(:y), @data[:y], mayberight(:y)] # left, center, right
53
+ end
54
+
55
+ # Get horizontal border elements
56
+ # @return [Array] a 6 element list of: [i-left, horizontal-bar, i-up/down, i-right, i-down, i-up]
57
+ def horizontal(_type)
58
+ x, i = @data[:x], @data[:i]
59
+ [maybeleft(:i), x, i, mayberight(:i), i, i]
60
+ end
61
+ end
62
+
63
+ class MarkdownBorder < AsciiBorder
64
+ def initialize
65
+ super
66
+ @top, @bottom = false, false
67
+ @data = { x: "-", y: "|", i: "|" }
68
+ end
69
+ end
70
+
71
+ class UnicodeBorder < Border
72
+
73
+ ALLOWED_SEPARATOR_BORDER_STYLES = %i[
74
+ top bot
75
+ div dash dot3 dot4
76
+ thick thick_dash thick_dot3 thick_dot4
77
+ heavy heavy_dash heavy_dot3 heavy_dot4
78
+ bold bold_dash bold_dot3 bold_dot4
79
+ double
80
+ ]
81
+
82
+ HORIZONTALS = %i[x sx ax bx nx bx_dot3 bx_dot4 bx_dash x_dot3 x_dot4 x_dash]
83
+ VERTICALS = %i[y yw ye]
84
+ INTERSECTIONS = %i[nw n ne nd
85
+ aw ai ae ad au
86
+ bw bi be bd bu
87
+ w i e dn up
88
+ sw s se su]
89
+ def initialize
90
+ super
91
+ @data = {
92
+ nil => nil,
93
+ nw: "┌", nx: "─", n: "┬", ne: "┐",
94
+ yw: "│", y: "│", ye: "│",
95
+ aw: "╞", ax: "═", ai: "╪", ae: "╡", ad: '╤', au: "╧", # double
96
+ bw: "┝", bx: "━", bi: "┿", be: "┥", bd: '┯', bu: "┷", # heavy/bold/thick
97
+ w: "├", x: "─", i: "┼", e: "┤", dn: "┬", up: "┴", # normal div
98
+ sw: "└", sx: "─", s: "┴", se: "┘",
99
+ # alternative dots/dashes
100
+ x_dot4: '┈', x_dot3: '┄', x_dash: '╌',
101
+ bx_dot4: '┉', bx_dot3: '┅', bx_dash: '╍',
102
+ }
103
+ end
104
+ # Get vertical border elements
105
+ # @return [Array] 3-element list of [left, center, right]
106
+ def vertical
107
+ [maybeleft(:yw), @data[:y], mayberight(:ye)]
108
+ end
109
+
110
+ # Get horizontal border elements
111
+ # @return [Array] a 6 element list of: [i-left, horizontal-bar, i-up/down, i-right, i-down, i-up]
112
+ def horizontal(type)
113
+ raise ArgumentError, "Border type is #{type.inspect}, must be one of #{ALLOWED_SEPARATOR_BORDER_STYLES.inspect}" unless ALLOWED_SEPARATOR_BORDER_STYLES.include?(type)
114
+ lookup = case type
115
+ when :top
116
+ [:nw, :nx, :n, :ne, :n, nil]
117
+ when :bot
118
+ [:sw, :sx, :s, :se, nil, :s]
119
+ when :double
120
+ # typically used for the separator below the heading row or above a footer row)
121
+ [:aw, :ax, :ai, :ae, :ad, :au]
122
+ when :thick, :thick_dash, :thick_dot3, :thick_dot4,
123
+ :heavy, :heavy_dash, :heavy_dot3, :heavy_dot4,
124
+ :bold, :bold_dash, :bold_dot3, :bold_dot4
125
+ # alternate thick/bold border
126
+ xref = type.to_s.sub(/^(thick|heavy|bold)/,'bx').to_sym
127
+ [:bw, xref, :bi, :be, :bd, :bu]
128
+ when :dash, :dot3, :dot4
129
+ # alternate thin dividers
130
+ xref = "x_#{type}".to_sym
131
+ [:w, xref, :i, :e, :dn, :up]
132
+ else # :div (center, non-emphasized)
133
+ [:w, :x, :i, :e, :dn, :up]
134
+ end
135
+ rval = lookup.map { |key| @data.fetch(key) }
136
+ rval[0] = '' unless @left
137
+ rval[3] = '' unless @right
138
+ rval
139
+ end
140
+ end
141
+
142
+ # Unicode Border With rounded edges
143
+ class UnicodeRoundBorder < UnicodeBorder
144
+ def initialize
145
+ super
146
+ @data.merge!({nw: '╭', ne: '╮', sw: '╰', se: '╯'})
147
+ end
148
+ end
149
+
150
+ # Unicode Border with thick outer edges
151
+ class UnicodeThickEdgeBorder < UnicodeBorder
152
+ def initialize
153
+ super
154
+ @data = {
155
+ nil => nil,
156
+ nw: "┏", nx: "━", n: "┯", ne: "┓", nd: nil,
157
+ yw: "┃", y: "│", ye: "┃",
158
+ aw: "┣", ax: "═", ai: "╪", ae: "┫", ad: '╤', au: "╧", # double
159
+ bw: "┣", bx: "━", bi: "┿", be: "┫", bd: '┯', bu: "┷", # heavy/bold/thick
160
+ w: "┠", x: "─", i: "┼", e: "┨", dn: "┬", up: "┴", # normal div
161
+ sw: "┗", sx: "━", s: "┷", se: "┛", su: nil,
162
+ # alternative dots/dashes
163
+ x_dot4: '┈', x_dot3: '┄', x_dash: '╌',
164
+ bx_dot4: '┉', bx_dot3: '┅', bx_dash: '╍',
165
+ }
166
+ end
167
+ end
168
+
3
169
  # A Style object holds all the formatting information for a Table object
4
170
  #
5
171
  # To create a table with a certain style, use either the constructor
@@ -22,17 +188,51 @@ module Terminal
22
188
  # Terminal::Table::Style.defaults = {:width => 80}
23
189
  #
24
190
  class Style
191
+ extend Forwardable
192
+ def_delegators :@border, :vertical, :horizontal, :remove_verticals, :remove_horizontals
193
+
25
194
  @@defaults = {
26
- :border_x => "-", :border_y => "|", :border_i => "+",
195
+ :border => AsciiBorder.new,
27
196
  :padding_left => 1, :padding_right => 1,
28
197
  :margin_left => '',
29
198
  :width => nil, :alignment => nil,
30
- :all_separators => false
199
+ :all_separators => false,
31
200
  }
32
201
 
33
- attr_accessor :border_x
34
- attr_accessor :border_y
35
- attr_accessor :border_i
202
+ ## settors/gettor for legacy ascii borders
203
+ def border_x=(val) ; @border[:x] = val ; end
204
+ def border_y=(val) ; @border[:y] = val ; end
205
+ def border_i=(val) ; @border[:i] = val ; end
206
+ def border_y ; @border[:y] ; end
207
+ def border_y_width ; Util::ansi_escape(@border[:y]).length ; end
208
+
209
+ # Accessor for instance of Border
210
+ attr_reader :border
211
+ def border=(val)
212
+ if val.is_a? Symbol
213
+ # convert symbol name like :foo_bar to get class FooBarBorder
214
+ klass_str = val.to_s.split('_').collect(&:capitalize).join + "Border"
215
+ begin
216
+ klass = Terminal::Table::const_get(klass_str)
217
+ @border = klass.new
218
+ rescue NameError
219
+ raise "Cannot lookup class Terminal::Table::#{klass_str} from symbol #{val.inspect}"
220
+ end
221
+ else
222
+ @border = val
223
+ end
224
+ end
225
+
226
+ def border_top=(val) ; @border.top = val ; end
227
+ def border_bottom=(val) ; @border.bottom = val ; end
228
+ def border_left=(val) ; @border.left = val ; end
229
+ def border_right=(val) ; @border.right = val ; end
230
+
231
+ def border_top ; @border.top ; end
232
+ def border_bottom ; @border.bottom ; end
233
+ def border_left ; @border.left ; end
234
+ def border_right ; @border.right ; end
235
+
36
236
 
37
237
  attr_accessor :padding_left
38
238
  attr_accessor :padding_right
@@ -44,23 +244,30 @@ module Terminal
44
244
 
45
245
  attr_accessor :all_separators
46
246
 
47
-
247
+
48
248
  def initialize options = {}
49
249
  apply self.class.defaults.merge(options)
50
250
  end
51
251
 
52
252
  def apply options
53
- options.each { |m, v| __send__ "#{m}=", v }
253
+ options.each do |m, v|
254
+ __send__ "#{m}=", v
255
+ end
54
256
  end
55
-
257
+
56
258
  class << self
57
259
  def defaults
58
- @@defaults
260
+ klass_defaults = @@defaults.dup
261
+ # border is an object that needs to be duplicated on instantiation,
262
+ # otherwise everything will be referencing the same object-id.
263
+ klass_defaults[:border] = klass_defaults[:border].dup
264
+ klass_defaults
59
265
  end
60
-
266
+
61
267
  def defaults= options
62
268
  @@defaults = defaults.merge(options)
63
269
  end
270
+
64
271
  end
65
272
 
66
273
  def on_change attr
@@ -71,6 +278,7 @@ module Terminal
71
278
  yield attr.to_sym, value
72
279
  end
73
280
  end
281
+
74
282
  end
75
283
  end
76
284
  end
@@ -7,9 +7,10 @@ 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
13
14
  @headings = []
14
15
  @rows = []
15
16
  @column_widths = []
@@ -29,6 +30,7 @@ module Terminal
29
30
  r = rows
30
31
  column(n).each_with_index do |col, i|
31
32
  cell = r[i][n]
33
+ next unless cell
32
34
  cell.alignment = alignment unless cell.alignment?
33
35
  end
34
36
  end
@@ -46,12 +48,12 @@ module Terminal
46
48
  ##
47
49
  # Add a separator.
48
50
 
49
- def add_separator
50
- self << :separator
51
+ def add_separator(border_type: :div)
52
+ @rows << Separator.new(self, border_type: border_type)
51
53
  end
52
54
 
53
55
  def cell_spacing
54
- cell_padding + style.border_y.length
56
+ cell_padding + style.border_y_width
55
57
  end
56
58
 
57
59
  def cell_padding
@@ -94,7 +96,7 @@ module Terminal
94
96
  # Return length of column _n_.
95
97
 
96
98
  def column_width n
97
- width = column_widths[n] || 0
99
+ column_widths[n] || 0
98
100
  end
99
101
  alias length_of_column column_width # for legacy support
100
102
 
@@ -118,28 +120,57 @@ module Terminal
118
120
  end
119
121
 
120
122
  ##
121
- # 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.
122
127
 
123
- def render
124
- separator = Separator.new(self)
125
- buffer = [separator]
128
+ def elaborate_rows
129
+
130
+ buffer = style.border_top ? [Separator.new(self, border_type: :top, implicit: true)] : []
126
131
  unless @title.nil?
127
132
  buffer << Row.new(self, [title_cell_options])
128
- buffer << separator
133
+ buffer << Separator.new(self, implicit: true)
129
134
  end
130
135
  @headings.each do |row|
131
136
  unless row.cells.empty?
132
137
  buffer << row
133
- buffer << separator
138
+ buffer << Separator.new(self, border_type: :double, implicit: true)
134
139
  end
135
140
  end
136
141
  if style.all_separators
137
- 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
138
148
  else
139
149
  buffer += @rows
140
- buffer << separator
150
+ buffer << Separator.new(self, border_type: :bot, implicit: true) if style.border_bottom
151
+ end
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
141
162
  end
142
- buffer.map { |r| style.margin_left + r.render.rstrip }.join("\n")
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")
143
174
  end
144
175
  alias :to_s :render
145
176
 
@@ -181,7 +212,7 @@ module Terminal
181
212
  private
182
213
 
183
214
  def columns_width
184
- 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
185
216
  end
186
217
 
187
218
  def recalc_column_widths
@@ -245,11 +276,11 @@ module Terminal
245
276
  end
246
277
 
247
278
  # sort reversely and store it back.
248
- dp[colspan][index] = rmap.sort.reverse.to_h
279
+ dp[colspan][index] = Hash[rmap.sort.reverse]
249
280
  end
250
281
  end
251
282
 
252
- resolve = -> (colspan, full_width, index = 0) do
283
+ resolve = lambda do |colspan, full_width, index = 0|
253
284
  # stop if reaches the bottom level.
254
285
  return @column_widths[index] = full_width if colspan == 1
255
286
 
@@ -300,7 +331,7 @@ module Terminal
300
331
 
301
332
  full_width = dp[n_cols][0].keys.first
302
333
  unless style.width.nil?
303
- new_width = style.width - space_width - style.border_y.length
334
+ new_width = style.width - space_width - style.border_y_width
304
335
  if new_width < full_width
305
336
  raise "Table width exceeds wanted width " +
306
337
  "of #{style.width} characters."
@@ -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.2'
3
+ VERSION = '3.0.1'
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", "< 3"]
26
26
  end