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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '009a5bcd619dc3fa975c6ab3b01d1494e002ccc760b6e8ac335026f63860671e'
4
- data.tar.gz: 205f8d0f1cdab77854a564c43761c30113f2989216ba2ed91b7ac66e77f2387f
3
+ metadata.gz: c81d8c96218da776dce71a58d8c6ab5065c106a0a6818ea7d73f16fb877e4e5c
4
+ data.tar.gz: f01f062343bcc4187861d2a984db97d624d2d810fd90f5f5720b446ad16fecaa
5
5
  SHA512:
6
- metadata.gz: bedd0da9ee879bcca97fc9faf3c597cf857a05b6b8cca59004e9fbaefdbbee13b885b89902d4909a2b5718d7877201780f44418be7e2f3044f65c9840e40fe58
7
- data.tar.gz: ac828f4bb8e7552b6a1b7310a0ba7be97d256710ce7f4aca3c523e0493b41e458fb722b33afafe01b90ab568338f14f6d56a3d546b29d191131986b07690acab
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
@@ -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(&:to_s).join(sep)
42
- add_line(line_text)
43
- render
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
- return {:name=>:enter}
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
@@ -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