ruvim 0.1.0 → 0.3.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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/AGENTS.md +84 -0
  4. data/CLAUDE.md +1 -0
  5. data/docs/binding.md +29 -0
  6. data/docs/command.md +101 -0
  7. data/docs/config.md +203 -84
  8. data/docs/done.md +21 -0
  9. data/docs/lib_cleanup_report.md +79 -0
  10. data/docs/plugin.md +13 -15
  11. data/docs/spec.md +195 -33
  12. data/docs/todo.md +183 -10
  13. data/docs/tutorial.md +1 -1
  14. data/docs/vim_diff.md +94 -171
  15. data/lib/ruvim/app.rb +1543 -172
  16. data/lib/ruvim/buffer.rb +35 -1
  17. data/lib/ruvim/cli.rb +12 -3
  18. data/lib/ruvim/clipboard.rb +2 -0
  19. data/lib/ruvim/command_invocation.rb +3 -1
  20. data/lib/ruvim/command_line.rb +2 -0
  21. data/lib/ruvim/command_registry.rb +2 -0
  22. data/lib/ruvim/config_dsl.rb +2 -0
  23. data/lib/ruvim/config_loader.rb +21 -5
  24. data/lib/ruvim/context.rb +2 -7
  25. data/lib/ruvim/dispatcher.rb +153 -13
  26. data/lib/ruvim/display_width.rb +28 -2
  27. data/lib/ruvim/editor.rb +622 -69
  28. data/lib/ruvim/ex_command_registry.rb +2 -0
  29. data/lib/ruvim/global_commands.rb +1386 -114
  30. data/lib/ruvim/highlighter.rb +16 -21
  31. data/lib/ruvim/input.rb +52 -29
  32. data/lib/ruvim/keymap_manager.rb +83 -0
  33. data/lib/ruvim/keyword_chars.rb +48 -0
  34. data/lib/ruvim/lang/base.rb +25 -0
  35. data/lib/ruvim/lang/csv.rb +18 -0
  36. data/lib/ruvim/lang/json.rb +18 -0
  37. data/lib/ruvim/lang/markdown.rb +170 -0
  38. data/lib/ruvim/lang/ruby.rb +236 -0
  39. data/lib/ruvim/lang/scheme.rb +44 -0
  40. data/lib/ruvim/lang/tsv.rb +19 -0
  41. data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
  42. data/lib/ruvim/rich_view/table_renderer.rb +176 -0
  43. data/lib/ruvim/rich_view.rb +93 -0
  44. data/lib/ruvim/screen.rb +851 -119
  45. data/lib/ruvim/terminal.rb +18 -1
  46. data/lib/ruvim/text_metrics.rb +28 -0
  47. data/lib/ruvim/version.rb +2 -2
  48. data/lib/ruvim/window.rb +37 -10
  49. data/lib/ruvim.rb +15 -0
  50. data/test/app_completion_test.rb +174 -0
  51. data/test/app_dot_repeat_test.rb +13 -0
  52. data/test/app_motion_test.rb +110 -2
  53. data/test/app_scenario_test.rb +998 -0
  54. data/test/app_startup_test.rb +197 -0
  55. data/test/arglist_test.rb +113 -0
  56. data/test/buffer_test.rb +49 -30
  57. data/test/config_loader_test.rb +37 -0
  58. data/test/dispatcher_test.rb +438 -0
  59. data/test/display_width_test.rb +18 -0
  60. data/test/editor_register_test.rb +23 -0
  61. data/test/fixtures/render_basic_snapshot.txt +7 -8
  62. data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
  63. data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
  64. data/test/highlighter_test.rb +121 -0
  65. data/test/indent_test.rb +201 -0
  66. data/test/input_screen_integration_test.rb +65 -14
  67. data/test/markdown_renderer_test.rb +279 -0
  68. data/test/on_save_hook_test.rb +150 -0
  69. data/test/rich_view_test.rb +478 -0
  70. data/test/screen_test.rb +470 -0
  71. data/test/window_test.rb +26 -0
  72. metadata +37 -2
@@ -1,5 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuVim
2
4
  module Highlighter
5
+ KEYWORD_COLOR = "\e[36m"
6
+ STRING_COLOR = "\e[32m"
7
+ NUMBER_COLOR = "\e[33m"
8
+ COMMENT_COLOR = "\e[90m"
9
+ VARIABLE_COLOR = "\e[93m"
10
+ CONSTANT_COLOR = "\e[96m"
11
+
3
12
  module_function
4
13
 
5
14
  def color_columns(filetype, line)
@@ -9,32 +18,18 @@ module RuVim
9
18
 
10
19
  case ft
11
20
  when "ruby"
12
- ruby_color_columns(text)
21
+ Lang::Ruby.color_columns(text)
13
22
  when "json"
14
- json_color_columns(text)
23
+ Lang::Json.color_columns(text)
24
+ when "markdown"
25
+ Lang::Markdown.color_columns(text)
26
+ when "scheme"
27
+ Lang::Scheme.color_columns(text)
15
28
  else
16
29
  {}
17
30
  end
18
31
  end
19
32
 
20
- def ruby_color_columns(text)
21
- cols = {}
22
- apply_regex(cols, text, /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/, "\e[32m")
23
- apply_regex(cols, text, /\b(?:def|class|module|end|if|elsif|else|unless|case|when|do|while|until|begin|rescue|ensure|return|yield)\b/, "\e[36m")
24
- apply_regex(cols, text, /\b\d+(?:\.\d+)?\b/, "\e[33m")
25
- apply_regex(cols, text, /#.*\z/, "\e[90m", override: true)
26
- cols
27
- end
28
-
29
- def json_color_columns(text)
30
- cols = {}
31
- apply_regex(cols, text, /"(?:\\.|[^"\\])*"\s*(?=:)/, "\e[36m")
32
- apply_regex(cols, text, /"(?:\\.|[^"\\])*"/, "\e[32m")
33
- apply_regex(cols, text, /\b(?:true|false|null)\b/, "\e[35m")
34
- apply_regex(cols, text, /-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/, "\e[33m")
35
- cols
36
- end
37
-
38
33
  def apply_regex(cols, text, regex, color, override: false)
39
34
  text.to_enum(:scan, regex).each do
40
35
  m = Regexp.last_match
@@ -47,6 +42,6 @@ module RuVim
47
42
  end
48
43
  end
49
44
 
50
- module_function :ruby_color_columns, :json_color_columns, :apply_regex
45
+ module_function :apply_regex
51
46
  end
52
47
  end
data/lib/ruvim/input.rb CHANGED
@@ -1,34 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuVim
2
4
  class Input
3
- def initialize(stdin: STDIN)
4
- @stdin = stdin
5
+ def initialize(input)
6
+ @input = input
5
7
  end
6
8
 
7
- def read_key(timeout: nil, wakeup_ios: [])
8
- ios = [@stdin, *wakeup_ios].compact
9
+ def read_key(timeout: nil, wakeup_ios: [], esc_timeout: nil)
10
+ ios = [@input, *wakeup_ios].compact
9
11
  readable = IO.select(ios, nil, nil, timeout)
10
12
  return nil unless readable
11
13
 
12
14
  ready = readable[0]
13
- wakeups = ready - [@stdin]
15
+ wakeups = ready - [@input]
14
16
  wakeups.each { |io| drain_io(io) }
15
- return nil unless ready.include?(@stdin)
16
-
17
- ch = @stdin.getch
18
- return :ctrl_c if ch == "\u0003"
19
- return :ctrl_i if ch == "\u0009"
20
- return :ctrl_n if ch == "\u000e"
21
- return :ctrl_o if ch == "\u000f"
22
- return :ctrl_p if ch == "\u0010"
23
- return :ctrl_r if ch == "\u0012"
24
- return :ctrl_v if ch == "\u0016"
25
- return :ctrl_w if ch == "\u0017"
26
- return :enter if ch == "\r" || ch == "\n"
27
- return :backspace if ch == "\u007f" || ch == "\b"
17
+ return nil unless ready.include?(@input)
28
18
 
29
- return read_escape_sequence if ch == "\e"
19
+ ch = @input.getch
20
+ case ch
21
+ when "\u0002" then :ctrl_b
22
+ when "\u0003" then :ctrl_c
23
+ when "\u0004" then :ctrl_d
24
+ when "\u0005" then :ctrl_e
25
+ when "\u0006" then :ctrl_f
26
+ when "\u0009" then :ctrl_i
27
+ when "\u000c" then :ctrl_l
28
+ when "\u000e" then :ctrl_n
29
+ when "\u000f" then :ctrl_o
30
+ when "\u0010" then :ctrl_p
31
+ when "\u0012" then :ctrl_r
32
+ when "\u0015" then :ctrl_u
33
+ when "\u0016" then :ctrl_v
34
+ when "\u0017" then :ctrl_w
35
+ when "\u0019" then :ctrl_y
36
+ when "\u001a" then :ctrl_z
37
+ when "\r", "\n" then :enter
38
+ when "\u007f", "\b" then :backspace
39
+ when "\e" then read_escape_sequence(timeout: esc_timeout)
40
+ else ch
41
+ end
42
+ end
30
43
 
31
- ch
44
+ def has_pending_input?
45
+ IO.select([@input], nil, nil, 0) != nil
32
46
  end
33
47
 
34
48
  private
@@ -41,23 +55,32 @@ module RuVim
41
55
  nil
42
56
  end
43
57
 
44
- def read_escape_sequence
58
+ def read_escape_sequence(timeout: nil)
45
59
  extra = +""
60
+ recognized = {
61
+ "[A" => :up,
62
+ "[B" => :down,
63
+ "[C" => :right,
64
+ "[D" => :left,
65
+ "[1;2A" => :shift_up,
66
+ "[1;2B" => :shift_down,
67
+ "[1;2C" => :shift_right,
68
+ "[1;2D" => :shift_left,
69
+ "[5~" => :pageup,
70
+ "[6~" => :pagedown
71
+ }
72
+ wait = timeout.nil? ? 0.005 : [timeout.to_f, 0.0].max
46
73
  begin
47
- while IO.select([@stdin], nil, nil, 0.005)
48
- extra << @stdin.read_nonblock(1)
74
+ while IO.select([@input], nil, nil, wait)
75
+ extra << @input.read_nonblock(1)
76
+ key = recognized[extra]
77
+ return key if key
49
78
  end
50
79
  rescue IO::WaitReadable, EOFError
51
80
  end
52
81
 
53
82
  case extra
54
83
  when "" then :escape
55
- when "[A" then :up
56
- when "[B" then :down
57
- when "[C" then :right
58
- when "[D" then :left
59
- when "[5~" then :pageup
60
- when "[6~" then :pagedown
61
84
  else
62
85
  [:escape_sequence, extra]
63
86
  end
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuVim
2
4
  class KeymapManager
3
5
  Match = Struct.new(:status, :invocation, keyword_init: true)
6
+ BindingEntry = Struct.new(:layer, :mode, :tokens, :id, :argv, :kwargs, :bang, :scope, keyword_init: true)
4
7
 
5
8
  def initialize
6
9
  @mode_maps = Hash.new { |h, k| h[k] = {} }
@@ -44,6 +47,25 @@ module RuVim
44
47
  resolve_layers(layers, pending_tokens)
45
48
  end
46
49
 
50
+ def binding_entries_for_context(editor, mode: nil)
51
+ buffer = editor.current_buffer
52
+ filetype = detect_filetype(buffer)
53
+ modes = normalized_mode_filter(mode)
54
+ entries = []
55
+
56
+ entries.concat(snapshot_plain_layer(@buffer_maps[buffer.id], layer: :buffer))
57
+
58
+ if filetype && @filetype_maps.key?(filetype)
59
+ ft_modes = @filetype_maps[filetype]
60
+ entries.concat(snapshot_mode_layers(ft_modes, layer: :filetype, modes:))
61
+ end
62
+
63
+ entries.concat(snapshot_mode_layers(@mode_maps, layer: :app, modes:, scope: :mode))
64
+ entries.concat(snapshot_plain_layer(@global_map, layer: :app, scope: :global))
65
+
66
+ entries
67
+ end
68
+
47
69
  private
48
70
 
49
71
  def build_invocation(id, argv:, kwargs:, bang:, tokens:)
@@ -73,6 +95,67 @@ module RuVim
73
95
  Match.new(status: has_prefix ? :pending : :none)
74
96
  end
75
97
 
98
+ def snapshot_plain_layer(layer_map, layer:, scope: nil)
99
+ return [] unless layer_map && !layer_map.empty?
100
+
101
+ layer_map.map do |tokens, inv|
102
+ snapshot_entry(tokens, inv, layer:, scope:)
103
+ end.sort_by { |e| [token_sort_key(e.tokens), e.id.to_s] }
104
+ end
105
+
106
+ def snapshot_mode_layers(mode_maps, layer:, modes: nil, scope: nil)
107
+ return [] unless mode_maps
108
+
109
+ selected = mode_maps.keys.map(&:to_sym)
110
+ selected &= modes if modes
111
+ selected.sort_by! { |m| mode_sort_key(m) }
112
+
113
+ selected.flat_map do |m|
114
+ next [] if mode_maps[m].nil? || mode_maps[m].empty?
115
+
116
+ mode_maps[m].map do |tokens, inv|
117
+ snapshot_entry(tokens, inv, layer:, mode: m, scope:)
118
+ end.sort_by { |e| [token_sort_key(e.tokens), e.id.to_s] }
119
+ end
120
+ end
121
+
122
+ def snapshot_entry(tokens, inv, layer:, mode: nil, scope: nil)
123
+ BindingEntry.new(
124
+ layer: layer,
125
+ mode: mode,
126
+ tokens: Array(tokens).map(&:dup),
127
+ id: inv.id.to_s,
128
+ argv: Array(inv.argv).map { |v| v.is_a?(String) ? v.dup : v },
129
+ kwargs: (inv.kwargs || {}).dup,
130
+ bang: !!inv.bang,
131
+ scope: scope
132
+ )
133
+ end
134
+
135
+ def normalized_mode_filter(mode)
136
+ return nil if mode.nil?
137
+
138
+ ary = Array(mode).compact.map { |m| m.to_sym }
139
+ ary.empty? ? nil : ary
140
+ end
141
+
142
+ def token_sort_key(tokens)
143
+ Array(tokens).join("\0")
144
+ end
145
+
146
+ def mode_sort_key(mode)
147
+ order = {
148
+ normal: 0,
149
+ insert: 1,
150
+ visual_char: 2,
151
+ visual_line: 3,
152
+ visual_block: 4,
153
+ operator_pending: 5,
154
+ command_line: 6
155
+ }
156
+ [order.fetch(mode.to_sym, 99), mode.to_s]
157
+ end
158
+
76
159
  def detect_filetype(buffer)
77
160
  ft = buffer.options["filetype"] if buffer.respond_to?(:options)
78
161
  return ft if ft && !ft.empty?
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module KeywordChars
5
+ module_function
6
+
7
+ DEFAULT_CHAR_CLASS = "[:alnum:]_".freeze
8
+ DEFAULT_REGEX = /[[:alnum:]_]/.freeze
9
+
10
+ def char_class(raw)
11
+ spec = raw.to_s
12
+ return DEFAULT_CHAR_CLASS if spec.empty?
13
+
14
+ @char_class_cache ||= {}
15
+ return @char_class_cache[spec] if @char_class_cache.key?(spec)
16
+
17
+ extra = []
18
+ spec.split(",").each do |tok|
19
+ t = tok.strip
20
+ next if t.empty? || t == "@"
21
+
22
+ if t.length == 1
23
+ extra << Regexp.escape(t)
24
+ elsif t.match?(/\A\d+-\d+\z/)
25
+ a, b = t.split("-", 2).map(&:to_i)
26
+ lo, hi = [a, b].minmax
27
+ next if lo < 0 || hi > 255
28
+ extra << "#{Regexp.escape(lo.chr)}-#{Regexp.escape(hi.chr)}"
29
+ end
30
+ end
31
+
32
+ @char_class_cache[spec] = "#{DEFAULT_CHAR_CLASS}#{extra.join}".freeze
33
+ end
34
+
35
+ def regex(raw)
36
+ spec = raw.to_s
37
+ return DEFAULT_REGEX if spec.empty?
38
+
39
+ @regex_cache ||= {}
40
+ return @regex_cache[spec] if @regex_cache.key?(spec)
41
+
42
+ klass = char_class(spec)
43
+ @regex_cache[spec] = /[#{klass}]/
44
+ rescue RegexpError
45
+ DEFAULT_REGEX
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Base
6
+ module_function
7
+
8
+ def indent_trigger?(_line)
9
+ false
10
+ end
11
+
12
+ def dedent_trigger(_char)
13
+ nil
14
+ end
15
+
16
+ def calculate_indent(_lines, _target_row, _shiftwidth)
17
+ nil
18
+ end
19
+
20
+ def on_save(_ctx, _path)
21
+ # no-op
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Csv
6
+ module_function
7
+
8
+ # Detect CSV from buffer content: commas > 0 (and not TSV)
9
+ def detect?(buffer)
10
+ sample = (0...[buffer.line_count, 20].min).map { |i| buffer.line_at(i) }
11
+ commas = sample.sum { |l| l.count(",") }
12
+ commas > 0
13
+ end
14
+ end
15
+ end
16
+
17
+ RichView.register("csv", RichView::TableRenderer, detector: Lang::Csv.method(:detect?))
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Json
6
+ module_function
7
+
8
+ def color_columns(text)
9
+ cols = {}
10
+ Highlighter.apply_regex(cols, text, /"(?:\\.|[^"\\])*"\s*(?=:)/, "\e[36m")
11
+ Highlighter.apply_regex(cols, text, /"(?:\\.|[^"\\])*"/, "\e[32m")
12
+ Highlighter.apply_regex(cols, text, /\b(?:true|false|null)\b/, "\e[35m")
13
+ Highlighter.apply_regex(cols, text, /-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/, "\e[33m")
14
+ cols
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Markdown
6
+ # --- Regex patterns ---
7
+
8
+ HEADING_RE = /\A(\s*)(\#{1,6})\s/
9
+ FENCE_RE = /\A(`{3,}|~{3,})/
10
+ HR_RE = /\A(\-{3,}|\*{3,}|_{3,})\s*\z/
11
+ BLOCK_QUOTE_RE = /\A\s*> /
12
+ TABLE_LINE_RE = /\A\s*\|.*\|\s*\z/
13
+ TABLE_SEPARATOR_RE = /\A\|[\s\-:|]+\|\z/
14
+
15
+ BOLD_RE = /\*\*([^*]+)\*\*/
16
+ ITALIC_RE = /(?<!\*)\*([^*]+)\*(?!\*)/
17
+ INLINE_CODE_RE = /`([^`]+)`/
18
+ LINK_RE = /\[([^\]]+)\]\(([^)]+)\)/
19
+ CHECKBOX_CHECKED_RE = /^(\s*-\s*)\[x\]/
20
+ CHECKBOX_UNCHECKED_RE = /^(\s*-\s*)\[ \]/
21
+
22
+ # --- Heading styles (for color_columns) ---
23
+
24
+ HEADING_COLORS = {
25
+ 1 => "\e[1;33m", # bold yellow
26
+ 2 => "\e[1;36m", # bold cyan
27
+ 3 => "\e[1;32m", # bold green
28
+ 4 => "\e[1;35m", # bold magenta
29
+ 5 => "\e[1;34m", # bold blue
30
+ 6 => "\e[1;90m" # bold dim
31
+ }.freeze
32
+
33
+ # --- FenceState: tracks code fence open/close across lines ---
34
+
35
+ class FenceState
36
+ attr_reader :in_code_block, :fence_marker
37
+
38
+ def initialize
39
+ @in_code_block = false
40
+ @fence_marker = nil
41
+ end
42
+
43
+ def scan_line(line)
44
+ stripped = line.to_s.strip
45
+ if @in_code_block
46
+ if fence_close?(stripped)
47
+ @in_code_block = false
48
+ @fence_marker = nil
49
+ end
50
+ else
51
+ marker = fence_open(stripped)
52
+ if marker
53
+ @in_code_block = true
54
+ @fence_marker = marker
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def fence_open(stripped)
62
+ if (m = stripped.match(/\A(`{3,})(.*)\z/))
63
+ m[1]
64
+ elsif (m = stripped.match(/\A(~{3,})(.*)\z/))
65
+ m[1]
66
+ end
67
+ end
68
+
69
+ def fence_close?(stripped)
70
+ return false unless @fence_marker
71
+ if @fence_marker.start_with?("`")
72
+ stripped.match?(/\A`{#{@fence_marker.length},}\s*\z/)
73
+ else
74
+ stripped.match?(/\A~{#{@fence_marker.length},}\s*\z/)
75
+ end
76
+ end
77
+ end
78
+
79
+ # --- Detection helpers ---
80
+
81
+ module_function
82
+
83
+ def heading?(line)
84
+ line.to_s.match?(HEADING_RE)
85
+ end
86
+
87
+ def heading_level(line)
88
+ m = line.to_s.match(HEADING_RE)
89
+ m ? m[2].length : 0
90
+ end
91
+
92
+ def fence_line?(stripped)
93
+ stripped.to_s.match?(FENCE_RE)
94
+ end
95
+
96
+ def horizontal_rule?(stripped)
97
+ stripped.to_s.match?(HR_RE)
98
+ end
99
+
100
+ def block_quote?(line)
101
+ line.to_s.match?(BLOCK_QUOTE_RE)
102
+ end
103
+
104
+ def table_line?(line)
105
+ stripped = line.to_s.strip
106
+ stripped.start_with?("|") && stripped.end_with?("|") && stripped.length > 1
107
+ end
108
+
109
+ def table_separator?(stripped)
110
+ stripped.to_s.match?(TABLE_SEPARATOR_RE)
111
+ end
112
+
113
+ def parse_table_cells(line)
114
+ stripped = line.to_s.strip
115
+ inner = stripped[1...-1] || ""
116
+ inner.split("|", -1).map(&:strip)
117
+ end
118
+
119
+ # --- Syntax highlight: color_columns ---
120
+
121
+ def color_columns(text)
122
+ cols = {}
123
+ return cols if text.nil? || text.empty?
124
+
125
+ stripped = text.strip
126
+
127
+ # Fence line: entire line dim
128
+ if fence_line?(stripped)
129
+ fill_line(cols, text, "\e[90m")
130
+ return cols
131
+ end
132
+
133
+ # HR: entire line dim
134
+ if horizontal_rule?(stripped)
135
+ fill_line(cols, text, "\e[90m")
136
+ return cols
137
+ end
138
+
139
+ # Heading: entire line colored by level
140
+ if (m = text.match(HEADING_RE))
141
+ level = m[2].length
142
+ color = HEADING_COLORS[level] || HEADING_COLORS[6]
143
+ fill_line(cols, text, color)
144
+ return cols
145
+ end
146
+
147
+ # Block quote marker
148
+ if (m = text.match(/\A(\s*>)/))
149
+ Highlighter.apply_regex(cols, text, /\A\s*>/, "\e[36m")
150
+ end
151
+
152
+ # Inline elements
153
+ Highlighter.apply_regex(cols, text, CHECKBOX_CHECKED_RE, "\e[32m")
154
+ Highlighter.apply_regex(cols, text, CHECKBOX_UNCHECKED_RE, "\e[90m")
155
+ Highlighter.apply_regex(cols, text, BOLD_RE, "\e[1m")
156
+ Highlighter.apply_regex(cols, text, ITALIC_RE, "\e[3m")
157
+ Highlighter.apply_regex(cols, text, INLINE_CODE_RE, "\e[33m")
158
+ Highlighter.apply_regex(cols, text, LINK_RE, "\e[4m")
159
+
160
+ cols
161
+ end
162
+
163
+ private
164
+
165
+ def self.fill_line(cols, text, color)
166
+ text.length.times { |i| cols[i] = color }
167
+ end
168
+ end
169
+ end
170
+ end