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/json.rb
CHANGED
|
@@ -1,254 +1,260 @@
|
|
|
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
|
-
|
|
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
|
-
def
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
when
|
|
189
|
-
segments << Segment.new(
|
|
190
|
-
when
|
|
191
|
-
segments << Segment.new(
|
|
192
|
-
when
|
|
193
|
-
|
|
194
|
-
when
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
segments << Segment.new(obj
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
segments << Segment.new(
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
ch = str[pos]
|
|
141
|
+
if ch == "\\"
|
|
142
|
+
# Skip the escaped character so an escaped backslash (\\) before the
|
|
143
|
+
# closing quote isn't mistaken for an escaped quote.
|
|
144
|
+
pos += 2
|
|
145
|
+
next
|
|
146
|
+
elsif ch == '"'
|
|
147
|
+
return pos
|
|
148
|
+
end
|
|
149
|
+
pos += 1
|
|
150
|
+
end
|
|
151
|
+
str.length - 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parse_style(style)
|
|
155
|
+
return nil if style.nil?
|
|
156
|
+
return style if style.is_a?(Style)
|
|
157
|
+
|
|
158
|
+
Style.parse(style)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Pretty printing of Ruby objects
|
|
164
|
+
module Pretty
|
|
165
|
+
class << self
|
|
166
|
+
# Pretty print a Ruby object
|
|
167
|
+
# @param obj [Object] Object to print
|
|
168
|
+
# @param indent [Integer] Indentation
|
|
169
|
+
# @return [Array<Segment>]
|
|
170
|
+
def render(obj, indent: 2)
|
|
171
|
+
segments = []
|
|
172
|
+
render_object(obj, 0, indent, segments)
|
|
173
|
+
segments
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Render to string
|
|
177
|
+
# @param obj [Object] Object to render
|
|
178
|
+
# @param color_system [Symbol] Color system
|
|
179
|
+
# @return [String]
|
|
180
|
+
def to_s(obj, color_system: ColorSystem::TRUECOLOR)
|
|
181
|
+
Segment.render(render(obj), color_system: color_system)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def render_object(obj, depth, indent, segments)
|
|
187
|
+
case obj
|
|
188
|
+
when NilClass
|
|
189
|
+
segments << Segment.new("nil", style: Style.parse("dim"))
|
|
190
|
+
when TrueClass, FalseClass
|
|
191
|
+
segments << Segment.new(obj.to_s, style: Style.parse("italic magenta"))
|
|
192
|
+
when Integer, Float
|
|
193
|
+
segments << Segment.new(obj.to_s, style: Style.parse("yellow"))
|
|
194
|
+
when String
|
|
195
|
+
segments << Segment.new(obj.inspect, style: Style.parse("green"))
|
|
196
|
+
when Symbol
|
|
197
|
+
segments << Segment.new(":#{obj}", style: Style.parse("cyan bold"))
|
|
198
|
+
when Array
|
|
199
|
+
render_array(obj, depth, indent, segments)
|
|
200
|
+
when Hash
|
|
201
|
+
render_hash(obj, depth, indent, segments)
|
|
202
|
+
else
|
|
203
|
+
segments << Segment.new(obj.inspect, style: Style.parse("white"))
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def render_array(arr, depth, indent, segments)
|
|
208
|
+
if arr.empty?
|
|
209
|
+
segments << Segment.new("[]", style: Style.parse("bold"))
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
segments << Segment.new("[", style: Style.parse("bold"))
|
|
214
|
+
segments << Segment.new("\n")
|
|
215
|
+
|
|
216
|
+
arr.each_with_index do |item, index|
|
|
217
|
+
segments << Segment.new(" " * ((depth + 1) * indent))
|
|
218
|
+
render_object(item, depth + 1, indent, segments)
|
|
219
|
+
segments << Segment.new(",") if index < arr.length - 1
|
|
220
|
+
segments << Segment.new("\n")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
segments << Segment.new(" " * (depth * indent))
|
|
224
|
+
segments << Segment.new("]", style: Style.parse("bold"))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def render_hash(hash, depth, indent, segments)
|
|
228
|
+
if hash.empty?
|
|
229
|
+
segments << Segment.new("{}", style: Style.parse("bold"))
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
segments << Segment.new("{", style: Style.parse("bold"))
|
|
234
|
+
segments << Segment.new("\n")
|
|
235
|
+
|
|
236
|
+
entries = hash.to_a
|
|
237
|
+
entries.each_with_index do |(key, value), index|
|
|
238
|
+
segments << Segment.new(" " * ((depth + 1) * indent))
|
|
239
|
+
|
|
240
|
+
# Key
|
|
241
|
+
if key.is_a?(Symbol)
|
|
242
|
+
segments << Segment.new(":#{key}", style: Style.parse("cyan"))
|
|
243
|
+
else
|
|
244
|
+
segments << Segment.new(key.inspect, style: Style.parse("cyan"))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
segments << Segment.new(" => ", style: Style.parse("dim"))
|
|
248
|
+
|
|
249
|
+
# Value
|
|
250
|
+
render_object(value, depth + 1, indent, segments)
|
|
251
|
+
segments << Segment.new(",") if index < entries.length - 1
|
|
252
|
+
segments << Segment.new("\n")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
segments << Segment.new(" " * (depth * indent))
|
|
256
|
+
segments << Segment.new("}", style: Style.parse("bold"))
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|