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) ⇒ AnsiRenderer

Returns a new instance of AnsiRenderer.



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

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

Instance Method Details



82
83
84
# File 'lib/fatty/markdown/ansi_renderer.rb', line 82

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

#block_code(code, language) ⇒ Object



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

def block_code(code, language)
  lexer = rouge_lexer(language.to_s, code.to_s)
  formatter = Rouge::Formatters::Terminal256.new
  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



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

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



65
66
67
# File 'lib/fatty/markdown/ansi_renderer.rb', line 65

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

#double_emphasis(text) ⇒ Object



69
70
71
# File 'lib/fatty/markdown/ansi_renderer.rb', line 69

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.



74
75
76
# File 'lib/fatty/markdown/ansi_renderer.rb', line 74

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

#h1(text) ⇒ Object



239
240
241
# File 'lib/fatty/markdown/ansi_renderer.rb', line 239

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

#h2(text) ⇒ Object



243
244
245
# File 'lib/fatty/markdown/ansi_renderer.rb', line 243

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

#h3(text) ⇒ Object



247
248
249
# File 'lib/fatty/markdown/ansi_renderer.rb', line 247

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

#header(text, level) ⇒ Object



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

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



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

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

#hruleObject



120
121
122
# File 'lib/fatty/markdown/ansi_renderer.rb', line 120

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

#indent_block(text, prefix) ⇒ Object



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

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



235
236
237
# File 'lib/fatty/markdown/ansi_renderer.rb', line 235

def inline(text)
  text.to_s
end

#linebreakObject



124
125
126
# File 'lib/fatty/markdown/ansi_renderer.rb', line 124

def linebreak
  "\n"
end


60
61
62
63
# File 'lib/fatty/markdown/ansi_renderer.rb', line 60

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



132
133
134
# File 'lib/fatty/markdown/ansi_renderer.rb', line 132

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

#list_item(text, _type) ⇒ Object



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

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



33
34
35
36
# File 'lib/fatty/markdown/ansi_renderer.rb', line 33

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

#pad_visible(text, width) ⇒ Object



229
230
231
232
233
# File 'lib/fatty/markdown/ansi_renderer.rb', line 229

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

#paragraph(text) ⇒ Object



128
129
130
# File 'lib/fatty/markdown/ansi_renderer.rb', line 128

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

#quote(text) ⇒ Object



86
87
88
# File 'lib/fatty/markdown/ansi_renderer.rb', line 86

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

#raw_html(html) ⇒ Object



38
39
40
41
# File 'lib/fatty/markdown/ansi_renderer.rb', line 38

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

#render_inline_html(text) ⇒ Object



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

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



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

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



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

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



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

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.



56
57
58
# File 'lib/fatty/markdown/ansi_renderer.rb', line 56

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

#table(header, body) ⇒ Object



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

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



194
195
196
# File 'lib/fatty/markdown/ansi_renderer.rb', line 194

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

#table_row(content) ⇒ Object



190
191
192
# File 'lib/fatty/markdown/ansi_renderer.rb', line 190

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

#table_widths(rows) ⇒ Object



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

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



255
256
257
# File 'lib/fatty/markdown/ansi_renderer.rb', line 255

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

#th(text) ⇒ Object



251
252
253
# File 'lib/fatty/markdown/ansi_renderer.rb', line 251

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