Module: Rich::Markup
- Defined in:
- lib/rich/markup.rb
Overview
Parser for Rich markup syntax: [style]text
Constant Summary collapse
- TAG_REGEX =
Tag regex for matching markup tags, excluding escaped ones
/(?<!\\)\[(?<closing>\/)?(?<tag>[^\[\]\/]*)\]/
Class Method Summary collapse
-
.contains_markup?(text) ⇒ Boolean
Check if text contains markup.
-
.escape(text) ⇒ String
Escape text for use in markup (escape square brackets).
-
.extract_tags(markup) ⇒ Array<Hash>
Extract all tags from markup.
-
.parse(markup, style: nil) ⇒ Text
Parse markup into a Text object.
-
.render(markup, color_system: ColorSystem::TRUECOLOR) ⇒ String
Render markup directly to ANSI string.
-
.strip(markup) ⇒ String
Strip markup tags from text.
-
.unescape(text) ⇒ String
Unescape markup text.
-
.valid?(markup) ⇒ Boolean
Check if markup is valid.
-
.validate(markup) ⇒ Array<String>
Validate markup (check for unclosed tags).
Class Method Details
.contains_markup?(text) ⇒ Boolean
Check if text contains markup
123 124 125 |
# File 'lib/rich/markup.rb', line 123 def contains_markup?(text) text.match?(TAG_REGEX) end |
.escape(text) ⇒ String
Escape text for use in markup (escape square brackets)
102 103 104 |
# File 'lib/rich/markup.rb', line 102 def escape(text) text.gsub(/[\[\]]/) { |m| "\\#{m}" } end |
.extract_tags(markup) ⇒ Array<Hash>
Extract all tags from markup
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/rich/markup.rb', line 130 def (markup) = [] markup.scan(TAG_REGEX) do match = Regexp.last_match << { position: match.begin(0), closing: !match[:closing].nil?, tag: match[:tag].to_s.strip, full_match: match[0] } end end |
.parse(markup, style: nil) ⇒ Text
Parse markup into a Text object
21 22 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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/rich/markup.rb', line 21 def parse(markup, style: nil) result_text = Text.new(style: style) # Each stack entry is [tag_name, style] so closing tags can match by # name (matching validate's semantics and Python Rich). style_stack = [] pos = 0 markup.scan(TAG_REGEX) do match = Regexp.last_match tag_start = match.begin(0) # Add text before tag if tag_start > pos pre_text = unescape(markup[pos...tag_start]) start_pos = result_text.length result_text.append(pre_text) # Apply stacked styles to this text style_stack.each do |(_name, stacked_style)| result_text.spans << Span.new(start_pos, result_text.length, stacked_style) end end # Process tag if match[:closing] # Closing tag - close the matching open tag by name; a bare [/] # closes the most recently opened tag. tag_name = match[:tag].strip unless style_stack.empty? if tag_name.empty? style_stack.pop else idx = style_stack.rindex { |(name, _)| name == tag_name } idx ? style_stack.delete_at(idx) : style_stack.pop end end else # Opening tag - parse and push style tag_content = match[:tag].strip if tag_content.empty? # Literal [] result_text.append("[]") else begin parsed_style = Style.parse(tag_content) style_stack << [tag_content, parsed_style] rescue StandardError # Invalid style, treat as literal text result_text.append("[#{tag_content}]") end end end pos = match.end(0) end # Add remaining text if pos < markup.length remaining = unescape(markup[pos..]) start_pos = result_text.length result_text.append(remaining) style_stack.each do |(_name, stacked_style)| result_text.spans << Span.new(start_pos, result_text.length, stacked_style) end end result_text end |
.render(markup, color_system: ColorSystem::TRUECOLOR) ⇒ String
Render markup directly to ANSI string
95 96 97 |
# File 'lib/rich/markup.rb', line 95 def render(markup, color_system: ColorSystem::TRUECOLOR) parse(markup).render(color_system: color_system) end |
.strip(markup) ⇒ String
Strip markup tags from text
116 117 118 |
# File 'lib/rich/markup.rb', line 116 def strip(markup) markup.gsub(TAG_REGEX, "") end |
.unescape(text) ⇒ String
Unescape markup text
109 110 111 |
# File 'lib/rich/markup.rb', line 109 def unescape(text) text.gsub(/\\([\[\]\\])/, '\1') end |
.valid?(markup) ⇒ Boolean
Check if markup is valid
181 182 183 |
# File 'lib/rich/markup.rb', line 181 def valid?(markup) validate(markup).empty? end |
.validate(markup) ⇒ Array<String>
Validate markup (check for unclosed tags)
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 176 |
# File 'lib/rich/markup.rb', line 149 def validate(markup) errors = [] = [] (markup).each do |tag| if tag[:closing] if .empty? errors << "Unexpected closing tag [/#{tag[:tag]}] at position #{tag[:position]}" else # In Rich, [/] closes the LAST tag, [ /tag] closes specific tag # Let's keep it simple for now: pop last. # If tag name matches, pop it. If it doesn't match and not empty, it's an error. last_tag = .pop if !tag[:tag].empty? && tag[:tag] != last_tag errors << "Mismatched closing tag [/#{tag[:tag]}] for [#{last_tag}]" end end else << tag[:tag] end end .each do |tag| errors << "Unclosed tag [#{tag}]" end errors end |