rich-ruby 1.0.1 → 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/CHANGELOG.md +80 -0
- data/LICENSE +21 -21
- data/README.md +547 -547
- data/docs/architecture.md +43 -0
- data/docs/cheat-sheet.md +52 -0
- data/docs/customization.md +53 -0
- data/docs/how-to-use.md +96 -0
- data/docs/test-report.md +112 -0
- data/docs/troubleshooting.md +36 -0
- data/docs/windows-notes.md +30 -0
- data/examples/demo.rb +106 -106
- data/examples/showcase.rb +420 -420
- data/examples/smoke_test.rb +41 -41
- data/examples/stress_test.rb +604 -604
- data/examples/syntax_markdown_demo.rb +166 -166
- data/examples/verify.rb +216 -215
- data/examples/visual_demo.rb +145 -145
- data/lib/rich/_palettes.rb +148 -148
- data/lib/rich/box.rb +342 -342
- data/lib/rich/cells.rb +524 -512
- data/lib/rich/color.rb +631 -628
- data/lib/rich/color_triplet.rb +227 -220
- data/lib/rich/console.rb +604 -549
- data/lib/rich/control.rb +332 -332
- data/lib/rich/json.rb +260 -254
- data/lib/rich/layout.rb +314 -314
- data/lib/rich/markdown.rb +531 -509
- data/lib/rich/markup.rb +186 -175
- data/lib/rich/panel.rb +318 -311
- data/lib/rich/progress.rb +430 -430
- data/lib/rich/segment.rb +387 -387
- data/lib/rich/style.rb +464 -433
- data/lib/rich/syntax.rb +1220 -1145
- data/lib/rich/table.rb +547 -525
- data/lib/rich/terminal_theme.rb +126 -126
- data/lib/rich/text.rb +460 -433
- data/lib/rich/tree.rb +220 -220
- data/lib/rich/version.rb +5 -5
- data/lib/rich/win32_console.rb +620 -582
- data/lib/rich.rb +108 -108
- metadata +15 -5
data/lib/rich/style.rb
CHANGED
|
@@ -1,433 +1,464 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "color"
|
|
4
|
-
|
|
5
|
-
module Rich
|
|
6
|
-
# Style attributes represented as bit flags
|
|
7
|
-
module StyleAttribute
|
|
8
|
-
BOLD = 1 << 0 # 1
|
|
9
|
-
DIM = 1 << 1 # 2
|
|
10
|
-
ITALIC = 1 << 2 # 4
|
|
11
|
-
UNDERLINE = 1 << 3 # 8
|
|
12
|
-
BLINK = 1 << 4 # 16
|
|
13
|
-
BLINK2 = 1 << 5 # 32 (rapid blink)
|
|
14
|
-
REVERSE = 1 << 6 # 64
|
|
15
|
-
CONCEAL = 1 << 7 # 128
|
|
16
|
-
STRIKE = 1 << 8 # 256
|
|
17
|
-
UNDERLINE2 = 1 << 9 # 512 (double underline)
|
|
18
|
-
FRAME = 1 << 10 # 1024
|
|
19
|
-
ENCIRCLE = 1 << 11 # 2048
|
|
20
|
-
OVERLINE = 1 << 12 # 4096
|
|
21
|
-
|
|
22
|
-
ALL = {
|
|
23
|
-
bold: BOLD,
|
|
24
|
-
dim: DIM,
|
|
25
|
-
italic: ITALIC,
|
|
26
|
-
underline: UNDERLINE,
|
|
27
|
-
blink: BLINK,
|
|
28
|
-
blink2: BLINK2,
|
|
29
|
-
reverse: REVERSE,
|
|
30
|
-
conceal: CONCEAL,
|
|
31
|
-
strike: STRIKE,
|
|
32
|
-
underline2: UNDERLINE2,
|
|
33
|
-
frame: FRAME,
|
|
34
|
-
encircle: ENCIRCLE,
|
|
35
|
-
overline: OVERLINE
|
|
36
|
-
}.freeze
|
|
37
|
-
|
|
38
|
-
NAMES = ALL.keys.freeze
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Represents a terminal style with colors and text attributes.
|
|
42
|
-
# Styles are immutable and can be combined using the + operator.
|
|
43
|
-
class Style
|
|
44
|
-
# ANSI reset and attribute codes
|
|
45
|
-
ANSI_CODES = {
|
|
46
|
-
bold: "1",
|
|
47
|
-
dim: "2",
|
|
48
|
-
italic: "3",
|
|
49
|
-
underline: "4",
|
|
50
|
-
blink: "5",
|
|
51
|
-
blink2: "6",
|
|
52
|
-
reverse: "7",
|
|
53
|
-
conceal: "8",
|
|
54
|
-
strike: "9",
|
|
55
|
-
underline2: "21",
|
|
56
|
-
frame: "51",
|
|
57
|
-
encircle: "52",
|
|
58
|
-
overline: "53"
|
|
59
|
-
}.freeze
|
|
60
|
-
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# @return [
|
|
82
|
-
attr_reader :
|
|
83
|
-
|
|
84
|
-
# @return [
|
|
85
|
-
attr_reader :
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# @
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# @
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
#
|
|
104
|
-
# @param
|
|
105
|
-
# @param
|
|
106
|
-
# @param
|
|
107
|
-
# @param
|
|
108
|
-
# @param
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
#
|
|
185
|
-
# @param
|
|
186
|
-
# @return [
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
parts << @
|
|
229
|
-
|
|
230
|
-
parts
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
attrs
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
@
|
|
300
|
-
@
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
#
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
#
|
|
341
|
-
# @param
|
|
342
|
-
# @param
|
|
343
|
-
# @param
|
|
344
|
-
# @param
|
|
345
|
-
# @
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
style
|
|
350
|
-
style.instance_variable_set(:@
|
|
351
|
-
style.instance_variable_set(:@
|
|
352
|
-
style.instance_variable_set(:@
|
|
353
|
-
style.instance_variable_set(:@
|
|
354
|
-
style.instance_variable_set(:@
|
|
355
|
-
style.
|
|
356
|
-
style
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
#
|
|
361
|
-
# @param
|
|
362
|
-
# @
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
#
|
|
369
|
-
# @
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
#
|
|
376
|
-
# @
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "color"
|
|
4
|
+
|
|
5
|
+
module Rich
|
|
6
|
+
# Style attributes represented as bit flags
|
|
7
|
+
module StyleAttribute
|
|
8
|
+
BOLD = 1 << 0 # 1
|
|
9
|
+
DIM = 1 << 1 # 2
|
|
10
|
+
ITALIC = 1 << 2 # 4
|
|
11
|
+
UNDERLINE = 1 << 3 # 8
|
|
12
|
+
BLINK = 1 << 4 # 16
|
|
13
|
+
BLINK2 = 1 << 5 # 32 (rapid blink)
|
|
14
|
+
REVERSE = 1 << 6 # 64
|
|
15
|
+
CONCEAL = 1 << 7 # 128
|
|
16
|
+
STRIKE = 1 << 8 # 256
|
|
17
|
+
UNDERLINE2 = 1 << 9 # 512 (double underline)
|
|
18
|
+
FRAME = 1 << 10 # 1024
|
|
19
|
+
ENCIRCLE = 1 << 11 # 2048
|
|
20
|
+
OVERLINE = 1 << 12 # 4096
|
|
21
|
+
|
|
22
|
+
ALL = {
|
|
23
|
+
bold: BOLD,
|
|
24
|
+
dim: DIM,
|
|
25
|
+
italic: ITALIC,
|
|
26
|
+
underline: UNDERLINE,
|
|
27
|
+
blink: BLINK,
|
|
28
|
+
blink2: BLINK2,
|
|
29
|
+
reverse: REVERSE,
|
|
30
|
+
conceal: CONCEAL,
|
|
31
|
+
strike: STRIKE,
|
|
32
|
+
underline2: UNDERLINE2,
|
|
33
|
+
frame: FRAME,
|
|
34
|
+
encircle: ENCIRCLE,
|
|
35
|
+
overline: OVERLINE
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
NAMES = ALL.keys.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Represents a terminal style with colors and text attributes.
|
|
42
|
+
# Styles are immutable and can be combined using the + operator.
|
|
43
|
+
class Style
|
|
44
|
+
# ANSI reset and attribute codes
|
|
45
|
+
ANSI_CODES = {
|
|
46
|
+
bold: "1",
|
|
47
|
+
dim: "2",
|
|
48
|
+
italic: "3",
|
|
49
|
+
underline: "4",
|
|
50
|
+
blink: "5",
|
|
51
|
+
blink2: "6",
|
|
52
|
+
reverse: "7",
|
|
53
|
+
conceal: "8",
|
|
54
|
+
strike: "9",
|
|
55
|
+
underline2: "21",
|
|
56
|
+
frame: "51",
|
|
57
|
+
encircle: "52",
|
|
58
|
+
overline: "53"
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Single-letter attribute aliases (expanded before parsing). Mirrors the
|
|
62
|
+
# shorthands documented in the cheat-sheet, e.g. [u] == [underline].
|
|
63
|
+
ATTRIBUTE_ALIASES = {
|
|
64
|
+
"b" => "bold",
|
|
65
|
+
"i" => "italic",
|
|
66
|
+
"u" => "underline",
|
|
67
|
+
"s" => "strike",
|
|
68
|
+
"d" => "dim",
|
|
69
|
+
"r" => "reverse",
|
|
70
|
+
"o" => "overline"
|
|
71
|
+
}.freeze
|
|
72
|
+
|
|
73
|
+
# Regex for parsing style definitions
|
|
74
|
+
STYLE_REGEX = /
|
|
75
|
+
(?<not>not\s+)?
|
|
76
|
+
(?<attr>bold|dim|italic|underline2?|blink2?|reverse|conceal|strike|frame|encircle|overline)|
|
|
77
|
+
(?<link>link\s+(?<url>\S+))|
|
|
78
|
+
(?<on>on\s+)?(?<color>\S+)
|
|
79
|
+
/x
|
|
80
|
+
|
|
81
|
+
# @return [Color, nil] Foreground color
|
|
82
|
+
attr_reader :color
|
|
83
|
+
|
|
84
|
+
# @return [Color, nil] Background color
|
|
85
|
+
attr_reader :bgcolor
|
|
86
|
+
|
|
87
|
+
# @return [Integer] Attributes that are explicitly set
|
|
88
|
+
attr_reader :set_attributes
|
|
89
|
+
|
|
90
|
+
# @return [Integer] Attribute values (0 = off, 1 = on)
|
|
91
|
+
attr_reader :attributes
|
|
92
|
+
|
|
93
|
+
# @return [String, nil] Hyperlink URL
|
|
94
|
+
attr_reader :link
|
|
95
|
+
|
|
96
|
+
# @return [Hash, nil] Meta information
|
|
97
|
+
attr_reader :meta
|
|
98
|
+
|
|
99
|
+
# Cache for parsed styles
|
|
100
|
+
@parse_cache = {}
|
|
101
|
+
@parse_cache_mutex = Mutex.new
|
|
102
|
+
|
|
103
|
+
# Create a new style
|
|
104
|
+
# @param color [Color, String, nil] Foreground color
|
|
105
|
+
# @param bgcolor [Color, String, nil] Background color
|
|
106
|
+
# @param bold [Boolean, nil] Bold attribute
|
|
107
|
+
# @param dim [Boolean, nil] Dim attribute
|
|
108
|
+
# @param italic [Boolean, nil] Italic attribute
|
|
109
|
+
# @param underline [Boolean, nil] Underline attribute
|
|
110
|
+
# @param blink [Boolean, nil] Blink attribute
|
|
111
|
+
# @param blink2 [Boolean, nil] Rapid blink attribute
|
|
112
|
+
# @param reverse [Boolean, nil] Reverse video attribute
|
|
113
|
+
# @param conceal [Boolean, nil] Conceal attribute
|
|
114
|
+
# @param strike [Boolean, nil] Strikethrough attribute
|
|
115
|
+
# @param underline2 [Boolean, nil] Double underline attribute
|
|
116
|
+
# @param frame [Boolean, nil] Frame attribute
|
|
117
|
+
# @param encircle [Boolean, nil] Encircle attribute
|
|
118
|
+
# @param overline [Boolean, nil] Overline attribute
|
|
119
|
+
# @param link [String, nil] Hyperlink URL
|
|
120
|
+
# @param meta [Hash, nil] Meta information
|
|
121
|
+
def initialize(
|
|
122
|
+
color: nil,
|
|
123
|
+
bgcolor: nil,
|
|
124
|
+
bold: nil,
|
|
125
|
+
dim: nil,
|
|
126
|
+
italic: nil,
|
|
127
|
+
underline: nil,
|
|
128
|
+
blink: nil,
|
|
129
|
+
blink2: nil,
|
|
130
|
+
reverse: nil,
|
|
131
|
+
conceal: nil,
|
|
132
|
+
strike: nil,
|
|
133
|
+
underline2: nil,
|
|
134
|
+
frame: nil,
|
|
135
|
+
encircle: nil,
|
|
136
|
+
overline: nil,
|
|
137
|
+
link: nil,
|
|
138
|
+
meta: nil
|
|
139
|
+
)
|
|
140
|
+
@color = parse_color(color)
|
|
141
|
+
@bgcolor = parse_color(bgcolor)
|
|
142
|
+
@link = link&.freeze
|
|
143
|
+
@meta = meta&.freeze
|
|
144
|
+
|
|
145
|
+
# Build attribute masks
|
|
146
|
+
@set_attributes = 0
|
|
147
|
+
@attributes = 0
|
|
148
|
+
|
|
149
|
+
attrs = {
|
|
150
|
+
bold: bold, dim: dim, italic: italic, underline: underline,
|
|
151
|
+
blink: blink, blink2: blink2, reverse: reverse, conceal: conceal,
|
|
152
|
+
strike: strike, underline2: underline2, frame: frame,
|
|
153
|
+
encircle: encircle, overline: overline
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
attrs.each do |name, value|
|
|
157
|
+
next if value.nil?
|
|
158
|
+
|
|
159
|
+
bit = StyleAttribute::ALL[name]
|
|
160
|
+
@set_attributes |= bit
|
|
161
|
+
@attributes |= bit if value
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Per-color-system render cache. freeze is shallow, so this Hash stays
|
|
165
|
+
# mutable even though the Style is frozen (Style is immutable, so the
|
|
166
|
+
# rendered escape for a given color system never changes).
|
|
167
|
+
@render_cache = {}
|
|
168
|
+
|
|
169
|
+
freeze
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Check if any attributes, colors, link, or meta are set
|
|
173
|
+
# @return [Boolean]
|
|
174
|
+
def blank?
|
|
175
|
+
@color.nil? && @bgcolor.nil? && @set_attributes == 0 && @link.nil? &&
|
|
176
|
+
(@meta.nil? || @meta.empty?)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @return [Boolean] False if blank
|
|
180
|
+
def present?
|
|
181
|
+
!blank?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Get a specific attribute value
|
|
185
|
+
# @param name [Symbol] Attribute name
|
|
186
|
+
# @return [Boolean, nil] Attribute value or nil if not set
|
|
187
|
+
def [](name)
|
|
188
|
+
bit = StyleAttribute::ALL[name]
|
|
189
|
+
return nil unless bit
|
|
190
|
+
|
|
191
|
+
return nil if (@set_attributes & bit) == 0
|
|
192
|
+
|
|
193
|
+
(@attributes & bit) != 0
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Attribute accessor methods
|
|
197
|
+
StyleAttribute::NAMES.each do |attr_name|
|
|
198
|
+
define_method(attr_name) { self[attr_name] }
|
|
199
|
+
define_method("#{attr_name}?") { self[attr_name] == true }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Generate ANSI escape codes for this style
|
|
203
|
+
# @param color_system [Symbol] Target color system
|
|
204
|
+
# @return [String] ANSI escape sequence
|
|
205
|
+
def render(color_system: ColorSystem::TRUECOLOR)
|
|
206
|
+
cache = @render_cache
|
|
207
|
+
return cache[color_system] if cache&.key?(color_system)
|
|
208
|
+
|
|
209
|
+
result = compute_render(color_system)
|
|
210
|
+
cache[color_system] = result if cache
|
|
211
|
+
result
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Generate the style definition string
|
|
215
|
+
# @return [String]
|
|
216
|
+
def to_s
|
|
217
|
+
parts = []
|
|
218
|
+
|
|
219
|
+
StyleAttribute::NAMES.each do |name|
|
|
220
|
+
value = self[name]
|
|
221
|
+
next if value.nil?
|
|
222
|
+
|
|
223
|
+
parts << (value ? name.to_s : "not #{name}")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
parts << @color.name if @color
|
|
227
|
+
parts << "on #{@bgcolor.name}" if @bgcolor
|
|
228
|
+
parts << "link #{@link}" if @link
|
|
229
|
+
|
|
230
|
+
parts.join(" ")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def inspect
|
|
234
|
+
attrs = []
|
|
235
|
+
attrs << "color=#{@color.name}" if @color
|
|
236
|
+
attrs << "bgcolor=#{@bgcolor.name}" if @bgcolor
|
|
237
|
+
|
|
238
|
+
StyleAttribute::NAMES.each do |name|
|
|
239
|
+
value = self[name]
|
|
240
|
+
attrs << "#{name}=#{value}" unless value.nil?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
attrs << "link=#{@link.inspect}" if @link
|
|
244
|
+
|
|
245
|
+
"#<Rich::Style #{attrs.join(' ')}>"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Combine two styles (right-hand style takes precedence)
|
|
249
|
+
# @param other [Style] Style to combine with
|
|
250
|
+
# @return [Style] Combined style
|
|
251
|
+
def +(other)
|
|
252
|
+
return self if other.nil? || other.blank?
|
|
253
|
+
return other if blank?
|
|
254
|
+
|
|
255
|
+
new_color = other.color || @color
|
|
256
|
+
new_bgcolor = other.bgcolor || @bgcolor
|
|
257
|
+
new_link = other.link || @link
|
|
258
|
+
new_meta = @meta || other.meta ? (@meta || {}).merge(other.meta || {}) : nil
|
|
259
|
+
|
|
260
|
+
# Merge attributes
|
|
261
|
+
new_set = @set_attributes | other.set_attributes
|
|
262
|
+
new_attrs = (@attributes & ~other.set_attributes) | (other.attributes & other.set_attributes)
|
|
263
|
+
|
|
264
|
+
Style.combine(
|
|
265
|
+
color: new_color,
|
|
266
|
+
bgcolor: new_bgcolor,
|
|
267
|
+
link: new_link,
|
|
268
|
+
meta: new_meta,
|
|
269
|
+
set_attributes: new_set,
|
|
270
|
+
attributes: new_attrs
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Get style with no colors
|
|
275
|
+
# @return [Style]
|
|
276
|
+
def without_color
|
|
277
|
+
Style.combine(
|
|
278
|
+
color: nil,
|
|
279
|
+
bgcolor: nil,
|
|
280
|
+
link: @link,
|
|
281
|
+
meta: @meta,
|
|
282
|
+
set_attributes: @set_attributes,
|
|
283
|
+
attributes: @attributes
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Get background-only style
|
|
288
|
+
# @return [Style]
|
|
289
|
+
def background_style
|
|
290
|
+
Style.new(bgcolor: @bgcolor)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def ==(other)
|
|
294
|
+
return false unless other.is_a?(Style)
|
|
295
|
+
|
|
296
|
+
@color == other.color &&
|
|
297
|
+
@bgcolor == other.bgcolor &&
|
|
298
|
+
@set_attributes == other.set_attributes &&
|
|
299
|
+
@attributes == other.attributes &&
|
|
300
|
+
@link == other.link
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
alias eql? ==
|
|
304
|
+
|
|
305
|
+
def hash
|
|
306
|
+
[@color, @bgcolor, @set_attributes, @attributes, @link].hash
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
class << self
|
|
310
|
+
# Parse a style definition string
|
|
311
|
+
# @param style [String, Style, nil] Style definition
|
|
312
|
+
# @return [Style]
|
|
313
|
+
def parse(style)
|
|
314
|
+
return null if style.nil? || (style.is_a?(String) && style.empty?)
|
|
315
|
+
return style if style.is_a?(Style)
|
|
316
|
+
|
|
317
|
+
style = style.to_s
|
|
318
|
+
|
|
319
|
+
@parse_cache_mutex.synchronize do
|
|
320
|
+
return @parse_cache[style] if @parse_cache.key?(style)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
result = parse_uncached(style)
|
|
324
|
+
|
|
325
|
+
@parse_cache_mutex.synchronize do
|
|
326
|
+
@parse_cache[style] = result
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
result
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Create a null (empty) style
|
|
333
|
+
# @return [Style]
|
|
334
|
+
def null
|
|
335
|
+
@null ||= new
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Create a combined style with explicit attributes (internal use)
|
|
339
|
+
# @param color [Color, nil] Foreground color
|
|
340
|
+
# @param bgcolor [Color, nil] Background color
|
|
341
|
+
# @param link [String, nil] Hyperlink
|
|
342
|
+
# @param meta [Hash, nil] Meta info
|
|
343
|
+
# @param set_attributes [Integer] Set attributes bitmask
|
|
344
|
+
# @param attributes [Integer] Attribute values bitmask
|
|
345
|
+
# @return [Style]
|
|
346
|
+
def combine(color:, bgcolor:, link:, meta:, set_attributes:, attributes:)
|
|
347
|
+
style = allocate
|
|
348
|
+
style.instance_variable_set(:@color, color)
|
|
349
|
+
style.instance_variable_set(:@bgcolor, bgcolor)
|
|
350
|
+
style.instance_variable_set(:@link, link&.freeze)
|
|
351
|
+
style.instance_variable_set(:@meta, meta&.freeze)
|
|
352
|
+
style.instance_variable_set(:@set_attributes, set_attributes)
|
|
353
|
+
style.instance_variable_set(:@attributes, attributes)
|
|
354
|
+
style.instance_variable_set(:@render_cache, {})
|
|
355
|
+
style.freeze
|
|
356
|
+
style
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Create a style from just colors
|
|
360
|
+
# @param color [Color, String, nil] Foreground color
|
|
361
|
+
# @param bgcolor [Color, String, nil] Background color
|
|
362
|
+
# @return [Style]
|
|
363
|
+
def from_color(color: nil, bgcolor: nil)
|
|
364
|
+
new(color: color, bgcolor: bgcolor)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Create a style with meta information
|
|
368
|
+
# @param meta [Hash] Meta data
|
|
369
|
+
# @return [Style]
|
|
370
|
+
def from_meta(meta)
|
|
371
|
+
new(meta: meta)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Normalize a style definition
|
|
375
|
+
# @param style [String] Style definition
|
|
376
|
+
# @return [String] Normalized style definition
|
|
377
|
+
def normalize(style)
|
|
378
|
+
parse(style).to_s
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
private
|
|
382
|
+
|
|
383
|
+
def parse_uncached(style_str)
|
|
384
|
+
attrs = {}
|
|
385
|
+
color = nil
|
|
386
|
+
bgcolor = nil
|
|
387
|
+
link = nil
|
|
388
|
+
|
|
389
|
+
# Expand single-letter attribute aliases (e.g. "u" -> "underline")
|
|
390
|
+
# token-by-token so color names like "blue" are never misread.
|
|
391
|
+
style_str = style_str.split(/\s+/).map { |tok| ATTRIBUTE_ALIASES[tok] || tok }.join(" ")
|
|
392
|
+
|
|
393
|
+
style_str.scan(STYLE_REGEX) do
|
|
394
|
+
match = Regexp.last_match
|
|
395
|
+
|
|
396
|
+
if match[:attr]
|
|
397
|
+
attr_name = match[:attr].to_sym
|
|
398
|
+
attrs[attr_name] = match[:not].nil?
|
|
399
|
+
elsif match[:link]
|
|
400
|
+
link = match[:url]
|
|
401
|
+
elsif match[:color]
|
|
402
|
+
color_name = match[:color]
|
|
403
|
+
begin
|
|
404
|
+
parsed_color = Color.parse(color_name)
|
|
405
|
+
if match[:on]
|
|
406
|
+
bgcolor = parsed_color
|
|
407
|
+
else
|
|
408
|
+
color = parsed_color
|
|
409
|
+
end
|
|
410
|
+
rescue ColorParseError
|
|
411
|
+
# Ignore invalid colors
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
new(
|
|
417
|
+
color: color,
|
|
418
|
+
bgcolor: bgcolor,
|
|
419
|
+
link: link,
|
|
420
|
+
**attrs
|
|
421
|
+
)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
private
|
|
426
|
+
|
|
427
|
+
# Build the ANSI escape sequence for the given color system (uncached).
|
|
428
|
+
def compute_render(color_system)
|
|
429
|
+
codes = []
|
|
430
|
+
|
|
431
|
+
# Add attribute codes
|
|
432
|
+
StyleAttribute::NAMES.each do |name|
|
|
433
|
+
value = self[name]
|
|
434
|
+
next if value.nil?
|
|
435
|
+
|
|
436
|
+
codes << ANSI_CODES[name] if value
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Add color codes
|
|
440
|
+
if @color
|
|
441
|
+
target_color = @color.downgrade(color_system)
|
|
442
|
+
codes.concat(target_color.ansi_codes(foreground: true))
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
if @bgcolor
|
|
446
|
+
target_color = @bgcolor.downgrade(color_system)
|
|
447
|
+
codes.concat(target_color.ansi_codes(foreground: false))
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
return "" if codes.empty?
|
|
451
|
+
|
|
452
|
+
"\e[#{codes.join(';')}m"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def parse_color(color)
|
|
456
|
+
return nil if color.nil?
|
|
457
|
+
return color if color.is_a?(Color)
|
|
458
|
+
|
|
459
|
+
Color.parse(color)
|
|
460
|
+
rescue ColorParseError
|
|
461
|
+
nil
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|