lib/review/epubmaker.rb in review-5.0.0 vs lib/review/epubmaker.rb in review-5.1.0

- old
+ new

@@ -1,6 +1,6 @@ -# Copyright (c) 2010-2020 Kenshi Muto and Masayoshi Takahashi +# Copyright (c) 2010-2021 Kenshi Muto and Masayoshi Takahashi # # This program is free software. # You can distribute or modify this program under the terms of # the GNU LGPL, Lesser General Public License version 2.1. # For details of the GNU LGPL, see the file "COPYING". @@ -11,32 +11,37 @@ require 'review/i18n' require 'review/book' require 'review/configure' require 'review/converter' require 'review/latexbuilder' -require 'review/yamlloader' require 'review/version' require 'review/htmltoc' require 'review/htmlbuilder' +require 'review/img_math' require 'rexml/document' require 'rexml/streamlistener' -require 'epubmaker' +require 'review/call_hook' +require 'review/epubmaker/producer' +require 'review/epubmaker/content' +require 'review/epubmaker/epubv2' +require 'review/epubmaker/epubv3' require 'review/epubmaker/reviewheaderlistener' require 'review/makerhelper' module ReVIEW class EPUBMaker - include ::EPUBMaker - include REXML include MakerHelper + include ReVIEW::CallHook def initialize @producer = nil @htmltoc = nil @buildlogtxt = 'build-log.txt' @logger = ReVIEW.logger + @img_math = nil + @basedir = nil end def error(msg) @logger.error msg exit 1 @@ -48,16 +53,10 @@ def log(msg) @logger.debug(msg) end - def load_yaml(yamlfile) - @producer = Producer.new(@config) - @producer.load(yamlfile) - @config = @producer.config - end - def self.execute(*args) self.new.execute(*args) end def parse_opts(args) @@ -88,21 +87,27 @@ error "#{yamlfile} not found." unless File.exist?(yamlfile) @config = ReVIEW::Configure.create(maker: 'epubmaker', yamlfile: yamlfile, config: cmd_config) - load_yaml(yamlfile) + @producer = ReVIEW::EPUBMaker::Producer.new(@config) update_log_level log("Loaded yaml file (#{yamlfile}).") + @basedir = File.absolute_path(File.dirname(yamlfile)) produce(yamlfile, exportfile) end def update_log_level if @config['debug'] - @logger.level = Logger::DEBUG - else + if @logger.ttylogger? + ReVIEW.logger = nil + @logger = ReVIEW.logger(level: 'debug') + else + @logger.level = Logger::DEBUG + end + elsif !@logger.ttylogger? @logger.level = Logger::INFO end end def build_path @@ -121,10 +126,11 @@ def produce(yamlfile, bookname = nil) I18n.setup(@config['language']) bookname ||= @config['bookname'] booktmpname = "#{bookname}-epub" + @img_math = ReVIEW::ImgMath.new(@config) begin @config.check_version(ReVIEW::VERSION) rescue ReVIEW::ConfigError => e warn e.message end @@ -133,35 +139,34 @@ FileUtils.rm_f("#{bookname}.epub") if @config['debug'] FileUtils.rm_rf(booktmpname) end - cleanup_mathimg + @img_math.cleanup_mathimg basetmpdir = build_path begin log("Created first temporary directory as #{basetmpdir}.") - call_hook('hook_beforeprocess', basetmpdir) + call_hook('hook_beforeprocess', basetmpdir, base_dir: @basedir) @htmltoc = ReVIEW::HTMLToc.new(basetmpdir) ## copy all files into basetmpdir copy_stylesheet(basetmpdir) copy_frontmatter(basetmpdir) - call_hook('hook_afterfrontmatter', basetmpdir) + call_hook('hook_afterfrontmatter', basetmpdir, base_dir: @basedir) build_body(basetmpdir, yamlfile) - call_hook('hook_afterbody', basetmpdir) + call_hook('hook_afterbody', basetmpdir, base_dir: @basedir) copy_backmatter(basetmpdir) - math_dir = "./#{@config['imagedir']}/_review_math" - if @config['imgmath'] && File.exist?(File.join(math_dir, '__IMGMATH_BODY__.map')) - make_math_images(math_dir) + if @config['math_format'] == 'imgmath' + @img_math.make_math_images end - call_hook('hook_afterbackmatter', basetmpdir) + call_hook('hook_afterbackmatter', basetmpdir, base_dir: @basedir) ## push contents in basetmpdir into @producer push_contents(basetmpdir) if @config['epubmaker']['verify_target_images'].present? @@ -173,11 +178,11 @@ copy_resources('covers', File.join(basetmpdir, @config['imagedir'])) copy_resources('adv', File.join(basetmpdir, @config['imagedir'])) copy_resources(@config['fontdir'], File.join(basetmpdir, 'fonts'), @config['font_ext']) - call_hook('hook_aftercopyimage', basetmpdir) + call_hook('hook_aftercopyimage', basetmpdir, base_dir: @basedir) @producer.import_imageinfo(File.join(basetmpdir, @config['imagedir']), basetmpdir) @producer.import_imageinfo(File.join(basetmpdir, 'fonts'), basetmpdir, @config['font_ext']) check_image_size(basetmpdir, @config['image_maxpixels'], @config['image_ext']) @@ -186,38 +191,28 @@ if @config['debug'].present? epubtmpdir = File.join(basetmpdir, booktmpname) Dir.mkdir(epubtmpdir) end log('Call ePUB producer.') - @producer.produce("#{bookname}.epub", basetmpdir, epubtmpdir) + @producer.produce("#{bookname}.epub", basetmpdir, epubtmpdir, base_dir: @basedir) log('Finished.') + @logger.success("built #{bookname}.epub") rescue ApplicationError => e raise if @config['debug'] + error(e.message) ensure FileUtils.remove_entry_secure(basetmpdir) unless @config['debug'] end end - def call_hook(hook_name, *params) - filename = @config['epubmaker'][hook_name] - log("Call #{hook_name}. (#{filename})") - if filename.present? && File.exist?(filename) && FileTest.executable?(filename) - if ENV['REVIEW_SAFE_MODE'].to_i & 1 > 0 - warn 'hook is prohibited in safe mode. ignored.' - else - system(filename, *params) - end - end - end - def verify_target_images(basetmpdir) @producer.contents.each do |content| case content.media when 'application/xhtml+xml' File.open("#{basetmpdir}/#{content.file}") do |f| - Document.new(File.new(f)).each_element('//img') do |e| + REXML::Document.new(File.new(f)).each_element('//img') do |e| @config['epubmaker']['force_include_images'].push(e.attributes['src']) if e.attributes['src'] =~ /svg\Z/i content.properties.push('svg') end end @@ -235,10 +230,11 @@ @config['epubmaker']['force_include_images'] = @config['epubmaker']['force_include_images'].compact.sort.uniq end def copy_images(resdir, destdir, allow_exts = nil) return nil unless File.exist?(resdir) + allow_exts ||= @config['image_ext'] FileUtils.mkdir_p(destdir) if @config['epubmaker']['verify_target_images'].present? @config['epubmaker']['force_include_images'].each do |file| unless File.exist?(file) @@ -257,19 +253,21 @@ end end def copy_resources(resdir, destdir, allow_exts = nil) return nil unless File.exist?(resdir) + allow_exts ||= @config['image_ext'] FileUtils.mkdir_p(destdir) recursive_copy_files(resdir, destdir, allow_exts) end def recursive_copy_files(resdir, destdir, allow_exts) Dir.open(resdir) do |dir| dir.each do |fname| next if fname.start_with?('.') + if FileTest.directory?(File.join(resdir, fname)) recursive_copy_files(File.join(resdir, fname), File.join(destdir, fname), allow_exts) elsif fname =~ /\.(#{allow_exts.join('|')})\Z/i FileUtils.mkdir_p(destdir) log("Copy #{resdir}/#{fname} to the temporary directory.") @@ -296,11 +294,11 @@ @tocdesc = [] basedir = File.dirname(yamlfile) base_path = Pathname.new(basedir) book = ReVIEW::Book::Base.new(basedir, config: @config) - @converter = ReVIEW::Converter.new(book, ReVIEW::HTMLBuilder.new) + @converter = ReVIEW::Converter.new(book, ReVIEW::HTMLBuilder.new(img_math: @img_math)) @compile_errors = nil book.parts.each do |part| if part.name.present? if part.file? @@ -325,23 +323,17 @@ end def build_part(part, basetmpdir, htmlfile) log("Create #{htmlfile} from a template.") File.open(File.join(basetmpdir, htmlfile), 'w') do |f| - @body = '' - @body << %Q(<div class="part">\n) - @body << %Q(<h1 class="part-number">#{h(ReVIEW::I18n.t('part', part.number))}</h1>\n) - if part.name.strip.present? - @body << %Q(<h2 class="part-title">#{h(part.name.strip)}</h2>\n) - end - @body << %Q(</div>\n) + @part_number = part.number + @part_title = part.name.strip + @body = ReVIEW::Template.generate(path: 'html/_part_body.html.erb', binding: binding) @language = @producer.config['language'] @stylesheets = @producer.config['stylesheet'] - tmplfile = File.expand_path(template_name, ReVIEW::Template::TEMPLATE_DIR) - tmpl = ReVIEW::Template.load(tmplfile) - f.write tmpl.result(binding) + f.write ReVIEW::Template.generate(path: template_name, binding: binding) end end def template_name if @producer.config['htmlversion'].to_i == 5 @@ -434,16 +426,23 @@ end end properties end - def write_info_body(basetmpdir, _id, filename, ispart = nil, chaptype = nil) + def parse_headlines(path) headlines = [] + + File.open(path) do |htmlio| + REXML::Document.parse_stream(htmlio, ReVIEWHeaderListener.new(headlines)) + end + + headlines + end + + def write_info_body(basetmpdir, _id, filename, ispart = nil, chaptype = nil) path = File.join(basetmpdir, filename) - htmlio = File.new(path) - Document.parse_stream(htmlio, ReVIEWHeaderListener.new(headlines)) - htmlio.close + headlines = parse_headlines(path) if headlines.empty? warn "#{filename} is discarded because there is no heading. Use `=[notoc]' or `=[nodisp]' to exclude headlines from the table of contents." return end @@ -478,74 +477,81 @@ end def push_contents(_basetmpdir) @htmltoc.each_item do |level, file, title, args| next if level.to_i > @config['toclevel'] && args[:force_include].nil? + log("Push #{file} to ePUB contents.") - hash = { 'file' => file, - 'level' => level.to_i, - 'title' => title, - 'chaptype' => args[:chaptype] } + params = { file: file, + level: level.to_i, + title: title, + chaptype: args[:chaptype] } if args[:id].present? - hash['id'] = args[:id] + params[:id] = args[:id] end if args[:properties].present? - hash['properties'] = args[:properties].split(' ') + params[:properties] = args[:properties].split(' ') # rubocop:disable Style/RedundantArgument end if args[:notoc].present? - hash['notoc'] = args[:notoc] + params[:notoc] = args[:notoc] end - @producer.contents.push(Content.new(hash)) + @producer.contents.push(ReVIEW::EPUBMaker::Content.new(**params)) end end def copy_stylesheet(basetmpdir) return if @config['stylesheet'].empty? + @config['stylesheet'].each do |sfile| unless File.exist?(sfile) - error "#{sfile} is not found." + error "stylesheet: #{sfile} is not found." end FileUtils.cp(sfile, basetmpdir) - @producer.contents.push(Content.new('file' => sfile)) + @producer.contents.push(ReVIEW::EPUBMaker::Content.new(file: sfile)) end end + def copy_static_file(configname, destdir, destfilename: nil) + destfilename ||= @config[configname] + unless File.exist?(@config[configname]) + error "#{configname}: #{@config[configname]} is not found." + end + FileUtils.cp(@config[configname], + File.join(destdir, destfilename)) + end + def copy_frontmatter(basetmpdir) if @config['cover'].present? && File.exist?(@config['cover']) - FileUtils.cp(@config['cover'], - File.join(basetmpdir, File.basename(@config['cover']))) + copy_static_file('cover', basetmpdir) end if @config['titlepage'] if @config['titlefile'].nil? build_titlepage(basetmpdir, "titlepage.#{@config['htmlext']}") else - FileUtils.cp(@config['titlefile'], - File.join(basetmpdir, "titlepage.#{@config['htmlext']}")) + copy_static_file('titlefile', basetmpdir, destfilename: "titlepage.#{@config['htmlext']}") end @htmltoc.add_item(1, "titlepage.#{@config['htmlext']}", - @producer.res.v('titlepagetitle'), + ReVIEW::I18n.t('titlepagetitle'), chaptype: 'pre') end - if @config['originaltitlefile'].present? && File.exist?(@config['originaltitlefile']) - FileUtils.cp(@config['originaltitlefile'], - File.join(basetmpdir, File.basename(@config['originaltitlefile']))) + if @config['originaltitlefile'].present? + copy_static_file('originaltitlefile', basetmpdir) @htmltoc.add_item(1, File.basename(@config['originaltitlefile']), - @producer.res.v('originaltitle'), + ReVIEW::I18n.t('originaltitle'), chaptype: 'pre') end - if @config['creditfile'].present? && File.exist?(@config['creditfile']) - FileUtils.cp(@config['creditfile'], - File.join(basetmpdir, File.basename(@config['creditfile']))) + if @config['creditfile'].present? + copy_static_file('creditfile', basetmpdir) @htmltoc.add_item(1, File.basename(@config['creditfile']), - @producer.res.v('credittitle'), + ReVIEW::I18n.t('credittitle'), chaptype: 'pre') end true end @@ -568,57 +574,46 @@ end @body << '</div>' @language = @producer.config['language'] @stylesheets = @producer.config['stylesheet'] - tmplfile = File.expand_path(template_name, ReVIEW::Template::TEMPLATE_DIR) - tmpl = ReVIEW::Template.load(tmplfile) - f.write tmpl.result(binding) + f.write ReVIEW::Template.generate(path: template_name, binding: binding) end end def copy_backmatter(basetmpdir) if @config['profile'] - FileUtils.cp(@config['profile'], - File.join(basetmpdir, File.basename(@config['profile']))) + copy_static_file('profile', basetmpdir) @htmltoc.add_item(1, File.basename(@config['profile']), - @producer.res.v('profiletitle'), + ReVIEW::I18n.t('profiletitle'), chaptype: 'post') end if @config['advfile'] - FileUtils.cp(@config['advfile'], - File.join(basetmpdir, File.basename(@config['advfile']))) + copy_static_file('advfile', basetmpdir) @htmltoc.add_item(1, File.basename(@config['advfile']), - @producer.res.v('advtitle'), + ReVIEW::I18n.t('advtitle'), chaptype: 'post') end if @config['colophon'] - if @config['colophon'].is_a?(String) # FIXME: should let obsolete this style? - FileUtils.cp(@config['colophon'], - File.join(basetmpdir, "colophon.#{@config['htmlext']}")) - else - filename = File.join(basetmpdir, "colophon.#{@config['htmlext']}") - File.open(filename, 'w') do |f| - @producer.colophon(f) - end + if @config['colophon'].is_a?(String) + copy_static_file('colophon', basetmpdir, destfilename: "colophon.#{@config['htmlext']}") # override pre-built colophon end @htmltoc.add_item(1, "colophon.#{@config['htmlext']}", - @producer.res.v('colophontitle'), + ReVIEW::I18n.t('colophontitle'), chaptype: 'post') end if @config['backcover'] - FileUtils.cp(@config['backcover'], - File.join(basetmpdir, File.basename(@config['backcover']))) + copy_static_file('backcover', basetmpdir) @htmltoc.add_item(1, File.basename(@config['backcover']), - @producer.res.v('backcovertitle'), + ReVIEW::I18n.t('backcovertitle'), chaptype: 'post') end true end @@ -640,11 +635,13 @@ pat = '\\.(' + allow_exts.delete_if { |t| %w[ttf woff otf].member?(t.downcase) }.join('|') + ')' extre = Regexp.new(pat, Regexp::IGNORECASE) Find.find(basetmpdir) do |fname| next unless fname.match(extre) + img = ImageSize.path(fname) next if img.width.nil? || img.width * img.height <= maxpixels + h = Math.sqrt(img.height * maxpixels / img.width) w = maxpixels / h fname.sub!("#{basetmpdir}/", '') warn "#{fname}: #{img.width}x#{img.height} exceeds a limit. suggeted value is #{w.to_i}x#{h.to_i}" end