ruby_rich 0.3.0 → 0.4.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/lib/ruby_rich/columns.rb +244 -0
- data/lib/ruby_rich/console.rb +20 -5
- data/lib/ruby_rich/markdown.rb +343 -0
- data/lib/ruby_rich/panel.rb +7 -2
- data/lib/ruby_rich/progress_bar.rb +229 -11
- data/lib/ruby_rich/status.rb +246 -0
- data/lib/ruby_rich/syntax.rb +171 -0
- data/lib/ruby_rich/table.rb +155 -9
- data/lib/ruby_rich/text.rb +111 -1
- data/lib/ruby_rich/tree.rb +200 -0
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich.rb +33 -3
- metadata +92 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c81d8c96218da776dce71a58d8c6ab5065c106a0a6818ea7d73f16fb877e4e5c
|
|
4
|
+
data.tar.gz: f01f062343bcc4187861d2a984db97d624d2d810fd90f5f5720b446ad16fecaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c43c87d6fe89f77962a9a4ddfd5148b5ff97c1c81e5435ff39eac99ecdfa36efb270818db5aef4869b6d4feb947c7b859721d8f08aafea82a2fb582b4d0ca265
|
|
7
|
+
data.tar.gz: 6aa8944f0096c5af0ed9172a2e6fc85a97f97662b3ce7f54a3d55cf035686fba3a4ab98ed70f52411b3a2dfa320d2bc943e47ed3cbed7075965c5e5147ae0794
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
module RubyRich
|
|
2
|
+
class Columns
|
|
3
|
+
class Column
|
|
4
|
+
attr_accessor :content, :width, :align, :padding, :title
|
|
5
|
+
|
|
6
|
+
def initialize(width: nil, align: :left, padding: 1, title: nil)
|
|
7
|
+
@content = []
|
|
8
|
+
@width = width
|
|
9
|
+
@align = align
|
|
10
|
+
@padding = padding
|
|
11
|
+
@title = title
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add(text)
|
|
15
|
+
@content << text.to_s
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def <<(text)
|
|
20
|
+
add(text)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def clear
|
|
24
|
+
@content.clear
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def lines
|
|
29
|
+
@content
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def height
|
|
33
|
+
@content.length
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :columns, :total_width, :gutter_width
|
|
38
|
+
|
|
39
|
+
def initialize(total_width: 80, gutter_width: 2)
|
|
40
|
+
@columns = []
|
|
41
|
+
@total_width = total_width
|
|
42
|
+
@gutter_width = gutter_width
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 添加列
|
|
46
|
+
def add_column(width: nil, align: :left, padding: 1, title: nil)
|
|
47
|
+
column = Column.new(width: width, align: align, padding: padding, title: title)
|
|
48
|
+
@columns << column
|
|
49
|
+
|
|
50
|
+
# 如果没有指定宽度,自动计算平均宽度
|
|
51
|
+
calculate_column_widths if width.nil?
|
|
52
|
+
|
|
53
|
+
column
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 删除列
|
|
57
|
+
def remove_column(index)
|
|
58
|
+
@columns.delete_at(index) if index >= 0 && index < @columns.length
|
|
59
|
+
calculate_column_widths
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 清空所有列的内容
|
|
63
|
+
def clear_all
|
|
64
|
+
@columns.each(&:clear)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 渲染列布局
|
|
69
|
+
def render(show_headers: true, show_borders: false, equal_height: true)
|
|
70
|
+
return "" if @columns.empty?
|
|
71
|
+
|
|
72
|
+
calculate_column_widths
|
|
73
|
+
lines = []
|
|
74
|
+
|
|
75
|
+
# 渲染标题行
|
|
76
|
+
if show_headers && @columns.any? { |col| col.title }
|
|
77
|
+
header_line = render_header_line(show_borders)
|
|
78
|
+
lines << header_line unless header_line.empty?
|
|
79
|
+
|
|
80
|
+
if show_borders
|
|
81
|
+
separator_line = render_separator_line
|
|
82
|
+
lines << separator_line
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# 准备内容行
|
|
87
|
+
max_height = equal_height ? @columns.map(&:height).max : 0
|
|
88
|
+
|
|
89
|
+
# 填充较短的列以达到相同高度
|
|
90
|
+
if equal_height && max_height > 0
|
|
91
|
+
@columns.each do |column|
|
|
92
|
+
while column.height < max_height
|
|
93
|
+
column.add("")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 渲染内容行
|
|
99
|
+
content_height = @columns.map(&:height).max || 0
|
|
100
|
+
content_height.times do |row_index|
|
|
101
|
+
line = render_content_line(row_index, show_borders)
|
|
102
|
+
lines << line
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# 渲染底部边框
|
|
106
|
+
if show_borders
|
|
107
|
+
bottom_line = render_separator_line
|
|
108
|
+
lines << bottom_line
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
lines.join("\n")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# 按比例设置列宽
|
|
115
|
+
def set_ratios(*ratios)
|
|
116
|
+
return if ratios.length != @columns.length
|
|
117
|
+
|
|
118
|
+
total_ratio = ratios.sum.to_f
|
|
119
|
+
available_width = @total_width - (@gutter_width * (@columns.length - 1))
|
|
120
|
+
|
|
121
|
+
@columns.each_with_index do |column, index|
|
|
122
|
+
column.width = (available_width * ratios[index] / total_ratio).to_i
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# 设置等宽列
|
|
129
|
+
def equal_widths
|
|
130
|
+
calculate_column_widths
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def calculate_column_widths
|
|
137
|
+
return if @columns.empty?
|
|
138
|
+
|
|
139
|
+
# 计算可用宽度(总宽度减去间隔)
|
|
140
|
+
available_width = @total_width - (@gutter_width * (@columns.length - 1))
|
|
141
|
+
|
|
142
|
+
# 为每列分配相等的宽度
|
|
143
|
+
base_width = available_width / @columns.length
|
|
144
|
+
remainder = available_width % @columns.length
|
|
145
|
+
|
|
146
|
+
@columns.each_with_index do |column, index|
|
|
147
|
+
column.width = base_width + (index < remainder ? 1 : 0)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def render_header_line(show_borders)
|
|
152
|
+
line_parts = []
|
|
153
|
+
|
|
154
|
+
@columns.each_with_index do |column, index|
|
|
155
|
+
header_text = column.title || ""
|
|
156
|
+
|
|
157
|
+
# 根据列的对齐方式格式化标题
|
|
158
|
+
formatted_header = format_text(header_text, column.width, column.align)
|
|
159
|
+
|
|
160
|
+
if show_borders
|
|
161
|
+
formatted_header = "│ #{formatted_header} │"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
line_parts << formatted_header
|
|
165
|
+
|
|
166
|
+
# 添加间隔(除了最后一列)
|
|
167
|
+
if index < @columns.length - 1 && !show_borders
|
|
168
|
+
line_parts << " " * @gutter_width
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
line_parts.join("")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_content_line(row_index, show_borders)
|
|
176
|
+
line_parts = []
|
|
177
|
+
|
|
178
|
+
@columns.each_with_index do |column, index|
|
|
179
|
+
content_text = column.lines[row_index] || ""
|
|
180
|
+
|
|
181
|
+
# 根据列的对齐方式格式化内容
|
|
182
|
+
formatted_content = format_text(content_text, column.width, column.align)
|
|
183
|
+
|
|
184
|
+
if show_borders
|
|
185
|
+
formatted_content = "│ #{formatted_content} │"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
line_parts << formatted_content
|
|
189
|
+
|
|
190
|
+
# 添加间隔(除了最后一列)
|
|
191
|
+
if index < @columns.length - 1 && !show_borders
|
|
192
|
+
line_parts << " " * @gutter_width
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
line_parts.join("")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def render_separator_line
|
|
200
|
+
line_parts = []
|
|
201
|
+
|
|
202
|
+
@columns.each_with_index do |column, index|
|
|
203
|
+
separator = "─" * (column.width + 2) # +2 for padding
|
|
204
|
+
line_parts << "├#{separator}┤"
|
|
205
|
+
|
|
206
|
+
if index < @columns.length - 1
|
|
207
|
+
line_parts << "┬"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
line_parts.join("")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def format_text(text, width, align)
|
|
215
|
+
# 移除 ANSI 转义序列计算实际显示宽度
|
|
216
|
+
display_text = text.gsub(/\e\[[0-9;]*m/, '')
|
|
217
|
+
|
|
218
|
+
if display_text.length > width
|
|
219
|
+
# 截断过长的文本
|
|
220
|
+
truncated = display_text[0, width - 3] + "..."
|
|
221
|
+
# 保持原有的 ANSI 样式
|
|
222
|
+
if text.include?("\e[")
|
|
223
|
+
style_start = text.match(/\e\[[0-9;]*m/)&.to_s || ""
|
|
224
|
+
truncated = style_start + truncated + "\e[0m"
|
|
225
|
+
end
|
|
226
|
+
truncated
|
|
227
|
+
else
|
|
228
|
+
# 根据对齐方式填充空格
|
|
229
|
+
padding_needed = width - display_text.length
|
|
230
|
+
|
|
231
|
+
case align
|
|
232
|
+
when :center
|
|
233
|
+
left_padding = padding_needed / 2
|
|
234
|
+
right_padding = padding_needed - left_padding
|
|
235
|
+
" " * left_padding + text + " " * right_padding
|
|
236
|
+
when :right
|
|
237
|
+
" " * padding_needed + text
|
|
238
|
+
else # :left
|
|
239
|
+
text + " " * padding_needed
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
data/lib/ruby_rich/console.rb
CHANGED
|
@@ -37,10 +37,23 @@ module RubyRich
|
|
|
37
37
|
@styles[name] = attributes
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def print(*objects, sep: ' ', end_char: "\n")
|
|
41
|
-
line_text = objects.map
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
def print(*objects, sep: ' ', end_char: "\n", immediate: false)
|
|
41
|
+
line_text = objects.map do |obj|
|
|
42
|
+
if obj.is_a?(String) && obj.include?('[')
|
|
43
|
+
# 处理 Rich markup 标记
|
|
44
|
+
RichText.markup(obj)
|
|
45
|
+
else
|
|
46
|
+
obj.to_s
|
|
47
|
+
end
|
|
48
|
+
end.join(sep)
|
|
49
|
+
|
|
50
|
+
if immediate
|
|
51
|
+
add_line(line_text)
|
|
52
|
+
render
|
|
53
|
+
else
|
|
54
|
+
# 简单输出,不使用 Console 的缓冲和渲染系统
|
|
55
|
+
Kernel.puts line_text
|
|
56
|
+
end
|
|
44
57
|
end
|
|
45
58
|
|
|
46
59
|
def log(message, *objects, sep: ' ', end_char: "\n")
|
|
@@ -78,7 +91,9 @@ module RubyRich
|
|
|
78
91
|
char = io.getch
|
|
79
92
|
# 优先处理回车键(ASCII 13 = \r,ASCII 10 = \n)
|
|
80
93
|
if char == "\r" || char == "\n"
|
|
81
|
-
|
|
94
|
+
# 检查是否有后续输入(粘贴内容会有多个字符)
|
|
95
|
+
has_more = IO.select([io], nil, nil, 0)
|
|
96
|
+
return has_more ? {:name => :string, :value => char} : {:name=>:enter}
|
|
82
97
|
end
|
|
83
98
|
# 单独处理 Tab 键(ASCII 9)
|
|
84
99
|
if char == "\t"
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
require 'redcarpet'
|
|
2
|
+
|
|
3
|
+
module RubyRich
|
|
4
|
+
class Markdown
|
|
5
|
+
# 简化的 Markdown 渲染器,将 Markdown 转换为带 ANSI 颜色的终端输出
|
|
6
|
+
class TerminalRenderer < Redcarpet::Render::Base
|
|
7
|
+
def initialize(options = {})
|
|
8
|
+
@options = {
|
|
9
|
+
width: 80,
|
|
10
|
+
indent: ' '
|
|
11
|
+
}.merge(options)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
# 表格状态
|
|
15
|
+
reset_table_state
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reset_table_state
|
|
19
|
+
@table_state = {
|
|
20
|
+
in_table: false,
|
|
21
|
+
headers: [],
|
|
22
|
+
current_row: [],
|
|
23
|
+
rows: []
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 段落
|
|
28
|
+
def paragraph(text)
|
|
29
|
+
wrap_text(text) + "\n\n"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 标题
|
|
33
|
+
def header(text, level)
|
|
34
|
+
case level
|
|
35
|
+
when 1
|
|
36
|
+
"\e[1m\e[96m#{text}\e[0m\n" + "\e[96m#{'=' * text.length}\e[0m\n\n"
|
|
37
|
+
when 2
|
|
38
|
+
"\e[1m\e[94m#{text}\e[0m\n" + "\e[94m#{'-' * text.length}\e[0m\n\n"
|
|
39
|
+
when 3
|
|
40
|
+
"\e[1m\e[93m### #{text}\e[0m\n\n"
|
|
41
|
+
else
|
|
42
|
+
"\e[1m\e[90m#{'#' * level} #{text}\e[0m\n\n"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 代码块
|
|
47
|
+
def block_code(code, language)
|
|
48
|
+
# 简单的代码格式,不使用语法高亮避免循环依赖
|
|
49
|
+
"\e[100m\e[37m" + indent_lines(code.strip) + "\e[0m\n\n"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 行内代码
|
|
53
|
+
def codespan(code)
|
|
54
|
+
"\e[47m\e[30m #{code} \e[0m"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 引用
|
|
58
|
+
def block_quote(quote)
|
|
59
|
+
lines = quote.strip.split("\n")
|
|
60
|
+
quoted_lines = lines.map { |line| "\e[90m│ \e[37m#{line.strip}" }
|
|
61
|
+
quoted_lines.join("\n") + "\e[0m\n\n"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 列表项
|
|
65
|
+
def list_item(text, list_type)
|
|
66
|
+
marker = list_type == :ordered ? '1.' : '•'
|
|
67
|
+
"\e[96m#{marker}\e[0m #{text.strip}\n"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 无序列表
|
|
71
|
+
def list(contents, list_type)
|
|
72
|
+
contents + "\n"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 强调
|
|
76
|
+
def emphasis(text)
|
|
77
|
+
"\e[3m#{text}\e[23m"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 加粗
|
|
81
|
+
def double_emphasis(text)
|
|
82
|
+
"\e[1m#{text}\e[22m"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 删除线
|
|
86
|
+
def strikethrough(text)
|
|
87
|
+
"\e[9m#{text}\e[29m"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 链接
|
|
91
|
+
def link(link, title, content)
|
|
92
|
+
if title && !title.empty?
|
|
93
|
+
"\e[94m\e[4m#{content}\e[24m\e[0m \e[90m(#{link} - #{title})\e[0m"
|
|
94
|
+
else
|
|
95
|
+
"\e[94m\e[4m#{content}\e[24m\e[0m \e[90m(#{link})\e[0m"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 图片
|
|
100
|
+
def image(link, title, alt_text)
|
|
101
|
+
if title && !title.empty?
|
|
102
|
+
"\e[95m[Image: #{alt_text}]\e[0m \e[90m(#{link} - #{title})\e[0m"
|
|
103
|
+
else
|
|
104
|
+
"\e[95m[Image: #{alt_text}]\e[0m \e[90m(#{link})\e[0m"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 水平线
|
|
109
|
+
def hrule
|
|
110
|
+
"\e[90m" + "─" * @options[:width] + "\e[0m\n\n"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 表格渲染 - 智能分割方法
|
|
114
|
+
def table(header, body)
|
|
115
|
+
return "" if header.nil? && body.nil?
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
# 尝试智能分割表格内容
|
|
119
|
+
headers = []
|
|
120
|
+
rows = []
|
|
121
|
+
|
|
122
|
+
if header && !header.strip.empty?
|
|
123
|
+
# 从header中提取列标题
|
|
124
|
+
header_content = header.strip
|
|
125
|
+
# 尝试按常见模式分割(大写字母开头的单词)
|
|
126
|
+
headers = split_table_content_intelligently(header_content)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if body && !body.strip.empty?
|
|
130
|
+
body_lines = body.strip.split("\n").reject(&:empty?)
|
|
131
|
+
body_lines.each do |line|
|
|
132
|
+
row_data = split_table_content_intelligently(line.strip)
|
|
133
|
+
rows << row_data unless row_data.all?(&:empty?)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# 如果成功解析出了数据,使用RubyRich表格
|
|
138
|
+
if !headers.empty? && !rows.empty?
|
|
139
|
+
table = RubyRich::Table.new(
|
|
140
|
+
headers: headers,
|
|
141
|
+
border_style: @options[:table_border_style] || :simple
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
rows.each do |row|
|
|
145
|
+
# 确保行长度与标题长度一致
|
|
146
|
+
padded_row = row + Array.new([0, headers.length - row.length].max, "")
|
|
147
|
+
table.add_row(padded_row[0...headers.length])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
return table.render + "\n\n"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
rescue => e
|
|
154
|
+
# 如果出错,继续使用fallback
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Fallback: 简单显示
|
|
158
|
+
result = "\n"
|
|
159
|
+
if header && !header.strip.empty?
|
|
160
|
+
result += "#{header.strip}\n"
|
|
161
|
+
result += "-" * [header.strip.length, 20].min + "\n"
|
|
162
|
+
end
|
|
163
|
+
if body && !body.strip.empty?
|
|
164
|
+
result += body.strip + "\n"
|
|
165
|
+
end
|
|
166
|
+
result + "\n"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def table_row(content)
|
|
170
|
+
content + "\n"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def table_cell(content, alignment)
|
|
174
|
+
content
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# 换行
|
|
178
|
+
def linebreak
|
|
179
|
+
"\n"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def wrap_text(text, width = nil)
|
|
185
|
+
width ||= @options[:width]
|
|
186
|
+
return text if text.length <= width
|
|
187
|
+
|
|
188
|
+
words = text.split(' ')
|
|
189
|
+
lines = []
|
|
190
|
+
current_line = ''
|
|
191
|
+
|
|
192
|
+
words.each do |word|
|
|
193
|
+
if (current_line + ' ' + word).length <= width
|
|
194
|
+
current_line += current_line.empty? ? word : ' ' + word
|
|
195
|
+
else
|
|
196
|
+
lines << current_line unless current_line.empty?
|
|
197
|
+
current_line = word
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
lines << current_line unless current_line.empty?
|
|
202
|
+
lines.join("\n")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def indent_lines(text)
|
|
206
|
+
text.split("\n").map { |line| @options[:indent] + line }.join("\n")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def parse_table_content(content)
|
|
210
|
+
lines = content.strip.split("\n").map(&:strip).reject(&:empty?)
|
|
211
|
+
return [] if lines.empty?
|
|
212
|
+
|
|
213
|
+
rows = []
|
|
214
|
+
lines.each do |line|
|
|
215
|
+
# 跳过分隔符行(如 |------|--------|)
|
|
216
|
+
next if line.match?(/^\|[\s\-\|:]+\|?$/)
|
|
217
|
+
|
|
218
|
+
# 解析表格行
|
|
219
|
+
if line.start_with?('|') && line.end_with?('|')
|
|
220
|
+
cells = line[1..-2].split('|').map(&:strip)
|
|
221
|
+
rows << cells unless cells.empty?
|
|
222
|
+
elsif line.include?('|')
|
|
223
|
+
cells = line.split('|').map(&:strip)
|
|
224
|
+
rows << cells unless cells.empty?
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
rows
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def render_table_with_ruby_rich(rows)
|
|
232
|
+
return "" if rows.empty?
|
|
233
|
+
|
|
234
|
+
# 使用第一行作为表头
|
|
235
|
+
headers = rows.first
|
|
236
|
+
data_rows = rows[1..-1] || []
|
|
237
|
+
|
|
238
|
+
# 创建RubyRich表格,使用简单边框样式来匹配markdown风格
|
|
239
|
+
table = RubyRich::Table.new(
|
|
240
|
+
headers: headers,
|
|
241
|
+
border_style: @options[:table_border_style] || :simple
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# 添加数据行
|
|
245
|
+
data_rows.each do |row|
|
|
246
|
+
# 确保行长度与标题长度一致
|
|
247
|
+
padded_row = row + Array.new([0, headers.length - row.length].max, "")
|
|
248
|
+
table.add_row(padded_row)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
table.render + "\n\n"
|
|
252
|
+
rescue => e
|
|
253
|
+
# 如果表格渲染失败,使用简单的文字格式
|
|
254
|
+
fallback_table_render(rows)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def fallback_table_render(rows)
|
|
258
|
+
return "" if rows.empty?
|
|
259
|
+
|
|
260
|
+
result = []
|
|
261
|
+
rows.each_with_index do |row, index|
|
|
262
|
+
result << "| " + row.join(" | ") + " |"
|
|
263
|
+
if index == 0 # 在标题下添加分隔线
|
|
264
|
+
result << "|" + ("-" * (row.join(" | ").length + 2)) + "|"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
result.join("\n") + "\n\n"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def split_table_content_intelligently(content)
|
|
272
|
+
return [] if content.nil? || content.strip.empty?
|
|
273
|
+
|
|
274
|
+
# 策略1:更智能的分割 - 先尝试找到数字字母边界
|
|
275
|
+
split_points = []
|
|
276
|
+
content.each_char.with_index do |char, index|
|
|
277
|
+
if index > 0 && index < content.length - 1
|
|
278
|
+
prev_char = content[index - 1]
|
|
279
|
+
# 在字母后跟数字,或数字后跟字母的地方分割
|
|
280
|
+
if (prev_char =~ /[A-Za-z]/ && char =~ /\d/) ||
|
|
281
|
+
(prev_char =~ /\d/ && char =~ /[A-Za-z]/)
|
|
282
|
+
split_points << index
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
if split_points.any?
|
|
288
|
+
parts = []
|
|
289
|
+
last_pos = 0
|
|
290
|
+
split_points.each do |pos|
|
|
291
|
+
parts << content[last_pos...pos] if pos > last_pos
|
|
292
|
+
last_pos = pos
|
|
293
|
+
end
|
|
294
|
+
parts << content[last_pos..-1] if last_pos < content.length
|
|
295
|
+
result = parts.reject(&:empty?)
|
|
296
|
+
return result if result.length >= 2
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# 策略2:如果没有数字字母边界,按大写字母分割(但要小心短词)
|
|
300
|
+
words = content.scan(/[A-Z][a-z]+|[A-Z]{2,}/)
|
|
301
|
+
if words.length >= 2 && words.all? { |w| w.length >= 2 }
|
|
302
|
+
return words
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# 策略3:按连续的字母和数字分组
|
|
306
|
+
parts = content.scan(/[A-Za-z]+|\d+/)
|
|
307
|
+
if parts.length >= 2
|
|
308
|
+
return parts
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# 策略4:按空格分割
|
|
312
|
+
space_parts = content.split(/\s+/).reject(&:empty?)
|
|
313
|
+
if space_parts.length >= 2
|
|
314
|
+
return space_parts
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# 最后的fallback:返回原始内容
|
|
318
|
+
[content]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def self.render(markdown_text, options = {})
|
|
323
|
+
renderer = TerminalRenderer.new(options)
|
|
324
|
+
markdown_processor = Redcarpet::Markdown.new(renderer, {
|
|
325
|
+
fenced_code_blocks: true,
|
|
326
|
+
tables: true,
|
|
327
|
+
autolink: true,
|
|
328
|
+
strikethrough: true,
|
|
329
|
+
space_after_headers: true
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
markdown_processor.render(markdown_text)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def initialize(options = {})
|
|
336
|
+
@options = options
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def render(markdown_text)
|
|
340
|
+
self.class.render(markdown_text, @options)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
data/lib/ruby_rich/panel.rb
CHANGED
|
@@ -134,10 +134,15 @@ module RubyRich
|
|
|
134
134
|
# Split text into tokens of ANSI codes and regular text
|
|
135
135
|
tokens = text.scan(/(\e\[[0-9;]*m)|(.)/)
|
|
136
136
|
.map { |m| m.compact.first }
|
|
137
|
-
|
|
137
|
+
start_color = nil
|
|
138
138
|
tokens.each do |token|
|
|
139
139
|
# Calculate width for regular text, ANSI codes have 0 width
|
|
140
140
|
if token.start_with?("\e[")
|
|
141
|
+
if token == "\e[0m"
|
|
142
|
+
start_color = nil
|
|
143
|
+
else
|
|
144
|
+
start_color = token
|
|
145
|
+
end
|
|
141
146
|
token_width = 0
|
|
142
147
|
else
|
|
143
148
|
token_width = token.chars.sum { |c| Unicode::DisplayWidth.of(c) }
|
|
@@ -148,7 +153,7 @@ module RubyRich
|
|
|
148
153
|
current_width += token_width
|
|
149
154
|
else
|
|
150
155
|
result << current_line
|
|
151
|
-
current_line = token
|
|
156
|
+
current_line = start_color.to_s+token
|
|
152
157
|
current_width = token_width
|
|
153
158
|
end
|
|
154
159
|
end
|