class Kramdown::Converter::Rfc2629

Converts a Kramdown::Document to HTML.

def self.hex_to_lin(h)

def self.hex_to_lin(h)
  h.to_i(16)**2.22        # approximating sRGB gamma
end

def capture_croak(t, err)

def capture_croak(t, err)
  if err != ''
    err.lines do |l|
      warn "*** [#{t}:] #{l.chomp}"
    end
  end
end

def clean_pcdata(parts) # hack, will become unnecessary with XML2RFCv3

hack, will become unnecessary with XML2RFCv3
def clean_pcdata(parts)    # hack, will become unnecessary with XML2RFCv3
  clean = ''
  irefs = ''
  # warn "clean_parts: #{parts.inspect}"
  parts.each do |p|
    md = p.match(%r{([^<]*)(.*)})
    clean << md[1]
    irefs << md[2]        # breaks for spanx... don't emphasize in headings!
  end
  [clean, irefs]
end

def convert(el, indent = -INDENTATION, opts = {})

def convert(el, indent = -INDENTATION, opts = {})
  if el.children[-1].type == :raw
    raw = convert1(el.children.pop, indent, opts)
  end
  "#{convert1(el, indent, opts)}#{end_sections(1, indent)}#{raw}"
end

def convert1(el, indent, opts = {})

def convert1(el, indent, opts = {})
  el.rfc2629_fix
  send("convert_#{el.type}", el, indent, opts)
end

def convert_a(el, indent, opts)

def convert_a(el, indent, opts)
  gi = el.attr.delete('gi')
  res = inner(el, indent, opts)
  target = el.attr['target']
  if target[0] == "#"     # handle [](#foo) as xref as in RFC 7328
    el.attr['target'] = target = target[1..-1]
    if target.downcase == res.downcase
      res = ''            # get rid of raw anchors leaking through
    end
    gi ||= "xref"
  else
    gi ||= "eref"
  end
  "<#{gi}#{el_html_attributes(el)}>#{res}</#{gi}>"
end

def convert_abbreviation(el, indent, opts) # XXX: This is wrong

XXX: This is wrong
def convert_abbreviation(el, indent, opts) # XXX: This is wrong
  title = @root.options[:abbrev_defs][el.value]
  title = nil if title.empty?
  value = el.value
  if item = title
    m = title.scan(Parser::RFC2629Kramdown::IREF_START)
    if m.empty?
      subitem = value
    else
      iref = m.map{|a,| iref_attr(a)}.join('')
    end
  else
    item = value
  end
  iref ||= "<iref#{html_attributes(item: item, subitem: subitem)}/>"
  "#{el.value}#{iref}"
end

def convert_blank(el, indent, opts)

def convert_blank(el, indent, opts)
  "\n"
end

def convert_blockquote(el, indent, opts)

def convert_blockquote(el, indent, opts)
  text = inner(el, indent, opts)
  text = "<t></t>" unless text =~ /</ # empty block quote
  "#{' '*indent}<t><list style='empty'#{el_html_attributes(el)}>\n#{text}#{' '*indent}</list></t>\n"
end

def convert_br(el, indent, opts)

def convert_br(el, indent, opts)
  "<vspace />"
end

def convert_codeblock(el, indent, opts)

def convert_codeblock(el, indent, opts)
  # el.attr['anchor'] ||= saner_generate_id(el.value) -- no longer in 1.0.6
  result = el.value
  blockclass = el.attr.delete('class')
  if blockclass == 'language-tbreak'
    result = result.lines.map {|line| [line.chomp, 0]}
    spaceind = 0
    result.each_with_index {|pair, index|
      if pair[0] == ''
        result[spaceind][1] += 1
        pair[0] = nil unless index == spaceind
      else
        spaceind = index
      end
    }
    # $stderr.puts(result.inspect)
    result = result.map {|line, space|
      "<![CDATA[#{line.gsub(/^\s+/) {|s| "\u00A0" * s.size}}]]><vspace blankLines=\"#{space}\"/>" if line
    }.compact.join("\n")
    "#{' '*indent}<t>#{result}</t>\n"
  else
    artwork_attr = {}
    t = nil
    if blockclass
      classes = blockclass.split(' ')
      classes.each do |cl|
        if md = cl.match(/\Alanguage-(.*)/)
          t = artwork_attr["type"] = md[1] # XXX overwrite
        else
          $stderr.puts "*** Unimplemented block class: #{cl}"
        end
      end
    end
    # compensate for XML2RFC idiosyncrasy by insisting on a blank line
    unless el.attr.delete('tight')
      result[0,0] = "\n" unless result[0,1] == "\n"
    end
    el.attr.each do |k, v|
      if md = k.match(/\Aartwork-(.*)/)
        el.attr.delete(k)
        artwork_attr[md[1]] = v
      end
    end
    case t
    when "goat", "ditaa", "mscgen", "plantuml", "plantuml-utxt", "mermaid", "math"
      result, result1 = memoize(:svg_tool_process, t, result)
      "#{' '*indent}<figure#{el_html_attributes(el)}><artset><artwork #{html_attributes(artwork_attr.merge("type"=> "svg"))}>#{result1.sub(/.*?<svg/m, "<svg")}</artwork><artwork #{html_attributes(artwork_attr.merge("type"=> "ascii-art"))}><![CDATA[#{result}#{result =~ /\n\Z/ ? '' : "\n"}]]></artwork></artset></figure>\n"
    else
      "#{' '*indent}<figure#{el_html_attributes(el)}><artwork#{html_attributes(artwork_attr)}><![CDATA[#{result}#{result =~ /\n\Z/ ? '' : "\n"}]]></artwork></figure>\n"
    end
  end
end

def convert_codespan(el, indent, opts)

def convert_codespan(el, indent, opts)
  attrstring = el_html_attributes_with(el, {"style" => 'verb'})
  "<spanx#{attrstring}>#{escape_html(el.value)}</spanx>"
end

def convert_comment(el, indent, opts)

def convert_comment(el, indent, opts)
't actually output all those comments into the XML:
   if el.options[:category] == :block
     "#{' '*indent}<!-- #{el.value} -->\n"
   else
     "<!-- #{el.value} -->"
   end
end

def convert_contact(el, indent, opts)

def convert_contact(el, indent, opts)
  "<contact#{el_html_attributes(el)}/>"
end

def convert_dd(el, indent, opts)

def convert_dd(el, indent, opts)
  if $options.v3
    out = ''
    if !opts[:haddt]
      out ="#{' '*indent}<dt/>\n" # you can't make this one up
    end
    opts[:haddt] = false
    out << "#{' '*indent}<dd#{el_html_attributes(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</dd>\n"
  else
  output = ' '*indent
  if @in_dt == 1
    @in_dt = 0
  else
    output << "<t#{el_html_attributes(el)}>"
  end
  res = inner(el, indent+INDENTATION, opts.merge(unpacked: true))
   if el.children.empty? || el.children.first.options[:category] != :block
    output << res << (res =~ /\n\Z/ ? ' '*indent : '')
   else                    FIXME: The latter case is needed for more complex cases
     output << "\n" << res << ' '*indent
   end
  output << "</t>\n"
  end
end

def convert_dl(el, indent, opts)

def convert_dl(el, indent, opts)
  if $options.v3
    if hangindent = el.attr.delete('hangIndent')
      el.attr['indent'] ||= hangindent # new attribute name wins
    end
    vspace = el.attr.delete('vspace')
    if vspace && !el.attr['newline']
      el.attr['newline'] = 'true'
    end
    "#{' '*indent}<dl#{el_html_attributes(el)}>\n#{inner(el, indent, opts.dup)}#{' '*indent}</dl>\n"
  else
    convert_ul(el, indent, opts)
  end
end

def convert_dt(el, indent, opts) # SERIOUSLY BAD HACK:

SERIOUSLY BAD HACK:
def convert_dt(el, indent, opts) # SERIOUSLY BAD HACK:
  if $options.v3
    out = ''
    if opts[:haddt]
      out ="#{' '*indent}<dd><t/></dd>\n" # you can't make this one up
    end
    opts[:haddt] = true
    out << "#{' '*indent}<dt#{el_html_attributes(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</dt>\n"
  else
  close = "#{' '*indent}</t>\n" * @in_dt
  @in_dt = 1
  vspace = opts[:vspace]
  vspaceel = "<vspace blankLines='#{vspace}'/>" if vspace
  ht = escape_html(inner(el, indent, opts), :attribute) # XXX this may leave gunk
  "#{close}#{' '*indent}<t#{el_html_attributes(el)} hangText=\"#{ht}\">#{vspaceel}\n"
  end
end

def convert_em(el, indent, opts)

def convert_em(el, indent, opts)
  if $options.v3
    gi = el.type
    "<#{gi}#{el_html_attributes(el)}>#{inner(el, indent, opts)}</#{gi}>"
  else
  attrstring = el_html_attributes_with(el, {"style" => EMPH[el.type]})
  span, irefs = clean_pcdata(inner_a(el, indent, opts))
  "<spanx#{attrstring}>#{span}</spanx>#{irefs}"
  end
end

def convert_entity(el, indent, opts)

def convert_entity(el, indent, opts)
  entity_to_str(el.value)
end

def convert_footnote(el, indent, opts) # XXX: footnotes into crefs???

XXX: footnotes into crefs???
def convert_footnote(el, indent, opts) # XXX: footnotes into crefs???
  # this would be more like xml2rfc v3:
  # "\n#{' '*indent}<cref>\n#{inner(el.value, indent, opts).rstrip}\n#{' '*indent}</cref>"
  content = inner(el.value, indent, opts).strip
  content = escape_html(content.sub(/\A<t>(.*)<\/t>\z/m) {$1}, :text) # text only...
  name = el.options[:name].sub(/\A[0-9]/) {"_" << $&}
  while @footnote_names_in_use[name] do
    if name =~ /:\d+\z/
      name.succ!
    else
      name << ":1"
    end
  end
  @footnote_names_in_use[name] = true
  attrstring = el_html_attributes_with(el, {"anchor" => name})
  "\n#{' '*indent}<cref#{attrstring}>#{content}</cref>"
end

def convert_header(el, indent, opts)

def convert_header(el, indent, opts)
  # todo: handle appendix tags
  el = el.deep_clone
  options = @doc ? @doc.options : @options # XXX: 0.11 vs. 0.12
  if options[:auto_ids] && !el.attr['anchor']
    el.attr['anchor'] = saner_generate_id(el.options[:raw_text])
  end
  if $options.v3
    if sl = el.attr.delete('slugifiedName') # could do general name- play
      attrstring = html_attributes({'slugifiedName' => sl})
    end
    irefs = "<name#{attrstring}>#{inner(el, indent, opts)}</name>" #
  else
  clean, irefs = clean_pcdata(inner_a(el, indent, opts))
  el.attr['title'] = clean
  end
  "#{end_sections(el.options[:level], indent)}#{' '*indent}<section#{@sec_level += 1; el_html_attributes(el)}>#{irefs}\n"
end

def convert_hr(el, indent, opts) # misuse for page break

misuse for page break
def convert_hr(el, indent, opts) # misuse for page break
  "#{' '*indent}<t><vspace blankLines='999' /></t>\n"
end

def convert_html_element(el, indent, opts)

def convert_html_element(el, indent, opts)
  res = inner(el, indent, opts)
  if el.options[:category] == :span
    "<#{el.value}#{el_html_attributes(el)}" << (!res.empty? ? ">#{res}</#{el.value}>" : " />")
  else
    output = ''
    output << ' '*indent if !el.options[:parent_is_raw]
    output << "<#{el.value}#{el_html_attributes(el)}"
    if !res.empty? && el.options[:parse_type] != :block
      output << ">#{res}</#{el.value}>"
    elsif !res.empty?
      output << ">\n#{res}"  << ' '*indent << "</#{el.value}>"
    elsif HTML_TAGS_WITH_BODY.include?(el.value)
      output << "></#{el.value}>"
    else
      output << " />"
    end
    output << "\n" if el.options[:outer_element] || !el.options[:parent_is_raw]
    output
  end
end

def convert_img(el, indent, opts) # misuse the tag!

misuse the tag!
def convert_img(el, indent, opts) # misuse the tag!
  if a = el.attr
    alt = a.delete('alt').strip
    alt = '' if alt == '!' # work around re-wrap uglyness
    if src = a.delete('src')
      a['target'] = src
    end
  end
  if alt == ":include:"   # Really bad misuse of tag...
    anchor = el.attr.delete('anchor') || (
      # not yet
      warn "*** missing anchor for '#{src}'"
      src
    )
    anchor.sub!(/\A[0-9]/) { "_#{$&}" } # can't start an ID with a number
    anchor.gsub!('/', '_')              # should take out all illegals
    to_insert = ""
    src.scan(/(W3C|3GPP|[A-Z-]+)[.]?([A-Za-z_0-9.\/\+-]+)/) do |t, n|
      fn = "reference.#{t}.#{n}.xml"
      sub, ttl, can_anchor = XML_RESOURCE_ORG_MAP[t]
      ttl ||= KRAMDOWN_REFCACHETTL  # everything but RFCs might change a lot
      puts "*** Huh: #{fn}" unless sub
      url = "#{XML_RESOURCE_ORG_PREFIX}/#{sub}/#{fn}"
      if can_anchor # create anchor server-side for stand_alone: false
        url << "?anchor=#{anchor}"
        fn[/.xml$/] = "--anchor=#{anchor}.xml"
      end
      to_insert = get_and_cache_resource(url, fn.gsub('/', '_'), ttl)
      to_insert.scrub! rescue nil # only do this for Ruby >= 2.1
      # this may be a bit controversial: Don't break the build if reference is broken
      if KRAMDOWN_OFFLINE
        unless to_insert
          to_insert = "<reference anchor='#{anchor}'> <front> <title>*** BROKEN REFERENCE ***</title> <author> <organization/> </author> <date/> </front> </reference>"
          warn "*** KRAMDOWN_OFFLINE: Inserting broken reference for #{fn}"
        end
      else
        exit 66 unless to_insert # EX_NOINPUT
      end
    end
    to_insert.sub(/<\?xml version=["']1.0["'] encoding=["']UTF-8["']\?>/, '')
      .sub(/\banchor=(?:"[^"]+"|'[^']+')/, "anchor=\"#{anchor}\"")
  else
    "<xref#{el_html_attributes(el)}>#{alt}</xref>"
  end
end

def convert_iref(el, indent, opts)

def convert_iref(el, indent, opts)
  iref_attr(el.attr['target'])
end

def convert_li(el, indent, opts)

def convert_li(el, indent, opts)
  res_a = inner_a(el, indent, opts)
  if el.children.empty? || el.children.first.options[:category] == :span
    res = res_a.join('')
  else                    # merge multiple <t> elements
    res = res_a.select { |x|
      x.strip != ''
    }.map { |x|
      x.sub(/\A\s*<t>(.*)<\/t>\s*\Z/m) { $1}
    }.join("#{' '*indent}<vspace blankLines='1'/>\n").gsub(%r{(</list>)\s*<vspace blankLines='1'/>}) { $1 }.gsub(%r{<vspace blankLines='1'/>\s*(<list)}) { $1 }
  end
  "#{' '*indent}<t#{el_html_attributes(el)}>#{res}#{(res =~ /\n\Z/ ? ' '*indent : '')}</t>\n"
end

def convert_math(el, indent, opts) # XXX: This is wrong

XXX: This is wrong
def convert_math(el, indent, opts) # XXX: This is wrong
  el = el.deep_clone
  if el.options[:category] == :block
    el.attr['artwork-type'] ||= ''
    el.attr['artwork-type'] += (el.attr['artwork-type'].empty? ? '' : ' ') + 'math'
    artwork_attr = {}
    el.attr.each do |k, v|
      if md = k.match(/\Aartwork-(.*)/)
        el.attr.delete(k)
        artwork_attr[md[1]] = v
      end
    end
    result, err, _s = Open3.capture3("tex2mail -noindent -ragged -by_par -linelength=69", stdin_data: el.value);
    # warn "*** tex2mail not in path?" unless s.success? -- doesn't have useful status
    capture_croak("tex2mail", err)
    "#{' '*indent}<figure#{el_html_attributes(el)}><artwork#{html_attributes(artwork_attr)}><![CDATA[#{result}#{result =~ /\n\Z/ ? '' : "\n"}]]></artwork></figure>\n"
  else
    type = 'spanx'
    if $options.v3
      type = 'contact'
      result = munge_latex(el.value)
      attrstring = el_html_attributes_with(el, {"fullname" => result.chomp, "asciiFullname" => ''})
    else
      warn "*** no support for inline math in XML2RFCv2"
      type = 'spanx'
      attrstring = el_html_attributes_with(el, {"style" => 'verb'})
      content = escape_html(el.value, :text)
    end
    "<#{type}#{attrstring}>#{content}</#{type}>"
  end
end

def convert_p(el, indent, opts)

def convert_p(el, indent, opts)
  if (el.children.size == 1 && el.children[0].type == :img) || opts[:unpacked]
    inner(el, indent, opts) # Part of the bad reference hack
  else
    "#{' '*indent}<t#{el_html_attributes(el)}>#{inner(el, indent, opts)}</t>\n"
  end
end

def convert_raw(el, indent, opts)

def convert_raw(el, indent, opts)
  end_sections(1, indent) +
  el.value + (el.options[:category] == :block ? "\n" : '')
end

def convert_root(el, indent, opts)

def convert_root(el, indent, opts)
  result = inner(el, indent, opts)
end

def convert_smart_quote(el, indent, opts)

def convert_smart_quote(el, indent, opts)
  entity_to_str(smart_quote_entity(el))
end

def convert_table(el, indent, opts) # This only works for tables with headers

This only works for tables with headers
def convert_table(el, indent, opts) # This only works for tables with headers
  alignment = el.options[:alignment].map { |al| ALIGNMENTS[al]}
  cols = (el.attr.delete("cols") || "").split(' ')
  "#{' '*indent}<texttable#{el_html_attributes(el)}>\n#{inner(el, indent, opts.merge(table_alignment: alignment, table_cols: cols))}#{' '*indent}</texttable>\n"
end

def convert_td(el, indent, opts)

def convert_td(el, indent, opts)
  if alignment = opts[:table_alignment]
    alignment = alignment.shift
    if cols = opts[:table_cols].shift
      md = cols.match(/(\d*(|em|[%*]))([lrc])/)
      if md[1].to_i != 0
        widthval = md[1]
        widthval << "em" if md[2].empty?
        widthopt = "width='#{widthval}' "
      end
      alignment = COLS_ALIGN[md[3]] || :left
    end
  end
  if alignment
    res, irefs = clean_pcdata(inner_a(el, indent, opts))
    warn "*** lost markup #{irefs} in table heading" unless irefs.empty?
    "#{' '*indent}<ttcol #{widthopt}align='#{alignment}'#{el_html_attributes(el)}>#{res.empty? ? "&#160;" : res}</ttcol>\n" # XXX need clean_pcdata
  else
    res = inner(el, indent, opts)
    "#{' '*indent}<c#{el_html_attributes(el)}>#{res.empty? ? "&#160;" : res}</c>\n"
  end
end

def convert_text(el, indent, opts)

def convert_text(el, indent, opts)
  escape_html(el.value, :text)
end

def convert_thead(el, indent, opts)

def convert_thead(el, indent, opts)
  inner(el, indent, opts)
end

def convert_typographic_sym(el, indent, opts)

def convert_typographic_sym(el, indent, opts)
  if (result = @options[:typographic_symbols][el.value])
    escape_html(result, :text)
  else
    TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e) }.join('')
  end
end

def convert_ul(el, indent, opts)

def convert_ul(el, indent, opts)
  opts = opts.merge(vspace: el.attr.delete('vspace'))
  attrstring = el_html_attributes_with(el, {"style" => STYLES[el.type]})
  if opts[:unpacked]
    "#{' '*indent}<list#{attrstring}>\n#{inner(el, indent, opts)}#{' '*indent}</list>\n"
    else
    "#{' '*indent}<t><list#{attrstring}>\n#{inner(el, indent, opts)}#{' '*indent}</list></t>\n"
  end
end

def convert_xml_comment(el, indent, opts)

def convert_xml_comment(el, indent, opts)
  if el.options[:category] == :block && !el.options[:parent_is_raw]
    ' '*indent + el.value + "\n"
  else
    el.value
  end
end

def convert_xref(el, indent, opts)

def convert_xref(el, indent, opts)
  gi = el.attr.delete('gi')
  target = el.attr['target']
  if target[0] == "&"
    "#{target};"
  else
    if target =~ %r{\A\w+:(?://|.*@)}
      gi ||= "eref"
    else
      gi ||= "xref"
    end
    "<#{gi}#{el_html_attributes(el)}/>"
  end
end

def el_html_attributes(el)

def el_html_attributes(el)
  html_attributes(el.attr)
end

def el_html_attributes_with(el, defattr)

def el_html_attributes_with(el, defattr)
  html_attributes(defattr.merge(el.attr))
end

def end_sections(to_level, indent)

def end_sections(to_level, indent)
  if indent < 0
    indent = 0
  end
  if @sec_level >= to_level
    delta = (@sec_level - to_level)
    @sec_level = to_level
    "#{' '*indent}</section>\n" * delta
  else
    $stderr.puts "Incorrect section nesting: Need to start with 1"
  end
end

def get_and_cache_resource(url, cachefile, tvalid = 7200, tn = Time.now)

this is now slightly dangerous as multiple urls could map to the same cachefile
def get_and_cache_resource(url, cachefile, tvalid = 7200, tn = Time.now)
  fn = "#{REFCACHEDIR}/#{cachefile}"
  Dir.mkdir(REFCACHEDIR) unless Dir.exists?(REFCACHEDIR)
  f = File.stat(fn) rescue nil unless KRAMDOWN_REFCACHE_REFETCH
  if !KRAMDOWN_OFFLINE && (!f || tn - f.mtime >= tvalid)
    if f
      message = "renewing (stale by #{"%.1f" % ((tn-f.mtime)/86400)} days)"
      fetch_timeout = 10 # seconds, give up quickly if just renewing
    else
      message = "fetching"
      fetch_timeout = 60 # seconds; long timeout needed for Travis
    end
    $stderr.puts "#{fn}: #{message}"
    if ENV["HAVE_WGET"]
      `cd #{REFCACHEDIR}; wget -t 3 -T #{fetch_timeout} -Nnv "#{url}"` # ignore errors if offline (hack)
      begin
        File.utime nil, nil, fn
      rescue Errno::ENOENT
        warn "Can't fetch #{url} -- is wget in path?"
      end
    else
      require 'open-uri'
      require 'socket'
      require 'openssl'
      require 'timeout'
      begin
        Timeout::timeout(fetch_timeout) do
          options = {}
          if ENV["KRAMDOWN_DONT_VERIFY_HTTPS"]
            options[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE
          end             # workaround for OpenSSL on Windows...
          URI.open(url, **options) do |uf|          # not portable to older versions
          OpenURI.open_uri(url, **options) do |uf|
            s = uf.read
            if uf.status[0] != "200"
              warn "*** Status code #{status} while fetching #{url}"
            else
              File.write(fn, s)
            end
          end
        end
      rescue OpenURI::HTTPError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED,
             SocketError, Timeout::Error => e
        warn "*** #{e} while fetching #{url}"
      end
    end
  end
  begin
    File.read(fn) # this blows up if no cache available after fetch attempt
  rescue Errno::ENOENT => e
    warn "*** #{e} for #{fn}"
  end
end

def initialize(*doc)

Initialize the XML converter with the given Kramdown document +doc+.
def initialize(*doc)
  super
  @sec_level = 1
  @in_dt = 0
  @footnote_names_in_use = {}
end

def inner(el, indent, opts)

def inner(el, indent, opts)
  inner_a(el, indent, opts).join('')
end

def inner_a(el, indent, opts)

def inner_a(el, indent, opts)
  indent += INDENTATION
  el.children.map do |inner_el|
    inner_el.rfc2629_fix
    send("convert_#{inner_el.type}", inner_el, indent, opts)
  end
end

def iref_attr(s)

def iref_attr(s)
  md = s.match(IREF_RE)
  attr = {
    item: md[2] || md[3],
    subitem: md[4] || md[5],
    primary: md[1] && 'true',
  }
  "<iref#{html_attributes(attr)}/>"
end

def memoize(meth, *args)

def memoize(meth, *args)
  require 'digest'
  Dir.mkdir(REFCACHEDIR) unless Dir.exists?(REFCACHEDIR)
  kdrfc_version = Gem.loaded_specs["kramdown-rfc2629"].version.to_s.gsub('.', '_') rescue "UNKNOWN"
  fn = "#{REFCACHEDIR}/kdrfc-#{kdrfc_version}-#{meth}-#{Digest::SHA256.hexdigest(Marshal.dump(args))[0...40]}.cache"
  begin
    out = Marshal.load(File.binread(fn))
  rescue StandardError => e
    # warn e.inspect
    out = method(meth).call(*args)
    File.binwrite(fn, Marshal.dump(out))
  end
  out
end

def munge_latex(s)

def munge_latex(s)
  MATH_REPLACEMENTS.each do |o, n|
    s.gsub!(o, n)
  end
  MATH_COMBININGMARKS.each do |m, n|
    re = /\\#{m[1..-1]}\{(\X)\}/
    s.gsub!(re) { "#$1#{n}" }
  end
  s
end

def saner_generate_id(value)

def saner_generate_id(value)
  generate_id(value).gsub(/-+/, '-')
end

def svg_clean(s) # expensive, risky

expensive, risky
def svg_clean(s)          # expensive, risky
  require "rexml/document"
  d = REXML::Document.new(s)
  REXML::XPath.each(d.root, "//*[@shape-rendering]") { |x| x.attributes["shape-rendering"] = nil }  #; warn x.inspect }
  REXML::XPath.each(d.root, "//*[@text-rendering]") { |x| x.attributes["text-rendering"] = nil }  #; warn x.inspect  }
  REXML::XPath.each(d.root, "//*[@stroke]") { |x| x.attributes["stroke"] = svg_munch_color(x.attributes["stroke"], false) }
  REXML::XPath.each(d.root, "//*[@fill]") { |x| x.attributes["fill"] = svg_munch_color(x.attributes["fill"], true) }
  REXML::XPath.each(d.root, "//*[@id]") { |x| x.attributes["id"] = svg_munch_id(x.attributes["id"]) }
  REXML::XPath.each(d.root, "//rect") { |x| x.attributes["style"] = "fill:none;stroke:black;stroke-width:1" unless x.attributes["style"] }
  d.to_s
end

def svg_munch_color(c, fill)

def svg_munch_color(c, fill)
  c = SVG_COLORS[c]
  case c
  when /\A#(..)(..)(..)\z/
    if hex_to_lin($1)*0.2126 + hex_to_lin($2)*0.7152 + hex_to_lin($3)*0.0722 >= B_W_THRESHOLD
      'white'
    else
      'black'
    end
  when 'none'
    'none' if fill        # delete for stroke
  else
    c
  end
end

def svg_munch_id(id)

def svg_munch_id(id)
  id.gsub(/[^-._A-Za-z0-9]/) {|x| "_%02X" % x.ord}
end

def svg_tool_process(t, result)

def svg_tool_process(t, result)
  require 'tempfile'
  file = Tempfile.new("kramdown-rfc")
  file.write(result)
  file.close
  case t
  when "goat"
    result1, err, _s = Open3.capture3("goat #{file.path}", stdin_data: result);
  when "ditaa"        # XXX: This needs some form of option-setting
    result1, err, _s = Open3.capture3("ditaa #{file.path} --svg -o -", stdin_data: result);
  when "mscgen"
    result1, err, _s = Open3.capture3("mscgen -T svg -i #{file.path} -o -", stdin_data: result);
  when "mermaid"
    result1, err, _s = Open3.capture3("mmdc -i #{file.path}", stdin_data: result); #  -b transparent
    outpath = file.path + ".svg"
    result1 = File.read(outpath)
    File.unlink(outpath)
  when "plantuml", "plantuml-utxt"
    plantuml = "@startuml\n#{result}\n@enduml"
    result1, err, _s = Open3.capture3("plantuml -pipe -tsvg", stdin_data: plantuml);
    result, err1, _s = Open3.capture3("plantuml -pipe -tutxt", stdin_data: plantuml) if t == "plantuml-utxt"
    err << err1
  when "math"
    result1, err, _s = Open3.capture3("tex2svg --font STIX --speech=false #{Shellwords.escape(' ' << result)}");
    result, err1, _s = Open3.capture3("asciitex -f #{file.path}")
    err << err1
  end
  capture_croak(t, err)
  # warn ["goat:", result1.inspect]
  file.unlink
  result1 = svg_clean(result1) unless t == "goat"
  result1, err, _s = Open3.capture3("svgcheck -qa", stdin_data: result1);
  capture_croak("svgcheck", err)
  # warn ["svgcheck:", result1.inspect]
  [result, result1]       # text, svg
end