class TTY::Markdown::Converter

Converts a Kramdown::Document tree to a terminal friendly output

Constants

NEWLINE
SPACE

Public Class Methods

new(root, options = {}) click to toggle source
Calls superclass method
# File lib/tty/markdown/converter.rb, line 18
def initialize(root, options = {})
  super
  @current_indent = 0
  @indent = options[:indent]
  @pastel = Pastel.new(enabled: options[:enabled])
  @color_opts = { mode: options[:mode],
                  color: @pastel.yellow.detach,
                  enabled: options[:enabled] }
  @width = options[:width]
  @theme = options[:theme].each_with_object({}) do |(key, val), acc|
             acc[key] = Array(val)
           end
  @symbols = options[:symbols]
  @footnote_no = 1
  @footnotes = {}
end

Public Instance Methods

convert(el, opts = { indent: 0 }) click to toggle source

Invoke an element conversion

@api public

# File lib/tty/markdown/converter.rb, line 38
def convert(el, opts = { indent: 0 })
  send("convert_#{el.type}", el, opts)
end

Private Instance Methods

border(column_widths, location) click to toggle source

Render horizontal border line

@param [Array<Integer>] column_widths

the table column widths

@param [Symbol] location

location out of :top, :mid, :bottom

@return [String]

@api private

# File lib/tty/markdown/converter.rb, line 479
def border(column_widths, location)
  result = []
  result << @symbols[:"#{location}_left"]
  column_widths.each.with_index do |width, i|
    result << @symbols[:"#{location}_center"] if i != 0
    result << (@symbols[:line] * (width + 2))
  end
  result << @symbols[:"#{location}_right"]
  @pastel.decorate(result.join, *@theme[:table])
end
convert_a(el, opts) click to toggle source

Convert a element

@param [Kramdown::Element] el

the `kd:a` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 643
def convert_a(el, opts)
  result = []

  if URI.parse(el.attr["href"]).class == URI::MailTo
    el.attr["href"] = URI.parse(el.attr["href"]).to
  end

  if el.children.size == 1 && el.children[0].type == :text &&
     el.children[0].value == el.attr["href"]

    if !el.attr["title"].nil? && !el.attr["title"].strip.empty?
      result << "(#{el.attr["title"]}) "
    end
    result << @pastel.decorate(el.attr["href"], *@theme[:link])

  elsif el.children.size > 0  &&
       (el.children[0].type != :text || !el.children[0].value.strip.empty?)

    content = inner(el, opts)

    result << content.join
    result << " #{@symbols[:arrow]} "
    if el.attr["title"]
      result << "(#{el.attr["title"]}) "
    end
    result << @pastel.decorate(el.attr["href"], *@theme[:link])
  end
  result
end
convert_abbreviation(el, opts) click to toggle source

Convert abbreviation element

@param [Kramdown::Element] el

the `kd:abbreviation` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 697
def convert_abbreviation(el, opts)
  title = @root.options[:abbrev_defs][el.value]
  if title.to_s.empty?
    el.value
  else
    "#{el.value}(#{title})"
  end
end
convert_blank(*) click to toggle source

Convert new line element

@param [Kramdown::Element] el

the `kd:blank` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 210
def convert_blank(*)
  NEWLINE
end
convert_blockquote(el, opts) click to toggle source

Convert blockquote element

@param [Kramdown::Element] el

the `kd:blockquote` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 266
def convert_blockquote(el, opts)
  indent = SPACE * @current_indent
  bar_symbol = @symbols[:bar]
  prefix = "#{indent}#{@pastel.decorate(bar_symbol, *@theme[:quote])}  "

  content = inner(el, opts)

  content.join.lines.map do |line|
    prefix + line
  end
end
convert_br(el, opts) click to toggle source
# File lib/tty/markdown/converter.rb, line 617
def convert_br(el, opts)
  NEWLINE
end
convert_codeblock(el, opts) click to toggle source

Convert codeblock element

@param [Kramdown::Element] el

the `kd:codeblock` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 253
def convert_codeblock(el, opts)
  indent = SPACE * @current_indent
  indent + convert_codespan(el, opts)
end
convert_codespan(el, opts) click to toggle source

Convert codespan element

@param [Kramdown::Element] el

the `kd:codespan` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 234
def convert_codespan(el, opts)
  indent = SPACE * @current_indent
  syntax_opts = @color_opts.merge(lang: el.options[:lang])
  raw_code = Strings.wrap(el.value, @width - @current_indent)
  highlighted = SyntaxHighliter.highlight(raw_code, **syntax_opts)

  highlighted.lines.map.with_index do |line, i|
    i.zero? ? line.chomp : indent + line.chomp
  end.join(NEWLINE)
end
convert_comment(el, opts)
Alias for: convert_xml_comment
convert_dd(el, opts) click to toggle source

Convert dd element

@param [Kramdown::Element] el

the `kd:dd` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 337
def convert_dd(el, opts)
  result = []
  @current_indent += @indent unless opts[:parent].type == :root
  content = inner(el, opts)
  @current_indent -= @indent unless opts[:parent].type == :root
  result << content.join
  result << NEWLINE if opts[:next] && opts[:next].type == :dt
  result
end
convert_dl(el, opts)
Alias for: convert_ul
convert_dt(el, opts) click to toggle source

Convert dt element

@param [Kramdown::Element] el

the `kd:dt` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 323
def convert_dt(el, opts)
  indent = SPACE * @current_indent
  content = inner(el, opts)
  indent + content.join + NEWLINE
end
convert_em(el, opts) click to toggle source

Convert em element

@param [Kramdown::Element] el

the `kd:em` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 194
def convert_em(el, opts)
  content = inner(el, opts)

  content.join.lines.map do |line|
    @pastel.decorate(line.chomp, *@theme[:em])
  end.join(NEWLINE)
end
convert_entity(el, opts) click to toggle source
# File lib/tty/markdown/converter.rb, line 710
def convert_entity(el, opts)
  unicode_char(el.value.code_point)
end
convert_footnote(el, opts) click to toggle source

Convert image element

@param [Kramdown::Element] element

the `kd:footnote` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 727
def convert_footnote(el, opts)
  name = el.options[:name]
  if footnote = @footnotes[name]
    number = footnote.last
  else
    number = @footnote_no
    @footnote_no += 1
    @footnotes[name] = [el.value, number]
  end

  content = "#{@symbols[:bracket_left]}#{number}#{@symbols[:bracket_right]}"
  @pastel.decorate(content, *@theme[:note])
end
convert_header(el, opts) click to toggle source

Convert header element

@param [Kramdown::Element] el

the `kd:header` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 106
def convert_header(el, opts)
  level = el.options[:level]
  if opts[:parent] && opts[:parent].type == :root
    # Header determines indentation only at top level
    @current_indent = (level - 1) * @indent
    indent = SPACE * (level - 1) * @indent
  else
    indent = SPACE * @current_indent
  end
  styles = @theme[:header].dup
  styles << :underline if level == 1

  content = inner(el, opts)

  content.join.lines.map do |line|
    indent + @pastel.decorate(line.chomp, *styles) + NEWLINE
  end
end
convert_hr(el, opts) click to toggle source

Convert hr element

@param [Kramdown::Element] el

the `kd:hr` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 629
def convert_hr(el, opts)
  width = @width - @symbols[:diamond].length * 2
  line = @symbols[:diamond] + @symbols[:line] * width + @symbols[:diamond]
  @pastel.decorate(line, *@theme[:hr]) + NEWLINE
end
convert_html_element(el, opts) click to toggle source

Convert html element

@param [Kramdown::Element] element

the `kd:html_element` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 772
def convert_html_element(el, opts)
  if el.value == "div"
    inner(el, opts)
  elsif %w[i em].include?(el.value)
    convert_em(el, opts)
  elsif %w[b strong].include?(el.value)
    convert_strong(el, opts)
  elsif el.value == "img"
    convert_img(el, opts)
  elsif el.value == "a"
    convert_a(el, opts)
  elsif el.value == "del"
    inner(el, opts).join.chars.to_a.map do |char|
      char + @symbols[:delete]
    end
  elsif el.value == "br"
    NEWLINE
  elsif !el.children.empty?
    inner(el, opts)
  else
    warning("HTML element '#{el.value.inspect}' not supported")
    ""
  end
end
convert_img(el, opts) click to toggle source

Convert image element

@param [Kramdown::Element] element

the `kd:img` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 753
def convert_img(el, opts)
  src = el.attr["src"]
  alt = el.attr["alt"]
  link = [@symbols[:paren_left]]
  unless alt.to_s.empty?
    link << "#{alt} #{@symbols[:ndash]} "
  end
  link << "#{src}#{@symbols[:paren_right]}"
  @pastel.decorate(link.join, *@theme[:image])
end
convert_li(el, opts) click to toggle source

Convert list element

@param [Kramdown::Element] el

the `kd:li` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 303
def convert_li(el, opts)
  index = opts[:index] + 1
  indent = SPACE * @current_indent
  prefix_type = opts[:parent].type == :ol ? "#{index}." : @symbols[:bullet]
  prefix = @pastel.decorate(prefix_type, *@theme[:list]) + SPACE
  opts[:strip] = true

  content = inner(el, opts)

  indent + prefix + content.join
end
convert_math(el, opts) click to toggle source

Convert math element

@param [Kramdown::Element] el

the `kd:math` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 681
def convert_math(el, opts)
  if el.options[:category] == :block
    convert_codeblock(el, opts) + NEWLINE
  else
    convert_codespan(el, opts)
  end
end
convert_ol(el, opts)
Alias for: convert_ul
convert_p(el, opts) click to toggle source

Convert paragraph element

@param [Kramdown::Element] el

the `kd:p` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 133
def convert_p(el, opts)
  indent = SPACE * @current_indent
  result = []

  if ![:blockquote, :li].include?(opts[:parent].type)
    result << indent
  end

  opts[:indent] = @current_indent
  if opts[:parent].type == :blockquote
    opts[:indent] = 0
  end

  content = inner(el, opts)

  result << content.join
  unless result.last.to_s.end_with?(NEWLINE)
    result << NEWLINE
  end
  result
end
convert_raw(*) click to toggle source
# File lib/tty/markdown/converter.rb, line 741
def convert_raw(*)
  warning("Raw content is not supported")
end
convert_root(el, opts) click to toggle source

Convert root element

@param [Kramdown::Element] el

the `kd:root` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 71
def convert_root(el, opts)
  content = inner(el, opts)
  return content.join if @footnotes.empty?

  content.join + footnotes_list(root, opts)
end
convert_smart_quote(el, opts) click to toggle source

Convert smart quote element

@param [Kramdown::Element] el

the `kd:smart_quote` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 222
def convert_smart_quote(el, opts)
  @symbols[el.value]
end
convert_strong(el, opts) click to toggle source

Convert strong element

@param [Kramdown::Element] element

the `kd:strong` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 178
def convert_strong(el, opts)
  content = inner(el, opts)

  content.join.lines.map do |line|
    @pastel.decorate(line.chomp, *@theme[:strong])
  end.join(NEWLINE)
end
convert_table(el, opts) click to toggle source

Convert table element

@param [Kramdown::Element] el

the `kd:table` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 355
def convert_table(el, opts)
  @row = 0
  @column = 0
  opts[:alignment] = el.options[:alignment]
  opts[:table_data] = extract_table_data(el, opts)
  opts[:column_widths] = distribute_widths(max_widths(opts[:table_data]))
  opts[:row_heights] = max_row_heights(opts[:table_data], opts[:column_widths])

  inner(el, opts).join
end
convert_tbody(el, opts) click to toggle source

Convert tbody element

@param [Kramdown::Element] el

the `kd:tbody` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 498
def convert_tbody(el, opts)
  indent = SPACE * @current_indent
  result = []

  result << indent
  if opts[:prev] && opts[:prev].type == :thead
    result << border(opts[:column_widths], :mid)
  else
    result << border(opts[:column_widths], :top)
  end
  result << "\n"

  content = inner(el, opts)

  result << content.join
  result << indent
  if opts[:next] && opts[:next].type == :tfoot
    result << border(opts[:column_widths], :mid)
  else
    result << border(opts[:column_widths], :bottom)
  end
  result << NEWLINE
  result.join
end
convert_td(el, opts) click to toggle source

Convert td element

@param [Kramdown::Element] el

the `kd:td` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 588
def convert_td(el, opts)
  indent = SPACE * @current_indent
  pipe_char = @symbols[:pipe]
  pipe = @pastel.decorate(pipe_char, *@theme[:table])
  suffix = " #{pipe} "

  cell_content = inner(el, opts)
  cell_width = opts[:column_widths][@column]
  cell_height = opts[:row_heights][@row]
  alignment = opts[:alignment][@column]
  align_opts = alignment == :default ? {} : { direction: alignment }

  wrapped = Strings.wrap(cell_content.join, cell_width)
  aligned = Strings.align(wrapped, cell_width, **align_opts)
  padded = if aligned.lines.size < cell_height
             Strings.pad(aligned, [0, 0, cell_height - aligned.lines.size, 0])
           else
             aligned.dup
           end

  content =  padded.lines.map do |line|
    # add pipe to first column
    (@column.zero? ? "#{indent}#{pipe} " : "") +
      (line.end_with?("\n") ? line.insert(-2, suffix) : line << suffix)
  end
  @column = (@column + 1) % opts[:column_widths].size
  content
end
convert_text(el, opts) click to toggle source

Convert text element

@param [Kramdown::Element] element

the `kd:text` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 163
def convert_text(el, opts)
  text = Strings.wrap(el.value, @width - @current_indent)
  text = text.chomp if opts[:strip]
  indent = SPACE * opts[:indent]
  text.gsub(/\n/, "#{NEWLINE}#{indent}")
end
convert_tfoot(el, opts) click to toggle source

Convert tfoot element

@param [Kramdown::Element] el

the `kd:tfoot` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 531
def convert_tfoot(el, opts)
  indent = SPACE * @current_indent

  inner(el, opts).join + indent +
    border(opts[:column_widths], :bottom) +
    NEWLINE
end
convert_thead(el, opts) click to toggle source

Convert thead element

@param [Kramdown::Element] el

the `kd:thead` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 455
def convert_thead(el, opts)
  indent = SPACE * @current_indent
  result = []

  result << indent
  result << border(opts[:column_widths], :top)
  result << NEWLINE

  content = inner(el, opts)

  result << content.join
  result.join
end
convert_tr(el, opts) click to toggle source

Convert td element

@param [Kramdown::Element] el

the `kd:td` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 547
def convert_tr(el, opts)
  indent = SPACE * @current_indent
  result = []

  if opts[:prev] && opts[:prev].type == :tr
    result << indent
    result << border(opts[:column_widths], :mid)
    result << NEWLINE
  end

  content = inner(el, opts)

  columns = content.count

  row = content.each_with_index.reduce([]) do |acc, (cell, i)|
    if cell.size > 1 # multiline
      cell.each_with_index do |c, j| # zip columns
        acc[j] = [] if acc[j].nil?
        acc[j] << c.chomp
        acc[j] << "\n" if i == (columns - 1)
      end
    else
      acc << cell
      acc << "\n" if i == (columns - 1)
    end
    acc
  end.join

  result << row
  @row += 1
  result.join
end
convert_typographic_sym(el, opts) click to toggle source
# File lib/tty/markdown/converter.rb, line 706
def convert_typographic_sym(el, opts)
  @symbols[el.value]
end
convert_ul(el, opts) click to toggle source

Convert ordered and unordered list element

@param [Kramdown::Element] el

the `kd:ul` or `kd:ol` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 286
def convert_ul(el, opts)
  @current_indent += @indent unless opts[:parent].type == :root
  content = inner(el, opts)
  @current_indent -= @indent unless opts[:parent].type == :root
  content.join
end
Also aliased as: convert_ol, convert_dl
convert_xml_comment(el, opts) click to toggle source

Convert xml comment element

@param [Kramdown::Element] element

the `kd:xml_comment` element

@param [Hash] opts

the element options

@api private

# File lib/tty/markdown/converter.rb, line 805
def convert_xml_comment(el, opts)
  block = el.options[:category] == :block
  indent = SPACE * @current_indent
  content = el.value
  content.gsub!(/^<!-{2,}\s*/, "") if content.start_with?("<!--")
  content.gsub!(/-{2,}>$/, "") if content.end_with?("-->")
  result = content.lines.map.with_index do |line, i|
    (i.zero? && !block ? "" : indent) +
      @pastel.decorate("#{@symbols[:hash]} " + line.chomp,
                       *@theme[:comment])
  end.join(NEWLINE)
  block ? result + NEWLINE : result
end
Also aliased as: convert_comment
distribute_widths(widths) click to toggle source

Distribute column widths inside total width

@return [Array<Integer>]

@api private

# File lib/tty/markdown/converter.rb, line 389
def distribute_widths(widths)
  indent = SPACE * @current_indent
  total_width = widths.reduce(&:+)
  screen_width = @width - (indent.length + 1) * 2 - (widths.size + 1)
  return widths if total_width <= screen_width

  extra_width = total_width - screen_width

  widths.map do |w|
    ratio = w / total_width.to_f
    w - (extra_width * ratio).floor
  end
end
extract_table_data(el, opts) click to toggle source

Extract table data

@param [Kramdown::Element] el

the `kd:table` element

@api private

# File lib/tty/markdown/converter.rb, line 372
def extract_table_data(el, opts)
  el.children.each_with_object([]) do |container, data|
    container.children.each do |row|
      data_row = []
      row.children.each do |cell|
        data_row << inner(cell, opts)
      end
      data << data_row
    end
  end
end
footnotes_list(root, opts) click to toggle source

Create an ordered list of footnotes

@param [Kramdown::Element] root

the `kd:root` element

@param [Hash] opts

the root element options

@api private

# File lib/tty/markdown/converter.rb, line 86
def footnotes_list(root, opts)
  ol = Kramdown::Element.new(:ol)
  @footnotes.values.each do |footnote|
    value, index = *footnote
    options = { index: index, parent: ol }
    li = Kramdown::Element.new(:li, nil, {}, options.merge(opts))
    li.children = Marshal.load(Marshal.dump(value.children))
    ol.children << li
  end
  convert_ol(ol, { parent: root }.merge(opts))
end
inner(el, opts) click to toggle source

Process children of this element

@param [Kramdown::Element] el

the element with child elements

@api private

# File lib/tty/markdown/converter.rb, line 50
def inner(el, opts)
  result = []
  el.children.each_with_index do |inner_el, i|
    options = opts.dup
    options[:parent] = el
    options[:prev] = (i.zero? ? nil : el.children[i - 1])
    options[:next] = (i == el.children.length - 1 ? nil : el.children[i + 1])
    options[:index] = i
    result << convert(inner_el, options)
  end
  result
end
max_row_height(row, column_widths) click to toggle source

Calculate maximum cell height for a given row

@return [Integer]

@api private

# File lib/tty/markdown/converter.rb, line 441
def max_row_height(row, column_widths)
  row.map.with_index do |column, col_index|
    Strings.wrap(column.join, column_widths[col_index]).lines.size
  end.max
end
max_row_heights(table_data, column_widths) click to toggle source

Calculate maximum heights for each row

@return [Array<Integer>]

@api private

# File lib/tty/markdown/converter.rb, line 430
def max_row_heights(table_data, column_widths)
  table_data.reduce([]) do |acc, row|
    acc << max_row_height(row, column_widths)
  end
end
max_width(table_data, col) click to toggle source

Calculate maximum cell width for a given column

@return [Integer]

@api private

# File lib/tty/markdown/converter.rb, line 419
def max_width(table_data, col)
  table_data.map do |row|
    Strings.sanitize(row[col].join).lines.map(&:length).max || 0
  end.max
end
max_widths(table_data) click to toggle source

Calculate maximum widths for each column

@return [Array<Integer>]

@api private

# File lib/tty/markdown/converter.rb, line 408
def max_widths(table_data)
  table_data.first.each_with_index.reduce([]) do |acc, (*, col)|
    acc << max_width(table_data, col)
  end
end
unicode_char(codepoint) click to toggle source

Convert codepoint to UTF-8 representation

# File lib/tty/markdown/converter.rb, line 715
def unicode_char(codepoint)
  [codepoint].pack("U*")
end