natty-ui 0.12.0 → 0.25.0

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +23 -24
  4. data/examples/24bit-colors.rb +4 -9
  5. data/examples/3bit-colors.rb +28 -8
  6. data/examples/8bit-colors.rb +18 -23
  7. data/examples/attributes.rb +30 -25
  8. data/examples/cols.rb +40 -0
  9. data/examples/elements.rb +31 -0
  10. data/examples/examples.rb +45 -0
  11. data/examples/illustration.rb +56 -54
  12. data/examples/ls.rb +16 -18
  13. data/examples/named-colors.rb +23 -0
  14. data/examples/sections.rb +29 -0
  15. data/examples/tables.rb +62 -0
  16. data/examples/tasks.rb +52 -0
  17. data/lib/natty-ui/attributes.rb +604 -0
  18. data/lib/natty-ui/choice.rb +56 -0
  19. data/lib/natty-ui/dumb_choice.rb +45 -0
  20. data/lib/natty-ui/element.rb +78 -0
  21. data/lib/natty-ui/features.rb +798 -0
  22. data/lib/natty-ui/framed.rb +51 -0
  23. data/lib/natty-ui/ls_renderer.rb +93 -0
  24. data/lib/natty-ui/progress.rb +187 -0
  25. data/lib/natty-ui/section.rb +69 -0
  26. data/lib/natty-ui/table.rb +241 -0
  27. data/lib/natty-ui/table_renderer.rb +147 -0
  28. data/lib/natty-ui/task.rb +44 -0
  29. data/lib/natty-ui/temporary.rb +38 -0
  30. data/lib/natty-ui/theme.rb +303 -0
  31. data/lib/natty-ui/utils.rb +79 -0
  32. data/lib/natty-ui/version.rb +1 -1
  33. data/lib/natty-ui/width_finder.rb +125 -0
  34. data/lib/natty-ui.rb +89 -147
  35. metadata +47 -56
  36. data/examples/animate.rb +0 -44
  37. data/examples/attributes_list.rb +0 -14
  38. data/examples/demo.rb +0 -53
  39. data/examples/message.rb +0 -32
  40. data/examples/progress.rb +0 -68
  41. data/examples/query.rb +0 -41
  42. data/examples/read_key.rb +0 -13
  43. data/examples/table.rb +0 -41
  44. data/lib/natty-ui/animation/binary.rb +0 -36
  45. data/lib/natty-ui/animation/default.rb +0 -38
  46. data/lib/natty-ui/animation/matrix.rb +0 -51
  47. data/lib/natty-ui/animation/rainbow.rb +0 -28
  48. data/lib/natty-ui/animation/type_writer.rb +0 -44
  49. data/lib/natty-ui/animation.rb +0 -69
  50. data/lib/natty-ui/ansi/constants.rb +0 -75
  51. data/lib/natty-ui/ansi.rb +0 -521
  52. data/lib/natty-ui/ansi_wrapper.rb +0 -199
  53. data/lib/natty-ui/frame.rb +0 -53
  54. data/lib/natty-ui/glyph.rb +0 -64
  55. data/lib/natty-ui/key_map.rb +0 -142
  56. data/lib/natty-ui/preload.rb +0 -12
  57. data/lib/natty-ui/spinner.rb +0 -120
  58. data/lib/natty-ui/text/east_asian_width.rb +0 -2529
  59. data/lib/natty-ui/text.rb +0 -203
  60. data/lib/natty-ui/wrapper/animate.rb +0 -17
  61. data/lib/natty-ui/wrapper/ask.rb +0 -78
  62. data/lib/natty-ui/wrapper/element.rb +0 -79
  63. data/lib/natty-ui/wrapper/features.rb +0 -21
  64. data/lib/natty-ui/wrapper/framed.rb +0 -45
  65. data/lib/natty-ui/wrapper/heading.rb +0 -60
  66. data/lib/natty-ui/wrapper/horizontal_rule.rb +0 -37
  67. data/lib/natty-ui/wrapper/list_in_columns.rb +0 -138
  68. data/lib/natty-ui/wrapper/message.rb +0 -109
  69. data/lib/natty-ui/wrapper/mixins.rb +0 -67
  70. data/lib/natty-ui/wrapper/progress.rb +0 -74
  71. data/lib/natty-ui/wrapper/query.rb +0 -89
  72. data/lib/natty-ui/wrapper/quote.rb +0 -25
  73. data/lib/natty-ui/wrapper/request.rb +0 -54
  74. data/lib/natty-ui/wrapper/section.rb +0 -118
  75. data/lib/natty-ui/wrapper/table.rb +0 -551
  76. data/lib/natty-ui/wrapper/task.rb +0 -55
  77. data/lib/natty-ui/wrapper.rb +0 -230
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/natty-ui'
4
+
5
+ ui.message '[b]​ᓚᕠᗢ NattyUI[/b] [i green]Print Tables[/]' do
6
+ ui.space
7
+ ui.table(border: :rounded, border_around: true) do |table|
8
+ table.add(
9
+ 'Header Col 0',
10
+ 'Header Col 1',
11
+ 'Header Col 2',
12
+ align: :centered,
13
+ style: %i[bold red]
14
+ )
15
+
16
+ table.add do |row|
17
+ row.add '[blue]Row 1 Col 0', align: :left, vertical: :middle
18
+ row.add '[blue]Row 1 Col 1', "Line 2\nLine 3", align: :right
19
+ row.add '[blue]Row 1 Col 2', align: :centered, vertical: :bottom
20
+ row.padding = [0, 2]
21
+ end
22
+
23
+ filler_text = <<~FILLER.tr("\n", ' ')
24
+ This is some filler text to demonstrate word-wise line breaks inside
25
+ a table cell. Please, just ignore this nonsense-text gently.
26
+ FILLER
27
+ table.add do |row|
28
+ row.add '[blue]Row 2 Col 0', filler_text, align: :right
29
+ row.add '[blue]Row 2 Col 1', filler_text, align: :centered
30
+ row.add '[blue]Row 2 Col 2', filler_text, align: :left
31
+ row.padding = [1, 2]
32
+ end
33
+ end
34
+
35
+ ui.space
36
+ ui.table(border_style: :bright_blue, border: :default) do |table|
37
+ table.add(*('A'..'Z').each_slice(2).map(&:join))
38
+ table.add(*('😀'..'😌'))
39
+ table.add(*(3..15).map { _1.to_s(3) })
40
+ table.each do |row|
41
+ row.align = :centered
42
+ row.style = :bright_yellow
43
+ end
44
+ end
45
+
46
+ ui.space
47
+ ui.table do |table|
48
+ table.add do |row|
49
+ row.add 'green', style: :on_green
50
+ row.add 'blue', style: :on_blue
51
+ row.add 'red', style: :on_red
52
+ row.width = 15
53
+ row.align = :centered
54
+ end
55
+ table.add do |row|
56
+ row.add 'yellow', style: :on_yellow
57
+ row.add 'magenta', style: :on_magenta
58
+ row.add 'cyan', style: :on_cyan
59
+ row.align = :centered
60
+ end
61
+ end
62
+ end
data/examples/tasks.rb ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/natty-ui'
4
+
5
+ ui.message '[b]​ᓚᕠᗢ NattyUI[/b] [i green]Tasks[/]' do
6
+ ui.space
7
+ ui.puts <<~TEXT, ignore_newline: true
8
+ Tasks are sections that are closed either successfully or with an error message.
9
+ If successful, their content is only displayed temporarily and can consist of all
10
+ other features, in particular further (sub)tasks. As an exception to this, some
11
+ elements can be “pinned” as permanent content.
12
+ TEXT
13
+
14
+ # to simulate some work:
15
+ def foo = sleep(0.25)
16
+ def some = sleep(0.75)
17
+
18
+ ui.space
19
+ ui.task 'Actualize Reading List' do
20
+ ui.puts('This is a simple which actualizes the book reading list.')
21
+
22
+ ui.task('Connect to Library') do
23
+ foo
24
+ ui.mark('Server Found', mark: :checkmark)
25
+ ui.task('Login...') { some }
26
+ end
27
+
28
+ ui.task('Request New Books') { some }
29
+
30
+ bar = ui.progress('Loading Books...', pin: true)
31
+ 11.times do
32
+ foo
33
+ bar.step
34
+ end
35
+ bar.ok 'Books Loaded'
36
+
37
+ ui.task('Disconnect from Library') { some }
38
+
39
+ ui.progress('Read Cover Images', max: 11) do |progress|
40
+ while progress.value < progress.max
41
+ foo
42
+ progress.step
43
+ end
44
+ end
45
+
46
+ ui.pin('New Books Marked', mark: :checkmark)
47
+
48
+ ui.task('Optimize Database') { some }
49
+
50
+ ui.task('Remove Dust') { some }
51
+ end
52
+ end
@@ -0,0 +1,604 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ # @todo This chapter needs more documentation.
5
+ #
6
+ module Attributes
7
+ class Base
8
+ # @return [Attributes] updated copy of itself
9
+ def merge(**attributes)
10
+ attributes.empty? ? dup : dup._assign(attributes)
11
+ end
12
+
13
+ # @return [Attributes] itself
14
+ def merge!(**attributes)
15
+ attributes.empty? ? self : _assign(attributes)
16
+ end
17
+
18
+ # @!visibility private
19
+ def to_hash
20
+ _store({})
21
+ end
22
+
23
+ # @!visibility private
24
+ def to_h(&block)
25
+ block ? _store({}).to_h(&block) : _store({})
26
+ end
27
+
28
+ private
29
+
30
+ def initialize(**attributes)
31
+ _init
32
+ _assign(attributes) unless attributes.empty?
33
+ end
34
+
35
+ def _init = nil
36
+ def _assign(_opt) = self
37
+ def _store(opt) = opt
38
+
39
+ def as_uint(value) = [0, value.to_i].max
40
+ def as_nint(value) = ([0, value.to_i].max if value)
41
+
42
+ def as_wh(value)
43
+ return unless value
44
+ return (value = value.to_i) > 0 ? value : nil if value.is_a?(Numeric)
45
+ value.is_a?(Range) ? wh_from(value.begin.to_i, value.end.to_i) : nil
46
+ end
47
+
48
+ def wh_from(min, max)
49
+ min = nil if min < 1
50
+ max = nil if max < 1
51
+ return max ? (..max) : nil unless min
52
+ return(min..) unless max
53
+ min == max ? min : Range.new(*[min, max].minmax)
54
+ end
55
+ end
56
+
57
+ module Align
58
+ # Horizontal element alignment.
59
+ #
60
+ # @return [:left, :right, :centered]
61
+ attr_reader :align
62
+
63
+ # @attribute [w] align
64
+ def align=(value)
65
+ @align = Utils.align(value)
66
+ end
67
+
68
+ protected
69
+
70
+ def _init
71
+ @align = :left
72
+ super
73
+ end
74
+
75
+ def _assign(opt)
76
+ self.align = opt[:align] if opt.key?(:align)
77
+ super
78
+ end
79
+
80
+ def _store(opt)
81
+ opt[:align] = @align if @align != :left
82
+ super
83
+ end
84
+ end
85
+
86
+ module Vertical
87
+ # Vertical element alignment.
88
+ #
89
+ # @return [:top, :bottom, :middle]
90
+ attr_reader :vertical
91
+
92
+ # @attribute [w] vertical
93
+ def vertical=(value)
94
+ @vertical = Utils.vertical(value)
95
+ end
96
+
97
+ protected
98
+
99
+ def _init
100
+ @vertical = :top
101
+ super
102
+ end
103
+
104
+ def _assign(opt)
105
+ self.vertical = opt[:vertical] if opt.key?(:vertical)
106
+ super
107
+ end
108
+
109
+ def _store(opt)
110
+ opt[:vertical] = @vertical if @vertical != :top
111
+ super
112
+ end
113
+ end
114
+
115
+ module Width
116
+ # Element width.
117
+ #
118
+ # @return [Integer] dedicated element width
119
+ # @return [Range] width range: {#min_width}..{#max_width}
120
+ # @return [nil] when unassigned
121
+ attr_reader :width
122
+
123
+ # @attribute [w] width
124
+ def width=(value)
125
+ @width = as_wh(value)
126
+ end
127
+
128
+ # Minimum element width.
129
+ #
130
+ # @attribute [r] min_width
131
+ # @return [Integer, nil]
132
+ def min_width
133
+ width.is_a?(Range) ? @width.begin : @width
134
+ end
135
+
136
+ # @attribute [w] min_width
137
+ def min_width=(value)
138
+ @width = wh_from(value.to_i, max_width.to_i)
139
+ end
140
+
141
+ # Maximum element width.
142
+ #
143
+ # @attribute [r] max_width
144
+ # @return [Integer, nil]
145
+ def max_width
146
+ width.is_a?(Range) ? @width.end : @width
147
+ end
148
+
149
+ # @attribute [w] max_width
150
+ def max_width=(value)
151
+ @width = wh_from(min_width.to_i, value.to_i)
152
+ end
153
+
154
+ protected
155
+
156
+ def _assign(opt)
157
+ @width = as_wh(opt[:width]) if opt.key?(:width)
158
+ self.min_width = opt[:min_width] if opt.key?(:min_width)
159
+ self.max_width = opt[:max_width] if opt.key?(:max_width)
160
+ super
161
+ end
162
+
163
+ def _store(opt)
164
+ opt[:width] = @width if @width
165
+ super
166
+ end
167
+ end
168
+
169
+ module Height
170
+ # Element height.
171
+ #
172
+ # @return [Integer] dedicated element height
173
+ # @return [Range] height range: {#min_height}..{#max_height}
174
+ # @return [nil] when unassigned
175
+ attr_reader :height
176
+
177
+ # @attribute [w] height
178
+ def height=(value)
179
+ @height = as_wh(value)
180
+ end
181
+
182
+ # Minimum element height.
183
+ #
184
+ # @attribute [r] min_height
185
+ # @return [Integer, nil]
186
+ def min_height
187
+ @height.is_a?(Range) ? @height.begin : @height
188
+ end
189
+
190
+ # @attribute [w] min_height
191
+ def min_height=(value)
192
+ @height = wh_from(value.to_i, max_height)
193
+ end
194
+
195
+ # Maximum element height.
196
+ #
197
+ # @attribute [r] max_height
198
+ # @return [Integer, nil]
199
+ def max_height
200
+ @height.is_a?(Range) ? @height.begin : @height
201
+ end
202
+
203
+ # @attribute [w] max_height
204
+ def max_height=(value)
205
+ @height = wh_from(min_height, value.to_i)
206
+ end
207
+
208
+ protected
209
+
210
+ def _assign(opt)
211
+ @height = as_wh(opt[:height]) if opt.key?(:height)
212
+ self.min_height = opt[:min_height] if opt.key?(:min_height)
213
+ self.max_height = opt[:max_height] if opt.key?(:max_height)
214
+ super
215
+ end
216
+
217
+ def _store(opt)
218
+ opt[:height] = @height if @height
219
+ super
220
+ end
221
+ end
222
+
223
+ module Padding
224
+ # Text padding within the element.
225
+ #
226
+ # @return [Array<Integer>] top, right, bottom, left
227
+ attr_reader :padding
228
+
229
+ # @attribute [w] padding
230
+ def padding=(*value)
231
+ @padding = Utils.padding(*value).freeze
232
+ end
233
+
234
+ # Text top padding.
235
+ #
236
+ # @attribute [r] padding_top
237
+ # @return [Integer]
238
+ def padding_top = @padding[0]
239
+
240
+ # @attribute [w] padding_top
241
+ def padding_top=(value)
242
+ @padding[0] = as_uint(value)
243
+ end
244
+
245
+ # Text right padding.
246
+ #
247
+ # @attribute [r] padding_right
248
+ # @return [Integer]
249
+ def padding_right = @padding[1]
250
+
251
+ # @attribute [w] padding_right
252
+ def padding_right=(value)
253
+ @padding[1] = as_uint(value)
254
+ end
255
+
256
+ # Text bottom padding.
257
+ #
258
+ # @attribute [r] padding_bottom
259
+ # @return [Integer]
260
+ def padding_bottom = @padding[2]
261
+
262
+ # @attribute [w] padding_bottom
263
+ def padding_bottom=(value)
264
+ @padding[2] = as_uint(value)
265
+ end
266
+
267
+ # Text left padding.
268
+ #
269
+ # @attribute [r] padding_left
270
+ # @return [Integer]
271
+ def padding_left = @padding[3]
272
+
273
+ # @attribute [w] padding_left
274
+ def padding_left=(value)
275
+ @padding[3] = as_uint(value)
276
+ end
277
+
278
+ protected
279
+
280
+ def _init
281
+ @padding = Array.new(4, 0)
282
+ super
283
+ end
284
+
285
+ def _assign(opt)
286
+ self.padding = opt[:padding] if opt.key?(:padding)
287
+ @padding[0] = as_uint(opt[:padding_top]) if opt.key?(:padding_top)
288
+ @padding[1] = as_uint(opt[:padding_right]) if opt.key?(:padding_right)
289
+ @padding[2] = as_uint(opt[:padding_bottom]) if opt.key?(:padding_bottom)
290
+ @padding[3] = as_uint(opt[:padding_left]) if opt.key?(:padding_left)
291
+ super
292
+ end
293
+
294
+ def _store(opt)
295
+ val = @padding.dup
296
+ if val[1] == val[3]
297
+ val.pop
298
+ if val[0] == val[2]
299
+ if val[0] == val[1]
300
+ opt[:padding] = val[0] if val[0] != 0
301
+ return super
302
+ end
303
+ val.pop
304
+ end
305
+ end
306
+ opt[:padding] = val
307
+ super
308
+ end
309
+
310
+ def initialize_copy(*_)
311
+ super
312
+ @padding = @padding.dup
313
+ end
314
+ end
315
+
316
+ module Margin
317
+ # Element margin.
318
+ #
319
+ # @return [Array<Integer>] [top, right, bottom, left]
320
+ attr_reader :margin
321
+
322
+ # @attribute [w] margin
323
+ def margin=(*value)
324
+ @margin = Utils.margin(*value).freeze
325
+ end
326
+
327
+ # Element top margin.
328
+ #
329
+ # @attribute [r] margin_top
330
+ # @return [Integer]
331
+ def margin_top = @margin[0]
332
+
333
+ # @attribute [w] margin_top
334
+ def margin_top=(value)
335
+ @margin[0] = as_uint(value)
336
+ end
337
+
338
+ # Element right margin.
339
+ #
340
+ # @attribute [r] margin_right
341
+ # @return [Integer]
342
+ def margin_right = @margin[1]
343
+
344
+ # @attribute [w] margin_right
345
+ def margin_right=(value)
346
+ @margin[1] = as_uint(value)
347
+ end
348
+
349
+ # Element bottom margin.
350
+ #
351
+ # @attribute [r] margin_bottom
352
+ # @return [Integer]
353
+ def margin_bottom = @margin[2]
354
+
355
+ # @attribute [w] margin_bottom
356
+ def margin_bottom=(value)
357
+ @margin[2] = as_uint(value)
358
+ end
359
+
360
+ # Element left margin.
361
+ #
362
+ # @attribute [r] margin_left
363
+ # @return [Integer]
364
+ def margin_left = @margin[3]
365
+
366
+ # @attribute [w] margin_left
367
+ def margin_left=(value)
368
+ @margin[3] = as_uint(value)
369
+ end
370
+
371
+ protected
372
+
373
+ def _init
374
+ @margin = Array.new(4, 0)
375
+ super
376
+ end
377
+
378
+ def _assign(opt)
379
+ self.margin = opt[:margin] if opt.key?(:margin)
380
+ @margin[0] = as_uint(opt[:margin_top]) if opt.key?(:margin_top)
381
+ @margin[1] = as_uint(opt[:margin_right]) if opt.key?(:margin_right)
382
+ @margin[2] = as_uint(opt[:margin_bottom]) if opt.key?(:margin_bottom)
383
+ @margin[3] = as_uint(opt[:margin_left]) if opt.key?(:margin_left)
384
+ super
385
+ end
386
+
387
+ def _store(opt)
388
+ val = @margin.dup
389
+ if val[1] == val[3]
390
+ val.pop
391
+ if val[0] == val[2]
392
+ if val[0] == val[1]
393
+ opt[:margin] = val[0] if val[0] != 0
394
+ return super
395
+ end
396
+ val.pop
397
+ end
398
+ end
399
+ opt[:margin] = val
400
+ super
401
+ end
402
+
403
+ def initialize_copy(*_)
404
+ super
405
+ @margin = @margin.dup
406
+ end
407
+ end
408
+
409
+ module Style
410
+ # Text style.
411
+ #
412
+ # @return [Array, nil]
413
+ attr_reader :style
414
+
415
+ # @attribute [w] style
416
+ def style=(value)
417
+ @style = Utils.style(value)
418
+ end
419
+
420
+ def style_bbcode
421
+ "[#{@style.join(' ')}]" if @style
422
+ end
423
+
424
+ protected
425
+
426
+ def _assign(opt)
427
+ @style = Utils.style(opt[:style]) if opt.key?(:style)
428
+ super
429
+ end
430
+
431
+ def _store(opt)
432
+ opt[:style] = @style if @style
433
+ super
434
+ end
435
+ end
436
+
437
+ module BorderStyle
438
+ # Border style.
439
+ #
440
+ # @return [Array, nil]
441
+ attr_reader :border_style
442
+
443
+ # @attribute [w] border_style
444
+ def border_style=(value)
445
+ @border_style = Utils.style(value)
446
+ end
447
+
448
+ def border_style_bbcode
449
+ "[#{@border_style.join(' ')}]" if @border_style
450
+ end
451
+
452
+ protected
453
+
454
+ def _assign(opt)
455
+ @border_style = Utils.style(opt[:border_style]) if opt.key?(
456
+ :border_style
457
+ )
458
+ super
459
+ end
460
+
461
+ def _store(opt)
462
+ opt[:border_style] = @border_style if @border_style
463
+ super
464
+ end
465
+ end
466
+
467
+ module BorderAround
468
+ # Whether the border is around an element.
469
+ #
470
+ # @return [true, false]
471
+ attr_reader :border_around
472
+
473
+ # @attribute [w] border_around
474
+ def border_around=(value)
475
+ @border_around = value ? true : false
476
+ end
477
+
478
+ protected
479
+
480
+ def _assign(opt)
481
+ @border_around = opt[:border_around]
482
+ super
483
+ end
484
+
485
+ def _store(opt)
486
+ opt[:border_around] = true if @border_around
487
+ super
488
+ end
489
+ end
490
+
491
+ module Border
492
+ # Border type.
493
+ #
494
+ # @return [
495
+ # :default,
496
+ # :rounded,
497
+ # :heavy,
498
+ # :double,
499
+ # :vintage,
500
+ # :defaulth,
501
+ # :defaultv,
502
+ # :heavyh,
503
+ # :heavyv,
504
+ # :doubleh,
505
+ # :doublev
506
+ # ]
507
+ # @return [nil] when element has no border
508
+ attr_reader :border
509
+
510
+ # @attribute [w] border
511
+ def border=(value)
512
+ if value
513
+ @border_chars = Theme.current.border(value)
514
+ @border = value
515
+ else
516
+ @border_chars = @border = nil
517
+ end
518
+ end
519
+
520
+ # @!visibility private
521
+ # @return [String, nil]
522
+ attr_reader :border_chars
523
+
524
+ protected
525
+
526
+ def _assign(opt)
527
+ self.border = opt[:border] if opt.key?(:border)
528
+ super
529
+ end
530
+
531
+ def _store(opt)
532
+ opt[:border] = @border if @border_chars
533
+ super
534
+ end
535
+ end
536
+ end
537
+
538
+ module WithAttributes
539
+ attr_reader :attributes
540
+
541
+ def attributes=(value)
542
+ @attributes =
543
+ if value.is_a?(self.class::Attributes)
544
+ value.dup
545
+ elsif value.respond_to?(:to_hash)
546
+ self.class::Attributes.new(**value.to_hash)
547
+ else
548
+ self.class::Attributes.new(**value.to_h)
549
+ end
550
+ end
551
+
552
+ private
553
+
554
+ def initialize(**attributes)
555
+ @attributes = self.class::Attributes.new(**attributes)
556
+ end
557
+
558
+ def initialize_copy(*_)
559
+ super
560
+ @attributes = @attributes.dup
561
+ end
562
+ end
563
+ private_constant :WithAttributes
564
+
565
+ module TextWithAttributes
566
+ include WithAttributes
567
+
568
+ attr_reader :text
569
+
570
+ def empty? = @text.empty?
571
+
572
+ alias _to_s to_s
573
+ private :_to_s
574
+
575
+ def to_str = @text.join("\n")
576
+ alias to_s to_str
577
+
578
+ def inspect
579
+ if (att = @attributes.to_hash).empty?
580
+ "#{_to_s.chop} @text=#{to_s.inspect}>"
581
+ else
582
+ "#{_to_s.chop} @attributes=#{att} @text=#{to_s.inspect}>"
583
+ end
584
+ end
585
+
586
+ private
587
+
588
+ def initialize(*text, **attributes)
589
+ @text = text
590
+ @attributes =
591
+ if text.last.is_a?(self.class::Attributes)
592
+ text.pop.merge(**@attributes)
593
+ else
594
+ self.class::Attributes.new(**attributes)
595
+ end
596
+ end
597
+
598
+ def initialize_copy(*_)
599
+ super
600
+ @text = @text.map(&:dup)
601
+ end
602
+ end
603
+ private_constant :TextWithAttributes
604
+ end