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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +0 -1
  3. data/README.md +6 -6
  4. data/examples/24bit-colors.rb +9 -5
  5. data/examples/3bit-colors.rb +7 -7
  6. data/examples/8bit-colors.rb +5 -5
  7. data/examples/attributes.rb +2 -3
  8. data/examples/elements.rb +9 -6
  9. data/examples/examples.rb +9 -9
  10. data/examples/frames.rb +31 -0
  11. data/examples/hbars.rb +6 -3
  12. data/examples/info.rb +13 -10
  13. data/examples/key-codes.rb +8 -9
  14. data/examples/ls.rb +24 -22
  15. data/examples/named-colors.rb +4 -3
  16. data/examples/sections.rb +27 -17
  17. data/examples/select.rb +28 -0
  18. data/examples/sh.rb +25 -7
  19. data/examples/tables.rb +19 -37
  20. data/examples/tasks.rb +32 -22
  21. data/examples/vbars.rb +5 -3
  22. data/lib/natty-ui/dumb_progress.rb +68 -0
  23. data/lib/natty-ui/element.rb +64 -65
  24. data/lib/natty-ui/features.rb +773 -872
  25. data/lib/natty-ui/frame.rb +87 -0
  26. data/lib/natty-ui/helper/table.rb +1376 -0
  27. data/lib/natty-ui/margin.rb +83 -0
  28. data/lib/natty-ui/progress.rb +116 -149
  29. data/lib/natty-ui/renderer/bars.rb +93 -0
  30. data/lib/natty-ui/renderer/choice.rb +56 -0
  31. data/lib/natty-ui/renderer/dumb_choice.rb +34 -0
  32. data/lib/natty-ui/renderer/dumb_select.rb +60 -0
  33. data/lib/natty-ui/renderer/dumb_shell_runner.rb +19 -0
  34. data/lib/natty-ui/renderer/heading.rb +26 -0
  35. data/lib/natty-ui/renderer/horizontal_rule.rb +32 -0
  36. data/lib/natty-ui/{ls_renderer.rb → renderer/ls.rb} +15 -27
  37. data/lib/natty-ui/renderer/mark.rb +13 -0
  38. data/lib/natty-ui/renderer/quote.rb +13 -0
  39. data/lib/natty-ui/renderer/select.rb +63 -0
  40. data/lib/natty-ui/renderer/shell.rb +15 -0
  41. data/lib/natty-ui/renderer/shell_runner.rb +29 -0
  42. data/lib/natty-ui/renderer/table_renderer.rb +429 -0
  43. data/lib/natty-ui/section.rb +142 -41
  44. data/lib/natty-ui/task.rb +39 -27
  45. data/lib/natty-ui/temporary.rb +27 -14
  46. data/lib/natty-ui/utils/border.rb +139 -0
  47. data/lib/natty-ui/utils/str_const.rb +62 -0
  48. data/lib/natty-ui/utils/utils.rb +47 -0
  49. data/lib/natty-ui/version.rb +1 -1
  50. data/lib/natty-ui.rb +87 -30
  51. metadata +31 -28
  52. data/examples/cols.rb +0 -38
  53. data/examples/illustration.rb +0 -60
  54. data/examples/options.rb +0 -28
  55. data/examples/themes.rb +0 -51
  56. data/lib/natty-ui/attributes.rb +0 -593
  57. data/lib/natty-ui/choice.rb +0 -67
  58. data/lib/natty-ui/dumb_choice.rb +0 -47
  59. data/lib/natty-ui/dumb_options.rb +0 -64
  60. data/lib/natty-ui/framed.rb +0 -51
  61. data/lib/natty-ui/hbars_renderer.rb +0 -66
  62. data/lib/natty-ui/options.rb +0 -78
  63. data/lib/natty-ui/shell_renderer.rb +0 -91
  64. data/lib/natty-ui/table.rb +0 -325
  65. data/lib/natty-ui/table_renderer.rb +0 -165
  66. data/lib/natty-ui/theme.rb +0 -403
  67. data/lib/natty-ui/utils.rb +0 -111
  68. data/lib/natty-ui/vbars_renderer.rb +0 -49
  69. data/lib/natty-ui/width_finder.rb +0 -137
  70. 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