rich-ruby 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/examples/demo.rb +106 -0
- data/examples/showcase.rb +420 -0
- data/examples/smoke_test.rb +41 -0
- data/examples/stress_test.rb +604 -0
- data/examples/syntax_markdown_demo.rb +166 -0
- data/examples/verify.rb +215 -0
- data/examples/visual_demo.rb +145 -0
- data/lib/rich/_palettes.rb +148 -0
- data/lib/rich/box.rb +342 -0
- data/lib/rich/cells.rb +512 -0
- data/lib/rich/color.rb +628 -0
- data/lib/rich/color_triplet.rb +220 -0
- data/lib/rich/console.rb +549 -0
- data/lib/rich/control.rb +332 -0
- data/lib/rich/json.rb +254 -0
- data/lib/rich/layout.rb +314 -0
- data/lib/rich/markdown.rb +509 -0
- data/lib/rich/markup.rb +175 -0
- data/lib/rich/panel.rb +311 -0
- data/lib/rich/progress.rb +430 -0
- data/lib/rich/segment.rb +387 -0
- data/lib/rich/style.rb +433 -0
- data/lib/rich/syntax.rb +1145 -0
- data/lib/rich/table.rb +525 -0
- data/lib/rich/terminal_theme.rb +126 -0
- data/lib/rich/text.rb +433 -0
- data/lib/rich/tree.rb +220 -0
- data/lib/rich/version.rb +5 -0
- data/lib/rich/win32_console.rb +582 -0
- data/lib/rich.rb +108 -0
- metadata +106 -0
data/lib/rich/control.rb
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rich
|
|
4
|
+
# Terminal control codes and escape sequences.
|
|
5
|
+
# Provides constants and methods for cursor movement, screen clearing,
|
|
6
|
+
# and other terminal control operations.
|
|
7
|
+
module Control
|
|
8
|
+
# Control code types for segment rendering
|
|
9
|
+
module ControlType
|
|
10
|
+
BELL = :bell
|
|
11
|
+
CARRIAGE_RETURN = :carriage_return
|
|
12
|
+
HOME = :home
|
|
13
|
+
CLEAR = :clear
|
|
14
|
+
SHOW_CURSOR = :show_cursor
|
|
15
|
+
HIDE_CURSOR = :hide_cursor
|
|
16
|
+
ENABLE_ALT_SCREEN = :enable_alt_screen
|
|
17
|
+
DISABLE_ALT_SCREEN = :disable_alt_screen
|
|
18
|
+
CURSOR_UP = :cursor_up
|
|
19
|
+
CURSOR_DOWN = :cursor_down
|
|
20
|
+
CURSOR_FORWARD = :cursor_forward
|
|
21
|
+
CURSOR_BACKWARD = :cursor_backward
|
|
22
|
+
CURSOR_MOVE_TO_COLUMN = :cursor_move_to_column
|
|
23
|
+
CURSOR_MOVE_TO = :cursor_move_to
|
|
24
|
+
ERASE_IN_LINE = :erase_in_line
|
|
25
|
+
SET_WINDOW_TITLE = :set_window_title
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ESC character for ANSI sequences
|
|
29
|
+
ESC = "\e"
|
|
30
|
+
|
|
31
|
+
# Control Sequence Introducer
|
|
32
|
+
CSI = "\e["
|
|
33
|
+
|
|
34
|
+
# Operating System Command
|
|
35
|
+
OSC = "\e]"
|
|
36
|
+
|
|
37
|
+
# String Terminator
|
|
38
|
+
ST = "\e\\"
|
|
39
|
+
|
|
40
|
+
# Bell character
|
|
41
|
+
BEL = "\a"
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# Generate bell/alert
|
|
45
|
+
# @return [String]
|
|
46
|
+
def bell
|
|
47
|
+
BEL
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Carriage return (move to column 0)
|
|
51
|
+
# @return [String]
|
|
52
|
+
def carriage_return
|
|
53
|
+
"\r"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Move cursor to home position (1,1)
|
|
57
|
+
# @return [String]
|
|
58
|
+
def home
|
|
59
|
+
"#{CSI}H"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Clear the screen
|
|
63
|
+
# @param mode [Integer] 0=cursor to end, 1=start to cursor, 2=entire screen
|
|
64
|
+
# @return [String]
|
|
65
|
+
def clear(mode = 2)
|
|
66
|
+
"#{CSI}#{mode}J"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Clear entire screen and move to home
|
|
70
|
+
# @return [String]
|
|
71
|
+
def clear_screen
|
|
72
|
+
"#{CSI}2J#{CSI}H"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Show the cursor
|
|
76
|
+
# @return [String]
|
|
77
|
+
def show_cursor
|
|
78
|
+
"#{CSI}?25h"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Hide the cursor
|
|
82
|
+
# @return [String]
|
|
83
|
+
def hide_cursor
|
|
84
|
+
"#{CSI}?25l"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Enable alternative screen buffer
|
|
88
|
+
# @return [String]
|
|
89
|
+
def enable_alt_screen
|
|
90
|
+
"#{CSI}?1049h"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Disable alternative screen buffer
|
|
94
|
+
# @return [String]
|
|
95
|
+
def disable_alt_screen
|
|
96
|
+
"#{CSI}?1049l"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Move cursor up
|
|
100
|
+
# @param count [Integer] Number of rows
|
|
101
|
+
# @return [String]
|
|
102
|
+
def cursor_up(count = 1)
|
|
103
|
+
return "" if count < 1
|
|
104
|
+
|
|
105
|
+
"#{CSI}#{count}A"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Move cursor down
|
|
109
|
+
# @param count [Integer] Number of rows
|
|
110
|
+
# @return [String]
|
|
111
|
+
def cursor_down(count = 1)
|
|
112
|
+
return "" if count < 1
|
|
113
|
+
|
|
114
|
+
"#{CSI}#{count}B"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Move cursor forward (right)
|
|
118
|
+
# @param count [Integer] Number of columns
|
|
119
|
+
# @return [String]
|
|
120
|
+
def cursor_forward(count = 1)
|
|
121
|
+
return "" if count < 1
|
|
122
|
+
|
|
123
|
+
"#{CSI}#{count}C"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Move cursor backward (left)
|
|
127
|
+
# @param count [Integer] Number of columns
|
|
128
|
+
# @return [String]
|
|
129
|
+
def cursor_backward(count = 1)
|
|
130
|
+
return "" if count < 1
|
|
131
|
+
|
|
132
|
+
"#{CSI}#{count}D"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Move cursor to next line
|
|
136
|
+
# @param count [Integer] Number of lines
|
|
137
|
+
# @return [String]
|
|
138
|
+
def cursor_next_line(count = 1)
|
|
139
|
+
return "" if count < 1
|
|
140
|
+
|
|
141
|
+
"#{CSI}#{count}E"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Move cursor to previous line
|
|
145
|
+
# @param count [Integer] Number of lines
|
|
146
|
+
# @return [String]
|
|
147
|
+
def cursor_prev_line(count = 1)
|
|
148
|
+
return "" if count < 1
|
|
149
|
+
|
|
150
|
+
"#{CSI}#{count}F"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Move cursor to column (1-based)
|
|
154
|
+
# @param column [Integer] Column number (1-based)
|
|
155
|
+
# @return [String]
|
|
156
|
+
def cursor_move_to_column(column)
|
|
157
|
+
"#{CSI}#{column}G"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Move cursor to position (1-based coordinates)
|
|
161
|
+
# @param row [Integer] Row (1-based)
|
|
162
|
+
# @param column [Integer] Column (1-based)
|
|
163
|
+
# @return [String]
|
|
164
|
+
def cursor_move_to(row, column)
|
|
165
|
+
"#{CSI}#{row};#{column}H"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Save cursor position
|
|
169
|
+
# @return [String]
|
|
170
|
+
def save_cursor
|
|
171
|
+
"#{CSI}s"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Restore cursor position
|
|
175
|
+
# @return [String]
|
|
176
|
+
def restore_cursor
|
|
177
|
+
"#{CSI}u"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Erase in line
|
|
181
|
+
# @param mode [Integer] 0=cursor to end, 1=start to cursor, 2=entire line
|
|
182
|
+
# @return [String]
|
|
183
|
+
def erase_line(mode = 2)
|
|
184
|
+
"#{CSI}#{mode}K"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Erase from cursor to end of line
|
|
188
|
+
# @return [String]
|
|
189
|
+
def erase_end_of_line
|
|
190
|
+
"#{CSI}0K"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Erase from start of line to cursor
|
|
194
|
+
# @return [String]
|
|
195
|
+
def erase_start_of_line
|
|
196
|
+
"#{CSI}1K"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Set window title
|
|
200
|
+
# @param title [String] Window title
|
|
201
|
+
# @return [String]
|
|
202
|
+
def set_title(title)
|
|
203
|
+
"#{OSC}2;#{title}#{ST}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Set icon name (some terminals)
|
|
207
|
+
# @param name [String] Icon name
|
|
208
|
+
# @return [String]
|
|
209
|
+
def set_icon_name(name)
|
|
210
|
+
"#{OSC}1;#{name}#{ST}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Set both icon name and window title
|
|
214
|
+
# @param title [String] Title/name
|
|
215
|
+
# @return [String]
|
|
216
|
+
def set_icon_and_title(title)
|
|
217
|
+
"#{OSC}0;#{title}#{ST}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Request cursor position (terminal will respond)
|
|
221
|
+
# @return [String]
|
|
222
|
+
def request_cursor_position
|
|
223
|
+
"#{CSI}6n"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Scroll up
|
|
227
|
+
# @param count [Integer] Number of lines
|
|
228
|
+
# @return [String]
|
|
229
|
+
def scroll_up(count = 1)
|
|
230
|
+
"#{CSI}#{count}S"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Scroll down
|
|
234
|
+
# @param count [Integer] Number of lines
|
|
235
|
+
# @return [String]
|
|
236
|
+
def scroll_down(count = 1)
|
|
237
|
+
"#{CSI}#{count}T"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Reset all attributes
|
|
241
|
+
# @return [String]
|
|
242
|
+
def reset
|
|
243
|
+
"#{CSI}0m"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Create a hyperlink
|
|
247
|
+
# @param url [String] URL
|
|
248
|
+
# @param text [String] Link text
|
|
249
|
+
# @param id [String, nil] Optional link ID
|
|
250
|
+
# @return [String]
|
|
251
|
+
def hyperlink(url, text, id: nil)
|
|
252
|
+
params = id ? "id=#{id}" : ""
|
|
253
|
+
"#{OSC}8;#{params};#{url}#{ST}#{text}#{OSC}8;;#{ST}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Start hyperlink
|
|
257
|
+
# @param url [String] URL
|
|
258
|
+
# @param id [String, nil] Optional link ID
|
|
259
|
+
# @return [String]
|
|
260
|
+
def hyperlink_start(url, id: nil)
|
|
261
|
+
params = id ? "id=#{id}" : ""
|
|
262
|
+
"#{OSC}8;#{params};#{url}#{ST}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# End hyperlink
|
|
266
|
+
# @return [String]
|
|
267
|
+
def hyperlink_end
|
|
268
|
+
"#{OSC}8;;#{ST}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Strip ANSI escape sequences from text
|
|
272
|
+
# @param text [String] Text to strip
|
|
273
|
+
# @return [String] Text without ANSI sequences
|
|
274
|
+
def strip_ansi(text)
|
|
275
|
+
text.gsub(/\e\[[0-9;]*[A-Za-z]/, "")
|
|
276
|
+
.gsub(/\e\][^\a\e]*(?:\a|\e\\)/, "")
|
|
277
|
+
.gsub(/\e[()][\dAB]/, "")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Check if text contains ANSI escape sequences
|
|
281
|
+
# @param text [String] Text to check
|
|
282
|
+
# @return [Boolean]
|
|
283
|
+
def contains_ansi?(text)
|
|
284
|
+
text.match?(/\e[\[\]()][^\a\e]*/)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Generate control code for a control type
|
|
288
|
+
# @param control_type [Symbol] Control type
|
|
289
|
+
# @param param1 [Object] First parameter
|
|
290
|
+
# @param param2 [Object] Second parameter
|
|
291
|
+
# @return [String]
|
|
292
|
+
def generate(control_type, param1 = nil, param2 = nil)
|
|
293
|
+
case control_type
|
|
294
|
+
when ControlType::BELL
|
|
295
|
+
bell
|
|
296
|
+
when ControlType::CARRIAGE_RETURN
|
|
297
|
+
carriage_return
|
|
298
|
+
when ControlType::HOME
|
|
299
|
+
home
|
|
300
|
+
when ControlType::CLEAR
|
|
301
|
+
clear
|
|
302
|
+
when ControlType::SHOW_CURSOR
|
|
303
|
+
show_cursor
|
|
304
|
+
when ControlType::HIDE_CURSOR
|
|
305
|
+
hide_cursor
|
|
306
|
+
when ControlType::ENABLE_ALT_SCREEN
|
|
307
|
+
enable_alt_screen
|
|
308
|
+
when ControlType::DISABLE_ALT_SCREEN
|
|
309
|
+
disable_alt_screen
|
|
310
|
+
when ControlType::CURSOR_UP
|
|
311
|
+
cursor_up(param1 || 1)
|
|
312
|
+
when ControlType::CURSOR_DOWN
|
|
313
|
+
cursor_down(param1 || 1)
|
|
314
|
+
when ControlType::CURSOR_FORWARD
|
|
315
|
+
cursor_forward(param1 || 1)
|
|
316
|
+
when ControlType::CURSOR_BACKWARD
|
|
317
|
+
cursor_backward(param1 || 1)
|
|
318
|
+
when ControlType::CURSOR_MOVE_TO_COLUMN
|
|
319
|
+
cursor_move_to_column(param1 || 1)
|
|
320
|
+
when ControlType::CURSOR_MOVE_TO
|
|
321
|
+
cursor_move_to(param1 || 1, param2 || 1)
|
|
322
|
+
when ControlType::ERASE_IN_LINE
|
|
323
|
+
erase_line(param1 || 2)
|
|
324
|
+
when ControlType::SET_WINDOW_TITLE
|
|
325
|
+
set_title(param1.to_s)
|
|
326
|
+
else
|
|
327
|
+
""
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
data/lib/rich/json.rb
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "style"
|
|
5
|
+
require_relative "segment"
|
|
6
|
+
|
|
7
|
+
module Rich
|
|
8
|
+
# JSON syntax highlighting and formatting
|
|
9
|
+
module JSON
|
|
10
|
+
# Default styles for JSON elements
|
|
11
|
+
DEFAULT_STYLES = {
|
|
12
|
+
key: "cyan",
|
|
13
|
+
string: "green",
|
|
14
|
+
number: "yellow",
|
|
15
|
+
boolean: "italic magenta",
|
|
16
|
+
null: "dim",
|
|
17
|
+
brace: "bold",
|
|
18
|
+
bracket: "bold",
|
|
19
|
+
comma: "dim",
|
|
20
|
+
colon: "dim"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Render JSON with syntax highlighting
|
|
25
|
+
# @param data [Object] Data to render as JSON
|
|
26
|
+
# @param indent [Integer] Indentation size
|
|
27
|
+
# @param styles [Hash] Style overrides
|
|
28
|
+
# @return [Array<Segment>]
|
|
29
|
+
def render(data, indent: 2, styles: {})
|
|
30
|
+
merged_styles = DEFAULT_STYLES.merge(styles)
|
|
31
|
+
json_str = ::JSON.pretty_generate(data, indent: " " * indent)
|
|
32
|
+
|
|
33
|
+
segments = []
|
|
34
|
+
tokenize(json_str, merged_styles, segments)
|
|
35
|
+
segments
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Render to string with ANSI codes
|
|
39
|
+
# @param data [Object] Data to render
|
|
40
|
+
# @param indent [Integer] Indentation
|
|
41
|
+
# @param color_system [Symbol] Color system
|
|
42
|
+
# @return [String]
|
|
43
|
+
def to_s(data, indent: 2, color_system: ColorSystem::TRUECOLOR)
|
|
44
|
+
segments = render(data, indent: indent)
|
|
45
|
+
Segment.render(segments, color_system: color_system)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parse and render a JSON string
|
|
49
|
+
# @param json_str [String] JSON string
|
|
50
|
+
# @param styles [Hash] Style overrides
|
|
51
|
+
# @return [Array<Segment>]
|
|
52
|
+
def highlight(json_str, styles: {})
|
|
53
|
+
merged_styles = DEFAULT_STYLES.merge(styles)
|
|
54
|
+
segments = []
|
|
55
|
+
tokenize(json_str, merged_styles, segments)
|
|
56
|
+
segments
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def tokenize(json_str, styles, segments)
|
|
62
|
+
pos = 0
|
|
63
|
+
|
|
64
|
+
while pos < json_str.length
|
|
65
|
+
char = json_str[pos]
|
|
66
|
+
|
|
67
|
+
case char
|
|
68
|
+
when "{"
|
|
69
|
+
segments << Segment.new("{", style: parse_style(styles[:brace]))
|
|
70
|
+
pos += 1
|
|
71
|
+
when "}"
|
|
72
|
+
segments << Segment.new("}", style: parse_style(styles[:brace]))
|
|
73
|
+
pos += 1
|
|
74
|
+
when "["
|
|
75
|
+
segments << Segment.new("[", style: parse_style(styles[:bracket]))
|
|
76
|
+
pos += 1
|
|
77
|
+
when "]"
|
|
78
|
+
segments << Segment.new("]", style: parse_style(styles[:bracket]))
|
|
79
|
+
pos += 1
|
|
80
|
+
when ","
|
|
81
|
+
segments << Segment.new(",", style: parse_style(styles[:comma]))
|
|
82
|
+
pos += 1
|
|
83
|
+
when ":"
|
|
84
|
+
segments << Segment.new(":", style: parse_style(styles[:colon]))
|
|
85
|
+
pos += 1
|
|
86
|
+
when '"'
|
|
87
|
+
# String - check if it's a key (followed by :)
|
|
88
|
+
str_end = find_string_end(json_str, pos)
|
|
89
|
+
str_content = json_str[pos..str_end]
|
|
90
|
+
|
|
91
|
+
# Look ahead to see if this is a key
|
|
92
|
+
look_ahead = json_str[str_end + 1..].lstrip
|
|
93
|
+
is_key = look_ahead.start_with?(":")
|
|
94
|
+
|
|
95
|
+
style = is_key ? styles[:key] : styles[:string]
|
|
96
|
+
segments << Segment.new(str_content, style: parse_style(style))
|
|
97
|
+
pos = str_end + 1
|
|
98
|
+
when /[0-9\-]/
|
|
99
|
+
# Number
|
|
100
|
+
num_end = pos
|
|
101
|
+
while num_end < json_str.length && json_str[num_end].match?(/[0-9eE.\-+]/)
|
|
102
|
+
num_end += 1
|
|
103
|
+
end
|
|
104
|
+
num_content = json_str[pos...num_end]
|
|
105
|
+
segments << Segment.new(num_content, style: parse_style(styles[:number]))
|
|
106
|
+
pos = num_end
|
|
107
|
+
when /[tfn]/
|
|
108
|
+
# Boolean or null
|
|
109
|
+
if json_str[pos, 4] == "true"
|
|
110
|
+
segments << Segment.new("true", style: parse_style(styles[:boolean]))
|
|
111
|
+
pos += 4
|
|
112
|
+
elsif json_str[pos, 5] == "false"
|
|
113
|
+
segments << Segment.new("false", style: parse_style(styles[:boolean]))
|
|
114
|
+
pos += 5
|
|
115
|
+
elsif json_str[pos, 4] == "null"
|
|
116
|
+
segments << Segment.new("null", style: parse_style(styles[:null]))
|
|
117
|
+
pos += 4
|
|
118
|
+
else
|
|
119
|
+
segments << Segment.new(char)
|
|
120
|
+
pos += 1
|
|
121
|
+
end
|
|
122
|
+
when /\s/
|
|
123
|
+
# Whitespace
|
|
124
|
+
ws_end = pos
|
|
125
|
+
while ws_end < json_str.length && json_str[ws_end].match?(/\s/)
|
|
126
|
+
ws_end += 1
|
|
127
|
+
end
|
|
128
|
+
segments << Segment.new(json_str[pos...ws_end])
|
|
129
|
+
pos = ws_end
|
|
130
|
+
else
|
|
131
|
+
segments << Segment.new(char)
|
|
132
|
+
pos += 1
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_string_end(str, start_pos)
|
|
138
|
+
pos = start_pos + 1
|
|
139
|
+
while pos < str.length
|
|
140
|
+
if str[pos] == '"' && str[pos - 1] != '\\'
|
|
141
|
+
return pos
|
|
142
|
+
end
|
|
143
|
+
pos += 1
|
|
144
|
+
end
|
|
145
|
+
str.length - 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def parse_style(style)
|
|
149
|
+
return nil if style.nil?
|
|
150
|
+
return style if style.is_a?(Style)
|
|
151
|
+
|
|
152
|
+
Style.parse(style)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Pretty printing of Ruby objects
|
|
158
|
+
module Pretty
|
|
159
|
+
class << self
|
|
160
|
+
# Pretty print a Ruby object
|
|
161
|
+
# @param obj [Object] Object to print
|
|
162
|
+
# @param indent [Integer] Indentation
|
|
163
|
+
# @return [Array<Segment>]
|
|
164
|
+
def render(obj, indent: 2)
|
|
165
|
+
segments = []
|
|
166
|
+
render_object(obj, 0, indent, segments)
|
|
167
|
+
segments
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Render to string
|
|
171
|
+
# @param obj [Object] Object to render
|
|
172
|
+
# @param color_system [Symbol] Color system
|
|
173
|
+
# @return [String]
|
|
174
|
+
def to_s(obj, color_system: ColorSystem::TRUECOLOR)
|
|
175
|
+
Segment.render(render(obj), color_system: color_system)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def render_object(obj, depth, indent, segments)
|
|
181
|
+
case obj
|
|
182
|
+
when NilClass
|
|
183
|
+
segments << Segment.new("nil", style: Style.parse("dim"))
|
|
184
|
+
when TrueClass, FalseClass
|
|
185
|
+
segments << Segment.new(obj.to_s, style: Style.parse("italic magenta"))
|
|
186
|
+
when Integer, Float
|
|
187
|
+
segments << Segment.new(obj.to_s, style: Style.parse("yellow"))
|
|
188
|
+
when String
|
|
189
|
+
segments << Segment.new(obj.inspect, style: Style.parse("green"))
|
|
190
|
+
when Symbol
|
|
191
|
+
segments << Segment.new(":#{obj}", style: Style.parse("cyan bold"))
|
|
192
|
+
when Array
|
|
193
|
+
render_array(obj, depth, indent, segments)
|
|
194
|
+
when Hash
|
|
195
|
+
render_hash(obj, depth, indent, segments)
|
|
196
|
+
else
|
|
197
|
+
segments << Segment.new(obj.inspect, style: Style.parse("white"))
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def render_array(arr, depth, indent, segments)
|
|
202
|
+
if arr.empty?
|
|
203
|
+
segments << Segment.new("[]", style: Style.parse("bold"))
|
|
204
|
+
return
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
segments << Segment.new("[", style: Style.parse("bold"))
|
|
208
|
+
segments << Segment.new("\n")
|
|
209
|
+
|
|
210
|
+
arr.each_with_index do |item, index|
|
|
211
|
+
segments << Segment.new(" " * ((depth + 1) * indent))
|
|
212
|
+
render_object(item, depth + 1, indent, segments)
|
|
213
|
+
segments << Segment.new(",") if index < arr.length - 1
|
|
214
|
+
segments << Segment.new("\n")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
segments << Segment.new(" " * (depth * indent))
|
|
218
|
+
segments << Segment.new("]", style: Style.parse("bold"))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def render_hash(hash, depth, indent, segments)
|
|
222
|
+
if hash.empty?
|
|
223
|
+
segments << Segment.new("{}", style: Style.parse("bold"))
|
|
224
|
+
return
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
segments << Segment.new("{", style: Style.parse("bold"))
|
|
228
|
+
segments << Segment.new("\n")
|
|
229
|
+
|
|
230
|
+
entries = hash.to_a
|
|
231
|
+
entries.each_with_index do |(key, value), index|
|
|
232
|
+
segments << Segment.new(" " * ((depth + 1) * indent))
|
|
233
|
+
|
|
234
|
+
# Key
|
|
235
|
+
if key.is_a?(Symbol)
|
|
236
|
+
segments << Segment.new(":#{key}", style: Style.parse("cyan"))
|
|
237
|
+
else
|
|
238
|
+
segments << Segment.new(key.inspect, style: Style.parse("cyan"))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
segments << Segment.new(" => ", style: Style.parse("dim"))
|
|
242
|
+
|
|
243
|
+
# Value
|
|
244
|
+
render_object(value, depth + 1, indent, segments)
|
|
245
|
+
segments << Segment.new(",") if index < entries.length - 1
|
|
246
|
+
segments << Segment.new("\n")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
segments << Segment.new(" " * (depth * indent))
|
|
250
|
+
segments << Segment.new("}", style: Style.parse("bold"))
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|