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.
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
- 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
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