lib/kramdown-rfc/kdrfc-processor.rb



require 'uri'
require 'net/http'
require 'net/http/persistent'
require 'open3'
require 'ostruct'
require 'json'

module KramdownRFC

class KDRFC

  attr_reader :options

  def initialize
    @options = OpenStruct.new
  end

  # )))

KDRFC_PREPEND = [ENV["KDRFC_PREPEND"]].compact
KDRFC_XML2RFC_FLAGS = Array(ENV["KDRFC_XML2RFC_FLAGS"]&.split(","))

def v3_flag?
  [*(@options.v3 ? ["--v3"] : []),
   *(@options.v2 ? ["--v2"] : [])]
end

def process_mkd(input, output)
  warn "* converting locally from markdown #{input} to xml #{output}" if @options.verbose
  o, s = Open3.capture2(*KDRFC_PREPEND, "kramdown-rfc2629", *v3_flag?, input)
  if s.success?
    File.open(output, "w") do |fo|
      fo.print(o)
    end
    warn "* #{output} written" if @options.verbose
  else
    raise IOError.new("*** kramdown-rfc failed, status #{s.exitstatus}")
  end
end

def filename_ct(fn, ext)
  bn = File.basename(fn, ".*")
  if r = ENV["KRAMDOWN_RFC_DOCREV"]
    bn << "-#{r}"
  end
  {filename: "#{bn}.#{ext}",
   content_type: "text/plain"}
end

def run_idnits(*args)
  if @options.remote
    run_idnits_remotely(*args)
  else
    run_idnits_locally(*args)
  end
end

def run_idnits_locally(txt_fn)
  warn "* running idnits locally in txt #{txt_fn}" if @options.verbose
  unless system("idnits", txt_fn)
    warn "*** problem #$? running idnits" if @options.verbose
    warn "*** problem running idnits -- falling back to remote idnits processing"
    run_idnits_remotely(txt_fn)
  end
end

# curl -s https://author-tools.ietf.org/api/idnits -X POST -F file=@draft-ietf-core-comi.txt -F hidetext=true
IDNITS_WEBSERVICE = ENV["KRAMDOWN_IDNITS_WEBSERVICE"] ||
                     'https://author-tools.ietf.org/api/idnits'

def run_idnits_remotely(txt_fn)
  url = URI(IDNITS_WEBSERVICE)
  req = Net::HTTP::Post.new(url)
  form = [["file", File.open(txt_fn),
           filename_ct(txt_fn, "txt")],
          ["hidetext", "true"]]
  diag = ["url/form: ", url, form].inspect
  req.set_form(form, 'multipart/form-data')
  warn "* requesting idnits at #{url}" if @options.verbose
  t0 = Time.now
  res = persistent_http.request(url, req)
  warn "* elapsed time: #{Time.now - t0}" if @options.verbose
  case res
  when Net::HTTPBadRequest
    result = checked_json(res.body)
    raise IOError.new("*** Remote Error: #{result["error"]}")
  when Net::HTTPOK
    case res.content_type
    when 'text/plain'
      if res.body == ''
        raise IOError.new("*** HTTP response is empty with status #{res.code}, not written")
      end
      puts res.body
    else
      warning = "*** HTTP response has unexpected content_type #{res.content_type} with status #{res.code}, #{diag}"
      warning << "\n"
      warning << res.body
      raise IOError.new(warning)
    end
  else
    raise IOError.new("*** HTTP response: #{res.code}, #{diag}")
  end
end


def process_xml(*args)
  if @options.remote
    process_xml_remotely(*args)
  else
    process_xml_locally(*args)
  end
end

def process_xml_locally(input, output, *flags)
  warn "* converting locally from xml #{input} to txt #{output}" if @options.verbose
  begin
    o, s = Open3.capture2(*KDRFC_PREPEND, "xml2rfc", *v3_flag?, *flags, *KDRFC_XML2RFC_FLAGS, input)
    puts o
    if s.success?
      warn "* #{output} written" if @options.verbose
    else
      raise IOError.new("*** xml2rfc failed, status #{s.exitstatus} (possibly try with -r)")
    end
  rescue Errno::ENOENT
    warn "*** falling back to remote xml2rfc processing (web service)" # if @options.verbose
    process_xml_remotely(input, output, *flags)
  end
end

# curl https://author-tools.ietf.org/api/render/text -X POST -F "file=@..."
XML2RFC_WEBSERVICE = ENV["KRAMDOWN_XML2RFC_WEBSERVICE"] ||
                     'https://author-tools.ietf.org/api/render/'

MODE_AS_FORMAT = {
    "--text" => "text",
    "--html" => "html",
    "--v2v3" => "xml",
    "--pdf" => "pdf",
}

def checked_json(t)
  begin
    JSON.load(t)
  rescue => e
    raise IOError.new("*** JSON result: #{e.detailed_message}, #{diag}")
  end
end

def persistent_http
  $http ||= Net::HTTP::Persistent.new name: 'kramdown-rfc'
end

def process_xml_remotely(input, output, *flags)

  format = flags[0] || "--text"
  warn "* converting remotely from xml #{input} to #{format} #{output}" if @options.verbose
  maf = MODE_AS_FORMAT[format]
  unless maf
    raise ArgumentError.new("*** don't know how to convert remotely from xml #{input} to #{format} #{output}")
  end
  url = URI(XML2RFC_WEBSERVICE + maf)
  req = Net::HTTP::Post.new(url)
  form = [["file", File.open(input),
           filename_ct(input, "xml")]]
  diag = ["url/form: ", url, form].inspect
  req.set_form(form, 'multipart/form-data')
  warn "* requesting at #{url}" if @options.verbose
  t0 = Time.now
  res = persistent_http.request(url, req)
  warn "* elapsed time: #{Time.now - t0}" if @options.verbose
  case res
  when Net::HTTPBadRequest
    result = checked_json(res.body)
    raise IOError.new("*** Remote Error: #{result["error"]}")
  when Net::HTTPOK
    case res.content_type
    when 'application/json'
      if res.body == ''
        raise IOError.new("*** HTTP response is empty with status #{res.code}, not written")
      end
      # warn "* res.body #{res.body}" if @options.verbose
      result = checked_json(res.body)
      if logs = result["logs"]
        if errors = logs["errors"]
          errors.each do |err|
            warn("*** Error: #{err}")
          end
        end
        if warnings = logs["warnings"]
          warnings.each do |w|
            warn("** Warning: #{w}")
          end
        end
      end
      raise IOError.new("*** No useful result from remote") unless result["url"]
      res = persistent_http.request(URI(result["url"]))
      warn "* result content type #{res.content_type}" if @options.verbose
      if res.body == ''
        raise IOError.new("*** Second HTTP response is empty with status #{res.code}, not written")
      end
      File.open(output, "w") do |fo|
        fo.print(res.body)
      end
      warn "* #{output} written" if @options.verbose
    else
      warning = "*** HTTP response has unexpected content_type #{res.content_type} with status #{res.code}, #{diag}"
      warning << "\n"
      warning << res.body
      raise IOError.new(warning)
    end
  else
    raise IOError.new("*** HTTP response: #{res.code}, #{diag}")
  end
end

def process_the_xml(fn, base)
  process_xml(fn, "#{base}.prepped.xml", "--preptool") if @options.prep
  process_xml(fn, "#{base}.v2v3.xml", "--v2v3") if @options.v2v3
  process_xml(fn, "#{base}.txt") if @options.txt || @options.idnits
  process_xml(fn, "#{base}.html", "--html") if @options.html
  process_xml(fn, "#{base}.pdf", "--pdf") if @options.pdf
  run_idnits("#{base}.txt") if @options.idnits
end

def process(fn)
  case fn
  when /(.*)\.xml\z/
    if @options.xml_only
      warn "*** You already have XML"
    else                        # FIXME: copy/paste
      process_the_xml(fn, $1)
    end
  when /(.*)\.mk?d\z/
    xml = "#$1.xml"
    process_mkd(fn, xml)
    process_the_xml(xml, $1) unless @options.xml_only
  when /(.*)\.txt\z/
    run_idnits(fn) if @options.idnits
  else
    raise ArgumentError.new("Unknown file type: #{fn}")
  end
end

# (((
end

end