ruby_rich 0.3.1 → 0.4.1

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.
@@ -0,0 +1,510 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbconfig'
4
+ require 'tty-cursor'
5
+ require_relative 'event'
6
+
7
+ module RubyRich
8
+ class Terminal
9
+ MOUSE_ENABLE = "\e[?1000h\e[?1002h\e[?1006h"
10
+ MOUSE_DISABLE = "\e[?1006l\e[?1002l\e[?1000l"
11
+ AUTOWRAP_ENABLE = "\e[?7h"
12
+ AUTOWRAP_DISABLE = "\e[?7l"
13
+ ALT_SCREEN_ENABLE = "\e[?1049h"
14
+ ALT_SCREEN_DISABLE = "\e[?1049l"
15
+ BRACKETED_PASTE_ENABLE = "\e[?2004h"
16
+ BRACKETED_PASTE_DISABLE = "\e[?2004l"
17
+ STD_INPUT_HANDLE = -10
18
+ STD_OUTPUT_HANDLE = -11
19
+ ENABLE_MOUSE_INPUT = 0x0010
20
+ ENABLE_WINDOW_INPUT = 0x0008
21
+ ENABLE_QUICK_EDIT_MODE = 0x0040
22
+ ENABLE_EXTENDED_FLAGS = 0x0080
23
+ ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
24
+ ENABLE_PROCESSED_OUTPUT = 0x0001
25
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
26
+ INPUT_RECORD_SIZE = 20
27
+ KEY_EVENT = 0x0001
28
+ MOUSE_EVENT = 0x0002
29
+ FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001
30
+ RIGHTMOST_BUTTON_PRESSED = 0x0002
31
+ FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004
32
+ MOUSE_MOVED = 0x0001
33
+ DOUBLE_CLICK = 0x0002
34
+ MOUSE_WHEELED = 0x0004
35
+ MOUSE_HWHEELED = 0x0008
36
+ RIGHT_ALT_PRESSED = 0x0001
37
+ LEFT_ALT_PRESSED = 0x0002
38
+ RIGHT_CTRL_PRESSED = 0x0004
39
+ LEFT_CTRL_PRESSED = 0x0008
40
+ SHIFT_PRESSED = 0x0010
41
+
42
+ class << self
43
+ attr_accessor :debug_input
44
+
45
+ def setup(mouse: false, hide_cursor: true, autowrap: true, alt_screen: false)
46
+ capture_state
47
+ enable_virtual_terminal_on_windows
48
+ system('stty -echo') unless windows?
49
+ enter_alt_screen if alt_screen
50
+ enable_bracketed_paste
51
+ set_autowrap(autowrap)
52
+ print TTY::Cursor.hide if hide_cursor
53
+ enable_mouse if mouse
54
+ end
55
+
56
+ def restore(mouse: false, show_cursor: true, autowrap: true, alt_screen: false)
57
+ disable_mouse if mouse
58
+ disable_bracketed_paste
59
+ set_autowrap(autowrap)
60
+ leave_alt_screen if alt_screen
61
+ restore_virtual_terminal_on_windows
62
+ system("stty #{@original_state}") if @original_state && !windows?
63
+ print TTY::Cursor.show if show_cursor
64
+ end
65
+
66
+ def enable_mouse
67
+ @mouse_reporting_enabled = true
68
+ enable_windows_input_mode if windows?
69
+ print MOUSE_ENABLE
70
+ $stdout.flush
71
+ end
72
+
73
+ def disable_mouse
74
+ @mouse_reporting_enabled = false
75
+ print MOUSE_DISABLE
76
+ $stdout.flush
77
+ end
78
+
79
+ def set_autowrap(enabled)
80
+ print(enabled ? AUTOWRAP_ENABLE : AUTOWRAP_DISABLE)
81
+ $stdout.flush
82
+ end
83
+
84
+ def enter_alt_screen
85
+ print ALT_SCREEN_ENABLE
86
+ $stdout.flush
87
+ end
88
+
89
+ def leave_alt_screen
90
+ print ALT_SCREEN_DISABLE
91
+ $stdout.flush
92
+ end
93
+
94
+ def enable_bracketed_paste
95
+ print BRACKETED_PASTE_ENABLE
96
+ $stdout.flush
97
+ end
98
+
99
+ def disable_bracketed_paste
100
+ print BRACKETED_PASTE_DISABLE
101
+ $stdout.flush
102
+ end
103
+
104
+ def clear
105
+ print "\e[2J\e[H"
106
+ $stdout.flush
107
+ end
108
+
109
+ def with_cooked(mouse: false, alt_screen: false)
110
+ restore(mouse: mouse, alt_screen: alt_screen)
111
+ yield
112
+ ensure
113
+ setup(mouse: mouse, alt_screen: alt_screen, autowrap: false)
114
+ end
115
+
116
+ def prepare_input
117
+ return unless windows?
118
+
119
+ enable_windows_input_mode
120
+ end
121
+
122
+ def windows_input_mode
123
+ return nil unless windows?
124
+
125
+ console_mode(get_std_handle.call(STD_INPUT_HANDLE))
126
+ rescue
127
+ nil
128
+ end
129
+
130
+ def windows?
131
+ RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
132
+ end
133
+
134
+ def windows_mouse_reporting?
135
+ windows? && @mouse_reporting_enabled
136
+ end
137
+
138
+ def read_windows_input_event
139
+ return nil unless windows?
140
+ pending = next_windows_pending_event
141
+ return pending if pending
142
+
143
+ enable_windows_input_mode
144
+ handle = get_std_handle.call(STD_INPUT_HANDLE)
145
+ record = Fiddle::Pointer.malloc(INPUT_RECORD_SIZE)
146
+ read_count = Fiddle::Pointer.malloc(4)
147
+
148
+ loop do
149
+ return nil unless read_console_input.call(handle, record, 1, read_count) != 0
150
+ next unless read_count[0, 4].unpack1('L') == 1
151
+
152
+ event = parse_windows_input_record(record)
153
+ event = coalesce_windows_paste(handle, event) if paste_seed_event?(event)
154
+ return event if event
155
+ end
156
+ rescue
157
+ nil
158
+ end
159
+
160
+ private
161
+
162
+ def capture_state
163
+ @original_state = windows? ? nil : `stty -g`.strip
164
+ rescue
165
+ @original_state = nil
166
+ end
167
+
168
+ def enable_virtual_terminal_on_windows
169
+ return unless windows?
170
+
171
+ @windows_console_modes ||= {}
172
+
173
+ enable_windows_output_mode
174
+ enable_windows_input_mode
175
+ rescue
176
+ nil
177
+ end
178
+
179
+ def enable_windows_output_mode
180
+ handle = get_std_handle.call(STD_OUTPUT_HANDLE)
181
+ mode = console_mode(handle)
182
+ return unless mode
183
+
184
+ @windows_console_modes ||= {}
185
+ @windows_console_modes[handle.to_i] ||= { handle: handle, mode: mode }
186
+ set_console_mode.call(handle, mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
187
+ end
188
+
189
+ def enable_windows_input_mode
190
+ handle = get_std_handle.call(STD_INPUT_HANDLE)
191
+ mode = console_mode(handle)
192
+ return unless mode
193
+
194
+ @windows_console_modes ||= {}
195
+ @windows_console_modes[handle.to_i] ||= { handle: handle, mode: mode }
196
+ next_mode = mode
197
+ next_mode |= ENABLE_EXTENDED_FLAGS
198
+ next_mode |= ENABLE_MOUSE_INPUT
199
+ next_mode |= ENABLE_WINDOW_INPUT
200
+ next_mode &= ~ENABLE_QUICK_EDIT_MODE
201
+ set_console_mode.call(handle, next_mode)
202
+ end
203
+
204
+ def restore_virtual_terminal_on_windows
205
+ return unless windows?
206
+ return unless @windows_console_modes && !@windows_console_modes.empty?
207
+
208
+ require 'fiddle'
209
+ kernel32 = Fiddle.dlopen('kernel32')
210
+ set_console_mode = Fiddle::Function.new(
211
+ kernel32['SetConsoleMode'],
212
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG],
213
+ Fiddle::TYPE_INT
214
+ )
215
+
216
+ @windows_console_modes.each_value do |entry|
217
+ set_console_mode.call(entry[:handle], entry[:mode])
218
+ end
219
+ @windows_console_modes.clear
220
+ rescue
221
+ nil
222
+ end
223
+
224
+ def console_mode(handle)
225
+ mode_ptr = Fiddle::Pointer.malloc(4)
226
+ return nil unless get_console_mode.call(handle, mode_ptr) != 0
227
+
228
+ mode_ptr[0, 4].unpack1('L')
229
+ end
230
+
231
+ def kernel32
232
+ @kernel32 ||= begin
233
+ ensure_fiddle
234
+ Fiddle.dlopen('kernel32')
235
+ end
236
+ end
237
+
238
+ def get_std_handle
239
+ ensure_fiddle
240
+ @get_std_handle ||= Fiddle::Function.new(
241
+ kernel32['GetStdHandle'],
242
+ [Fiddle::TYPE_LONG],
243
+ Fiddle::TYPE_VOIDP
244
+ )
245
+ end
246
+
247
+ def get_console_mode
248
+ ensure_fiddle
249
+ @get_console_mode ||= Fiddle::Function.new(
250
+ kernel32['GetConsoleMode'],
251
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
252
+ Fiddle::TYPE_INT
253
+ )
254
+ end
255
+
256
+ def set_console_mode
257
+ ensure_fiddle
258
+ @set_console_mode ||= Fiddle::Function.new(
259
+ kernel32['SetConsoleMode'],
260
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG],
261
+ Fiddle::TYPE_INT
262
+ )
263
+ end
264
+
265
+ def read_console_input
266
+ ensure_fiddle
267
+ @read_console_input ||= Fiddle::Function.new(
268
+ kernel32['ReadConsoleInputW'],
269
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP],
270
+ Fiddle::TYPE_INT
271
+ )
272
+ end
273
+
274
+ def get_number_of_console_input_events
275
+ ensure_fiddle
276
+ @get_number_of_console_input_events ||= Fiddle::Function.new(
277
+ kernel32['GetNumberOfConsoleInputEvents'],
278
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
279
+ Fiddle::TYPE_INT
280
+ )
281
+ end
282
+
283
+ def parse_windows_input_record(record)
284
+ event_type = record[0, 2].unpack1('S')
285
+
286
+ case event_type
287
+ when KEY_EVENT
288
+ parse_windows_key_record(record)
289
+ when MOUSE_EVENT
290
+ parse_windows_mouse_record(record)
291
+ end
292
+ end
293
+
294
+ def next_windows_pending_event
295
+ return nil unless @windows_pending_events && !@windows_pending_events.empty?
296
+
297
+ @windows_pending_events.shift
298
+ end
299
+
300
+ def paste_seed_event?(event)
301
+ event && event[:type] == :key && [:string, :enter, :tab].include?(event[:name])
302
+ end
303
+
304
+ def coalesce_windows_paste(handle, first_event)
305
+ events = [first_event]
306
+ sleep 0.002
307
+ available = windows_input_event_count(handle)
308
+
309
+ while available && available.positive?
310
+ record = Fiddle::Pointer.malloc(INPUT_RECORD_SIZE)
311
+ read_count = Fiddle::Pointer.malloc(4)
312
+ break unless read_console_input.call(handle, record, 1, read_count) != 0
313
+ break unless read_count[0, 4].unpack1('L') == 1
314
+
315
+ event = parse_windows_input_record(record)
316
+ if paste_seed_event?(event)
317
+ events << event
318
+ elsif event
319
+ (@windows_pending_events ||= []) << event
320
+ end
321
+ available = windows_input_event_count(handle)
322
+ end
323
+
324
+ finish_windows_coalesced_events(events, first_event)
325
+ rescue
326
+ first_event
327
+ end
328
+
329
+ def finish_windows_coalesced_events(events, first_event)
330
+ if windows_enter_burst?(events)
331
+ first_event
332
+ elsif windows_paste_burst?(events)
333
+ Event.key(:paste, value: events.map { |event| windows_paste_text(event) }.join)
334
+ else
335
+ (@windows_pending_events ||= []).concat(events[1..] || [])
336
+ first_event
337
+ end
338
+ end
339
+
340
+ def windows_enter_burst?(events)
341
+ events.length <= 2 && events.all? { |event| event[:name] == :enter }
342
+ end
343
+
344
+ def windows_paste_burst?(events)
345
+ return false if events.length <= 1
346
+
347
+ events.any? { |event| event[:name] == :enter } || events.length >= 4
348
+ end
349
+
350
+ def windows_paste_text(event)
351
+ case event[:name]
352
+ when :string
353
+ event[:value].to_s
354
+ when :enter
355
+ "\n"
356
+ when :tab
357
+ "\t"
358
+ else
359
+ ""
360
+ end
361
+ end
362
+
363
+ def windows_input_event_count(handle)
364
+ count_ptr = Fiddle::Pointer.malloc(4)
365
+ return nil unless get_number_of_console_input_events.call(handle, count_ptr) != 0
366
+
367
+ count_ptr[0, 4].unpack1('L')
368
+ end
369
+
370
+ def parse_windows_key_record(record)
371
+ key_down = record[4, 4].unpack1('L') != 0
372
+ return nil unless key_down
373
+
374
+ virtual_key = record[10, 2].unpack1('S')
375
+ control_state = record[12, 4].unpack1('L')
376
+ char_code = record[14, 2].unpack1('S')
377
+ char = char_code.zero? ? nil : char_code.chr(Encoding::UTF_8)
378
+ modifiers = windows_key_modifiers(control_state)
379
+
380
+ case virtual_key
381
+ when 13
382
+ return annotate_windows_key_event(Event.key(:alt_enter), virtual_key, char_code, control_state, modifiers) if modifiers.include?(:alt)
383
+ return annotate_windows_key_event(Event.key(:ctrl_enter), virtual_key, char_code, control_state, modifiers) if char_code == 10 && modifiers.include?(:ctrl)
384
+ return annotate_windows_key_event(Event.key(:shift_enter), virtual_key, char_code, control_state, modifiers) if char_code == 10
385
+
386
+ annotate_windows_key_event(Event.key(:enter), virtual_key, char_code, control_state, modifiers)
387
+ when 9
388
+ modifiers.include?(:shift) ? Event.key(:shift_tab) : Event.key(:tab)
389
+ when 8 then Event.key(:backspace)
390
+ when 27 then Event.key(:escape)
391
+ when 37 then Event.key(:left)
392
+ when 38 then Event.key(:up)
393
+ when 39 then Event.key(:right)
394
+ when 40 then Event.key(:down)
395
+ when 33 then Event.key(:page_up)
396
+ when 34 then Event.key(:page_down)
397
+ when 35 then Event.key(:end)
398
+ when 36 then Event.key(:home)
399
+ when 46 then Event.key(:delete)
400
+ else
401
+ ctrl_event = windows_ctrl_key_event(char_code)
402
+ return ctrl_event if ctrl_event
403
+ return Event.key(:ctrl_c) if char_code == 3
404
+ return nil unless char && !char.empty?
405
+
406
+ Event.key(:string, value: char)
407
+ end
408
+ rescue RangeError
409
+ nil
410
+ end
411
+
412
+ def parse_windows_mouse_record(record)
413
+ raw_x = record[4, 2].unpack1('s')
414
+ raw_y = record[6, 2].unpack1('s')
415
+ button_state = record[8, 4].unpack1('L')
416
+ control_state = record[12, 4].unpack1('L')
417
+ event_flags = record[16, 4].unpack1('L')
418
+
419
+ if (event_flags & MOUSE_WHEELED) == MOUSE_WHEELED
420
+ delta = signed_high_word(button_state)
421
+ return Event.mouse(
422
+ :mouse_wheel,
423
+ button: :wheel,
424
+ x: raw_x,
425
+ y: raw_y,
426
+ raw_x: raw_x + 1,
427
+ raw_y: raw_y + 1,
428
+ code: button_state,
429
+ modifiers: windows_mouse_modifiers(control_state),
430
+ direction: delta.negative? ? :down : :up
431
+ )
432
+ end
433
+
434
+ if (event_flags & MOUSE_MOVED) == MOUSE_MOVED
435
+ return nil if button_state.zero?
436
+
437
+ return windows_mouse_event(:mouse_drag, raw_x, raw_y, button_state, control_state)
438
+ end
439
+
440
+ if button_state.zero?
441
+ return windows_mouse_event(:mouse_up, raw_x, raw_y, button_state, control_state)
442
+ end
443
+
444
+ windows_mouse_event(:mouse_down, raw_x, raw_y, button_state, control_state)
445
+ end
446
+
447
+ def windows_mouse_event(name, raw_x, raw_y, button_state, control_state)
448
+ Event.mouse(
449
+ name,
450
+ button: windows_mouse_button(button_state),
451
+ x: raw_x,
452
+ y: raw_y,
453
+ raw_x: raw_x + 1,
454
+ raw_y: raw_y + 1,
455
+ code: button_state,
456
+ modifiers: windows_mouse_modifiers(control_state)
457
+ )
458
+ end
459
+
460
+ def windows_mouse_button(button_state)
461
+ return :left if (button_state & FROM_LEFT_1ST_BUTTON_PRESSED) != 0
462
+ return :right if (button_state & RIGHTMOST_BUTTON_PRESSED) != 0
463
+ return :middle if (button_state & FROM_LEFT_2ND_BUTTON_PRESSED) != 0
464
+
465
+ :unknown
466
+ end
467
+
468
+ def windows_mouse_modifiers(control_state)
469
+ windows_key_modifiers(control_state)
470
+ end
471
+
472
+ def windows_key_modifiers(control_state)
473
+ modifiers = []
474
+ modifiers << :shift if (control_state & SHIFT_PRESSED) != 0
475
+ modifiers << :ctrl if (control_state & LEFT_CTRL_PRESSED) != 0 || (control_state & RIGHT_CTRL_PRESSED) != 0
476
+ modifiers << :alt if (control_state & LEFT_ALT_PRESSED) != 0 || (control_state & RIGHT_ALT_PRESSED) != 0
477
+ modifiers
478
+ end
479
+
480
+ def windows_ctrl_key_event(char_code)
481
+ return nil unless char_code.between?(1, 26)
482
+
483
+ ctrl_char = (char_code + 64).chr.downcase
484
+ Event.key("ctrl_#{ctrl_char}".to_sym)
485
+ end
486
+
487
+ def annotate_windows_key_event(event, virtual_key, char_code, control_state, modifiers)
488
+ return event unless @debug_input
489
+
490
+ event.merge(
491
+ raw: {
492
+ virtual_key: virtual_key,
493
+ char_code: char_code,
494
+ control_state: control_state,
495
+ modifiers: modifiers
496
+ }
497
+ )
498
+ end
499
+
500
+ def signed_high_word(value)
501
+ high = (value >> 16) & 0xffff
502
+ high >= 0x8000 ? high - 0x10000 : high
503
+ end
504
+
505
+ def ensure_fiddle
506
+ require 'fiddle'
507
+ end
508
+ end
509
+ end
510
+ end
@@ -1,6 +1,6 @@
1
1
  module RubyRich
2
2
  class RichText
3
- # 默认主题
3
+ # Default theme
4
4
  @@theme = {
5
5
  error: { color: :red, bold: true },
6
6
  success: { color: :green, bold: true },
@@ -8,6 +8,43 @@ module RubyRich
8
8
  warning: { color: :yellow, bold: true }
9
9
  }
10
10
 
11
+ # Rich markup 标记映射
12
+ MARKUP_PATTERNS = {
13
+ # Basic colors
14
+ /\[red\](.*?)\[\/red\]/m => proc { |text| "\e[31m#{text}\e[0m" },
15
+ /\[green\](.*?)\[\/green\]/m => proc { |text| "\e[32m#{text}\e[0m" },
16
+ /\[yellow\](.*?)\[\/yellow\]/m => proc { |text| "\e[33m#{text}\e[0m" },
17
+ /\[blue\](.*?)\[\/blue\]/m => proc { |text| "\e[34m#{text}\e[0m" },
18
+ /\[magenta\](.*?)\[\/magenta\]/m => proc { |text| "\e[35m#{text}\e[0m" },
19
+ /\[cyan\](.*?)\[\/cyan\]/m => proc { |text| "\e[36m#{text}\e[0m" },
20
+ /\[white\](.*?)\[\/white\]/m => proc { |text| "\e[37m#{text}\e[0m" },
21
+ /\[black\](.*?)\[\/black\]/m => proc { |text| "\e[30m#{text}\e[0m" },
22
+
23
+ # Bright colors
24
+ /\[bright_red\](.*?)\[\/bright_red\]/m => proc { |text| "\e[91m#{text}\e[0m" },
25
+ /\[bright_green\](.*?)\[\/bright_green\]/m => proc { |text| "\e[92m#{text}\e[0m" },
26
+ /\[bright_yellow\](.*?)\[\/bright_yellow\]/m => proc { |text| "\e[93m#{text}\e[0m" },
27
+ /\[bright_blue\](.*?)\[\/bright_blue\]/m => proc { |text| "\e[94m#{text}\e[0m" },
28
+ /\[bright_magenta\](.*?)\[\/bright_magenta\]/m => proc { |text| "\e[95m#{text}\e[0m" },
29
+ /\[bright_cyan\](.*?)\[\/bright_cyan\]/m => proc { |text| "\e[96m#{text}\e[0m" },
30
+ /\[bright_white\](.*?)\[\/bright_white\]/m => proc { |text| "\e[97m#{text}\e[0m" },
31
+
32
+ # Text styles
33
+ /\[bold\](.*?)\[\/bold\]/m => proc { |text| "\e[1m#{text}\e[22m" },
34
+ /\[dim\](.*?)\[\/dim\]/m => proc { |text| "\e[2m#{text}\e[22m" },
35
+ /\[italic\](.*?)\[\/italic\]/m => proc { |text| "\e[3m#{text}\e[23m" },
36
+ /\[underline\](.*?)\[\/underline\]/m => proc { |text| "\e[4m#{text}\e[24m" },
37
+ /\[blink\](.*?)\[\/blink\]/m => proc { |text| "\e[5m#{text}\e[25m" },
38
+ /\[reverse\](.*?)\[\/reverse\]/m => proc { |text| "\e[7m#{text}\e[27m" },
39
+ /\[strikethrough\](.*?)\[\/strikethrough\]/m => proc { |text| "\e[9m#{text}\e[29m" },
40
+
41
+ # Combined styles
42
+ /\[bold\s+(\w+)\](.*?)\[\/bold\s+\1\]/m => proc do |text, color_match|
43
+ color_code = color_to_ansi(color_match)
44
+ "\e[1m#{color_code}#{text}\e[0m"
45
+ end
46
+ }.freeze
47
+
11
48
  def self.set_theme(new_theme)
12
49
  @@theme.merge!(new_theme)
13
50
  end
@@ -34,11 +71,84 @@ module RubyRich
34
71
  end
35
72
 
36
73
  def render
37
- "#{@styles.join}#{@text}#{AnsiCode.reset}"
74
+ processed_text = process_markup(@text)
75
+ "#{@styles.join}#{processed_text}#{AnsiCode.reset}"
76
+ end
77
+
78
+ # 处理 Rich markup 标记语言
79
+ def self.markup(text)
80
+ new(text).render_markup
81
+ end
82
+
83
+ def render_markup
84
+ process_markup(@text)
38
85
  end
39
86
 
40
87
  private
41
88
 
89
+ def process_markup(text)
90
+ result = text.dup
91
+
92
+ # 处理组合样式 (如 [bold red])
93
+ result.gsub!(/\[(\w+)\s+(\w+)\](.*?)\[\/\1\s+\2\]/m) do |match|
94
+ style1, style2, content = $1, $2, $3
95
+
96
+ # 确定哪个是样式,哪个是颜色
97
+ if is_color?(style2)
98
+ apply_combined_style(content, style1, style2)
99
+ elsif is_color?(style1)
100
+ apply_combined_style(content, style2, style1)
101
+ else
102
+ match # 无法处理的组合,返回原文
103
+ end
104
+ end
105
+
106
+ # 处理基本样式和颜色
107
+ MARKUP_PATTERNS.each do |pattern, processor|
108
+ result.gsub!(pattern) do |match|
109
+ if pattern.source.include?('\\s+')
110
+ # 这是组合样式,已经在上面处理过了
111
+ match
112
+ else
113
+ processor.call($1)
114
+ end
115
+ end
116
+ end
117
+
118
+ result
119
+ end
120
+
121
+ def apply_combined_style(content, style, color)
122
+ color_code = color_to_ansi(color)
123
+ style_code = style_to_ansi(style)
124
+ "#{style_code}#{color_code}#{content}\e[0m"
125
+ end
126
+
127
+ def is_color?(word)
128
+ %w[red green yellow blue magenta cyan white black bright_red bright_green bright_yellow bright_blue bright_magenta bright_cyan bright_white].include?(word)
129
+ end
130
+
131
+ def color_to_ansi(color)
132
+ color_map = {
133
+ 'red' => "\e[31m", 'green' => "\e[32m", 'yellow' => "\e[33m",
134
+ 'blue' => "\e[34m", 'magenta' => "\e[35m", 'cyan' => "\e[36m",
135
+ 'white' => "\e[37m", 'black' => "\e[30m",
136
+ 'bright_red' => "\e[91m", 'bright_green' => "\e[92m", 'bright_yellow' => "\e[93m",
137
+ 'bright_blue' => "\e[94m", 'bright_magenta' => "\e[95m", 'bright_cyan' => "\e[96m",
138
+ 'bright_white' => "\e[97m"
139
+ }
140
+ color_map[color] || ""
141
+ end
142
+
143
+ def style_to_ansi(style)
144
+ style_map = {
145
+ 'bold' => "\e[1m", 'dim' => "\e[2m", 'italic' => "\e[3m",
146
+ 'underline' => "\e[4m", 'blink' => "\e[5m", 'reverse' => "\e[7m",
147
+ 'strikethrough' => "\e[9m"
148
+ }
149
+ style_map[style] || ""
150
+ end
151
+
42
152
  def add_style(code, error_message)
43
153
  if code
44
154
  @styles << code