rich-ruby 1.0.1 → 1.0.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/CHANGELOG.md +80 -0
- data/LICENSE +21 -21
- data/README.md +547 -547
- data/docs/architecture.md +43 -0
- data/docs/cheat-sheet.md +52 -0
- data/docs/customization.md +53 -0
- data/docs/how-to-use.md +96 -0
- data/docs/test-report.md +112 -0
- data/docs/troubleshooting.md +36 -0
- data/docs/windows-notes.md +30 -0
- data/examples/demo.rb +106 -106
- data/examples/showcase.rb +420 -420
- data/examples/smoke_test.rb +41 -41
- data/examples/stress_test.rb +604 -604
- data/examples/syntax_markdown_demo.rb +166 -166
- data/examples/verify.rb +216 -215
- data/examples/visual_demo.rb +145 -145
- data/lib/rich/_palettes.rb +148 -148
- data/lib/rich/box.rb +342 -342
- data/lib/rich/cells.rb +524 -512
- data/lib/rich/color.rb +631 -628
- data/lib/rich/color_triplet.rb +227 -220
- data/lib/rich/console.rb +604 -549
- data/lib/rich/control.rb +332 -332
- data/lib/rich/json.rb +260 -254
- data/lib/rich/layout.rb +314 -314
- data/lib/rich/markdown.rb +531 -509
- data/lib/rich/markup.rb +186 -175
- data/lib/rich/panel.rb +318 -311
- data/lib/rich/progress.rb +430 -430
- data/lib/rich/segment.rb +387 -387
- data/lib/rich/style.rb +464 -433
- data/lib/rich/syntax.rb +1220 -1145
- data/lib/rich/table.rb +547 -525
- data/lib/rich/terminal_theme.rb +126 -126
- data/lib/rich/text.rb +460 -433
- data/lib/rich/tree.rb +220 -220
- data/lib/rich/version.rb +5 -5
- data/lib/rich/win32_console.rb +620 -582
- data/lib/rich.rb +108 -108
- metadata +15 -5
data/lib/rich/markup.rb
CHANGED
|
@@ -1,175 +1,186 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "style"
|
|
4
|
-
require_relative "text"
|
|
5
|
-
|
|
6
|
-
module Rich
|
|
7
|
-
# Markup parsing error
|
|
8
|
-
class MarkupError < StandardError
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
# Parser for Rich markup syntax: [style]text[/style]
|
|
12
|
-
module Markup
|
|
13
|
-
# Tag regex for matching markup tags, excluding escaped ones
|
|
14
|
-
TAG_REGEX = /(?<!\\)\[(?<closing>\/)?(?<tag>[^\[\]\/]*)\]/
|
|
15
|
-
|
|
16
|
-
class << self
|
|
17
|
-
# Parse markup into a Text object
|
|
18
|
-
# @param markup [String] Markup text
|
|
19
|
-
# @param style [Style, String, nil] Base style
|
|
20
|
-
# @return [Text]
|
|
21
|
-
def parse(markup, style: nil)
|
|
22
|
-
result_text = Text.new(style: style)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "style"
|
|
4
|
+
require_relative "text"
|
|
5
|
+
|
|
6
|
+
module Rich
|
|
7
|
+
# Markup parsing error
|
|
8
|
+
class MarkupError < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Parser for Rich markup syntax: [style]text[/style]
|
|
12
|
+
module Markup
|
|
13
|
+
# Tag regex for matching markup tags, excluding escaped ones
|
|
14
|
+
TAG_REGEX = /(?<!\\)\[(?<closing>\/)?(?<tag>[^\[\]\/]*)\]/
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Parse markup into a Text object
|
|
18
|
+
# @param markup [String] Markup text
|
|
19
|
+
# @param style [Style, String, nil] Base style
|
|
20
|
+
# @return [Text]
|
|
21
|
+
def parse(markup, style: nil)
|
|
22
|
+
result_text = Text.new(style: style)
|
|
23
|
+
# Each stack entry is [tag_name, style] so closing tags can match by
|
|
24
|
+
# name (matching validate's semantics and Python Rich).
|
|
25
|
+
style_stack = []
|
|
26
|
+
pos = 0
|
|
27
|
+
|
|
28
|
+
markup.scan(TAG_REGEX) do
|
|
29
|
+
match = Regexp.last_match
|
|
30
|
+
tag_start = match.begin(0)
|
|
31
|
+
|
|
32
|
+
# Add text before tag
|
|
33
|
+
if tag_start > pos
|
|
34
|
+
pre_text = unescape(markup[pos...tag_start])
|
|
35
|
+
start_pos = result_text.length
|
|
36
|
+
result_text.append(pre_text)
|
|
37
|
+
|
|
38
|
+
# Apply stacked styles to this text
|
|
39
|
+
style_stack.each do |(_name, stacked_style)|
|
|
40
|
+
result_text.spans << Span.new(start_pos, result_text.length, stacked_style)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Process tag
|
|
45
|
+
if match[:closing]
|
|
46
|
+
# Closing tag - close the matching open tag by name; a bare [/]
|
|
47
|
+
# closes the most recently opened tag.
|
|
48
|
+
tag_name = match[:tag].strip
|
|
49
|
+
unless style_stack.empty?
|
|
50
|
+
if tag_name.empty?
|
|
51
|
+
style_stack.pop
|
|
52
|
+
else
|
|
53
|
+
idx = style_stack.rindex { |(name, _)| name == tag_name }
|
|
54
|
+
idx ? style_stack.delete_at(idx) : style_stack.pop
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
# Opening tag - parse and push style
|
|
59
|
+
tag_content = match[:tag].strip
|
|
60
|
+
if tag_content.empty?
|
|
61
|
+
# Literal []
|
|
62
|
+
result_text.append("[]")
|
|
63
|
+
else
|
|
64
|
+
begin
|
|
65
|
+
parsed_style = Style.parse(tag_content)
|
|
66
|
+
style_stack << [tag_content, parsed_style]
|
|
67
|
+
rescue StandardError
|
|
68
|
+
# Invalid style, treat as literal text
|
|
69
|
+
result_text.append("[#{tag_content}]")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
pos = match.end(0)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Add remaining text
|
|
78
|
+
if pos < markup.length
|
|
79
|
+
remaining = unescape(markup[pos..])
|
|
80
|
+
start_pos = result_text.length
|
|
81
|
+
result_text.append(remaining)
|
|
82
|
+
|
|
83
|
+
style_stack.each do |(_name, stacked_style)|
|
|
84
|
+
result_text.spans << Span.new(start_pos, result_text.length, stacked_style)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
result_text
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Render markup directly to ANSI string
|
|
92
|
+
# @param markup [String] Markup text
|
|
93
|
+
# @param color_system [Symbol] Color system
|
|
94
|
+
# @return [String]
|
|
95
|
+
def render(markup, color_system: ColorSystem::TRUECOLOR)
|
|
96
|
+
parse(markup).render(color_system: color_system)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Escape text for use in markup (escape square brackets)
|
|
100
|
+
# @param text [String] Text to escape
|
|
101
|
+
# @return [String]
|
|
102
|
+
def escape(text)
|
|
103
|
+
text.gsub(/[\[\]]/) { |m| "\\#{m}" }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Unescape markup text
|
|
107
|
+
# @param text [String] Text to unescape
|
|
108
|
+
# @return [String]
|
|
109
|
+
def unescape(text)
|
|
110
|
+
text.gsub(/\\([\[\]\\])/, '\1')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Strip markup tags from text
|
|
114
|
+
# @param markup [String] Markup text
|
|
115
|
+
# @return [String]
|
|
116
|
+
def strip(markup)
|
|
117
|
+
markup.gsub(TAG_REGEX, "")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if text contains markup
|
|
121
|
+
# @param text [String] Text to check
|
|
122
|
+
# @return [Boolean]
|
|
123
|
+
def contains_markup?(text)
|
|
124
|
+
text.match?(TAG_REGEX)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Extract all tags from markup
|
|
128
|
+
# @param markup [String] Markup text
|
|
129
|
+
# @return [Array<Hash>] Array of tag info
|
|
130
|
+
def extract_tags(markup)
|
|
131
|
+
tags = []
|
|
132
|
+
|
|
133
|
+
markup.scan(TAG_REGEX) do
|
|
134
|
+
match = Regexp.last_match
|
|
135
|
+
tags << {
|
|
136
|
+
position: match.begin(0),
|
|
137
|
+
closing: !match[:closing].nil?,
|
|
138
|
+
tag: match[:tag].to_s.strip,
|
|
139
|
+
full_match: match[0]
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
tags
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Validate markup (check for unclosed tags)
|
|
147
|
+
# @param markup [String] Markup to validate
|
|
148
|
+
# @return [Array<String>] List of errors (empty if valid)
|
|
149
|
+
def validate(markup)
|
|
150
|
+
errors = []
|
|
151
|
+
open_tags = []
|
|
152
|
+
|
|
153
|
+
extract_tags(markup).each do |tag|
|
|
154
|
+
if tag[:closing]
|
|
155
|
+
if open_tags.empty?
|
|
156
|
+
errors << "Unexpected closing tag [/#{tag[:tag]}] at position #{tag[:position]}"
|
|
157
|
+
else
|
|
158
|
+
# In Rich, [/] closes the LAST tag, [ /tag] closes specific tag
|
|
159
|
+
# Let's keep it simple for now: pop last.
|
|
160
|
+
# If tag name matches, pop it. If it doesn't match and not empty, it's an error.
|
|
161
|
+
last_tag = open_tags.pop
|
|
162
|
+
if !tag[:tag].empty? && tag[:tag] != last_tag
|
|
163
|
+
errors << "Mismatched closing tag [/#{tag[:tag]}] for [#{last_tag}]"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
open_tags << tag[:tag]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
open_tags.each do |tag|
|
|
172
|
+
errors << "Unclosed tag [#{tag}]"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
errors
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Check if markup is valid
|
|
179
|
+
# @param markup [String] Markup to check
|
|
180
|
+
# @return [Boolean]
|
|
181
|
+
def valid?(markup)
|
|
182
|
+
validate(markup).empty?
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|