terminal-table 1.7.2 → 3.0.1

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,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