require "isodoc"
require "htmlentities"
require_relative "collection_fileprocess"
require_relative "fontist_utils"
require_relative "util"
require_relative "files_lookup"
require_relative "collection_render_utils"
require_relative "collection_render_word"
module Metanorma
# XML collection renderer
class CollectionRenderer
FORMATS = %i[html xml doc pdf].freeze
attr_accessor :isodoc, :nested
attr_reader :xml, :compile, :compile_options, :documents
# This is only going to render the HTML collection
# @param xml [Metanorma::Collection] input XML collection
# @param folder [String] input folder
# @param options [Hash]
# @option options [String] :coverpage cover page HTML (Liquid template)
# @option options [Array] :format list of formats (xml,html,doc,pdf)
# @option options [String] :output_folder output directory
#
# We presuppose that the bibdata of the document is equivalent to that of
# the collection, and that the flavour gem can sensibly process it. We may
# need to enhance metadata in the flavour gems isodoc/metadata.rb with
# collection metadata
def initialize(collection, folder, options = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
check_options options
@xml = Nokogiri::XML collection.to_xml # @xml is the collection manifest
@lang = @xml.at("//xmlns:bibdata/xmlns:language")&.text || "en"
@script = @xml.at("//xmlns:bibdata/xmlns:script")&.text || "Latn"
@locale = @xml.at("//xmlns:bibdata/xmlns:locale")&.text
@doctype = doctype
@compile = Compile.new
@compile.load_flavor(@doctype)
@isodoc = isodoc_create # output processor for flavour
@outdir = dir_name_cleanse(options[:output_folder])
@coverpage = options[:coverpage] || collection.coverpage
@format = Util.sort_extensions_execution(options[:format])
@compile_options = options[:compile] || {}
@compile_options[:no_install_fonts] = true if options[:no_install_fonts]
@log = options[:log]
@documents = collection.documents
@bibdata = collection.documents
@directives = collection.directives
@dirname = collection.dirname
@disambig = Util::DisambigFiles.new
@c = HTMLEntities.new
@files_to_delete = []
@nested = options[:nested] # if false, this is the root instance of Renderer
# if true, then this is not the last time Renderer will be run
# (e.g. this is sectionsplit)
# list of files in the collection
@files = Metanorma::FileLookup.new(folder, self)
@files.add_section_split
isodoc_populate
create_non_existing_directory(@outdir)
end
def flush_files
warn "\n\n\n\n\nDone: #{DateTime.now.strftime('%H:%M:%S')}"
warn @files.files_to_delete
@files.files_to_delete.each { |f| FileUtils.rm_f(f) }
@files_to_delete.each { |f| FileUtils.rm_f(f) }
end
# @param col [Metanorma::Collection] XML collection
# @param options [Hash]
# @option options [String] :coverpage cover page HTML (Liquid template)
# @option options [Array] :format list of formats
# @option options [Strong] :ourput_folder output directory
def self.render(col, options = {})
warn "\n\n\n\n\nRender Init: #{DateTime.now.strftime('%H:%M:%S')}"
cr = new(col, File.dirname(col.file), options)
cr.files
cr.concatenate(col, options)
options[:format]&.include?(:html) and cr.coverpage
cr.flush_files
cr
end
def concatenate(col, options)
warn "\n\n\n\n\nConcatenate: #{DateTime.now.strftime('%H:%M:%S')}"
(options[:format] & %i(pdf doc)).empty? or
options[:format] << :presentation
concatenate_prep(col, options)
concatenate_outputs(options)
end
def concatenate_prep(col, options)
%i(xml presentation).each do |e|
options[:format].include?(e) or next
ext = e == :presentation ? "presentation.xml" : e.to_s
File.open(File.join(@outdir, "collection.#{ext}"), "w:UTF-8") do |f|
f.write(concatenate1(col.clone, e).to_xml)
end
end
end
def concatenate_outputs(options)
pres = File.join(@outdir, "collection.presentation.xml")
options[:format].include?(:pdf) and pdfconv.convert(pres)
options[:format].include?(:doc) and docconv_convert(pres)
end
def concatenate1(out, ext)
out.directives << "documents-inline"
out.bibdatas.each_key do |ident|
id = @isodoc.docid_prefix(nil, ident.dup)
@files.get(id, :attachment) || @files.get(id, :outputs).nil? and next
out.documents[Util::key id] =
Metanorma::Document.raw_file(@files.get(id, :outputs)[ext])
end
out
end
# infer the flavour from the first document identifier; relaton does that
def doctype
if (docid = @xml.at("//xmlns:bibdata/xmlns:docidentifier/@type")&.text)
dt = docid.downcase
elsif (docid = @xml.at("//xmlns:bibdata/xmlns:docidentifier")&.text)
dt = docid.sub(/\s.*$/, "").lowercase
else return "standoc"
end
@registry = Metanorma::Registry.instance
@registry.alias(dt.to_sym)&.to_s || dt
end
# populate liquid template of ARGV[1] with metadata extracted from
# collection manifest
def coverpage
@coverpage or return
warn "\n\n\n\n\nCoverpage: #{DateTime.now.strftime('%H:%M:%S')}"
File.open(File.join(@outdir, "index.html"), "w:UTF-8") do |f|
f.write @isodoc.populate_template(File.read(@coverpage))
end
end
# @param elm [Nokogiri::XML::Element]
# @return [String]
def indexfile_title(elm)
elm.at(ns("./title"))&.text
end
# uses the identifier to label documents; other attributes (title) can be
# looked up in @files[id][:bibdata]
#
# @param elm [Nokogiri::XML::Element]
# @param builder [Nokogiri::XML::Builder]
def indexfile_docref(elm, builder)
return "" unless elm.at(ns("./docref[@index = 'true']"))
builder.ul { |b| docrefs(elm, b) }
end
# @param elm [Nokogiri::XML::Element]
# @param builder [Nokogiri::XML::Builder]
def docrefs(elm, builder)
elm.xpath(ns("./docref[@index = 'true']")).each do |d|
ident = docref_ident(d)
builder.li do |li|
li.a href: index_link(d, ident) do |a|
a << ident.split(/([<>&])/).map do |x|
/[<>&]/.match?(x) ? x : @c.encode(x, :hexadecimal)
end.join
end
end
end
end
def docref_ident(docref)
ident = docref.at(ns("./identifier")).children.to_xml
@c.decode(@isodoc.docid_prefix(nil, ident))
end
def index_link(docref, ident)
if docref["fileref"]
@files.get(ident, :out_path).sub(/\.xml$/, ".html")
else "#{docref['id']}.html"
end
end
# single level navigation list, with hierarchical nesting
#
# @param elm [Nokogiri::XML::Element]
# @return [String] XML
def indexfile(elm)
Nokogiri::HTML::Builder.new do |b|
b.ul do
b.li indexfile_title(elm)
indexfile_docref(elm, b)
elm.xpath(ns("./manifest")).each do |d|
b << indexfile(d)
end
end
end.doc.root.to_html
end
# object to construct navigation out of in Liquid
def index_object(elm)
c = elm.xpath(ns("./manifest")).each_with_object([]) do |d, b|
b << index_object(d)
end
c.empty? and c = nil
r = Nokogiri::HTML::Builder.new do |b|
indexfile_docref(elm, b)
end
r &&= r.doc.root&.to_html&.gsub("\n", " ")
{ title: indexfile_title(elm),
docrefs: r, children: c }.compact
end
def liquid_docrefs
@xml.xpath(ns("//docref[@index = 'true']")).each_with_object([]) do |d, m|
ident = d.at(ns("./identifier")).children.to_xml
ident = @c.decode(@isodoc.docid_prefix(nil, ident))
title = d.at(ns("./bibdata/title[@type = 'main']")) ||
d.at(ns("./bibdata/title")) || d.at(ns("./title"))
m << { "identifier" => ident, "file" => index_link(d, ident),
"title" => title&.children&.to_xml,
"level" => d.at(ns("./level"))&.text }
end
end
end
end