ruby_rich 0.5.0 → 0.5.2
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/markdown.rb +306 -35
- data/lib/ruby_rich/table.rb +2 -2
- data/lib/ruby_rich/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0245a4d95c0aa26f8f5ec3075ee2d5eaf8aecb151cd3f62635369da837d0a7e9
|
|
4
|
+
data.tar.gz: dad422979d8bb74c9be0cc6ecf2fb32166b54958ea117cbae1cc1daa2a86dcf3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2aec4aecc0f07687a61cb4f1c1d34dd5f2fcd8618541355f1fa521aaf040688454bae6ac4dfe3dc13571c53f7816333d8a9d155e77b29e5fb4760b051e93cb4
|
|
7
|
+
data.tar.gz: e0c2aae4eb11397504b4d5ee9324102df7156504d5ac60a85132528cb949deba16b57d47a29809626aa65cb3b57e6a514f0d0206332c51aa333f2f3c78efedb4
|
data/lib/ruby_rich/markdown.rb
CHANGED
|
@@ -185,11 +185,18 @@ module RubyRich
|
|
|
185
185
|
code_lines.pop while code_lines.last&.strip&.empty?
|
|
186
186
|
return "#{tc(:code_border)}┌─ #{lang || "text"} ─┐#{AnsiCode.reset}\n#{tc(:code_border)}└──┘#{AnsiCode.reset}\n\n" if code_lines.empty?
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
# Mermaid diagram support
|
|
189
|
+
if lang == "mermaid" || lang == "mmd"
|
|
190
|
+
rendered = MermaidRenderer.render(code, @width.to_i)
|
|
191
|
+
return "#{rendered}\n\n"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# LaTeX math block support (```latex ... ``` / ```tex ... ```)
|
|
195
|
+
if lang == "latex" || lang == "tex"
|
|
196
|
+
rendered = LatexConverter.convert(code)
|
|
197
|
+
color = tc(:math)
|
|
198
|
+
return "#{color}#{rendered}#{AnsiCode.reset}\n\n"
|
|
199
|
+
end
|
|
193
200
|
|
|
194
201
|
total_lines = code_lines.length
|
|
195
202
|
digit_width = [total_lines.to_s.length, 1].max
|
|
@@ -286,8 +293,9 @@ module RubyRich
|
|
|
286
293
|
UNORDERED_MARKERS = { 1 => '•', 2 => '◦', 3 => '▸' }.freeze
|
|
287
294
|
|
|
288
295
|
def convert_li(el)
|
|
289
|
-
depth = [@
|
|
296
|
+
depth = [@list_types.length, 1].max
|
|
290
297
|
list_type = @list_types.last
|
|
298
|
+
indent = " " * (depth - 1)
|
|
291
299
|
marker = if list_type == :ol
|
|
292
300
|
@list_counters[-1] += 1
|
|
293
301
|
"#{@list_counters[-1]}."
|
|
@@ -295,11 +303,11 @@ module RubyRich
|
|
|
295
303
|
UNORDERED_MARKERS[depth.clamp(1, 3)] || '▸'
|
|
296
304
|
end
|
|
297
305
|
task = detect_task_marker(el)
|
|
298
|
-
text = inline_content(el)
|
|
306
|
+
text = inline_content(el).gsub(/\n{2,}/, "\n")
|
|
299
307
|
if task
|
|
300
|
-
"#{tc(task ? :task_checked : :task_unchecked)}#{task}#{AnsiCode.reset} #{text.strip}\n"
|
|
308
|
+
"#{indent}#{tc(task ? :task_checked : :task_unchecked)}#{task}#{AnsiCode.reset} #{text.strip}\n"
|
|
301
309
|
else
|
|
302
|
-
"#{tc(list_type == :ol ? :ordered_list : :"list_level_#{depth.clamp(1, 3)}")}#{marker}#{AnsiCode.reset} #{text.strip}\n"
|
|
310
|
+
"#{indent}#{tc(list_type == :ol ? :ordered_list : :"list_level_#{depth.clamp(1, 3)}")}#{marker}#{AnsiCode.reset} #{text.strip}\n"
|
|
303
311
|
end
|
|
304
312
|
end
|
|
305
313
|
|
|
@@ -737,34 +745,119 @@ module RubyRich
|
|
|
737
745
|
return formula if formula.nil? || formula.strip.empty?
|
|
738
746
|
|
|
739
747
|
result = formula.dup
|
|
740
|
-
result = process_frac(result)
|
|
741
|
-
result = process_sqrt(result)
|
|
742
748
|
result = process_cases(result)
|
|
743
|
-
result = process_scripts(result)
|
|
744
749
|
result = replace_symbols(result)
|
|
750
|
+
result = process_scripts(result)
|
|
751
|
+
result = process_frac(result)
|
|
752
|
+
result = process_sqrt(result)
|
|
745
753
|
result = strip_delim_spacing(result)
|
|
746
754
|
result
|
|
747
755
|
end
|
|
748
756
|
|
|
749
|
-
#
|
|
757
|
+
# Find the index of the } that matches the { at `open_pos`.
|
|
758
|
+
# Returns nil when braces are unbalanced.
|
|
759
|
+
def self.find_matching_brace(text, open_pos)
|
|
760
|
+
return nil unless text[open_pos] == '{'
|
|
761
|
+
depth = 1
|
|
762
|
+
i = open_pos + 1
|
|
763
|
+
while i < text.length && depth > 0
|
|
764
|
+
case text[i]
|
|
765
|
+
when '{' then depth += 1
|
|
766
|
+
when '}' then depth -= 1
|
|
767
|
+
when '\\' then i += 1
|
|
768
|
+
end
|
|
769
|
+
i += 1
|
|
770
|
+
end
|
|
771
|
+
depth == 0 ? i - 1 : nil
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# \frac{num}{den} / \dfrac{num}{den} / \tfrac{num}{den}
|
|
775
|
+
# → (num)/(den) when num/den include operators, otherwise num/den
|
|
750
776
|
def self.process_frac(text)
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
777
|
+
result = +""
|
|
778
|
+
i = 0
|
|
779
|
+
while i < text.length
|
|
780
|
+
cmd_len = nil
|
|
781
|
+
if text[i..].start_with?('\\dfrac') || text[i..].start_with?('\\tfrac')
|
|
782
|
+
cmd_len = 6
|
|
783
|
+
elsif text[i..].start_with?('\\frac')
|
|
784
|
+
cmd_len = 5
|
|
785
|
+
end
|
|
786
|
+
if cmd_len
|
|
787
|
+
j = i + cmd_len
|
|
788
|
+
while j < text.length && text[j] =~ /\s/
|
|
789
|
+
j += 1
|
|
790
|
+
end
|
|
791
|
+
if j < text.length && text[j] == '{'
|
|
792
|
+
num_start = j
|
|
793
|
+
num_end = find_matching_brace(text, num_start)
|
|
794
|
+
if num_end
|
|
795
|
+
k = num_end + 1
|
|
796
|
+
while k < text.length && text[k] =~ /\s/
|
|
797
|
+
k += 1
|
|
798
|
+
end
|
|
799
|
+
if k < text.length && text[k] == '{'
|
|
800
|
+
den_start = k
|
|
801
|
+
den_end = find_matching_brace(text, den_start)
|
|
802
|
+
if den_end
|
|
803
|
+
num = text[num_start + 1...num_end]
|
|
804
|
+
den = text[den_start + 1...den_end]
|
|
805
|
+
# Only wrap in parens when the expression includes
|
|
806
|
+
# operators that would change precedence without them.
|
|
807
|
+
op_rx = /[+\-±∓×÷=<>]/
|
|
808
|
+
num_wrap = num =~ op_rx ? "(#{num})" : num
|
|
809
|
+
den_wrap = den =~ op_rx ? "(#{den})" : den
|
|
810
|
+
result << "#{num_wrap}/#{den_wrap}"
|
|
811
|
+
i = den_end + 1
|
|
812
|
+
next
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
result << text[i]
|
|
819
|
+
i += 1
|
|
757
820
|
end
|
|
821
|
+
result
|
|
758
822
|
end
|
|
759
823
|
|
|
760
824
|
# \sqrt{x} → √(x) \sqrt[n]{x} → ⁿ√(x)
|
|
761
825
|
def self.process_sqrt(text)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
826
|
+
result = +""
|
|
827
|
+
i = 0
|
|
828
|
+
while i < text.length
|
|
829
|
+
if text[i..].start_with?('\\sqrt')
|
|
830
|
+
j = i + 5
|
|
831
|
+
deg_text = nil
|
|
832
|
+
while j < text.length && text[j] =~ /\s/
|
|
833
|
+
j += 1
|
|
834
|
+
end
|
|
835
|
+
if j < text.length && text[j] == '['
|
|
836
|
+
close_br = text.index(']', j)
|
|
837
|
+
if close_br
|
|
838
|
+
deg_text = text[j + 1...close_br]
|
|
839
|
+
j = close_br + 1
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
while j < text.length && text[j] =~ /\s/
|
|
843
|
+
j += 1
|
|
844
|
+
end
|
|
845
|
+
if j < text.length && text[j] == '{'
|
|
846
|
+
rad_start = j
|
|
847
|
+
rad_end = find_matching_brace(text, rad_start)
|
|
848
|
+
if rad_end
|
|
849
|
+
rad = text[rad_start + 1...rad_end]
|
|
850
|
+
prefix = deg_text ? script_chars(deg_text, SUPERSCRIPTS) : ''
|
|
851
|
+
result << "#{prefix}√(#{rad})"
|
|
852
|
+
i = rad_end + 1
|
|
853
|
+
next
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
result << text[i]
|
|
858
|
+
i += 1
|
|
767
859
|
end
|
|
860
|
+
result
|
|
768
861
|
end
|
|
769
862
|
|
|
770
863
|
# \begin{cases} ... \end{cases} → ⎧ … ⎨ … ⎩ …
|
|
@@ -811,8 +904,15 @@ module RubyRich
|
|
|
811
904
|
|
|
812
905
|
# Replace \command tokens with Unicode equivalents.
|
|
813
906
|
def self.replace_symbols(text)
|
|
814
|
-
# Handle \text{
|
|
815
|
-
|
|
907
|
+
# Handle brace-wrapped font/formatting commands: \text{ab}, \mathbf{ab}, \mathbb{R}, etc.
|
|
908
|
+
# Strip the wrapper, keep the content.
|
|
909
|
+
text = text.gsub(/\\(?:text\w*|math[bif]|mathbf|mathrm|mathit|mathsf|mathtt|mathcal|mathfrak|mathbb|mathscr|boldsymbol|bm|emph)\s*\{(.*?)\}/) {
|
|
910
|
+
Regexp.last_match(1)
|
|
911
|
+
}
|
|
912
|
+
# Handle font commands with single-char arg (space-separated): \mathbf E
|
|
913
|
+
text = text.gsub(/\\(?:mathbf|mathrm|mathit|mathsf|mathtt|mathcal|mathfrak|mathbb|mathscr|boldsymbol|bm)\s+([a-zA-Z0-9])/) {
|
|
914
|
+
Regexp.last_match(1)
|
|
915
|
+
}
|
|
816
916
|
# Replace all other \commands
|
|
817
917
|
text.gsub(/\\([a-zA-Z]+)/) { |m|
|
|
818
918
|
SYMBOLS[Regexp.last_match(1)] || m
|
|
@@ -825,6 +925,8 @@ module RubyRich
|
|
|
825
925
|
.gsub(/\[\s+/, '[').gsub(/\s+\]/, ']')
|
|
826
926
|
.gsub(/\{\s+/, '{').gsub(/\s+\}/, '}')
|
|
827
927
|
.gsub(/\\s+/, ' ')
|
|
928
|
+
.gsub(/([·×÷]) +/, '\1')
|
|
929
|
+
.gsub(/ +([·×÷])/, '\1')
|
|
828
930
|
end
|
|
829
931
|
end
|
|
830
932
|
|
|
@@ -834,6 +936,23 @@ module RubyRich
|
|
|
834
936
|
module MermaidRenderer
|
|
835
937
|
BAR_MAX = 32
|
|
836
938
|
|
|
939
|
+
LEAF_BIN = "leaf"
|
|
940
|
+
|
|
941
|
+
def self.leaf_available?
|
|
942
|
+
@leaf_available ||= system("which #{LEAF_BIN} > /dev/null 2>&1")
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def self.render_via_leaf(source, width)
|
|
946
|
+
return nil unless leaf_available?
|
|
947
|
+
IO.popen([LEAF_BIN, "--inline", "plain:#{width}"], "r+", err: "/dev/null") do |io|
|
|
948
|
+
io.write(source)
|
|
949
|
+
io.close_write
|
|
950
|
+
io.read.strip
|
|
951
|
+
end
|
|
952
|
+
rescue
|
|
953
|
+
nil
|
|
954
|
+
end
|
|
955
|
+
|
|
837
956
|
def self.render(source, width = 80)
|
|
838
957
|
trimmed = source.strip
|
|
839
958
|
return "" if trimmed.empty?
|
|
@@ -842,6 +961,10 @@ module RubyRich
|
|
|
842
961
|
case type
|
|
843
962
|
when :pie
|
|
844
963
|
render_pie(trimmed, width)
|
|
964
|
+
when :flowchart, :sequence, :class, :gantt, :state, :generic
|
|
965
|
+
# Prefer leaf for high-quality ASCII-art rendering
|
|
966
|
+
result = render_via_leaf("```mermaid\n#{trimmed}\n```\n", width)
|
|
967
|
+
result && !result.empty? ? result : render_fallback(trimmed, type, width)
|
|
845
968
|
else
|
|
846
969
|
render_fallback(trimmed, type, width)
|
|
847
970
|
end
|
|
@@ -917,6 +1040,148 @@ module RubyRich
|
|
|
917
1040
|
].join("\n")
|
|
918
1041
|
end
|
|
919
1042
|
|
|
1043
|
+
# Flowchart / graph → edge-list rendering with node labels.
|
|
1044
|
+
def self.render_flowchart(source, width)
|
|
1045
|
+
lines = source.lines.map(&:chomp)
|
|
1046
|
+
# Build node registry: id => label
|
|
1047
|
+
nodes = {}
|
|
1048
|
+
edges = []
|
|
1049
|
+
|
|
1050
|
+
lines.each do |line|
|
|
1051
|
+
stripped = line.strip
|
|
1052
|
+
next if stripped.empty?
|
|
1053
|
+
next if stripped.downcase.start_with?("flowchart", "graph")
|
|
1054
|
+
|
|
1055
|
+
# Parse edge: src ---|label|---> tgt
|
|
1056
|
+
m = stripped.match(
|
|
1057
|
+
/\A(.+?)\s*(-+>|==+>|-\.+>|=+>)\s*(\|(.*?)\|)?\s*(.+)\z/
|
|
1058
|
+
)
|
|
1059
|
+
if m
|
|
1060
|
+
src_raw = m[1].strip
|
|
1061
|
+
tgt_raw = m[5].strip
|
|
1062
|
+
arrow = m[2]
|
|
1063
|
+
label = m[4]&.strip
|
|
1064
|
+
|
|
1065
|
+
src_id, src_lbl = parse_node(src_raw)
|
|
1066
|
+
tgt_id, tgt_lbl = parse_node(tgt_raw)
|
|
1067
|
+
|
|
1068
|
+
# Only store shaped labels — don't let bare IDs overwrite them
|
|
1069
|
+
nodes[src_id] = src_lbl if src_lbl && src_raw =~ /[\[\(\{]/
|
|
1070
|
+
nodes[tgt_id] = tgt_lbl if tgt_lbl && tgt_raw =~ /[\[\(\{]/
|
|
1071
|
+
|
|
1072
|
+
edges << {
|
|
1073
|
+
src: src_id, src_label: src_lbl || src_id,
|
|
1074
|
+
tgt: tgt_id, tgt_label: tgt_lbl || tgt_id,
|
|
1075
|
+
edge_label: label
|
|
1076
|
+
}
|
|
1077
|
+
next
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
# Standalone node definition: id[text] / id{text} / id(text)
|
|
1081
|
+
nm = stripped.match(/\A([A-Za-z0-9_]+)\s*[\[\(\{].+[\]\)\}]\z/)
|
|
1082
|
+
if nm
|
|
1083
|
+
nid, nlbl = parse_node(stripped)
|
|
1084
|
+
nodes[nid] = nlbl if nlbl
|
|
1085
|
+
end
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
return "[Mermaid flowchart: no edges found]" if edges.empty?
|
|
1089
|
+
|
|
1090
|
+
out = +""
|
|
1091
|
+
edges.each do |e|
|
|
1092
|
+
src = nodes[e[:src]] || e[:src_label]
|
|
1093
|
+
tgt = nodes[e[:tgt]] || e[:tgt_label]
|
|
1094
|
+
lbl = e[:edge_label] ? " ─#{e[:edge_label]}─▶ " : " ──▶ "
|
|
1095
|
+
out << "#{src}#{lbl}#{tgt}\n"
|
|
1096
|
+
end
|
|
1097
|
+
out.strip
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# Extract [id, label] from a node token like "A[开始]" or "B{是否通过?}"
|
|
1101
|
+
def self.parse_node(raw)
|
|
1102
|
+
raw = raw.strip
|
|
1103
|
+
# Square brackets
|
|
1104
|
+
if raw =~ /\A([A-Za-z0-9_]+)\s*\[(.+)\]\z/
|
|
1105
|
+
[$1, $2]
|
|
1106
|
+
# Curly braces (diamond)
|
|
1107
|
+
elsif raw =~ /\A([A-Za-z0-9_]+)\s*\{(.+)\}\z/
|
|
1108
|
+
[$1, $2]
|
|
1109
|
+
# Round parens
|
|
1110
|
+
elsif raw =~ /\A([A-Za-z0-9_]+)\s*\((.+)\)\z/
|
|
1111
|
+
[$1, $2]
|
|
1112
|
+
# Just an id
|
|
1113
|
+
elsif raw =~ /\A([A-Za-z0-9_]+)\z/
|
|
1114
|
+
[$1, $1]
|
|
1115
|
+
else
|
|
1116
|
+
[raw, raw]
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
# Sequence diagram → participant-message listing.
|
|
1121
|
+
def self.render_sequence(source, width)
|
|
1122
|
+
lines = source.lines.map(&:chomp)
|
|
1123
|
+
participants = []
|
|
1124
|
+
messages = []
|
|
1125
|
+
|
|
1126
|
+
lines.each do |line|
|
|
1127
|
+
stripped = line.strip
|
|
1128
|
+
next if stripped.empty? || stripped.downcase.start_with?("sequencediagram")
|
|
1129
|
+
|
|
1130
|
+
# participant / actor definition
|
|
1131
|
+
if stripped =~ /\A(?:participant|actor)\s+(.+)\z/i
|
|
1132
|
+
participants << $1.strip
|
|
1133
|
+
next
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# Note
|
|
1137
|
+
if stripped =~ /\ANote\s+(?:over\s+)?(.+?):\s*(.+)\z/i
|
|
1138
|
+
messages << { type: :note, target: $1.strip, text: $2.strip }
|
|
1139
|
+
next
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
# Message: A->>B: text / A-->>B: text / A-)B: text
|
|
1143
|
+
if stripped =~ /\A(.+?)\s*(-+>>?|-->>|-\)|-[xX])\s*(.+?)\s*:\s*(.+)\z/
|
|
1144
|
+
src = $1.strip
|
|
1145
|
+
arrow_type = $2.strip
|
|
1146
|
+
tgt = $3.strip
|
|
1147
|
+
text = $4.strip
|
|
1148
|
+
participants |= [src, tgt] unless participants.include?(src) && participants.include?(tgt)
|
|
1149
|
+
dashed = arrow_type.start_with?("--")
|
|
1150
|
+
messages << { type: :msg, src: src, tgt: tgt, text: text, dashed: dashed }
|
|
1151
|
+
end
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
return "[Mermaid sequence: no messages found]" if messages.empty?
|
|
1155
|
+
|
|
1156
|
+
max_participant = participants.map(&:length).max
|
|
1157
|
+
max_participant = 8 if max_participant < 8
|
|
1158
|
+
|
|
1159
|
+
out = +""
|
|
1160
|
+
participants.each do |p|
|
|
1161
|
+
out << sprintf("%-#{max_participant + 4}s", "[#{p}]")
|
|
1162
|
+
end
|
|
1163
|
+
out << "\n#{'─' * ((max_participant + 4) * participants.size)}\n"
|
|
1164
|
+
|
|
1165
|
+
messages.each do |m|
|
|
1166
|
+
case m[:type]
|
|
1167
|
+
when :note
|
|
1168
|
+
out << " 📝 #{m[:target]}: #{m[:text]}\n"
|
|
1169
|
+
when :msg
|
|
1170
|
+
src_idx = participants.index(m[:src]) || 0
|
|
1171
|
+
tgt_idx = participants.index(m[:tgt]) || participants.size - 1
|
|
1172
|
+
rightward = src_idx <= tgt_idx
|
|
1173
|
+
if m[:dashed]
|
|
1174
|
+
arrow = rightward ? "╌╌▶" : "◀╌╌"
|
|
1175
|
+
else
|
|
1176
|
+
arrow = rightward ? "──▶" : "◀──"
|
|
1177
|
+
end
|
|
1178
|
+
out << sprintf(" %-#{max_participant}s #{arrow} %s: %s\n",
|
|
1179
|
+
m[:src], m[:tgt], m[:text])
|
|
1180
|
+
end
|
|
1181
|
+
end
|
|
1182
|
+
out.strip
|
|
1183
|
+
end
|
|
1184
|
+
|
|
920
1185
|
# Proxy theme colour access (same instance as TerminalConverter).
|
|
921
1186
|
def self.tc(key)
|
|
922
1187
|
color, bright = MarkdownTheme[key]
|
|
@@ -947,14 +1212,18 @@ module RubyRich
|
|
|
947
1212
|
VERTICAL_THRESHOLD = 5
|
|
948
1213
|
|
|
949
1214
|
def self.extract(markdown_text)
|
|
950
|
-
|
|
1215
|
+
# Strip leading blank lines so that a heredoc like <<~'MD'\n\n---\n
|
|
1216
|
+
# is still recognised as having frontmatter.
|
|
1217
|
+
stripped = markdown_text.lstrip
|
|
1218
|
+
return [markdown_text, nil, false] unless stripped.start_with?("---")
|
|
951
1219
|
|
|
952
|
-
rest =
|
|
953
|
-
offset =
|
|
1220
|
+
rest = stripped[3..]
|
|
1221
|
+
offset = 3
|
|
954
1222
|
rest.each_line do |line|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1223
|
+
trimmed = line.strip
|
|
1224
|
+
if trimmed == "---" || trimmed == "..."
|
|
1225
|
+
fm_block = stripped[3...offset]
|
|
1226
|
+
content = stripped[(offset + line.length)..] || ""
|
|
958
1227
|
pairs = parse_pairs(fm_block)
|
|
959
1228
|
return [markdown_text, nil, false] if pairs.empty?
|
|
960
1229
|
vertical = pairs.length >= VERTICAL_THRESHOLD
|
|
@@ -980,8 +1249,9 @@ module RubyRich
|
|
|
980
1249
|
raw_value = trimmed[(colon_pos + 1)..].strip
|
|
981
1250
|
i += 1 and next if key.empty?
|
|
982
1251
|
|
|
983
|
-
if ["
|
|
984
|
-
# Multiline value
|
|
1252
|
+
if [">-", ">", "|", "|-"].include?(raw_value)
|
|
1253
|
+
# Multiline value with explicit indicator
|
|
1254
|
+
i += 1
|
|
985
1255
|
parts = []
|
|
986
1256
|
while i < lines.length && lines[i].start_with?(' ', "\t")
|
|
987
1257
|
part = lines[i].strip
|
|
@@ -990,7 +1260,8 @@ module RubyRich
|
|
|
990
1260
|
end
|
|
991
1261
|
pairs << [key, parts.join(" ")]
|
|
992
1262
|
elsif raw_value.empty?
|
|
993
|
-
#
|
|
1263
|
+
# Empty value: could be a list or an implicit multiline string.
|
|
1264
|
+
i += 1
|
|
994
1265
|
items = []
|
|
995
1266
|
while i < lines.length && lines[i].start_with?(' ', "\t")
|
|
996
1267
|
item = lines[i].strip
|
data/lib/ruby_rich/table.rb
CHANGED
|
@@ -127,7 +127,7 @@ module RubyRich
|
|
|
127
127
|
|
|
128
128
|
row_content = row.map.with_index do |cell, i|
|
|
129
129
|
rendered = cell.render.sub(/\e\[0m\z/, '')
|
|
130
|
-
content = bold ? rendered :
|
|
130
|
+
content = bold ? "\e[1m#{rendered}" : rendered
|
|
131
131
|
aligned_content = align_cell(content, column_widths[i]).sub(/\e\[0m\z/, '')
|
|
132
132
|
" #{aligned_content} "
|
|
133
133
|
end.join(border_chars[:vertical])
|
|
@@ -214,7 +214,7 @@ module RubyRich
|
|
|
214
214
|
def render_row(row, column_widths, bold: false)
|
|
215
215
|
row.map.with_index do |cell, i|
|
|
216
216
|
rendered = cell.render.sub(/\e\[0m\z/, '')
|
|
217
|
-
content = bold ? rendered :
|
|
217
|
+
content = bold ? "\e[1m#{rendered}" : rendered
|
|
218
218
|
align_cell(content, column_widths[i]).sub(/\e\[0m\z/, '')
|
|
219
219
|
end.join(" | ").prepend("| ").concat(" |\e[0m")
|
|
220
220
|
end
|
data/lib/ruby_rich/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_rich
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- zhuang biaowei
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-06-13 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rake
|
|
@@ -176,6 +177,7 @@ homepage: https://github.com/zhuangbiaowei/ruby_rich
|
|
|
176
177
|
licenses:
|
|
177
178
|
- MIT
|
|
178
179
|
metadata: {}
|
|
180
|
+
post_install_message:
|
|
179
181
|
rdoc_options: []
|
|
180
182
|
require_paths:
|
|
181
183
|
- lib
|
|
@@ -190,7 +192,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
190
192
|
- !ruby/object:Gem::Version
|
|
191
193
|
version: '0'
|
|
192
194
|
requirements: []
|
|
193
|
-
rubygems_version:
|
|
195
|
+
rubygems_version: 3.5.23
|
|
196
|
+
signing_key:
|
|
194
197
|
specification_version: 4
|
|
195
198
|
summary: Rich text formatting and console output for Ruby
|
|
196
199
|
test_files: []
|