Class: Fatty::AnsiRenderer

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

Constant Summary collapse

HARD_BREAK =
"\uE000"
CELL_SEP =
"\u001F"
TABLE_BAR =
""
TABLE_DASH =
""

Instance Method Summary collapse

Constructor Details

#initialize(width: 80, palette: nil, theme: nil, truecolor: false) ⇒ AnsiRenderer

Returns a new instance of AnsiRenderer.



10
11
12
13
14
15
16
# File 'lib/fatty/markdown/ansi_renderer.rb', line 10

def initialize(width: 80, palette: nil, theme: nil, truecolor: false)
  super()
  @width = width.to_i
  @palette = palette || {}
  @theme = theme || {}
  @truecolor = truecolor
end

Instance Method Details



84
85
86
# File 'lib/fatty/markdown/ansi_renderer.rb', line 84

def autolink(link, _link_type)
  md(link.to_s, :markdown_link)
end

#block_code(code, language) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/fatty/markdown/ansi_renderer.rb', line 18

def block_code(code, language)
  lexer = rouge_lexer(language.to_s, code.to_s)
  formatter = rouge_formatter
  gutter = md("", :markdown_code_gutter)

  highlighted = formatter.format(lexer.lex(code.to_s))
  lines = highlighted.lines.map(&:chomp)

  lines.pop while lines.any? && Fatty::Ansi.plain_text(lines.last).strip.empty?

  body = lines.map { |line|
    "#{gutter}#{line}"
  }.join("\n")

  "#{body}\n\n"
end

#block_quote(quote) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/fatty/markdown/ansi_renderer.rb', line 92

def block_quote(quote)
  gutter = md("", :markdown_quote_gutter)
  quote = CGI.unescapeHTML(quote.to_s)
  paragraphs = quote.to_s.split(/\n{2,}/).map do |para|
    text = para.lines.map(&:strip).reject(&:empty?).join(" ")

    if text.empty?
      gutter
    else
      wrap(text, first_prefix: gutter, rest_prefix: gutter)
    end
  end
  paragraphs.join("\n#{gutter}\n") + "\n\n"
end

#codespan(code) ⇒ Object



67
68
69
# File 'lib/fatty/markdown/ansi_renderer.rb', line 67

def codespan(code)
  md(code.to_s, :markdown_code)
end

#double_emphasis(text) ⇒ Object



71
72
73
# File 'lib/fatty/markdown/ansi_renderer.rb', line 71

def double_emphasis(text)
  md(text.to_s, :markdown_strong)
end

#emphasis(text) ⇒ Object

Curses does not reliably render true italics, so we use underline instead.



76
77
78
# File 'lib/fatty/markdown/ansi_renderer.rb', line 76

def emphasis(text)
  md(text.to_s, :markdown_emphasis)
end

#h1(text) ⇒ Object



241
242
243
# File 'lib/fatty/markdown/ansi_renderer.rb', line 241

def h1(text)
  md(text.to_s, :markdown_h1)
end

#h2(text) ⇒ Object



245
246
247
# File 'lib/fatty/markdown/ansi_renderer.rb', line 245

def h2(text)
  md(text.to_s, :markdown_h2)
end

#h3(text) ⇒ Object



249
250
251
# File 'lib/fatty/markdown/ansi_renderer.rb', line 249

def h3(text)
  md(text.to_s, :markdown_h3)
end

#header(text, level) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/fatty/markdown/ansi_renderer.rb', line 107

def header(text, level)
  body =
    case level
    when 1
      h1(text)
    when 2
      h2(text)
    when 3
      h3(text)
    else
      Rainbow(text.to_s).bright.to_s
    end
  "#{body}\n\n"
end

#highlight(text) ⇒ Object



80
81
82
# File 'lib/fatty/markdown/ansi_renderer.rb', line 80

def highlight(text)
  md(text.to_s, :markdown_highlight)
end

#hruleObject



122
123
124
# File 'lib/fatty/markdown/ansi_renderer.rb', line 122

def hrule
  "#{md(TABLE_DASH * @width.to_i.clamp(20, 80), :markdown_hrule)}\n\n"
end

#indent_block(text, prefix) ⇒ Object



156
157
158
159
160
161
162
163
164
# File 'lib/fatty/markdown/ansi_renderer.rb', line 156

def indent_block(text, prefix)
  text.to_s.lines.map { |line|
    if line.strip.empty?
      line
    else
      "#{prefix}#{line}"
    end
  }.join
end

#inline(text) ⇒ Object



237
238
239
# File 'lib/fatty/markdown/ansi_renderer.rb', line 237

def inline(text)
  text.to_s
end

#linebreakObject



126
127
128
# File 'lib/fatty/markdown/ansi_renderer.rb', line 126

def linebreak
  "\n"
end


62
63
64
65
# File 'lib/fatty/markdown/ansi_renderer.rb', line 62

def link(link, _title, content)
  label = content.to_s.empty? ? link : content
  "#{md(label, :markdown_link)} #{md("<#{link}>", :markdown_url)}"
end

#list(contents, _type) ⇒ Object



134
135
136
# File 'lib/fatty/markdown/ansi_renderer.rb', line 134

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

#list_item(text, _type) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/fatty/markdown/ansi_renderer.rb', line 138

def list_item(text, _type)
  text = render_inline_html(CGI.unescapeHTML(text.to_s))
  head, rest = text.to_s.strip.split(/\n+/, 2)
  out = wrap(
    head.to_s.strip,
    first_prefix: "",
    rest_prefix: "    ",
  )

  if rest && !rest.empty?
    out << "\n"
    out << indent_block(rest.rstrip, "  ")
  end

  out << "\n"
  out
end

#normal_text(text) ⇒ Object



35
36
37
38
# File 'lib/fatty/markdown/ansi_renderer.rb', line 35

def normal_text(text)
  text = render_inline_html(CGI.unescapeHTML(text.to_s))
  text
end

#pad_visible(text, width) ⇒ Object



231
232
233
234
235
# File 'lib/fatty/markdown/ansi_renderer.rb', line 231

def pad_visible(text, width)
  padding = width - Fatty::Ansi.visible_length(text)
  padding = 0 if padding.negative?
  "#{text}#{' ' * padding}"
end

#paragraph(text) ⇒ Object



130
131
132
# File 'lib/fatty/markdown/ansi_renderer.rb', line 130

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

#quote(text) ⇒ Object



88
89
90
# File 'lib/fatty/markdown/ansi_renderer.rb', line 88

def quote(text)
  "#{text}"
end

#raw_html(html) ⇒ Object



40
41
42
43
# File 'lib/fatty/markdown/ansi_renderer.rb', line 40

def raw_html(html)
  text = CGI.unescapeHTML(html.to_s)
  render_inline_html(text)
end

#render_inline_html(text) ⇒ Object



45
46
47
48
49
50
51
# File 'lib/fatty/markdown/ansi_renderer.rb', line 45

def render_inline_html(text)
  text.to_s
    .gsub(%r{<br\s*/?>}i, HARD_BREAK)
    .gsub(%r{<span\s+class=["']underline["']>(.*?)</span>}m) do
      md(Regexp.last_match(1), :markdown_underline)
  end
end

#render_table_row_cells(row, widths, header: false) ⇒ Object



213
214
215
216
217
218
219
220
221
# File 'lib/fatty/markdown/ansi_renderer.rb', line 213

def render_table_row_cells(row, widths, header: false)
  cells = widths.each_with_index.map do |width, index|
    text = row[index].to_s
    text = header ? th(text) : td(text)
    pad_visible(text, width)
  end

  "#{cells.join('')}"
end

#render_table_separator(widths) ⇒ Object



223
224
225
226
227
228
229
# File 'lib/fatty/markdown/ansi_renderer.rb', line 223

def render_table_separator(widths)
  parts = widths.map do |width|
    TABLE_DASH * (width + 2)
  end

  "  #{TABLE_BAR}#{parts.join(TABLE_BAR)}#{TABLE_BAR}"
end

#rouge_lexer(language, code) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/fatty/markdown/ansi_renderer.rb', line 261

def rouge_lexer(language, code)
  lexer =
    if !language.empty?
      Rouge::Lexer.find_fancy(language, code)
    else
      Rouge::Lexer.guess(source: code)
    end

  lexer || Rouge::Lexers::PlainText.new
rescue StandardError
  Rouge::Lexers::PlainText.new
end

#strikethrough(text) ⇒ Object

Curses does not support strike-through, but this emulates it with unicode "combining characters" by adding a "Long Stroke Overlay" (U+0366) after each character, which overlays each with a strike-through overlay. We just had to make sure that Ansi.visible_length takes these into account in computing width.



58
59
60
# File 'lib/fatty/markdown/ansi_renderer.rb', line 58

def strikethrough(text)
  text.to_s.each_char.map { |ch| "#{ch}\u0336" }.join
end

#table(header, body) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/fatty/markdown/ansi_renderer.rb', line 170

def table(header, body)
  rows = (header + body).lines.map do |line|
    line.chomp.split(CELL_SEP, -1)
  end

  widths = table_widths(rows)

  rendered = rows.each_with_index.map do |row, index|
    header_row = index.zero?

    line = render_table_row_cells(row, widths, header: header_row)

    if header_row
      [line, render_table_separator(widths)].join("\n")
    else
      line
    end
  end

  rendered.join("\n") + "\n\n"
end

#table_cell(content, _alignment) ⇒ Object



196
197
198
# File 'lib/fatty/markdown/ansi_renderer.rb', line 196

def table_cell(content, _alignment)
  inline(content.strip) + CELL_SEP
end

#table_row(content) ⇒ Object



192
193
194
# File 'lib/fatty/markdown/ansi_renderer.rb', line 192

def table_row(content)
  content.chomp.delete_suffix(CELL_SEP) + "\n"
end

#table_widths(rows) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/fatty/markdown/ansi_renderer.rb', line 200

def table_widths(rows)
  widths = []

  rows.each do |row|
    row.each_with_index do |cell, index|
      width = Fatty::Ansi.visible_length(cell)
      widths[index] = [widths[index] || 0, width].max
    end
  end

  widths
end

#td(text) ⇒ Object



257
258
259
# File 'lib/fatty/markdown/ansi_renderer.rb', line 257

def td(text)
  md(text.to_s, :markdown_table_cell)
end

#th(text) ⇒ Object



253
254
255
# File 'lib/fatty/markdown/ansi_renderer.rb', line 253

def th(text)
  md(text.to_s, :markdown_table_header)
end