ruvim 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +84 -0
- data/CLAUDE.md +1 -0
- data/docs/binding.md +23 -0
- data/docs/command.md +85 -0
- data/docs/config.md +2 -2
- data/docs/done.md +21 -0
- data/docs/spec.md +157 -12
- data/docs/todo.md +1 -5
- data/docs/vim_diff.md +94 -172
- data/lib/ruvim/app.rb +882 -69
- data/lib/ruvim/buffer.rb +35 -1
- data/lib/ruvim/cli.rb +12 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +455 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +890 -63
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +39 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/json.rb +18 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +93 -0
- data/lib/ruvim/screen.rb +503 -106
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +14 -0
- data/test/app_completion_test.rb +73 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +729 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/dispatcher_test.rb +322 -0
- data/test/editor_register_test.rb +23 -0
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +478 -0
- data/test/screen_test.rb +304 -0
- metadata +33 -2
data/lib/ruvim/highlighter.rb
CHANGED
|
@@ -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
|
-
|
|
21
|
+
Lang::Ruby.color_columns(text)
|
|
13
22
|
when "json"
|
|
14
|
-
|
|
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 :
|
|
45
|
+
module_function :apply_regex
|
|
51
46
|
end
|
|
52
47
|
end
|
data/lib/ruvim/input.rb
CHANGED
|
@@ -1,41 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class Input
|
|
3
|
-
def initialize(
|
|
4
|
-
@
|
|
5
|
+
def initialize(input)
|
|
6
|
+
@input = input
|
|
5
7
|
end
|
|
6
8
|
|
|
7
9
|
def read_key(timeout: nil, wakeup_ios: [], esc_timeout: nil)
|
|
8
|
-
ios = [@
|
|
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 - [@
|
|
15
|
+
wakeups = ready - [@input]
|
|
14
16
|
wakeups.each { |io| drain_io(io) }
|
|
15
|
-
return nil unless ready.include?(@
|
|
16
|
-
|
|
17
|
-
ch = @stdin.getch
|
|
18
|
-
return :ctrl_b if ch == "\u0002"
|
|
19
|
-
return :ctrl_c if ch == "\u0003"
|
|
20
|
-
return :ctrl_d if ch == "\u0004"
|
|
21
|
-
return :ctrl_e if ch == "\u0005"
|
|
22
|
-
return :ctrl_f if ch == "\u0006"
|
|
23
|
-
return :ctrl_i if ch == "\u0009"
|
|
24
|
-
return :ctrl_l if ch == "\u000c"
|
|
25
|
-
return :ctrl_n if ch == "\u000e"
|
|
26
|
-
return :ctrl_o if ch == "\u000f"
|
|
27
|
-
return :ctrl_p if ch == "\u0010"
|
|
28
|
-
return :ctrl_r if ch == "\u0012"
|
|
29
|
-
return :ctrl_u if ch == "\u0015"
|
|
30
|
-
return :ctrl_v if ch == "\u0016"
|
|
31
|
-
return :ctrl_w if ch == "\u0017"
|
|
32
|
-
return :ctrl_y if ch == "\u0019"
|
|
33
|
-
return :enter if ch == "\r" || ch == "\n"
|
|
34
|
-
return :backspace if ch == "\u007f" || ch == "\b"
|
|
17
|
+
return nil unless ready.include?(@input)
|
|
35
18
|
|
|
36
|
-
|
|
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
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
def has_pending_input?
|
|
45
|
+
IO.select([@input], nil, nil, 0) != nil
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
private
|
|
@@ -55,13 +62,17 @@ module RuVim
|
|
|
55
62
|
"[B" => :down,
|
|
56
63
|
"[C" => :right,
|
|
57
64
|
"[D" => :left,
|
|
65
|
+
"[1;2A" => :shift_up,
|
|
66
|
+
"[1;2B" => :shift_down,
|
|
67
|
+
"[1;2C" => :shift_right,
|
|
68
|
+
"[1;2D" => :shift_left,
|
|
58
69
|
"[5~" => :pageup,
|
|
59
70
|
"[6~" => :pagedown
|
|
60
71
|
}
|
|
61
72
|
wait = timeout.nil? ? 0.005 : [timeout.to_f, 0.0].max
|
|
62
73
|
begin
|
|
63
|
-
while IO.select([@
|
|
64
|
-
extra << @
|
|
74
|
+
while IO.select([@input], nil, nil, wait)
|
|
75
|
+
extra << @input.read_nonblock(1)
|
|
65
76
|
key = recognized[extra]
|
|
66
77
|
return key if key
|
|
67
78
|
end
|
data/lib/ruvim/keymap_manager.rb
CHANGED
|
@@ -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?
|
data/lib/ruvim/keyword_chars.rb
CHANGED
|
@@ -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
|