natty-ui 0.34.0 → 1.0.2
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.
- checksums.yaml +4 -4
- data/.yardopts +0 -1
- data/README.md +6 -6
- data/examples/24bit-colors.rb +9 -5
- data/examples/3bit-colors.rb +7 -7
- data/examples/8bit-colors.rb +5 -5
- data/examples/attributes.rb +2 -3
- data/examples/elements.rb +9 -6
- data/examples/examples.rb +9 -9
- data/examples/frames.rb +31 -0
- data/examples/hbars.rb +6 -3
- data/examples/info.rb +13 -10
- data/examples/key-codes.rb +8 -9
- data/examples/ls.rb +24 -22
- data/examples/named-colors.rb +4 -3
- data/examples/sections.rb +27 -17
- data/examples/select.rb +28 -0
- data/examples/sh.rb +25 -7
- data/examples/tables.rb +19 -37
- data/examples/tasks.rb +32 -22
- data/examples/vbars.rb +5 -3
- data/lib/natty-ui/dumb_progress.rb +68 -0
- data/lib/natty-ui/element.rb +64 -65
- data/lib/natty-ui/features.rb +773 -872
- data/lib/natty-ui/frame.rb +87 -0
- data/lib/natty-ui/helper/table.rb +1376 -0
- data/lib/natty-ui/margin.rb +83 -0
- data/lib/natty-ui/progress.rb +116 -149
- data/lib/natty-ui/renderer/bars.rb +93 -0
- data/lib/natty-ui/renderer/choice.rb +56 -0
- data/lib/natty-ui/renderer/dumb_choice.rb +34 -0
- data/lib/natty-ui/renderer/dumb_select.rb +60 -0
- data/lib/natty-ui/renderer/dumb_shell_runner.rb +19 -0
- data/lib/natty-ui/renderer/heading.rb +26 -0
- data/lib/natty-ui/renderer/horizontal_rule.rb +32 -0
- data/lib/natty-ui/{ls_renderer.rb → renderer/ls.rb} +15 -27
- data/lib/natty-ui/renderer/mark.rb +13 -0
- data/lib/natty-ui/renderer/quote.rb +13 -0
- data/lib/natty-ui/renderer/select.rb +63 -0
- data/lib/natty-ui/renderer/shell.rb +15 -0
- data/lib/natty-ui/renderer/shell_runner.rb +29 -0
- data/lib/natty-ui/renderer/table_renderer.rb +429 -0
- data/lib/natty-ui/section.rb +142 -41
- data/lib/natty-ui/task.rb +39 -27
- data/lib/natty-ui/temporary.rb +27 -14
- data/lib/natty-ui/utils/border.rb +139 -0
- data/lib/natty-ui/utils/str_const.rb +62 -0
- data/lib/natty-ui/utils/utils.rb +47 -0
- data/lib/natty-ui/version.rb +1 -1
- data/lib/natty-ui.rb +87 -30
- metadata +31 -28
- data/examples/cols.rb +0 -38
- data/examples/illustration.rb +0 -60
- data/examples/options.rb +0 -28
- data/examples/themes.rb +0 -51
- data/lib/natty-ui/attributes.rb +0 -593
- data/lib/natty-ui/choice.rb +0 -67
- data/lib/natty-ui/dumb_choice.rb +0 -47
- data/lib/natty-ui/dumb_options.rb +0 -64
- data/lib/natty-ui/framed.rb +0 -51
- data/lib/natty-ui/hbars_renderer.rb +0 -66
- data/lib/natty-ui/options.rb +0 -78
- data/lib/natty-ui/shell_renderer.rb +0 -91
- data/lib/natty-ui/table.rb +0 -325
- data/lib/natty-ui/table_renderer.rb +0 -165
- data/lib/natty-ui/theme.rb +0 -403
- data/lib/natty-ui/utils.rb +0 -111
- data/lib/natty-ui/vbars_renderer.rb +0 -49
- data/lib/natty-ui/width_finder.rb +0 -137
- data/natty-ui.gemspec +0 -34
|
@@ -0,0 +1,1376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
module NattyUI
|
|
6
|
+
# Data structure for building terminal tables.
|
|
7
|
+
#
|
|
8
|
+
# A `Table` is populated through its {#rows} and {#columns} collections and
|
|
9
|
+
# then passed to {Features#table} for rendering. Cells accept text and
|
|
10
|
+
# formatting attributes; rows and columns can carry default attributes that
|
|
11
|
+
# are merged with individual cell attributes during rendering.
|
|
12
|
+
#
|
|
13
|
+
# @example Build and render a simple table
|
|
14
|
+
# ui.table border_frame: :double do |t|
|
|
15
|
+
# t.add_row '[b]Name', '[b]Score', align: :center
|
|
16
|
+
# t.add_row 'Alice', 42
|
|
17
|
+
# t.add_row 'Bob', 17
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Access cells by row and column index
|
|
21
|
+
# ui.table do |t|
|
|
22
|
+
# t[0, 0] = 'Header'
|
|
23
|
+
# t[1, 0] = 'Row 1'
|
|
24
|
+
# end
|
|
25
|
+
class Table
|
|
26
|
+
# A single cell in a {Table}.
|
|
27
|
+
#
|
|
28
|
+
# Cells are created implicitly when rows are populated via {Row#add},
|
|
29
|
+
# {Row#add_text}, {Row#fill}, and similar helpers. Each cell holds
|
|
30
|
+
# {#text} content and a set of {#attributes} that control its layout.
|
|
31
|
+
class Cell
|
|
32
|
+
# Formatting attributes for a {Cell}, {Row}, or {Column}.
|
|
33
|
+
#
|
|
34
|
+
# An `Attributes` instance is exposed on each {Cell} (via {Cell#attributes}),
|
|
35
|
+
# each {Row} (via {Row#attributes}), and each {Column} (via
|
|
36
|
+
# {Column#attributes}). Attributes set on a row or column serve as
|
|
37
|
+
# defaults that are merged with individual cell attributes during rendering.
|
|
38
|
+
#
|
|
39
|
+
# @example Set alignment on a cell
|
|
40
|
+
# cell.attributes.align = :center
|
|
41
|
+
#
|
|
42
|
+
# @example Set padding via the bulk helper
|
|
43
|
+
# cell.attributes.assign(padding: [1, 2], vertical: :middle)
|
|
44
|
+
class Attributes
|
|
45
|
+
# Whether line breaks inside the text are collapsed to spaces.
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
attr_reader :eol
|
|
49
|
+
|
|
50
|
+
# @attribute [w] eol
|
|
51
|
+
# @param value [Boolean]
|
|
52
|
+
def eol=(value)
|
|
53
|
+
@eol = value ? true : false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether whitespace are preserved.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
attr_reader :spaces
|
|
60
|
+
|
|
61
|
+
# @attribute [w] spaces
|
|
62
|
+
# @param value [Boolean]
|
|
63
|
+
def spaces=(value)
|
|
64
|
+
@spaces = value ? true : false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Horizontal text alignment within the cell.
|
|
68
|
+
#
|
|
69
|
+
# @return [:left, :center, :right, nil]
|
|
70
|
+
attr_reader :align
|
|
71
|
+
|
|
72
|
+
# @attribute [w] align
|
|
73
|
+
# @param value [:left, :center, :right, nil]
|
|
74
|
+
# @raise [ArgumentError] if value is not one of the accepted symbols
|
|
75
|
+
def align=(value)
|
|
76
|
+
return @align = value if ALIGN_VALUES.include?(value)
|
|
77
|
+
raise(ArgumentError, "value must be one of #{ALIGN_VALUES.inspect}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Vertical text alignment within the cell.
|
|
81
|
+
#
|
|
82
|
+
# @return [:top, :middle, :bottom, nil]
|
|
83
|
+
attr_reader :vertical
|
|
84
|
+
|
|
85
|
+
# @attribute [w] vertical
|
|
86
|
+
# @param value [:top, :middle, :bottom, nil]
|
|
87
|
+
# @raise [ArgumentError] if value is not one of the accepted symbols
|
|
88
|
+
def vertical=(value)
|
|
89
|
+
return @vertical = value if VERTICAL_VALUES.include?(value)
|
|
90
|
+
raise(
|
|
91
|
+
ArgumentError,
|
|
92
|
+
"value must be one of #{VERTICAL_VALUES.inspect}"
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Minimum column width in characters for this cell.
|
|
97
|
+
#
|
|
98
|
+
# @return [Integer, Float, nil]
|
|
99
|
+
attr_reader :min_width
|
|
100
|
+
|
|
101
|
+
# @attribute [w] min_width
|
|
102
|
+
# @param value [Integer, Float, nil]
|
|
103
|
+
# @raise [ArgumentError] if value is not `Integer`, `Float`, or `nil`
|
|
104
|
+
def min_width=(value)
|
|
105
|
+
case value
|
|
106
|
+
when nil
|
|
107
|
+
@min_width = nil
|
|
108
|
+
when Integer, Float
|
|
109
|
+
value = [0, value].max
|
|
110
|
+
@min_width = @max_width ? [value, @max_width].min : value
|
|
111
|
+
else
|
|
112
|
+
raise(ArgumentError, 'value must be an Integer, Float or nil')
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Maximum column width in characters for this cell.
|
|
117
|
+
#
|
|
118
|
+
# @return [Integer, Float, nil]
|
|
119
|
+
attr_reader :max_width
|
|
120
|
+
|
|
121
|
+
# @attribute [w] max_width
|
|
122
|
+
# @param value [Integer, Float, nil]
|
|
123
|
+
# @raise [ArgumentError] if value is not `Integer`, `Float`, or `nil`
|
|
124
|
+
def max_width=(value)
|
|
125
|
+
case value
|
|
126
|
+
when nil
|
|
127
|
+
@max_width = nil
|
|
128
|
+
when Integer, Float
|
|
129
|
+
value = [0, value].max
|
|
130
|
+
@max_width = @min_width ? [@min_width, value].max : value
|
|
131
|
+
else
|
|
132
|
+
raise(ArgumentError, 'value must be an Integer, Float or nil')
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Width constraint for this cell.
|
|
137
|
+
#
|
|
138
|
+
# Returns an exact `Integer` or `Float` when `min_width` equals
|
|
139
|
+
# `max_width`, a `Range` when they differ, or `nil` when neither is set.
|
|
140
|
+
#
|
|
141
|
+
# @example
|
|
142
|
+
# cell.attributes.width # => nil, Integer, Float, or Range
|
|
143
|
+
#
|
|
144
|
+
# @example Set exact width
|
|
145
|
+
# cell.attributes.width = 20
|
|
146
|
+
#
|
|
147
|
+
# @example Set width range
|
|
148
|
+
# cell.attributes.width = (10..30)
|
|
149
|
+
#
|
|
150
|
+
# @attribute [r] width
|
|
151
|
+
# @return [Integer, Float, Range, nil]
|
|
152
|
+
def width
|
|
153
|
+
return @min_width if @min_width == @max_width
|
|
154
|
+
(@min_width..@max_width) if @min_width || @max_width
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @attribute [w] width
|
|
158
|
+
# @param value [Integer, Float, Range, nil]
|
|
159
|
+
# - `Integer` or `Float` — sets an exact width
|
|
160
|
+
# - `Range` — bounds must be `Integer` or `Float`; an open end means
|
|
161
|
+
# no constraint on that side
|
|
162
|
+
# - `nil` — clears both constraints
|
|
163
|
+
# @raise [ArgumentError] if value is not one of the accepted types
|
|
164
|
+
def width=(value)
|
|
165
|
+
case value
|
|
166
|
+
when nil
|
|
167
|
+
@min_width = @max_width = nil
|
|
168
|
+
when Integer, Float
|
|
169
|
+
@min_width = @max_width = [0, value].max
|
|
170
|
+
when Range
|
|
171
|
+
b = value.begin
|
|
172
|
+
e = value.end
|
|
173
|
+
|
|
174
|
+
if Integer === b || Float === b
|
|
175
|
+
@min_width = [0, b].max
|
|
176
|
+
return @max_width = @min_width if b == e
|
|
177
|
+
return @max_width = e && @min_width < e ? e : nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if Integer === e || Float === e
|
|
181
|
+
@max_width = [0, e].max
|
|
182
|
+
return @min_width = nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
raise(
|
|
186
|
+
ArgumentError,
|
|
187
|
+
'value can only be a range of Integer or Float'
|
|
188
|
+
)
|
|
189
|
+
else
|
|
190
|
+
raise(ArgumentError, 'value must be an Integer, Range or nil')
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Minimum row height in lines for this cell.
|
|
195
|
+
#
|
|
196
|
+
# @return [Integer, nil]
|
|
197
|
+
attr_reader :min_height
|
|
198
|
+
|
|
199
|
+
# @attribute [w] min_height
|
|
200
|
+
# @param value [Integer, Float, nil]
|
|
201
|
+
# `Float` is rounded to the nearest integer
|
|
202
|
+
# @raise [ArgumentError] if value is not `Integer`, `Float`, or `nil`
|
|
203
|
+
def min_height=(value)
|
|
204
|
+
case value
|
|
205
|
+
when nil
|
|
206
|
+
@min_height = nil
|
|
207
|
+
when Integer, Float
|
|
208
|
+
value = [0, value.round].max
|
|
209
|
+
@min_height = @max_height ? [value, @max_height].min : value
|
|
210
|
+
else
|
|
211
|
+
raise(ArgumentError, 'value must be an Integer, Float or nil')
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Maximum row height in lines for this cell.
|
|
216
|
+
#
|
|
217
|
+
# @return [Integer, nil]
|
|
218
|
+
attr_reader :max_height
|
|
219
|
+
|
|
220
|
+
# @attribute [w] max_height
|
|
221
|
+
# @param value [Integer, Float, nil]
|
|
222
|
+
# `Float` is rounded to the nearest integer
|
|
223
|
+
# @raise [ArgumentError] if value is not `Integer`, `Float`, or `nil`
|
|
224
|
+
def max_height=(value)
|
|
225
|
+
case value
|
|
226
|
+
when nil
|
|
227
|
+
@max_height = nil
|
|
228
|
+
when Integer, Float
|
|
229
|
+
value = [0, value.round].max
|
|
230
|
+
@max_height = @min_height ? [@min_height, value].max : value
|
|
231
|
+
else
|
|
232
|
+
raise(ArgumentError, 'value must be an Integer, Float or nil')
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Height constraint for this cell.
|
|
237
|
+
#
|
|
238
|
+
# Returns an exact `Integer` when both bounds are equal, a `Range` when
|
|
239
|
+
# they differ, or `nil` when neither is set.
|
|
240
|
+
#
|
|
241
|
+
# @example
|
|
242
|
+
# cell.attributes.height # => nil, Integer, or Range
|
|
243
|
+
#
|
|
244
|
+
# @example Set exact height
|
|
245
|
+
# cell.attributes.height = 3
|
|
246
|
+
#
|
|
247
|
+
# @example Set height range
|
|
248
|
+
# cell.attributes.height = (2..5)
|
|
249
|
+
#
|
|
250
|
+
# @attribute [r] height
|
|
251
|
+
# @return [Integer, Range, nil]
|
|
252
|
+
def height
|
|
253
|
+
return @min_height if @min_height == @max_height
|
|
254
|
+
(@min_height..@max_height) if @min_height || @max_height
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# @attribute [w] height
|
|
258
|
+
# @param value [Integer, Range, nil]
|
|
259
|
+
# - `Integer` — sets an exact height
|
|
260
|
+
# - `Range` — bounds must be `Integer` or `Float` (floats are rounded)
|
|
261
|
+
# - `nil` — clears both constraints
|
|
262
|
+
# @raise [ArgumentError] if value is not one of the accepted types
|
|
263
|
+
def height=(value)
|
|
264
|
+
case value
|
|
265
|
+
when nil
|
|
266
|
+
@min_height = @max_height = nil
|
|
267
|
+
when Integer
|
|
268
|
+
value = [0, value.round].max
|
|
269
|
+
@min_height = @max_height = value
|
|
270
|
+
when Range
|
|
271
|
+
b = value.begin
|
|
272
|
+
e = value.end
|
|
273
|
+
|
|
274
|
+
if Integer === b || Float === b
|
|
275
|
+
@min_height = [0, b.round].max
|
|
276
|
+
return @max_height = @min_height if b == e
|
|
277
|
+
return @max_height = e && @min_height < e ? e.round : nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if Integer === e || Float === e
|
|
281
|
+
@max_height = [0, e.round].max
|
|
282
|
+
return @min_height = nil
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
raise(
|
|
286
|
+
ArgumentError,
|
|
287
|
+
'value can only be a range of Integer or Float'
|
|
288
|
+
)
|
|
289
|
+
else
|
|
290
|
+
raise(ArgumentError, 'value must be an Integer, Range or nil')
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Cell padding as a four-element array `[top, right, bottom, left]`.
|
|
295
|
+
#
|
|
296
|
+
# @return [Array(Integer, Integer, Integer, Integer)]
|
|
297
|
+
attr_reader :padding
|
|
298
|
+
|
|
299
|
+
# Sets cell padding.
|
|
300
|
+
#
|
|
301
|
+
# @example All sides
|
|
302
|
+
# cell.attributes.padding = 2
|
|
303
|
+
#
|
|
304
|
+
# @example Vertical / horizontal
|
|
305
|
+
# cell.attributes.padding = [1, 4]
|
|
306
|
+
#
|
|
307
|
+
# @attribute [w] padding
|
|
308
|
+
# @param value [Integer, #map, nil]
|
|
309
|
+
# - `Integer` — all four sides
|
|
310
|
+
# - `#map` (Enumerable) of 1–4 `#to_int` values:
|
|
311
|
+
# 1 element = all sides; 2 elements = [vertical, horizontal];
|
|
312
|
+
# 3 elements = [top, horizontal, bottom];
|
|
313
|
+
# 4 elements = [top, right, bottom, left]
|
|
314
|
+
# - `nil` — resets all sides to zero
|
|
315
|
+
# @raise [ArgumentError] if value has more than 4 elements or wrong type
|
|
316
|
+
def padding=(value)
|
|
317
|
+
return @padding = Array.new(4, 0) if value.nil?
|
|
318
|
+
return @padding = Array.new(4, value) if Integer === value
|
|
319
|
+
unless value.respond_to?(:map)
|
|
320
|
+
raise(ArgumentError, 'value must be an Integer, Enumerable or nil')
|
|
321
|
+
end
|
|
322
|
+
value = value.map { [0, it.to_int].max }
|
|
323
|
+
@padding =
|
|
324
|
+
case value.size
|
|
325
|
+
when 0
|
|
326
|
+
Array.new(4, 0)
|
|
327
|
+
when 1
|
|
328
|
+
Array.new(4, value)
|
|
329
|
+
when 2
|
|
330
|
+
value * 2
|
|
331
|
+
when 3
|
|
332
|
+
value << value[1]
|
|
333
|
+
when 4
|
|
334
|
+
value
|
|
335
|
+
else
|
|
336
|
+
raise(ArgumentError, 'too many values Enumerable')
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Top padding in lines.
|
|
341
|
+
#
|
|
342
|
+
# @attribute [r] top_padding
|
|
343
|
+
# @return [Integer]
|
|
344
|
+
def top_padding = @padding[0]
|
|
345
|
+
|
|
346
|
+
# @attribute [w] top_padding
|
|
347
|
+
# @param value [#to_int]
|
|
348
|
+
def top_padding=(value)
|
|
349
|
+
@padding[0] = [0, value.to_int].max
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Right padding in characters.
|
|
353
|
+
#
|
|
354
|
+
# @attribute [r] right_padding
|
|
355
|
+
# @return [Integer]
|
|
356
|
+
def right_padding = @padding[1]
|
|
357
|
+
|
|
358
|
+
# @attribute [w] right_padding
|
|
359
|
+
# @param value [#to_int]
|
|
360
|
+
def right_padding=(value)
|
|
361
|
+
@padding[1] = [0, value.to_int].max
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Bottom padding in lines.
|
|
365
|
+
#
|
|
366
|
+
# @attribute [r] bottom_padding
|
|
367
|
+
# @return [Integer]
|
|
368
|
+
def bottom_padding = @padding[2]
|
|
369
|
+
|
|
370
|
+
# @attribute [w] bottom_padding
|
|
371
|
+
# @param value [#to_int]
|
|
372
|
+
def bottom_padding=(value)
|
|
373
|
+
@padding[2] = [0, value.to_int].max
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Left padding in characters.
|
|
377
|
+
#
|
|
378
|
+
# @attribute [r] padding_left
|
|
379
|
+
# @return [Integer]
|
|
380
|
+
def left_padding = @padding[3]
|
|
381
|
+
|
|
382
|
+
# @attribute [w] padding_left
|
|
383
|
+
# @param value [#to_int]
|
|
384
|
+
def padding_left=(value)
|
|
385
|
+
@padding[3] = [0, value.to_int].max
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Returns `true` when all attributes are at their default values.
|
|
389
|
+
#
|
|
390
|
+
# @return [Boolean]
|
|
391
|
+
def empty?
|
|
392
|
+
@align.nil? && @vertical.nil? && @min_width.nil? && @max_width.nil? &&
|
|
393
|
+
@min_height.nil? && @max_height.nil? && @padding.all?(&:zero?)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# @private
|
|
397
|
+
def to_hash
|
|
398
|
+
ret = {}
|
|
399
|
+
ret[:eol] = false unless @eol
|
|
400
|
+
ret[:spaces] = false unless @spaces
|
|
401
|
+
ret[:align] = @align if @align
|
|
402
|
+
ret[:vertical] = @vertical if @vertical
|
|
403
|
+
value = width and ret[:width] = value
|
|
404
|
+
value = height and ret[:height] = value
|
|
405
|
+
value = real_padding and ret[:padding] = value
|
|
406
|
+
ret
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Applies attribute values from a hash.
|
|
410
|
+
#
|
|
411
|
+
# @example
|
|
412
|
+
# cell.attributes.assign(align: :center, padding: [0, 2])
|
|
413
|
+
#
|
|
414
|
+
# @param attributes [#to_hash] attribute hash, see {#initialize}
|
|
415
|
+
# @return [Cell::Attributes]
|
|
416
|
+
def assign(attributes)
|
|
417
|
+
attributes = attributes.to_hash
|
|
418
|
+
unless attributes.empty?
|
|
419
|
+
@eol = false if attributes[:eol] == false
|
|
420
|
+
@spaces = false if attributes[:spaces] == false
|
|
421
|
+
self.align = attributes[:align] if attributes.key?(:align)
|
|
422
|
+
self.vertical = attributes[:vertical] if attributes.key?(:vertical)
|
|
423
|
+
assign_padding(attributes)
|
|
424
|
+
assign_width(attributes)
|
|
425
|
+
assign_height(attributes)
|
|
426
|
+
end
|
|
427
|
+
self
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# @param attributes [Hash] a customizable set of attributes
|
|
431
|
+
# @option attributes [Boolean] :eol (true)
|
|
432
|
+
# see {#eol}
|
|
433
|
+
# @option attributes [Boolean] :spaces (true)
|
|
434
|
+
# see {#spaces}
|
|
435
|
+
# @option attributes [:left, :center, :right, nil] :align (nil)
|
|
436
|
+
# see {#align}
|
|
437
|
+
# @option attributes [:top, :middle, :bottom, nil] :vertical (nil)
|
|
438
|
+
# see {#vertical}
|
|
439
|
+
# @option attributes [Integer, Float, Range, nil] :width (nil)
|
|
440
|
+
# see {#width}
|
|
441
|
+
# @option attributes [Integer, Float, nil] :min_width (nil)
|
|
442
|
+
# see {#min_width}
|
|
443
|
+
# @option attributes [Integer, Float, nil] :max_width (nil)
|
|
444
|
+
# see {#max_width}
|
|
445
|
+
# @option attributes [Integer, Range, nil] :height (nil)
|
|
446
|
+
# see {#height}
|
|
447
|
+
# @option attributes [Integer, nil] :min_height (nil)
|
|
448
|
+
# see {#min_height}
|
|
449
|
+
# @option attributes [Integer, nil] :max_height (nil)
|
|
450
|
+
# see {#max_height}
|
|
451
|
+
# @option attributes [Integer, #map, nil] :padding (0)
|
|
452
|
+
# see {#padding}
|
|
453
|
+
# @option attributes [Integer, nil] :top_padding (0)
|
|
454
|
+
# see {#top_padding}
|
|
455
|
+
# @option attributes [Integer, nil] :right_padding (0)
|
|
456
|
+
# see {#right_padding}
|
|
457
|
+
# @option attributes [Integer, nil] :bottom_padding (0)
|
|
458
|
+
# see {#bottom_padding}
|
|
459
|
+
# @option attributes [Integer, nil] :left_padding (0)
|
|
460
|
+
# see {#left_padding}
|
|
461
|
+
def initialize(**attributes)
|
|
462
|
+
@eol = @spaces = true
|
|
463
|
+
@padding = [0, 0, 0, 0]
|
|
464
|
+
assign(attributes) unless attributes.empty?
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# @private
|
|
468
|
+
def dup = Attributes.new(**to_hash)
|
|
469
|
+
|
|
470
|
+
private
|
|
471
|
+
|
|
472
|
+
alias _to_s to_s
|
|
473
|
+
private :_to_s, :clone
|
|
474
|
+
|
|
475
|
+
# @private
|
|
476
|
+
def inspect
|
|
477
|
+
return _to_s if (opts = to_hash).empty?
|
|
478
|
+
"#{_to_s.chop} #{opts.map { |k, v| "#{k}=#{v}" }.join(', ')}>"
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def assign_padding(options)
|
|
482
|
+
self.padding = options[:padding] if options.key?(:padding)
|
|
483
|
+
self.top_padding = options[:top_padding] if options.key?(:top_padding)
|
|
484
|
+
self.right_padding = options[:right_padding] if options.key?(
|
|
485
|
+
:right_padding
|
|
486
|
+
)
|
|
487
|
+
self.bottom_padding = options[:bottom_padding] if options.key?(
|
|
488
|
+
:bottom_padding
|
|
489
|
+
)
|
|
490
|
+
self.padding_left = options[:padding_left] if options.key?(
|
|
491
|
+
:padding_left
|
|
492
|
+
)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def assign_width(options)
|
|
496
|
+
if options.key?(:min_width)
|
|
497
|
+
if options.key?(:max_width)
|
|
498
|
+
return self.width = (options[:min_width]..options[:max_width])
|
|
499
|
+
end
|
|
500
|
+
return self.min_width = options[:min_width]
|
|
501
|
+
end
|
|
502
|
+
return self.max_width = options[:max_width] if options.key?(
|
|
503
|
+
:max_width
|
|
504
|
+
)
|
|
505
|
+
self.width = options[:width] if options.key?(:width)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def assign_height(options)
|
|
509
|
+
if options.key?(:min_height)
|
|
510
|
+
if options.key?(:max_height)
|
|
511
|
+
return self.height = (options[:min_height]..options[:max_height])
|
|
512
|
+
end
|
|
513
|
+
return self.min_height = options[:min_height]
|
|
514
|
+
end
|
|
515
|
+
if options.key?(:max_height)
|
|
516
|
+
return self.max_height = options[:max_height]
|
|
517
|
+
end
|
|
518
|
+
self.height = options[:height] if options.key?(:height)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def real_padding
|
|
522
|
+
ret = @padding.dup
|
|
523
|
+
return ret if ret[-1] != ret[1]
|
|
524
|
+
ret.pop
|
|
525
|
+
return ret if ret[-1] != ret[0]
|
|
526
|
+
return (ret = ret[0]).zero? ? nil : ret if ret[0] == ret[1]
|
|
527
|
+
ret.pop
|
|
528
|
+
ret
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Returns `true` when this cell has no text and default attributes.
|
|
533
|
+
#
|
|
534
|
+
# @attribute [r] empty?
|
|
535
|
+
# @return [Boolean]
|
|
536
|
+
def empty? = @text.empty? && @attributes.empty?
|
|
537
|
+
|
|
538
|
+
# Text content of this cell.
|
|
539
|
+
#
|
|
540
|
+
# @return [Array] array of text values passed at construction
|
|
541
|
+
attr_reader :text
|
|
542
|
+
|
|
543
|
+
# Formatting attributes of this cell.
|
|
544
|
+
#
|
|
545
|
+
# @return [Cell::Attributes]
|
|
546
|
+
attr_reader :attributes
|
|
547
|
+
|
|
548
|
+
# @param text [#to_s, ...]
|
|
549
|
+
# cell text
|
|
550
|
+
# @param attributes
|
|
551
|
+
# cell attributes (see {Cell::Attributes#initialize})
|
|
552
|
+
def initialize(*text, **attributes)
|
|
553
|
+
@text = text
|
|
554
|
+
@attributes = Attributes.new(**attributes)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# @private
|
|
558
|
+
EMPTY = new.freeze
|
|
559
|
+
|
|
560
|
+
private
|
|
561
|
+
|
|
562
|
+
def initialize_copy(*)
|
|
563
|
+
super
|
|
564
|
+
@text.map!(&:dup)
|
|
565
|
+
@attributes = @attributes.dup
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
alias _to_s to_s
|
|
569
|
+
private :_to_s
|
|
570
|
+
|
|
571
|
+
def inspect
|
|
572
|
+
return _to_s if (att = @attributes.to_hash).empty?
|
|
573
|
+
"#{_to_s.chop} #{att.map { |k, v| "#{k}=#{v}" }.join(', ')}>"
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
ALIGN_VALUES = [:left, :center, :right, nil].freeze
|
|
577
|
+
VERTICAL_VALUES = [:top, :middle, :bottom, nil].freeze
|
|
578
|
+
private_constant :ALIGN_VALUES, :VERTICAL_VALUES
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# A single row in a {Table}.
|
|
582
|
+
#
|
|
583
|
+
# Rows are accessed through {Table#rows} or created implicitly by
|
|
584
|
+
# {Table#add_row} and {Table#[]}. A row holds an ordered list of {Cell}
|
|
585
|
+
# objects and carries default formatting {#attributes} that apply to all
|
|
586
|
+
# cells in the row during rendering.
|
|
587
|
+
class Row
|
|
588
|
+
include Enumerable
|
|
589
|
+
extend Forwardable
|
|
590
|
+
|
|
591
|
+
def_delegators :@cells,
|
|
592
|
+
:size,
|
|
593
|
+
:length,
|
|
594
|
+
:empty?,
|
|
595
|
+
:any?,
|
|
596
|
+
:none?,
|
|
597
|
+
:at,
|
|
598
|
+
:clear,
|
|
599
|
+
:pop
|
|
600
|
+
|
|
601
|
+
# @attribute [r] size
|
|
602
|
+
# Number of columns.
|
|
603
|
+
# @return [Integer]
|
|
604
|
+
|
|
605
|
+
# @attribute [r] empty?
|
|
606
|
+
# Wheter the row is empty.
|
|
607
|
+
# @return [Boolean]
|
|
608
|
+
|
|
609
|
+
# @attribute [r] any?
|
|
610
|
+
# Wheter the row contains at least one {Cell}.
|
|
611
|
+
# @return [Boolean]
|
|
612
|
+
|
|
613
|
+
# @attribute [r] none?
|
|
614
|
+
# Wheter the row contains at no {Cell}.
|
|
615
|
+
# @return [Boolean]
|
|
616
|
+
|
|
617
|
+
# Default formatting attributes for all cells in this row.
|
|
618
|
+
#
|
|
619
|
+
# @return [Cell::Attributes]
|
|
620
|
+
attr_reader :attributes
|
|
621
|
+
|
|
622
|
+
# @!method at(index)
|
|
623
|
+
# Returns the {Cell} at `index`, or `nil` if it does not exist.
|
|
624
|
+
# @param index [#to_int] zero-based column index
|
|
625
|
+
# @return [Cell, nil]
|
|
626
|
+
|
|
627
|
+
# Returns the cell at `index`, creating an empty cell if none exists.
|
|
628
|
+
#
|
|
629
|
+
# @param index (see #at)
|
|
630
|
+
# @return [Cell]
|
|
631
|
+
def [](index) = @cells[index.to_int] ||= Cell.new
|
|
632
|
+
|
|
633
|
+
# Sets or replaces the cell at `index`.
|
|
634
|
+
#
|
|
635
|
+
# Accepts the same arguments as {Cell#initialize}: optional text values
|
|
636
|
+
# followed by an optional keyword hash of {Cell::Attributes} options.
|
|
637
|
+
#
|
|
638
|
+
# @example
|
|
639
|
+
# row[1] = 'Hello', align: :center
|
|
640
|
+
#
|
|
641
|
+
# @param index (see #fill)
|
|
642
|
+
def []=(index, *args)
|
|
643
|
+
opts = args.pop if args[-1].is_a?(Hash)
|
|
644
|
+
fill(index, *args, **opts)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Appends a new cell to this row.
|
|
648
|
+
#
|
|
649
|
+
# @example Append and return the new cell
|
|
650
|
+
# row.add 'Alice', align: :left
|
|
651
|
+
#
|
|
652
|
+
# @example Append and configure via block
|
|
653
|
+
# row.add 'Alice' do |cell|
|
|
654
|
+
# cell.attributes.align = :center
|
|
655
|
+
# end
|
|
656
|
+
#
|
|
657
|
+
# @param (see Cell#initialize)
|
|
658
|
+
# @yield [cell] the new {Cell}
|
|
659
|
+
# @yieldparam cell [Cell]
|
|
660
|
+
# @return [Object] return value of the block
|
|
661
|
+
# @return [Cell] the new {Cell}, if no block is specified
|
|
662
|
+
def add(*text, **attributes)
|
|
663
|
+
@cells << (cell = Cell.new(*text, **attributes))
|
|
664
|
+
block_given? ? yield(cell) : cell
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Inserts a new cell at the given index.
|
|
668
|
+
#
|
|
669
|
+
# @example Insert and return the new cell
|
|
670
|
+
# row.insert 0, 'Header', align: :center
|
|
671
|
+
#
|
|
672
|
+
# @example Insert and configure via block
|
|
673
|
+
# row.insert(0, 'Header') { |c| c.attributes.align = :center }
|
|
674
|
+
#
|
|
675
|
+
# @param index [#to_int] position to insert at
|
|
676
|
+
# @param (see #add)
|
|
677
|
+
# @yield (see #add)
|
|
678
|
+
# @yieldparam (see #add)
|
|
679
|
+
# @return (see #add)
|
|
680
|
+
def insert(index, *texts, **attributes)
|
|
681
|
+
@cells.insert(index.to_int, cell = Cell.new(*texts, **attributes))
|
|
682
|
+
block_given? ? yield(cell) : cell
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Appends one cell per text value.
|
|
686
|
+
#
|
|
687
|
+
# @example Append cells and return them
|
|
688
|
+
# row.add_text 'A', 'B', 'C', align: :center
|
|
689
|
+
#
|
|
690
|
+
# @example Append cells and configure via block
|
|
691
|
+
# row.add_text 'X', 'Y' do |cells|
|
|
692
|
+
# cells.each { it.attributes.align = :right }
|
|
693
|
+
# end
|
|
694
|
+
#
|
|
695
|
+
# @param texts [#to_s, ...] one text value per cell
|
|
696
|
+
# @param attributes (see Cell#initialize)
|
|
697
|
+
# @yield [cells] array of the new {Cell} objects
|
|
698
|
+
# @yieldparam cells [Array<Cell>]
|
|
699
|
+
# @return [Object] return value of the block
|
|
700
|
+
# @return [Array<Cell>] array of the new {Cell} objects,
|
|
701
|
+
# if no block is specified
|
|
702
|
+
def add_text(*texts, **attributes)
|
|
703
|
+
cells = texts.map! { Cell.new(it, **attributes) }
|
|
704
|
+
@cells.concat(cells)
|
|
705
|
+
block_given? ? yield(cells) : cells
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Sets the cell at one or more indices.
|
|
709
|
+
#
|
|
710
|
+
# @example Single index
|
|
711
|
+
# row.fill 2, 'value'
|
|
712
|
+
#
|
|
713
|
+
# @example Multiple indices (same content)
|
|
714
|
+
# row.fill [0, 2, 4], 'x', align: :center
|
|
715
|
+
#
|
|
716
|
+
# @example Configure via block
|
|
717
|
+
# row.fill(1, 'val') { |cell| cell.attributes.vertical = :middle }
|
|
718
|
+
#
|
|
719
|
+
# @param index [#to_int, #each] a single index or an enumerable of indices
|
|
720
|
+
# @param (see #add)
|
|
721
|
+
# @yield (see #add_text)
|
|
722
|
+
# @yieldparam (see #add_text)
|
|
723
|
+
# @return (see #add_text)
|
|
724
|
+
def fill(index, *text, **attributes)
|
|
725
|
+
ret =
|
|
726
|
+
if index.respond_to?(:each)
|
|
727
|
+
index.each.map { @cells[it] = Cell.new(*text, **attributes) }
|
|
728
|
+
else
|
|
729
|
+
@cells[index] = Cell.new(*text, **attributes)
|
|
730
|
+
end
|
|
731
|
+
block_given? ? yield(ret) : ret
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Fills cells sequentially by calling the block for each index.
|
|
735
|
+
#
|
|
736
|
+
# The block receives the current `Integer` index and must return an
|
|
737
|
+
# `Array` whose elements are the text/attribute arguments for the new
|
|
738
|
+
# cell (an optional trailing `Hash` is treated as attribute options).
|
|
739
|
+
# Returning `nil` or `false` stops iteration.
|
|
740
|
+
#
|
|
741
|
+
# @example
|
|
742
|
+
# row.fill_while do |i|
|
|
743
|
+
# break if i >= 3
|
|
744
|
+
# ["Col #{i}", { align: :center }]
|
|
745
|
+
# end
|
|
746
|
+
#
|
|
747
|
+
# @param index [#to_int] starting index
|
|
748
|
+
# @yield [index] current column index
|
|
749
|
+
# @yieldparam index [Integer]
|
|
750
|
+
# @yieldreturn [Array, nil]
|
|
751
|
+
# arguments for {Cell#initialize}, or `nil` to stop
|
|
752
|
+
def fill_while(index = 0)
|
|
753
|
+
index = index.to_int
|
|
754
|
+
while (args = yield(index))
|
|
755
|
+
opts = args.pop if args[-1].is_a?(Hash)
|
|
756
|
+
@cells[index] = Cell.new(*args, **opts)
|
|
757
|
+
index += 1
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Fills consecutive cells starting at `index` with one cell per text value.
|
|
762
|
+
#
|
|
763
|
+
# @example Fill and return the cells
|
|
764
|
+
# row.fill_text 1, 'B', 'C', 'D'
|
|
765
|
+
#
|
|
766
|
+
# @example Fill and configure via block
|
|
767
|
+
# row.fill_text(1, 'B', 'C') do |cells|
|
|
768
|
+
# cells.each { it.attributes.align = :right }
|
|
769
|
+
# end
|
|
770
|
+
#
|
|
771
|
+
# @param index [#to_int] starting column index
|
|
772
|
+
# @param (see #add_text)
|
|
773
|
+
# @yield (see #add_text)
|
|
774
|
+
# @yieldparam (see #add_text)
|
|
775
|
+
# @return (see #add_text)
|
|
776
|
+
def fill_text(index, *texts, **attributes)
|
|
777
|
+
index = index.to_int - 1
|
|
778
|
+
cells = texts.map! { @cells[index += 1] = Cell.new(it, **attributes) }
|
|
779
|
+
block_given? ? yield(cells) : cells
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Removes a cell from this row.
|
|
783
|
+
#
|
|
784
|
+
# @param index [#to_int, Cell] column index or the {Cell} object itself
|
|
785
|
+
# @return [Boolean] `true` if a cell was removed, `false` otherwise
|
|
786
|
+
def delete(index)
|
|
787
|
+
cell =
|
|
788
|
+
if Cell === index
|
|
789
|
+
@cells.delete(index)
|
|
790
|
+
else
|
|
791
|
+
@cells.delete_at(index.to_int)
|
|
792
|
+
end
|
|
793
|
+
!cell.freeze.nil?
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Iterates over non-nil cells in this row.
|
|
797
|
+
#
|
|
798
|
+
# @example With a block
|
|
799
|
+
# row.each { |cell| cell.attributes.align = :center }
|
|
800
|
+
#
|
|
801
|
+
# @yield [cell] each non-nil {Cell}
|
|
802
|
+
# @yieldparam cell [Cell]
|
|
803
|
+
# @return [nil] if block is specified
|
|
804
|
+
# @return [Enumerator] if no block is specified
|
|
805
|
+
def each
|
|
806
|
+
return to_enum unless block_given?
|
|
807
|
+
@cells.each { yield(it) if it }
|
|
808
|
+
nil
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
private
|
|
812
|
+
|
|
813
|
+
def initialize(**)
|
|
814
|
+
@cells = []
|
|
815
|
+
@attributes = Cell::Attributes.new(**)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
alias _to_s to_s
|
|
819
|
+
private :_to_s, :dup, :clone
|
|
820
|
+
|
|
821
|
+
# @private
|
|
822
|
+
def inspect = "#{_to_s.chop} index=#{@index}>"
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# A vertical slice through a {Table} — all cells at a given column index.
|
|
826
|
+
#
|
|
827
|
+
# Columns are accessed through {Table#columns}. A `Column` holds a
|
|
828
|
+
# reference to its owning {Table} and provides a view of cells at its
|
|
829
|
+
# {#index}. The {#attributes} object carries default formatting applied to
|
|
830
|
+
# all cells in the column during rendering.
|
|
831
|
+
class Column
|
|
832
|
+
include Enumerable
|
|
833
|
+
|
|
834
|
+
# Zero-based index of this column in the table.
|
|
835
|
+
#
|
|
836
|
+
# @return [Integer]
|
|
837
|
+
attr_reader :index
|
|
838
|
+
|
|
839
|
+
# Default formatting attributes for all cells in this column.
|
|
840
|
+
#
|
|
841
|
+
# @return [Cell::Attributes]
|
|
842
|
+
attr_reader :attributes
|
|
843
|
+
|
|
844
|
+
# Number of rows in the owning table.
|
|
845
|
+
#
|
|
846
|
+
# @attribute [r] size
|
|
847
|
+
# @return [Integer]
|
|
848
|
+
def size = @table.rows.size
|
|
849
|
+
|
|
850
|
+
# Returns the cell at row `index`, or `nil` if it does not exist.
|
|
851
|
+
#
|
|
852
|
+
# @param index [#to_int] zero-based row index
|
|
853
|
+
# @return [Cell, nil]
|
|
854
|
+
def at(index) = @table.at(index, @index)
|
|
855
|
+
|
|
856
|
+
# Returns the cell at row `index`, creating an empty cell if needed.
|
|
857
|
+
#
|
|
858
|
+
# @param index [#to_int] zero-based row index
|
|
859
|
+
# @return [Cell]
|
|
860
|
+
def [](index) = @table[index, @index]
|
|
861
|
+
|
|
862
|
+
# Sets or replaces the cell at `index`.
|
|
863
|
+
#
|
|
864
|
+
# Accepts the same arguments as {Cell#initialize}: optional text values
|
|
865
|
+
# followed by an optional keyword hash of {Cell::Attributes} options.
|
|
866
|
+
#
|
|
867
|
+
# @example
|
|
868
|
+
# col[0] = 'Header', align: :center
|
|
869
|
+
#
|
|
870
|
+
# @param index (see #fill)
|
|
871
|
+
# @param (see Cell#initialize)
|
|
872
|
+
def []=(index, *args)
|
|
873
|
+
opts = args.pop if args[-1].is_a?(Hash)
|
|
874
|
+
fill(index, *args, **opts)
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# Sets the cell at one or more row indices.
|
|
878
|
+
#
|
|
879
|
+
# @example Single index
|
|
880
|
+
# col.fill 0, 'Header', align: :center
|
|
881
|
+
#
|
|
882
|
+
# @example Multiple indices and configure via block
|
|
883
|
+
# col.fill([0, 1], 'x') { |cells| cells.each { it.attributes.align = :right } }
|
|
884
|
+
#
|
|
885
|
+
# @param index [#to_int, #each] row index or enumerable of row indices
|
|
886
|
+
# @param (see Cell#initialize)
|
|
887
|
+
# @yield [result] cell or array of cells
|
|
888
|
+
# @return [Object] return value of the block
|
|
889
|
+
# @return [Cell, Array<Cell>] cell or array of cells,
|
|
890
|
+
# if no block is specified
|
|
891
|
+
def fill(index, *text, **attributes)
|
|
892
|
+
ret =
|
|
893
|
+
if index.respond_to?(:each)
|
|
894
|
+
index.each.map { @table.rows[it].fill(@index, *text, **attributes) }
|
|
895
|
+
else
|
|
896
|
+
@table.rows[index].fill(@index, *text, **attributes)
|
|
897
|
+
end
|
|
898
|
+
block_given? ? yield(ret) : ret
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# Fills consecutive rows starting at `index` with one cell per text value.
|
|
902
|
+
#
|
|
903
|
+
# @example Fill and return the cells
|
|
904
|
+
# col.fill_text 0, 'A', 'B', 'C'
|
|
905
|
+
#
|
|
906
|
+
# @example Fill and configure via block
|
|
907
|
+
# col.fill_text(0, 'A', 'B') { |cells| cells.each { it.attributes.align = :center } }
|
|
908
|
+
#
|
|
909
|
+
# @param index [#to_int] starting row index
|
|
910
|
+
# @param texts [#to_s, ...] one text value per row
|
|
911
|
+
# @param attributes (see Cell#initialize)
|
|
912
|
+
# @yield [cells] array of created cells
|
|
913
|
+
# @return [Object] return value of the block
|
|
914
|
+
# @return [Array<Cell>] array of created cells,
|
|
915
|
+
# if no block is specified
|
|
916
|
+
def fill_text(index, *texts, **attributes)
|
|
917
|
+
index = index.to_int - 1
|
|
918
|
+
rows =
|
|
919
|
+
texts.map! { @table.rows[index += 1].fill(@index, it, **attributes) }
|
|
920
|
+
block_given? ? yield(rows) : rows
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# Fills cells in this column sequentially by calling the block for each row.
|
|
924
|
+
#
|
|
925
|
+
# The block receives the current `Integer` row index and must return an
|
|
926
|
+
# `Array` of arguments for the cell (optional trailing `Hash` for
|
|
927
|
+
# attributes), or `nil` / `false` to stop.
|
|
928
|
+
#
|
|
929
|
+
# @example
|
|
930
|
+
# col.fill_while { |i| i < 3 ? ["Row #{i}"] : nil }
|
|
931
|
+
#
|
|
932
|
+
# @param index [#to_int] starting row index
|
|
933
|
+
# @yield [index] current row index
|
|
934
|
+
# @yieldparam index [Integer]
|
|
935
|
+
# @yieldreturn [Array, nil]
|
|
936
|
+
# arguments for {Cell#initialize}, or `nil` to stop
|
|
937
|
+
def fill_while(index = 0)
|
|
938
|
+
index = index.to_int
|
|
939
|
+
while (args = yield(index))
|
|
940
|
+
opts = args.pop if args[-1].is_a?(Hash)
|
|
941
|
+
@table.rows[index].fill(@index, *args, **opts)
|
|
942
|
+
index += 1
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
# Removes this column from the table (deletes all cells at this index).
|
|
947
|
+
#
|
|
948
|
+
# @return [Integer, nil] number of cells deleted, or `nil` if the column
|
|
949
|
+
# was already empty
|
|
950
|
+
def delete!
|
|
951
|
+
@table.rows.each.find_all { it.delete(@index) }.size.nonzero?
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
# Iterates over non-nil cells in this column.
|
|
955
|
+
#
|
|
956
|
+
# @example
|
|
957
|
+
# col.each { |cell| cell.attributes.align = :right }
|
|
958
|
+
#
|
|
959
|
+
# @yield [cell] each non-nil {Cell}
|
|
960
|
+
# @yieldparam cell [Cell]
|
|
961
|
+
# @return [nil] if block is specified
|
|
962
|
+
# @return [Enumerator] if no block is specified
|
|
963
|
+
def each
|
|
964
|
+
return to_enum unless block_given?
|
|
965
|
+
@table.rows.each { (cell = it.at(@index)) and yield(cell) }
|
|
966
|
+
nil
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
private
|
|
970
|
+
|
|
971
|
+
def initialize(table, index)
|
|
972
|
+
@table = table
|
|
973
|
+
@index = index
|
|
974
|
+
@attributes = Cell::Attributes.new
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
alias _to_s to_s
|
|
978
|
+
private :_to_s
|
|
979
|
+
|
|
980
|
+
# @private
|
|
981
|
+
def inspect = "#{_to_s.chop} #{@index}>"
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
# Ordered collection of {Row} objects belonging to a {Table}.
|
|
985
|
+
# Accessed via {Table#rows}.
|
|
986
|
+
class RowCollection
|
|
987
|
+
include Enumerable
|
|
988
|
+
|
|
989
|
+
extend Forwardable
|
|
990
|
+
|
|
991
|
+
def_delegators :@rows,
|
|
992
|
+
:size,
|
|
993
|
+
:length,
|
|
994
|
+
:empty?,
|
|
995
|
+
:any?,
|
|
996
|
+
:none?,
|
|
997
|
+
:clear,
|
|
998
|
+
:pop
|
|
999
|
+
|
|
1000
|
+
# @attribute [r] size
|
|
1001
|
+
# Number of rows.
|
|
1002
|
+
# @return [Integer]
|
|
1003
|
+
|
|
1004
|
+
# @attribute [r] empty?
|
|
1005
|
+
# Wheter the collection is empty.
|
|
1006
|
+
# @return [Boolean]
|
|
1007
|
+
|
|
1008
|
+
# @attribute [r] any?
|
|
1009
|
+
# Wheter the collection contains at least one {Row}.
|
|
1010
|
+
# @return [Boolean]
|
|
1011
|
+
|
|
1012
|
+
# @attribute [r] none?
|
|
1013
|
+
# Wheter the collection contains at no {Row}.
|
|
1014
|
+
# @return [Boolean]
|
|
1015
|
+
|
|
1016
|
+
# Returns the row at `index`, or `nil` if it does not exist.
|
|
1017
|
+
#
|
|
1018
|
+
# @example
|
|
1019
|
+
# rows.at(0) # => first Row or nil
|
|
1020
|
+
#
|
|
1021
|
+
# @param index [#to_int] zero-based row index
|
|
1022
|
+
# @return [Row, nil]
|
|
1023
|
+
def at(index) = @rows[index]
|
|
1024
|
+
|
|
1025
|
+
# Returns the row at `index`, creating an empty row if none exists.
|
|
1026
|
+
#
|
|
1027
|
+
# @example
|
|
1028
|
+
# rows[0] # => Row (created if missing)
|
|
1029
|
+
#
|
|
1030
|
+
# @param index [#to_int] zero-based row index
|
|
1031
|
+
# @return [Row]
|
|
1032
|
+
def [](index) = @rows[index.to_int] ||= Row.new
|
|
1033
|
+
|
|
1034
|
+
# Appends a new row, optionally pre-populated with text cells.
|
|
1035
|
+
#
|
|
1036
|
+
# @example Append and return the new row
|
|
1037
|
+
# rows.add 'Alice', 'Bob', align: :center
|
|
1038
|
+
#
|
|
1039
|
+
# @example Append and populate via block
|
|
1040
|
+
# rows.add do |row|
|
|
1041
|
+
# row.add 'Name', align: :right
|
|
1042
|
+
# row.add 'Score', align: :left
|
|
1043
|
+
# end
|
|
1044
|
+
#
|
|
1045
|
+
# @param texts [#to_s, ...] text values; each becomes a cell in the new row
|
|
1046
|
+
# @param attributes [Hash] default attributes applied to the row,
|
|
1047
|
+
# see {Cell::Attributes#initialize}
|
|
1048
|
+
# @yield [row] the new {Row}
|
|
1049
|
+
# @yieldparam row [Row]
|
|
1050
|
+
# @return [Object] return value of the block
|
|
1051
|
+
# @return [Row] the new row, if no block is specified
|
|
1052
|
+
def add(*texts, **attributes)
|
|
1053
|
+
@rows << (row = Row.new(**attributes))
|
|
1054
|
+
row.add_text(*texts) unless texts.empty?
|
|
1055
|
+
block_given? ? yield(row) : row
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
# Inserts a new row at the given index.
|
|
1059
|
+
#
|
|
1060
|
+
# @example Insert and return the new row
|
|
1061
|
+
# rows.insert 0, 'Header A', 'Header B', align: :center
|
|
1062
|
+
#
|
|
1063
|
+
# @example Insert and populate via block
|
|
1064
|
+
# rows.insert(0) { |row| row.add 'Header' }
|
|
1065
|
+
#
|
|
1066
|
+
# @param index [#to_int] position to insert at
|
|
1067
|
+
# @param (see #add)
|
|
1068
|
+
# @yield (see #add)
|
|
1069
|
+
# @yieldparam (see #add)
|
|
1070
|
+
# @return (see #add)
|
|
1071
|
+
def insert(index, *texts, **attributes)
|
|
1072
|
+
@rows.insert(index.to_int, row = Row.new(**attributes))
|
|
1073
|
+
row.add_text(*texts) unless texts.empty?
|
|
1074
|
+
block_given? ? yield(row) : row
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
# Appends one new row per text value (each value becomes a single-cell row).
|
|
1078
|
+
#
|
|
1079
|
+
# @example Append and return the rows
|
|
1080
|
+
# rows.add_text 'Row A', 'Row B'
|
|
1081
|
+
#
|
|
1082
|
+
# @example Append and configure via block
|
|
1083
|
+
# rows.add_text('Row A', 'Row B') { |rows| rows.each { it.attributes.align = :center } }
|
|
1084
|
+
#
|
|
1085
|
+
# @param texts [#to_s, ...] one text value per row
|
|
1086
|
+
# @param attributes [Hash] default attributes applied to each row,
|
|
1087
|
+
# see {Cell::Attributes#initialize}
|
|
1088
|
+
# @yield [rows] array of the new {Row} objects
|
|
1089
|
+
# @yieldparam rows [Array<Row>]
|
|
1090
|
+
# @return [Object] return value of the block
|
|
1091
|
+
# @return [Array<Row>] the new rows, if no block is specified
|
|
1092
|
+
def add_text(*texts, **attributes)
|
|
1093
|
+
@rows.concat(
|
|
1094
|
+
rows =
|
|
1095
|
+
texts.map! do |txt|
|
|
1096
|
+
row = Row.new(**attributes)
|
|
1097
|
+
row.add(*txt)
|
|
1098
|
+
row
|
|
1099
|
+
end
|
|
1100
|
+
)
|
|
1101
|
+
block_given? ? yield(rows) : rows
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
# Removes a row from the collection.
|
|
1105
|
+
#
|
|
1106
|
+
# @param index [#to_int, Row] row index or the {Row} object itself
|
|
1107
|
+
# @return [Boolean] `true` if a row was removed, `false` otherwise
|
|
1108
|
+
def delete(index)
|
|
1109
|
+
row =
|
|
1110
|
+
if Row === index
|
|
1111
|
+
@rows.delete(index)
|
|
1112
|
+
else
|
|
1113
|
+
@rows.delete_at(index.to_int)
|
|
1114
|
+
end
|
|
1115
|
+
!row.freeze.nil?
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# Iterates over all non-nil rows.
|
|
1119
|
+
#
|
|
1120
|
+
# @example
|
|
1121
|
+
# rows.each { |r| r.attributes.align = :center }
|
|
1122
|
+
#
|
|
1123
|
+
# @yield [row] each non-nil {Row}
|
|
1124
|
+
# @yieldparam row [Row]
|
|
1125
|
+
# @return [nil] if block is specified
|
|
1126
|
+
# @return [Enumerator] if no block is specified
|
|
1127
|
+
def each
|
|
1128
|
+
return to_enum unless block_given?
|
|
1129
|
+
@rows.each { yield(it) if it }
|
|
1130
|
+
nil
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
# Iterates over non-nil rows that contain at least one cell.
|
|
1134
|
+
#
|
|
1135
|
+
# @yield [row] each non-empty {Row}
|
|
1136
|
+
# @yieldparam (see #each)
|
|
1137
|
+
# @return (see #each)
|
|
1138
|
+
def each_filled
|
|
1139
|
+
return to_enum(__method__) unless block_given?
|
|
1140
|
+
@rows.each { yield(it) if it&.any? }
|
|
1141
|
+
nil
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
private
|
|
1145
|
+
|
|
1146
|
+
def initialize
|
|
1147
|
+
@rows = []
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
alias _to_s to_s
|
|
1151
|
+
|
|
1152
|
+
# @private
|
|
1153
|
+
def inspect = "#{_to_s.chop} size=#{size}>"
|
|
1154
|
+
private :_to_s, :dup, :clone
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
# Collection of {Column} objects for a {Table}.
|
|
1158
|
+
#
|
|
1159
|
+
# Accessed via {Table#columns}. Columns are created on demand; accessing
|
|
1160
|
+
# a column index that has no data still returns a valid {Column} object
|
|
1161
|
+
# whose cells reference the underlying table rows.
|
|
1162
|
+
class ColumnCollection
|
|
1163
|
+
include Enumerable
|
|
1164
|
+
|
|
1165
|
+
# @private
|
|
1166
|
+
def max = @table.rows.each.max_by(&:size)&.size || 0
|
|
1167
|
+
|
|
1168
|
+
# @private
|
|
1169
|
+
def min = @table.rows.each.min_by(&:size)&.size || 0
|
|
1170
|
+
|
|
1171
|
+
alias size max
|
|
1172
|
+
alias length max
|
|
1173
|
+
|
|
1174
|
+
# @attribute [r] size
|
|
1175
|
+
# Number of columns.
|
|
1176
|
+
# @return [Integer]
|
|
1177
|
+
|
|
1178
|
+
# Returns the column at `index`, or `nil` if no cells exist at that index.
|
|
1179
|
+
#
|
|
1180
|
+
# @param index [#to_int] zero-based column index
|
|
1181
|
+
# @return [Column, nil]
|
|
1182
|
+
def at(index)
|
|
1183
|
+
@columns.key?(index = index.to_int) and @columns[index]
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
# Returns the column at `index`, or `nil` if the index is beyond the
|
|
1187
|
+
# table width.
|
|
1188
|
+
#
|
|
1189
|
+
# @param (see #at)
|
|
1190
|
+
# @return (see #at)
|
|
1191
|
+
def [](index)
|
|
1192
|
+
index = index.to_int
|
|
1193
|
+
@columns[index] if index < max
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
# Appends a new column (adds cells to each existing row) and returns it.
|
|
1197
|
+
#
|
|
1198
|
+
# @example Append and return the column
|
|
1199
|
+
# columns.add 'Alice', 'Bob', 'Carol', align: :left
|
|
1200
|
+
#
|
|
1201
|
+
# @example Append and configure via block
|
|
1202
|
+
# columns.add('X', 'Y') { |col| col.attributes.align = :center }
|
|
1203
|
+
#
|
|
1204
|
+
# @param texts [#to_s, ...] one text value per row in the new column
|
|
1205
|
+
# @param attributes [Hash] attribute options applied to the column,
|
|
1206
|
+
# (see Cell#initialize)
|
|
1207
|
+
# @yield [column] the {Column}
|
|
1208
|
+
# @yieldparam column [Column]
|
|
1209
|
+
# @return [Object] return value of the block
|
|
1210
|
+
# @return [Column] the {Column}, if no block is specified
|
|
1211
|
+
def add(*texts, **attributes)
|
|
1212
|
+
col = @columns[max]
|
|
1213
|
+
col.attributes.assign(attributes) unless attributes.empty?
|
|
1214
|
+
col.fill_text(0, *texts)
|
|
1215
|
+
block_given? ? yield(col) : col
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
# Iterates over columns that contain at least one non-nil cell.
|
|
1219
|
+
#
|
|
1220
|
+
# @example
|
|
1221
|
+
# columns.each { |col| col.attributes.align = :center }
|
|
1222
|
+
#
|
|
1223
|
+
# @yield [column] each non-empty {Column}
|
|
1224
|
+
# @yieldparam column [Column]
|
|
1225
|
+
# @return [nil] if block is specified
|
|
1226
|
+
# @return [Enumerator] if no block is specified
|
|
1227
|
+
def each
|
|
1228
|
+
return to_enum unless block_given?
|
|
1229
|
+
max.times do |index|
|
|
1230
|
+
@table.rows.any? { it.at(index) } or next
|
|
1231
|
+
yield(@columns[index])
|
|
1232
|
+
end
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
private
|
|
1236
|
+
|
|
1237
|
+
def initialize(table)
|
|
1238
|
+
@table = table
|
|
1239
|
+
@columns =
|
|
1240
|
+
Hash.new { |h, k| h[k] = Column.new(table, k) }.compare_by_identity
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
# @private
|
|
1244
|
+
alias inspect to_s
|
|
1245
|
+
private :dup, :clone
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
# Row collection for this table.
|
|
1249
|
+
#
|
|
1250
|
+
# @return [RowCollection]
|
|
1251
|
+
attr_reader :rows
|
|
1252
|
+
|
|
1253
|
+
# Column collection for this table.
|
|
1254
|
+
#
|
|
1255
|
+
# @return [ColumnCollection]
|
|
1256
|
+
attr_reader :columns
|
|
1257
|
+
|
|
1258
|
+
# Returns `true` when the table has no cells.
|
|
1259
|
+
#
|
|
1260
|
+
# @attribute [r] empty?
|
|
1261
|
+
# @return [Boolean] whether the table is empty
|
|
1262
|
+
def empty? = @rows.none?
|
|
1263
|
+
|
|
1264
|
+
# Returns the row at `row_index`, or the cell at `[row_index, column_index]`.
|
|
1265
|
+
#
|
|
1266
|
+
# Returns `nil` when the row (or cell) does not exist.
|
|
1267
|
+
#
|
|
1268
|
+
# @example Get a row
|
|
1269
|
+
# table.at(0)
|
|
1270
|
+
#
|
|
1271
|
+
# @example Get a cell
|
|
1272
|
+
# table.at(0, 2)
|
|
1273
|
+
#
|
|
1274
|
+
# @param row_index [#to_int] zero-based row index
|
|
1275
|
+
# @param column_index [#to_int, nil] zero-based column index, or `nil` for the row
|
|
1276
|
+
# @return [Row, Cell, nil]
|
|
1277
|
+
def at(row_index, column_index = nil)
|
|
1278
|
+
row = @rows.at(row_index) or return
|
|
1279
|
+
column_index ? row.at(column_index) : row
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
# Returns (or creates) the row at `row_index`, or the cell at
|
|
1283
|
+
# `[row_index, column_index]`.
|
|
1284
|
+
#
|
|
1285
|
+
# @example Get or create a row
|
|
1286
|
+
# table[0]
|
|
1287
|
+
#
|
|
1288
|
+
# @example Get or create a cell
|
|
1289
|
+
# table[1, 2] = 'value'
|
|
1290
|
+
#
|
|
1291
|
+
# @param row_index [#to_int] zero-based row index
|
|
1292
|
+
# @param column_index [#to_int, nil] zero-based column index, or `nil` for the row
|
|
1293
|
+
# @return [Row, Cell]
|
|
1294
|
+
def [](row_index, column_index = nil)
|
|
1295
|
+
row = @rows[row_index]
|
|
1296
|
+
column_index ? row[column_index] : row
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
# Appends a new row to the table.
|
|
1300
|
+
#
|
|
1301
|
+
# @example
|
|
1302
|
+
# table.add_row 'Alice', 42, align: :center
|
|
1303
|
+
#
|
|
1304
|
+
# @param (see RowCollection#add)
|
|
1305
|
+
# @yield (see RowCollection#add)
|
|
1306
|
+
# @yieldparam (see RowCollection#add)
|
|
1307
|
+
# @return (see RowCollection#add)
|
|
1308
|
+
def add_row(*texts, **attributes, &)
|
|
1309
|
+
@rows.add(*texts, **attributes, &)
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
# Appends a new column to the table.
|
|
1313
|
+
#
|
|
1314
|
+
# @example
|
|
1315
|
+
# table.add_column 'Header A', 'Header B', align: :center
|
|
1316
|
+
#
|
|
1317
|
+
# @param (see ColumnCollection#add)
|
|
1318
|
+
# @yield (see ColumnCollection#add)
|
|
1319
|
+
# @yieldparam (see ColumnCollection#add)
|
|
1320
|
+
# @return (see ColumnCollection#add)
|
|
1321
|
+
def add_column(*texts, **attributes, &)
|
|
1322
|
+
@columns.add(*texts, **attributes, &)
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
# Iterates over all rows in the table.
|
|
1326
|
+
#
|
|
1327
|
+
# @example
|
|
1328
|
+
# table.each_row { |r| r.attributes.align = :center }
|
|
1329
|
+
#
|
|
1330
|
+
# @yield (see RowCollection#each)
|
|
1331
|
+
# @yieldparam (see RowCollection#each)
|
|
1332
|
+
# @return (see RowCollection#each)
|
|
1333
|
+
def each_row(&) = @rows.each(&)
|
|
1334
|
+
|
|
1335
|
+
# Iterates over all columns in the table.
|
|
1336
|
+
#
|
|
1337
|
+
# @example
|
|
1338
|
+
# table.each_column { |col| col.attributes.align = :right }
|
|
1339
|
+
#
|
|
1340
|
+
# @yield (see ColumnCollection#each)
|
|
1341
|
+
# @yieldparam (see ColumnCollection#each)
|
|
1342
|
+
# @return (see ColumnCollection#each)
|
|
1343
|
+
def each_column(&) = @columns.each(&)
|
|
1344
|
+
|
|
1345
|
+
# @private
|
|
1346
|
+
def to_ary
|
|
1347
|
+
return [] if (max = @columns.max).zero?
|
|
1348
|
+
@rows.each_filled.map do |row|
|
|
1349
|
+
Array.new(max) { |idx| (cell = row.at(idx)) ? cell.dup : Cell::EMPTY }
|
|
1350
|
+
end
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
# @private
|
|
1354
|
+
def texts
|
|
1355
|
+
return [] if (max = @columns.max).zero?
|
|
1356
|
+
@rows.each_filled.map do |row|
|
|
1357
|
+
Array.new(max) do |idx|
|
|
1358
|
+
(cell = row.at(idx)) ? cell.text.join("\n") : ''
|
|
1359
|
+
end
|
|
1360
|
+
end
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
private
|
|
1364
|
+
|
|
1365
|
+
def initialize
|
|
1366
|
+
@rows = RowCollection.new
|
|
1367
|
+
@columns = ColumnCollection.new(self)
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
alias _to_s to_s
|
|
1371
|
+
|
|
1372
|
+
# @private
|
|
1373
|
+
def inspect = "#{_to_s.chop} rows=#{@rows.size} columns=#{@columns.size}>"
|
|
1374
|
+
private :_to_s, :dup, :clone
|
|
1375
|
+
end
|
|
1376
|
+
end
|