metanorma/metanorma

View on GitHub
lib/metanorma/compile.rb

Summary

Maintainability
B
6 hrs
Test Coverage
require "fileutils"
require "nokogiri"
require "htmlentities"
require "yaml"
require "fontist"
require "fontist/manifest/install"
require_relative "compile_validate"
require_relative "compile_options"
require_relative "fontist_utils"
require_relative "util"
require_relative "sectionsplit"
require_relative "extract"
require_relative "worker_pool"

module Metanorma
  class Compile
    # @return [Array<String>]
    attr_reader :errors, :processor

    def initialize
      @registry = Metanorma::Registry.instance
      @errors = []
      @isodoc = IsoDoc::Convert.new({})
      @fontist_installed = false
      @log = Metanorma::Utils::Log.new
    end

    def compile(filename, options = {})
      options_process(filename, options)
      @processor = @registry.find_processor(options[:type].to_sym)
      extensions = get_extensions(options) or return nil
      (file, isodoc = process_input(filename, options)) or return nil
      relaton_export(isodoc, options)
      extract(isodoc, options[:extract], options[:extract_type])
      process_exts(filename, extensions, file, isodoc, options)
      clean_exit(options)
    end

    def options_process(filename, options)
      require_libraries(options)
      options = options_extract(filename, options)
      validate_options(options)
      @log.save_to(filename, options[:output_dir])
      options[:log] = @log
    end

    def clean_exit(options)
      options[:novalid] and return
      @log.write
    end

    def process_input(filename, options)
      case extname = File.extname(filename)
      when ".adoc" then process_input_adoc(filename, options)
      when ".xml" then process_input_xml(filename, options)
      else
        Util.log("[metanorma] Error: file extension #{extname} " \
                 "is not supported.", :error)
        nil
      end
    end

    def process_input_adoc(filename, options)
      Util.log("[metanorma] Processing: AsciiDoc input.", :info)
      file = read_file(filename)
      options[:asciimath] and
        file.sub!(/^(=[^\n]+\n)/, "\\1:mn-keep-asciimath:\n")
      dir = File.dirname(filename)
      dir != "." and
        file = file.gsub(/^include::/, "include::#{dir}/")
          .gsub(/^embed::/, "embed::#{dir}/")
      [file, @processor.input_to_isodoc(file, filename, options)]
    end

    def process_input_xml(filename, _options)
      Util.log("[metanorma] Processing: Metanorma XML input.", :info)
      # TODO NN: this is a hack -- we should provide/bridge the
      # document attributes in Metanorma XML
      ["", read_file(filename)]
    end

    def read_file(filename)
      File.read(filename, encoding: "utf-8").gsub("\r\n", "\n")
    end

    def relaton_export(isodoc, options)
      return unless options[:relaton]

      xml = Nokogiri::XML(isodoc) { |config| config.huge }
      bibdata = xml.at("//bibdata") || xml.at("//xmlns:bibdata")
      # docid = bibdata&.at("./xmlns:docidentifier")&.text || options[:filename]
      # outname = docid.sub(/^\s+/, "").sub(/\s+$/, "").gsub(/\s+/, "-") + ".xml"
      File.open(options[:relaton], "w:UTF-8") { |f| f.write bibdata.to_xml }
    end

    def export_output(fname, content, **options)
      mode = options[:binary] ? "wb" : "w:UTF-8"
      File.open(fname, mode) { |f| f.write content }
    end

    def wrap_html(options, file_extension, outfilename)
      if options[:wrapper] && /html$/.match(file_extension)
        outfilename = outfilename.sub(/\.html$/, "")
        FileUtils.mkdir_p outfilename
        FileUtils.mv "#{outfilename}.html", outfilename
        FileUtils.mv "#{outfilename}_images", outfilename, force: true
      end
    end

    # isodoc is Raw Metanorma XML
    def process_exts(filename, extensions, file, isodoc, options)
      f = File.expand_path(change_output_dir(options))
      fnames = { xml: f.sub(/\.[^.]+$/, ".xml"), f: f,
                 orig_filename: File.expand_path(filename),
                 presentationxml: f.sub(/\.[^.]+$/, ".presentation.xml") }
      @queue = ::Metanorma::WorkersPool
        .new(ENV["METANORMA_PARALLEL"]&.to_i || 3)
      gather_and_install_fonts(file, options.dup, extensions)
      process_exts_run(fnames, file, isodoc, extensions, options)
      @queue.shutdown
    end

    def process_exts_run(fnames, file, isodoc, extensions, options)
      Util.sort_extensions_execution(extensions).each do |ext|
        process_ext(ext, file, isodoc, fnames, options) or break
      end
    end

    def gather_and_install_fonts(file, options, extensions)
      Util.sort_extensions_execution(extensions).each do |ext|
        isodoc_options = get_isodoc_options(file, options, ext)
        font_install(isodoc_options.merge(options))
      end
    end

    def process_ext(ext, file, isodoc, fnames, options)
      fnames[:ext] = @processor.output_formats[ext]
      fnames[:out] = fnames[:f].sub(/\.[^.]+$/, ".#{fnames[:ext]}")
      isodoc_options = get_isodoc_options(file, options, ext)
      thread = true
      unless process_ext_simple(ext, isodoc, fnames, options,
                                isodoc_options)
        thread = process_exts1(ext, fnames, isodoc, options, isodoc_options)
      end
      thread
    end

    def process_ext_simple(ext, isodoc, fnames, options, isodoc_options)
      if ext == :rxl
        relaton_export(isodoc, options.merge(relaton: fnames[:out]))
      elsif options[:passthrough_presentation_xml] && ext == :presentation
        f = File.exist?(fnames[:f]) ? fnames[:f] : fnames[:orig_filename]
        FileUtils.cp f, fnames[:presentationxml]
      elsif ext == :html && options[:sectionsplit]
        sectionsplit_convert(fnames[:xml], isodoc, fnames[:out],
                             isodoc_options)
      else return false
      end
      true
    end

    def process_exts1(ext, fnames, isodoc, options, isodoc_options)
      if @processor.use_presentation_xml(ext)
        @queue.schedule(ext, fnames.dup, options.dup,
                        isodoc_options.dup) do |a, b, c, d|
          process_output_threaded(a, b, c, d)
        end
      else
        process_output_unthreaded(ext, fnames, isodoc, isodoc_options)
      end
    end

    def process_output_threaded(ext, fnames1, options1, isodoc_options1)
      @processor.output(nil, fnames1[:presentationxml], fnames1[:out], ext,
                        isodoc_options1)
      wrap_html(options1, fnames1[:ext], fnames1[:out])
    rescue StandardError => e
      strict = ext == :presentation || isodoc_options1[:strict] == true
      isodoc_error_process(e, strict, false)
    end

    def process_output_unthreaded(ext, fnames, isodoc, isodoc_options)
      @processor.output(isodoc, fnames[:xml], fnames[:out], ext,
                        isodoc_options)
      true # return as Thread
    rescue StandardError => e
      strict = ext == :presentation || isodoc_options[:strict] == "true"
      isodoc_error_process(e, strict, true)
      ext != :presentation
    end

     # assume we pass in Presentation XML, but we want to recover Semantic XML
    def sectionsplit_convert(input_filename, file, output_filename = nil,
                             opts = {})
      @isodoc ||= IsoDoc::PresentationXMLConvert.new({})
      input_filename += ".xml" unless input_filename.match?(/\.xml$/)
      File.exist?(input_filename) or
        File.open(input_filename, "w:UTF-8") { |f| f.write(file) }
      presxml = File.read(input_filename, encoding: "utf-8")
      _xml, filename, dir = @isodoc.convert_init(presxml, input_filename, false)
      Sectionsplit.new(input: input_filename, isodoc: @isodoc, xml: presxml,
                       base: File.basename(output_filename || filename),
                       output: output_filename || filename,
                       dir: dir, compile_opts: opts).build_collection
    end

    private

    def isodoc_error_process(err, strict, must_abort)
      if strict || err.message.include?("Fatal:")
        @errors << err.message
      else
        puts err.message
      end
      puts err.backtrace.join("\n")
      must_abort and 1
    end

    # @param options [Hash]
    # @return [String]
    def change_output_dir(options)
      if options[:output_dir]
        File.join options[:output_dir], File.basename(options[:filename])
      else options[:filename]
      end
    end
  end
end