metanorma/metanorma

View on GitHub
lib/metanorma/collection.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require "relaton"
require "relaton/cli"
require "metanorma/collection_manifest"
require "metanorma-utils"
require_relative "util"

module Metanorma
  # Metanorma collection of documents
  class Collection
    attr_reader :file

    # @return [Array<String>] documents-inline to inject the XML into
    #   the collection manifest; documents-external to keeps them outside
    attr_accessor :directives, :documents, :bibdatas, :coverpage, :dirname
    attr_accessor :disambig, :manifest

    # @param file [String] path to source file
    # @param dirname [String] directory of source file
    # @param directives [Array<String>] documents-inline to inject the XML into
    #   the collection manifest; documents-external to keeps them outside
    # @param bibdata [RelatonBib::BibliographicItem]
    # @param manifest [Metanorma::CollectionManifest]
    # @param documents [Hash<String, Metanorma::Document>]
    # @param prefatory [String]
    # @param coverpage [String]
    # @param final [String]
    # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
    def initialize(**args)
      @file = args[:file]
      @dirname = File.dirname(@file)
      @directives = args[:directives] || []
      @bibdata = args[:bibdata]
      @manifest = args[:manifest]
      @manifest.collection = self
      @coverpage = Util::hash_key_detect(@directives, "coverpage", @coverpage)
      @coverpage_style = Util::hash_key_detect(@directives, "coverpage-style",
                                               @coverpage_style)
      @documents = args[:documents] || {}
      @bibdatas = args[:documents] || {}
      if (@documents.any? || @manifest) &&
          (%w(documents-inline documents-external) & @directives).empty?
        @directives << "documents-inline"
      end
      @documents.merge! @manifest.documents(@dirname)
      @bibdatas.merge! @manifest.documents(@dirname)
      @documents.transform_keys { |k| Util::key(k) }
      @bibdatas.transform_keys { |k| Util::key(k) }
      @prefatory = args[:prefatory]
      @final = args[:final]
      @compile = Metanorma::Compile.new
      @log = Metanorma::Utils::Log.new
      @disambig = Util::DisambigFiles.new
    end

    # rubocop:enable Metrics/AbcSize,Metrics/MethodLength
    def clean_exit
      @log.write(File.join(@dirname,
                           "#{File.basename(@file, '.*')}.err.html"))
    end

    # @return [String] XML
    def to_xml
      b = Nokogiri::XML::Builder.new do |xml|
        xml.send(:"metanorma-collection",
                 "xmlns" => "http://metanorma.org") do |mc|
          collection_body(mc)
        end
      end
      b.to_xml
    end

    def collection_body(coll)
      coll << @bibdata.to_xml(bibdata: true, date_format: :full)
      @directives.each do |d|
        coll << "<directives>#{obj_to_xml(d)}</directives>"
      end
      @manifest.to_xml coll
      content_to_xml "prefatory", coll
      doccontainer coll
      content_to_xml "final", coll
    end

    def obj_to_xml(elem)
      case elem
      when ::Array
        elem.each_with_object([]) do |v, m|
          m << "<value>#{obj_to_xml(v)}</value>"
        end.join
      when ::Hash
        elem.each_with_object([]) do |(k, v), m|
          m << "<#{k}>#{obj_to_xml(v)}</#{k}>"
        end.join
      else elem end
    end

    def render(opts)
      CollectionRenderer.render self, opts.merge(log: @log)
      clean_exit
    end

    class << self
      # @param file [String]
      # @return [RelatonBib::BibliographicItem,
      #   RelatonIso::IsoBibliographicItem]
      def parse(file)
        case file
        when /\.xml$/ then parse_xml(file)
        when /.ya?ml$/ then parse_yaml(file)
        end
      end

      private

      def parse_xml(file)
        xml = Nokogiri::XML(File.read(file, encoding: "UTF-8"), &:huge)
        (b = xml.at("/xmlns:metanorma-collection/xmlns:bibdata")) and
          bd = Relaton::Cli.parse_xml(b)
        mnf_xml = xml.at("/xmlns:metanorma-collection/xmlns:manifest")
        mnf = CollectionManifest.from_xml mnf_xml
        pref = pref_final_content xml.at("//xmlns:prefatory-content")
        fnl = pref_final_content xml.at("//xmlns:final-content")
        cov = pref_final_content xml.at("//xmlns:coverpage")
        new(file: file, bibdata: bd, manifest: mnf,
            directives: directives_from_xml(xml.xpath("//xmlns:directives")),
            documents: docs_from_xml(xml, mnf),
            bibdatas: docs_from_xml(xml, mnf),
            prefatory: pref, final: fnl, coverpage: cov)
      end

      # TODO refine
      def directives_from_xml(dir)
        dir.each_with_object([]) do |d, m|
          m << if d.at("./xmlns:value")
                 x.xpath("./xmlns:value").map(&:text)
               elsif d.at("./*")
                 d.elements.each_with_object({}) do |e, ret|
                   ret[e.name] = e.children.to_xml
                 end
               else d.children.to_xml
               end
        end
      end

      def parse_yaml(file)
        yaml = YAML.load_file file
        if yaml["bibdata"]
          bd = Relaton::Cli::YAMLConvertor.convert_single_file yaml["bibdata"]
        end
        mnf = CollectionManifest.from_yaml yaml["manifest"]
        dirs = yaml["directives"]
        pref = yaml["prefatory-content"]
        fnl = yaml["final-content"]
        new(file: file, directives: dirs, bibdata: bd, manifest: mnf,
            prefatory: pref, final: fnl)
      end

      # @param xml [Nokogiri::XML::Document]
      # @parma mnf [Metanorma::CollectionManifest]
      # @return [Hash{String=>Metanorma::Document}]
      def docs_from_xml(xml, mnf)
        xml.xpath("//xmlns:doc-container//xmlns:bibdata")
          .each_with_object({}) do |b, m|
          bd = Relaton::Cli.parse_xml b
          docref = mnf.docref_by_id bd.docidentifier.first.id
          m[docref["identifier"]] = Document.new bd, docref["fileref"]
          m
        end
      end

      # @param xml [Nokogiri::XML::Element, nil]
      # @return [String, nil]
      def pref_final_content(xml)
        xml or return
        <<~CONT

            == #{xml.at('title')&.text}
          #{xml.at('p')&.text}
        CONT
      end
    end

    private

    # @return [String, nil]
    attr_reader :prefatory, :final

    # @return [String]
    def dummy_header
      <<~DUMMY
        = X
        A

      DUMMY
    end

    # @param elm [String] 'prefatory' or 'final'
    # @param builder [Nokogiri::XML::Builder]
    def content_to_xml(elm, builder)
      return unless (cnt = send(elm))

      @compile.load_flavor(doctype)
      out = sections(dummy_header + cnt.strip)
      builder.send("#{elm}-content") { |b| b << out }
    end

    # @param cnt [String] prefatory/final content
    # @return [String] XML
    def sections(cnt)
      c = Asciidoctor.convert(cnt, backend: doctype.to_sym, header_footer: true)
      Nokogiri::XML(c, &:huge).at("//xmlns:sections").children.to_xml
    end

    # @param builder [Nokogiri::XML::Builder]
    def doccontainer(builder)
      Array(@directives).include? "documents-inline" or return
      documents.each_with_index do |(_, d), i|
        doccontainer1(builder, d, i)
      end
    end

    def doccontainer1(builder, doc, idx)
      id = format("doc%<index>09d", index: idx)
      builder.send(:"doc-container", id: id) do |b|
        if doc.attachment
          doc.bibitem and b << doc.bibitem.root.to_xml
          b.attachment Vectory::Utils::datauri(doc.file)
        else doc.to_xml b
        end
      end
    end

    def doctype
      @doctype ||= fetch_doctype || "standoc"
    end

    def fetch_doctype
      docid = @bibdata.docidentifier.first
      docid or return
      docid.type&.downcase || docid.id&.sub(/\s.*$/, "")&.downcase
    end
  end
end