# -*- coding: utf-8; frozen_string_literal: true -*- # #-- # Copyright (C) 2009-2019 Thomas Leitner # # This file is part of kramdown which is licensed under the MIT. #++ # require 'kramdown/converter' module Kramdown module Converter # Converts a Kramdown::Document to a manpage in groff format. See man(7), groff_man(7) and # man-pages(7) for information regarding the output. class Man < Base def convert(el, opts = {indent: 0, result: +''}) #:nodoc: send("convert_#{el.type}", el, opts) end private def inner(el, opts, use = :all) arr = el.children.reject {|e| e.type == :blank } arr.each_with_index do |inner_el, index| next if use == :rest && index == 0 break if use == :first && index > 0 options = opts.dup options[:parent] = el options[:index] = index options[:prev] = (index == 0 ? nil : arr[index - 1]) options[:next] = (index == arr.length - 1 ? nil : arr[index + 1]) convert(inner_el, options) end end def convert_root(el, opts) @title_done = false opts[:result] = +".\\\" generated by kramdown\n" inner(el, opts) opts[:result] end def convert_blank(*) end alias convert_hr convert_blank alias convert_xml_pi convert_blank def convert_p(el, opts) if (opts[:index] != 0 && opts[:prev].type != :header) || (opts[:parent].type == :blockquote && opts[:index] == 0) opts[:result] << macro("P") end inner(el, opts) newline(opts[:result]) end def convert_header(el, opts) return unless opts[:parent].type == :root case el.options[:level] when 1 unless @title_done @title_done = true data = el.options[:raw_text].scan(/([^(]+)\s*\((\d\w*)\)(?:\s*-+\s*(.*))?/).first || el.options[:raw_text].scan(/([^\s]+)\s*(?:-*\s+)?()(.*)/).first return unless data && data[0] name = data[0] section = (data[1].to_s.empty? ? el.attr['data-section'] || '7' : data[1]) description = (data[2].to_s.empty? ? nil : " - #{data[2]}") date = el.attr['data-date'] ? quote(el.attr['data-date']) : nil extra = (el.attr['data-extra'] ? quote(escape(el.attr['data-extra'].to_s)) : nil) opts[:result] << macro("TH", quote(escape(name.upcase)), quote(section), date, extra) if description opts[:result] << macro("SH", "NAME") << escape("#{name}#{description}") << "\n" end end when 2 opts[:result] << macro("SH", quote(escape(el.options[:raw_text]))) when 3 opts[:result] << macro("SS", quote(escape(el.options[:raw_text]))) else warning("Header levels greater than three are not supported") end end def convert_codeblock(el, opts) opts[:result] << macro("sp") << macro("RS", 4) << macro("EX") opts[:result] << newline(escape(el.value, true)) opts[:result] << macro("EE") << macro("RE") end def convert_blockquote(el, opts) opts[:result] << macro("RS") inner(el, opts) opts[:result] << macro("RE") end def convert_ul(el, opts) compact = (el.attr['class'] =~ /\bcompact\b/) opts[:result] << macro("sp") << macro("PD", 0) if compact inner(el, opts) opts[:result] << macro("PD") if compact end alias convert_dl convert_ul alias convert_ol convert_ul def convert_li(el, opts) sym = (opts[:parent].type == :ul ? '\(bu' : "#{opts[:index] + 1}.") opts[:result] << macro("IP", sym, 4) inner(el, opts, :first) if el.children.size > 1 opts[:result] << macro("RS") inner(el, opts, :rest) opts[:result] << macro("RE") end end def convert_dt(el, opts) opts[:result] << macro(opts[:prev] && opts[:prev].type == :dt ? "TQ" : "TP") inner(el, opts) opts[:result] << "\n" end def convert_dd(el, opts) inner(el, opts, :first) if el.children.size > 1 opts[:result] << macro("RS") inner(el, opts, :rest) opts[:result] << macro("RE") end opts[:result] << macro("sp") if opts[:next] && opts[:next].type == :dd end TABLE_CELL_ALIGNMENT = {left: 'l', center: 'c', right: 'r', default: 'l'} def convert_table(el, opts) opts[:alignment] = el.options[:alignment].map {|a| TABLE_CELL_ALIGNMENT[a] } table_options = ["box"] table_options << "center" if el.attr['class'] =~ /\bcenter\b/ opts[:result] << macro("TS") << "#{table_options.join(' ')} ;\n" inner(el, opts) opts[:result] << macro("TE") << macro("sp") end def convert_thead(el, opts) opts[:result] << opts[:alignment].map {|a| "#{a}b" }.join(' ') << " .\n" inner(el, opts) opts[:result] << "=\n" end def convert_tbody(el, opts) opts[:result] << ".T&\n" if opts[:index] != 0 opts[:result] << opts[:alignment].join(' ') << " .\n" inner(el, opts) opts[:result] << (opts[:next].type == :tfoot ? "=\n" : "_\n") if opts[:next] end def convert_tfoot(el, opts) inner(el, opts) end def convert_tr(el, opts) inner(el, opts) opts[:result] << "\n" end def convert_td(el, opts) result = opts[:result] opts[:result] = +'' inner(el, opts) if opts[:result] =~ /\n/ warning("Table cells using links are not supported") result << "\t" else result << opts[:result] << "\t" end end def convert_html_element(*) warning("HTML elements are not supported") end def convert_xml_comment(el, opts) newline(opts[:result]) << ".\"#{escape(el.value, true).rstrip.gsub(/\n/, "\n.\"")}\n" end alias convert_comment convert_xml_comment def convert_a(el, opts) if el.children.size == 1 && el.children[0].type == :text && el.attr['href'] == el.children[0].value newline(opts[:result]) << macro("UR", escape(el.attr['href'])) << macro("UE") elsif el.attr['href'].start_with?('mailto:') newline(opts[:result]) << macro("MT", escape(el.attr['href'].sub(/^mailto:/, ''))) << macro("UE") else newline(opts[:result]) << macro("UR", escape(el.attr['href'])) inner(el, opts) newline(opts[:result]) << macro("UE") end end def convert_img(_el, _opts) warning("Images are not supported") end def convert_em(el, opts) opts[:result] << '\fI' inner(el, opts) opts[:result] << '\fP' end def convert_strong(el, opts) opts[:result] << '\fB' inner(el, opts) opts[:result] << '\fP' end def convert_codespan(el, opts) opts[:result] << "\\fB#{escape(el.value)}\\fP" end def convert_br(_el, opts) newline(opts[:result]) << macro("br") end def convert_abbreviation(el, opts) opts[:result] << escape(el.value) end def convert_math(el, opts) if el.options[:category] == :block convert_codeblock(el, opts) else convert_codespan(el, opts) end end def convert_footnote(*) warning("Footnotes are not supported") end def convert_raw(*) warning("Raw content is not supported") end def convert_text(el, opts) text = escape(el.value) text.lstrip! if opts[:result][-1] == "\n" opts[:result] << text end def convert_entity(el, opts) opts[:result] << unicode_char(el.value.code_point) end def convert_smart_quote(el, opts) opts[:result] << unicode_char(::Kramdown::Utils::Entities.entity(el.value.to_s).code_point) end TYPOGRAPHIC_SYMS_MAP = { mdash: '\(em', ndash: '\(em', hellip: '\.\.\.', laquo_space: '\[Fo]', raquo_space: '\[Fc]', laquo: '\[Fo]', raquo: '\[Fc]' } def convert_typographic_sym(el, opts) opts[:result] << TYPOGRAPHIC_SYMS_MAP[el.value] end def macro(name, *args) ".#{[name, *args].compact.join(' ')}\n" end def newline(text) text << "\n" unless text[-1] == "\n" text end def quote(text) "\"#{text.gsub(/"/, '\\"')}\"" end def escape(text, preserve_whitespace = false) text = (preserve_whitespace ? text.dup : text.gsub(/\s+/, ' ')) text.gsub!('\\', "\\e") text.gsub!(/^\./, '\\\\&.') text.gsub!(/[.'-]/) {|m| "\\#{m}" } text end def unicode_char(codepoint) "\\[u#{codepoint.to_s(16).rjust(4, '0')}]" end end end end