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.
@@ -2,36 +2,182 @@ require 'unicode/display_width'
2
2
 
3
3
  module RubyRich
4
4
  class Table
5
- attr_accessor :headers, :rows, :align, :row_height
5
+ attr_accessor :rows, :align, :row_height, :border_style
6
+ attr_reader :headers
7
+
8
+ # 边框样式定义
9
+ BORDER_STYLES = {
10
+ none: {
11
+ top: '', bottom: '', left: '', right: '',
12
+ horizontal: '-', vertical: '|',
13
+ top_left: '', top_right: '', bottom_left: '', bottom_right: '',
14
+ cross: '', top_cross: '', bottom_cross: '', left_cross: '', right_cross: ''
15
+ },
16
+ simple: {
17
+ top: '-', bottom: '-', left: '|', right: '|',
18
+ horizontal: '-', vertical: '|',
19
+ top_left: '+', top_right: '+', bottom_left: '+', bottom_right: '+',
20
+ cross: '+', top_cross: '+', bottom_cross: '+', left_cross: '+', right_cross: '+'
21
+ },
22
+ full: {
23
+ top: '─', bottom: '─', left: '│', right: '│',
24
+ horizontal: '─', vertical: '│',
25
+ top_left: '┌', top_right: '┐', bottom_left: '└', bottom_right: '┘',
26
+ cross: '┼', top_cross: '┬', bottom_cross: '┴', left_cross: '├', right_cross: '┤'
27
+ }
28
+ }.freeze
6
29
 
7
- def initialize(headers: [], align: :left, row_height: 1)
30
+ def initialize(headers: [], align: :left, row_height: 1, border_style: :none)
8
31
  @headers = headers.map { |h| format_cell(h) }
9
32
  @rows = []
10
33
  @align = align
11
34
  @row_height = row_height
35
+ @border_style = border_style
12
36
  end
13
37
 
14
38
  def add_row(row)
15
39
  @rows << row.map { |cell| format_cell(cell) }
16
40
  end
41
+
42
+ def headers=(new_headers)
43
+ @headers = new_headers.map { |h| format_cell(h) }
44
+ end
17
45
 
18
46
  def render
47
+ return render_empty_table if @headers.empty? && @rows.empty?
48
+
19
49
  column_widths = calculate_column_widths
20
50
  lines = []
21
-
51
+ border_chars = BORDER_STYLES[@border_style] || BORDER_STYLES[:none]
52
+
53
+ # Render top border
54
+ if @border_style != :none && !@headers.empty?
55
+ lines << render_horizontal_border(column_widths, :top)
56
+ end
57
+
22
58
  # Render headers
23
- lines << render_row(@headers, column_widths, bold: true)
24
- lines << "-" * (column_widths.sum { |w| w + 3 } + 1)
25
-
59
+ unless @headers.empty?
60
+ lines.concat(render_styled_row(@headers, column_widths, bold: true))
61
+ # Header separator line
62
+ lines << render_horizontal_border(column_widths, :middle)
63
+ end
64
+
26
65
  # Render rows
27
- @rows.each do |row|
28
- lines.concat(render_multiline_row(row, column_widths))
66
+ @rows.each_with_index do |row, index|
67
+ lines.concat(render_styled_multiline_row(row, column_widths))
29
68
  end
30
-
69
+
70
+ # Render bottom border
71
+ if @border_style != :none && (!@headers.empty? || !@rows.empty?)
72
+ lines << render_horizontal_border(column_widths, :bottom)
73
+ end
74
+
31
75
  lines.join("\n")
32
76
  end
33
77
 
34
78
  private
79
+
80
+ def render_empty_table
81
+ if @border_style == :none
82
+ return "| |\n-"
83
+ else
84
+ border_chars = BORDER_STYLES[@border_style]
85
+ return "#{border_chars[:top_left]}#{border_chars[:horizontal] * 2}#{border_chars[:top_right]}\n" +
86
+ "#{border_chars[:left]} #{border_chars[:right]}\n" +
87
+ "#{border_chars[:bottom_left]}#{border_chars[:horizontal] * 2}#{border_chars[:bottom_right]}"
88
+ end
89
+ end
90
+
91
+ def render_horizontal_border(column_widths, position)
92
+ return "-" * (column_widths.sum { |w| w + 3 } + 1) if @border_style == :none
93
+
94
+ border_chars = BORDER_STYLES[@border_style]
95
+
96
+ # 计算每列的边框宽度(包括左右的空格)
97
+ segments = column_widths.map { |width| border_chars[:horizontal] * (width + 2) }
98
+
99
+ case position
100
+ when :top
101
+ left_char = border_chars[:top_left]
102
+ right_char = border_chars[:top_right]
103
+ join_char = border_chars[:top_cross]
104
+ when :middle
105
+ left_char = border_chars[:left_cross]
106
+ right_char = border_chars[:right_cross]
107
+ join_char = border_chars[:cross]
108
+ when :bottom
109
+ left_char = border_chars[:bottom_left]
110
+ right_char = border_chars[:bottom_right]
111
+ join_char = border_chars[:bottom_cross]
112
+ else
113
+ left_char = border_chars[:left_cross]
114
+ right_char = border_chars[:right_cross]
115
+ join_char = border_chars[:cross]
116
+ end
117
+
118
+ left_char + segments.join(join_char) + right_char
119
+ end
120
+
121
+ def render_styled_row(row, column_widths, bold: false)
122
+ if @border_style == :none
123
+ return [render_row(row, column_widths, bold: bold)]
124
+ end
125
+
126
+ border_chars = BORDER_STYLES[@border_style]
127
+
128
+ row_content = row.map.with_index do |cell, i|
129
+ content = bold ? cell.render : align_cell(cell.render, column_widths[i])
130
+ aligned_content = align_cell(content, column_widths[i])
131
+ " #{aligned_content} "
132
+ end.join(border_chars[:vertical])
133
+
134
+ ["#{border_chars[:left]}#{row_content}#{border_chars[:right]}"]
135
+ end
136
+
137
+ def render_styled_multiline_row(row, column_widths)
138
+ if @border_style == :none
139
+ return render_multiline_row(row, column_widths)
140
+ end
141
+
142
+ border_chars = BORDER_STYLES[@border_style]
143
+
144
+ # Prepare each cell's lines
145
+ row_lines = row.map.with_index do |cell, i|
146
+ # 获取单元格的样式序列
147
+ style_sequence = cell.render.match(/\e\[[0-9;]*m/)&.to_s || ""
148
+ reset_sequence = style_sequence.empty? ? "" : "\e[0m"
149
+
150
+ # 分割成多行并保持样式
151
+ cell_content = cell.render.split("\n")
152
+
153
+ # 为每一行添加样式
154
+ cell_content.map! { |line|
155
+ line = line.gsub(/\e\[[0-9;]*m/, '') # 移除可能存在的样式序列
156
+ style_sequence + line + reset_sequence
157
+ }
158
+
159
+ # 填充到指定的行高
160
+ padded_content = cell_content + [" "] * [@row_height - cell_content.size, 0].max
161
+
162
+ # 对每一行应用对齐,保持样式
163
+ padded_content.map { |line| align_cell(line, column_widths[i]) }
164
+ end
165
+
166
+ # Normalize row height
167
+ max_height = row_lines.map(&:size).max
168
+ row_lines.each do |lines|
169
+ width = column_widths[row_lines.index(lines)]
170
+ style_sequence = lines.first.match(/\e\[[0-9;]*m/)&.to_s || ""
171
+ reset_sequence = style_sequence.empty? ? "" : "\e[0m"
172
+ lines.fill(style_sequence + " " * width + reset_sequence, lines.size...max_height)
173
+ end
174
+
175
+ # Render each line of the row
176
+ (0...max_height).map do |line_index|
177
+ row_content = row_lines.map { |lines| " #{lines[line_index]} " }.join(border_chars[:vertical])
178
+ "#{border_chars[:left]}#{row_content}#{border_chars[:right]}"
179
+ end
180
+ end
35
181
 
36
182
  def format_cell(cell)
37
183
  cell.is_a?(RubyRich::RichText) ? cell : RubyRich::RichText.new(cell.to_s)
@@ -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
@@ -0,0 +1,200 @@
1
+ module RubyRich
2
+ class Tree
3
+ # 树形结构显示的字符集
4
+ TREE_CHARS = {
5
+ default: {
6
+ vertical: '│',
7
+ horizontal: '─',
8
+ branch: '├',
9
+ last: '└',
10
+ space: ' '
11
+ },
12
+ ascii: {
13
+ vertical: '|',
14
+ horizontal: '-',
15
+ branch: '+',
16
+ last: '+',
17
+ space: ' '
18
+ },
19
+ rounded: {
20
+ vertical: '│',
21
+ horizontal: '─',
22
+ branch: '├',
23
+ last: '└',
24
+ space: ' '
25
+ },
26
+ double: {
27
+ vertical: '║',
28
+ horizontal: '═',
29
+ branch: '╠',
30
+ last: '╚',
31
+ space: ' '
32
+ }
33
+ }.freeze
34
+
35
+ # 树节点类
36
+ class Node
37
+ attr_accessor :name, :children, :data, :expanded
38
+
39
+ def initialize(name, data: nil)
40
+ @name = name
41
+ @children = []
42
+ @data = data
43
+ @expanded = true
44
+ end
45
+
46
+ def add_child(child_node)
47
+ @children << child_node
48
+ child_node
49
+ end
50
+
51
+ def add(name, data: nil)
52
+ child = Node.new(name, data: data)
53
+ add_child(child)
54
+ child
55
+ end
56
+
57
+ def leaf?
58
+ @children.empty?
59
+ end
60
+
61
+ def has_children?
62
+ !@children.empty?
63
+ end
64
+ end
65
+
66
+ attr_reader :root, :style
67
+
68
+ def initialize(root_name = 'Root', style: :default)
69
+ @root = Node.new(root_name)
70
+ @style = style
71
+ @chars = TREE_CHARS[@style] || TREE_CHARS[:default]
72
+ end
73
+
74
+ # 从哈希构建树
75
+ def self.from_hash(hash, root_name = 'Root', style: :default)
76
+ tree = new(root_name, style: style)
77
+ build_from_hash(tree.root, hash)
78
+ tree
79
+ end
80
+
81
+ # 从文件路径构建树
82
+ def self.from_paths(paths, root_name = 'Root', style: :default)
83
+ tree = new(root_name, style: style)
84
+
85
+ paths.each do |path|
86
+ parts = path.split('/')
87
+ current_node = tree.root
88
+
89
+ parts.each do |part|
90
+ next if part.empty?
91
+
92
+ # 查找是否已存在该子节点
93
+ existing_child = current_node.children.find { |child| child.name == part }
94
+
95
+ if existing_child
96
+ current_node = existing_child
97
+ else
98
+ current_node = current_node.add(part)
99
+ end
100
+ end
101
+ end
102
+
103
+ tree
104
+ end
105
+
106
+ # 添加节点到根节点
107
+ def add(name, data: nil)
108
+ @root.add(name, data: data)
109
+ end
110
+
111
+ # 渲染树形结构
112
+ def render(show_guides: true, colors: true)
113
+ lines = []
114
+ render_node(@root, '', true, lines, show_guides, colors)
115
+ lines.join("\n")
116
+ end
117
+
118
+ # 渲染为字符串(别名)
119
+ def to_s
120
+ render
121
+ end
122
+
123
+ private
124
+
125
+ def self.build_from_hash(node, hash)
126
+ hash.each do |key, value|
127
+ child = node.add(key.to_s)
128
+ if value.is_a?(Hash)
129
+ build_from_hash(child, value)
130
+ elsif value.is_a?(Array)
131
+ value.each_with_index do |item, index|
132
+ if item.is_a?(Hash)
133
+ item_child = child.add("[#{index}]")
134
+ build_from_hash(item_child, item)
135
+ else
136
+ child.add(item.to_s)
137
+ end
138
+ end
139
+ else
140
+ child.add(value.to_s) unless value.nil?
141
+ end
142
+ end
143
+ end
144
+
145
+ def render_node(node, prefix, is_last, lines, show_guides, colors)
146
+ # 根节点特殊处理
147
+ if node == @root
148
+ if colors
149
+ lines << "\e[1m\e[96m#{node.name}\e[0m"
150
+ else
151
+ lines << node.name
152
+ end
153
+
154
+ node.children.each_with_index do |child, index|
155
+ is_child_last = (index == node.children.length - 1)
156
+ render_node(child, '', is_child_last, lines, show_guides, colors)
157
+ end
158
+ return
159
+ end
160
+
161
+ # 构建当前行的前缀和连接符
162
+ if show_guides
163
+ connector = is_last ? @chars[:last] : @chars[:branch]
164
+ current_prefix = prefix + connector + @chars[:horizontal] + @chars[:space]
165
+ else
166
+ current_prefix = prefix + @chars[:space] * 4
167
+ end
168
+
169
+ # 渲染当前节点
170
+ node_text = if colors
171
+ case
172
+ when node.leaf?
173
+ "\e[92m#{node.name}\e[0m" # 绿色叶子节点
174
+ when node.has_children?
175
+ "\e[94m#{node.name}/\e[0m" # 蓝色目录节点
176
+ else
177
+ node.name
178
+ end
179
+ else
180
+ node.leaf? ? node.name : "#{node.name}/"
181
+ end
182
+
183
+ lines << current_prefix + node_text
184
+
185
+ # 递归渲染子节点
186
+ if node.expanded && node.has_children?
187
+ next_prefix = if show_guides
188
+ prefix + (is_last ? @chars[:space] : @chars[:vertical]) + @chars[:space] * 3
189
+ else
190
+ prefix + @chars[:space] * 4
191
+ end
192
+
193
+ node.children.each_with_index do |child, index|
194
+ is_child_last = (index == node.children.length - 1)
195
+ render_node(child, next_prefix, is_child_last, lines, show_guides, colors)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -1,3 +1,3 @@
1
1
  module RubyRich
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/ruby_rich.rb CHANGED
@@ -16,6 +16,11 @@ require_relative 'ruby_rich/text'
16
16
  require_relative 'ruby_rich/print'
17
17
  require_relative 'ruby_rich/panel'
18
18
  require_relative 'ruby_rich/dialog'
19
+ require_relative 'ruby_rich/syntax'
20
+ require_relative 'ruby_rich/markdown'
21
+ require_relative 'ruby_rich/tree'
22
+ require_relative 'ruby_rich/columns'
23
+ require_relative 'ruby_rich/status'
19
24
  require_relative 'ruby_rich/ansi_code'
20
25
  require_relative 'ruby_rich/version'
21
26
 
@@ -34,7 +39,32 @@ module RubyRich
34
39
  end
35
40
 
36
41
  # 提供一个便捷方法来创建表格
37
- def self.table
38
- Table.new
42
+ def self.table(border_style: :none)
43
+ Table.new(border_style: border_style)
39
44
  end
40
- end
45
+
46
+ # 提供一个便捷方法来进行语法高亮
47
+ def self.syntax(code, language = nil, theme: :default)
48
+ Syntax.highlight(code, language, theme: theme)
49
+ end
50
+
51
+ # 提供一个便捷方法来渲染 Markdown
52
+ def self.markdown(text, options = {})
53
+ Markdown.render(text, options)
54
+ end
55
+
56
+ # 提供一个便捷方法来创建树形结构
57
+ def self.tree(root_name = 'Root', style: :default)
58
+ Tree.new(root_name, style: style)
59
+ end
60
+
61
+ # 提供一个便捷方法来创建多列布局
62
+ def self.columns(total_width: 80, gutter_width: 2)
63
+ Columns.new(total_width: total_width, gutter_width: gutter_width)
64
+ end
65
+
66
+ # 提供一个便捷方法来创建状态指示器
67
+ def self.status(type, **options)
68
+ Status.indicator(type, **options)
69
+ end
70
+ end