Class: RubyRich::Markdown::TerminalRenderer

Inherits:
Redcarpet::Render::Base
  • Object
show all
Defined in:
lib/ruby_rich/markdown.rb

Overview

Converts markdown to ANSI-styled terminal output. Uses Redcarpet for block parsing with custom inline processing.

Constant Summary collapse

INLINE_MARKERS =
{
  # triple-backtick must come before double-backtick
  %r{```(.+?)```}m => ->(m) { codespan_compat(Regexp.last_match(1)) },
  %r{``(.+?)``}m   => ->(m) { codespan_compat(Regexp.last_match(1)) },
  %r{`(.+?)`}      => ->(m) { codespan_compat(Regexp.last_match(1)) },
  %r{\*\*\*(.+?)\*\*\*} => ->(m) { "#{AnsiCode.bold}#{AnsiCode.italic}#{Regexp.last_match(1)}#{AnsiCode.reset}" },
  %r{\*\*(.+?)\*\*}     => ->(m) { "#{AnsiCode.bold}#{Regexp.last_match(1)}#{AnsiCode.reset}" },
  %r{(?<!\*)\*([^*]+)\*(?!\*)} => ->(m) { "#{AnsiCode.italic}#{Regexp.last_match(1)}#{AnsiCode.reset}" },
  %r{~~(.+?)~~}     => ->(m) { "#{AnsiCode.strikethrough}#{Regexp.last_match(1)}#{AnsiCode.reset}" },
  %r{\[([^\]]+)\]\(([^)]+)\)} => ->(m) {
    link_text = Regexp.last_match(1)
    url = Regexp.last_match(2)
    "#{AnsiCode.color(:blue, true)}#{AnsiCode.underline}#{link_text}#{AnsiCode.reset} #{AnsiCode.color(:black, true)}(#{url})#{AnsiCode.reset}"
  }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ TerminalRenderer

Returns a new instance of TerminalRenderer.



24
25
26
27
28
29
30
31
# File 'lib/ruby_rich/markdown.rb', line 24

def initialize(options = {})
  @options = {
    width: 80,
    indent: '  '
  }.merge(options)
  super()
  reset_table_state
end

Class Method Details

.codespan_compat(code) ⇒ Object



158
159
160
# File 'lib/ruby_rich/markdown.rb', line 158

def self.codespan_compat(code)
  "#{AnsiCode.background(:white)}#{AnsiCode.color(:black)} #{code} #{AnsiCode.reset}"
end

Instance Method Details

#block_code(code, language) ⇒ Object



53
54
55
56
57
58
59
60
61
# File 'lib/ruby_rich/markdown.rb', line 53

def block_code(code, language)
  lang = language&.strip
  lang = nil if lang && lang.empty?
  highlighted = Syntax.highlight(code.strip, lang)
  bg  = AnsiCode.background(:black, true)
  fg  = AnsiCode.color(:white, true)
  pad = @options[:indent]
  "#{bg}#{fg}#{indent_lines(highlighted)}#{AnsiCode.reset}\n\n"
end

#block_quote(quote) ⇒ Object



67
68
69
70
71
# File 'lib/ruby_rich/markdown.rb', line 67

def block_quote(quote)
  lines = quote.strip.split("\n")
  quoted_lines = lines.map { |line| "#{AnsiCode.color(:black, true)}#{AnsiCode.color(:white, true)}#{process_inline(line.strip)}" }
  "#{quoted_lines.join("\n")}#{AnsiCode.reset}\n\n"
end

#codespan(code) ⇒ Object



63
64
65
# File 'lib/ruby_rich/markdown.rb', line 63

def codespan(code)
  "#{AnsiCode.background(:white)}#{AnsiCode.color(:black)} #{code} #{AnsiCode.reset}"
end

#double_emphasis(text) ⇒ Object



83
# File 'lib/ruby_rich/markdown.rb', line 83

def double_emphasis(text) = "#{AnsiCode.bold}#{text}#{AnsiCode.reset}"

#emphasis(text) ⇒ Object



82
# File 'lib/ruby_rich/markdown.rb', line 82

def emphasis(text)      = "#{AnsiCode.italic}#{text}#{AnsiCode.reset}"

#header(text, level) ⇒ Object



43
44
45
46
47
48
49
50
51
# File 'lib/ruby_rich/markdown.rb', line 43

def header(text, level)
  processed = process_inline(text)
  case level
  when 1 then "#{AnsiCode.font(:cyan, font_bright: true, bold: true)}#{processed}#{AnsiCode.reset}\n#{AnsiCode.color(:cyan, true)}#{'=' * visible_width(text)}#{AnsiCode.reset}\n\n"
  when 2 then "#{AnsiCode.font(:blue, font_bright: true, bold: true)}#{processed}#{AnsiCode.reset}\n#{AnsiCode.color(:blue, true)}#{'-' * visible_width(text)}#{AnsiCode.reset}\n\n"
  when 3 then "#{AnsiCode.font(:yellow, font_bright: true, bold: true)}### #{processed}#{AnsiCode.reset}\n\n"
  else        "#{AnsiCode.font(:black, font_bright: true, bold: true)}#{'#' * level} #{processed}#{AnsiCode.reset}\n\n"
  end
end

#hruleObject



96
97
98
# File 'lib/ruby_rich/markdown.rb', line 96

def hrule
  "#{AnsiCode.color(:black, true)}#{"" * @options[:width]}#{AnsiCode.reset}\n\n"
end

#image(link, title, alt_text) ⇒ Object



91
92
93
94
# File 'lib/ruby_rich/markdown.rb', line 91

def image(link, title, alt_text)
  title_part = title && !title.empty? ? " - #{title}" : ""
  "#{AnsiCode.color(:magenta, true)}[Image: #{alt_text}]#{AnsiCode.reset} #{AnsiCode.color(:black, true)}(#{link}#{title_part})#{AnsiCode.reset}"
end

#linebreakObject



100
# File 'lib/ruby_rich/markdown.rb', line 100

def linebreak = "\n"


86
87
88
89
# File 'lib/ruby_rich/markdown.rb', line 86

def link(link, title, content)
  title_part = title && !title.empty? ? " - #{title}" : ""
  "#{AnsiCode.color(:blue, true)}#{AnsiCode.underline}#{content}#{AnsiCode.reset} #{AnsiCode.color(:black, true)}(#{link}#{title_part})#{AnsiCode.reset}"
end

#list(contents, list_type) ⇒ Object



78
79
80
# File 'lib/ruby_rich/markdown.rb', line 78

def list(contents, list_type)
  "#{contents}\n"
end

#list_item(text, list_type) ⇒ Object



73
74
75
76
# File 'lib/ruby_rich/markdown.rb', line 73

def list_item(text, list_type)
  marker = list_type == :ordered ? '1.' : ''
  "#{AnsiCode.color(:cyan, true)}#{marker}#{AnsiCode.reset} #{process_inline(text.strip)}\n"
end

#paragraph(text) ⇒ Object

—- block-level callbacks —-



39
40
41
# File 'lib/ruby_rich/markdown.rb', line 39

def paragraph(text)
  "#{process_inline(text)}\n\n"
end

#reset_table_stateObject



33
34
35
# File 'lib/ruby_rich/markdown.rb', line 33

def reset_table_state
  @table_state = { current_row: [], all_rows: [] }
end

#strikethrough(text) ⇒ Object



84
# File 'lib/ruby_rich/markdown.rb', line 84

def strikethrough(text)   = "#{AnsiCode.strikethrough}#{text}#{AnsiCode.reset}"

#table(header, body) ⇒ Object

—- table callbacks —-



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
# File 'lib/ruby_rich/markdown.rb', line 104

def table(header, body)
  all_rows = @table_state[:all_rows]
  reset_table_state
  return "" if all_rows.empty?

  header_line_count = [header.to_s.strip.split("\n").size, 1].max
  header_rows = all_rows[0...header_line_count]
  body_rows = all_rows[header_line_count..] || []

  return "" if header_rows.empty? || body_rows.empty?

  headers = header_rows.last.map { |c| process_inline(c) }
  begin
    tbl = RubyRich::Table.new(headers: headers, border_style: @options[:table_border_style] || :simple)
    body_rows.each do |row|
      processed = row.map { |c| process_inline(c) }
      padded = processed + Array.new([0, headers.length - processed.length].max, "")
      tbl.add_row(padded[0...headers.length])
    end
    return "#{tbl.render}\n\n"
  rescue
    # fallback
  end

  result = "\n"
  result += "#{header.strip}\n"
  result += "#{"-" * [header.strip.length, 20].min}\n"
  result += "#{body.strip}\n" if body && !body.strip.empty?
  "#{result}\n"
end

#table_cell(content, alignment) ⇒ Object



141
142
143
144
# File 'lib/ruby_rich/markdown.rb', line 141

def table_cell(content, alignment)
  @table_state[:current_row] << content.strip
  content
end

#table_row(content) ⇒ Object



135
136
137
138
139
# File 'lib/ruby_rich/markdown.rb', line 135

def table_row(content)
  @table_state[:all_rows] << @table_state[:current_row].dup
  @table_state[:current_row] = []
  "#{content}\n"
end