lib/review/epubmaker.rb in review-2.0.0.beta1 vs lib/review/epubmaker.rb in review-2.0.0

- old
+ new

@@ -1,15 +1,28 @@ # encoding: utf-8 # -# Copyright (c) 2010-2015 Kenshi Muto and Masayoshi Takahashi +# Copyright (c) 2010-2016 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". # -require 'review' + +require 'tmpdir' + +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/yamlloader' require 'rexml/document' require 'rexml/streamlistener' require 'epubmaker' module ReVIEW @@ -17,42 +30,63 @@ include ::EPUBMaker include REXML def initialize @producer = nil - @tochtmltxt = "toc-html.txt" + @htmltoc = nil @buildlogtxt = "build-log.txt" end def log(s) puts s if @params["debug"].present? end def load_yaml(yamlfile) - @params = ReVIEW::Configure.values.merge(YAML.load_file(yamlfile)) # FIXME:設定がRe:VIEW側とepubmaker/producer.rb側の2つに分かれて面倒 + loader = ReVIEW::YAMLLoader.new + @params = ReVIEW::Configure.values.deep_merge(loader.load_file(yamlfile)) @producer = Producer.new(@params) @producer.load(yamlfile) @params = @producer.params + @params.maker = "epubmaker" end + def build_path + if @params["debug"] + path = File.expand_path("#{@params["bookname"]}-epub", Dir.pwd) + if File.exist?(path) + FileUtils.rm_rf(path, :secure => true) + end + Dir.mkdir(path) + return path + else + return Dir.mktmpdir("#{@params["bookname"]}-epub-") + end + end + def produce(yamlfile, bookname=nil) load_yaml(yamlfile) I18n.setup(@params["language"]) bookname = @params["bookname"] if bookname.nil? booktmpname = "#{bookname}-epub" + begin + @params.check_version(ReVIEW::VERSION) + rescue ReVIEW::ConfigError => e + warn e.message + end log("Loaded yaml file (#{yamlfile}). I will produce #{bookname}.epub.") FileUtils.rm_f("#{bookname}.epub") FileUtils.rm_rf(booktmpname) if @params["debug"] - basetmpdir = Dir.mktmpdir("#{bookname}-", Dir.pwd) + basetmpdir = build_path() begin log("Created first temporary directory as #{basetmpdir}.") call_hook("hook_beforeprocess", basetmpdir) + @htmltoc = ReVIEW::HTMLToc.new(basetmpdir) ## copy all files into basetmpdir copy_stylesheet(basetmpdir) copy_frontmatter(basetmpdir) call_hook("hook_afterfrontmatter", basetmpdir) @@ -82,18 +116,20 @@ @producer.import_imageinfo("#{basetmpdir}/images", basetmpdir) @producer.import_imageinfo("#{basetmpdir}/fonts", basetmpdir, @params["font_ext"]) epubtmpdir = nil if @params["debug"].present? - epubtmpdir = "#{Dir.pwd}/#{booktmpname}" + epubtmpdir = "#{basetmpdir}/#{booktmpname}" Dir.mkdir(epubtmpdir) end log("Call ePUB producer.") @producer.produce("#{bookname}.epub", basetmpdir, epubtmpdir) log("Finished.") ensure - FileUtils.remove_entry_secure basetmpdir if @params["debug"].nil? + unless @params["debug"] + FileUtils.remove_entry_secure basetmpdir + end end end def call_hook(hook_name, *params) filename = @params["epubmaker"][hook_name] @@ -160,11 +196,11 @@ end def recursive_copy_files(resdir, destdir, allow_exts) Dir.open(resdir) do |dir| dir.each do |fname| - next if fname =~ /\A\./ + next if fname.start_with?('.') if FileTest.directory?("#{resdir}/#{fname}") recursive_copy_files("#{resdir}/#{fname}", "#{destdir}/#{fname}", allow_exts) else if fname =~ /\.(#{allow_exts.join("|")})\Z/i FileUtils.mkdir_p(destdir) @@ -174,72 +210,87 @@ end end end end + def check_compile_status + return unless @compile_errors + + $stderr.puts "compile error, No EPUB file output." + exit 1 + end + def build_body(basetmpdir, yamlfile) @precount = 0 @bodycount = 0 @postcount = 0 @manifeststr = "" @ncxstr = "" @tocdesc = Array.new - # toccount = 2 ## not used - basedir = Dir.pwd + basedir = File.dirname(yamlfile) base_path = Pathname.new(basedir) book = ReVIEW::Book.load(basedir) - book.load_config(yamlfile) + book.config = @params + @converter = ReVIEW::Converter.new(book, ReVIEW::HTMLBuilder.new) + @compile_errors = nil book.parts.each do |part| htmlfile = nil if part.name.present? if part.file? - build_chap(part, base_path, basetmpdir, yamlfile, true) + build_chap(part, base_path, basetmpdir, true) else htmlfile = "part_#{part.number}.#{@params["htmlext"]}" build_part(part, basetmpdir, htmlfile) title = ReVIEW::I18n.t("part", part.number) title += ReVIEW::I18n.t("chapter_postfix") + part.name.strip if part.name.strip.present? - write_tochtmltxt(basetmpdir, "0\t#{htmlfile}\t#{title}\tchaptype=part") + @htmltoc.add_item(0, htmlfile, title, {:chaptype => "part"}) write_buildlogtxt(basetmpdir, htmlfile, "") end end part.chapters.each do |chap| - build_chap(chap, base_path, basetmpdir, yamlfile, nil) + build_chap(chap, base_path, basetmpdir, false) end end + check_compile_status() end def build_part(part, basetmpdir, htmlfile) log("Create #{htmlfile} from a template.") File.open("#{basetmpdir}/#{htmlfile}", "w") do |f| - f.puts header(CGI.escapeHTML(@params["booktitle"])) - f.puts <<EOT -<div class="part"> -<h1 class="part-number">#{ReVIEW::I18n.t("part", part.number)}</h1> -EOT + @body = "" + @body << "<div class=\"part\">\n" + @body << "<h1 class=\"part-number\">#{CGI.escapeHTML(ReVIEW::I18n.t("part", part.number))}</h1>\n" if part.name.strip.present? - f.puts <<EOT -<h2 class="part-title">#{part.name.strip}</h2> -EOT + @body << "<h2 class=\"part-title\">#{CGI.escapeHTML(part.name.strip)}</h2>\n" end + @body << "</div>\n" - f.puts <<EOT -</div> -EOT - f.puts footer + @language = @producer.params['language'] + @stylesheets = @producer.params["stylesheet"] + tmplfile = File.expand_path(template_name, ReVIEW::Template::TEMPLATE_DIR) + tmpl = ReVIEW::Template.load(tmplfile) + f.write tmpl.result(binding) end end - def build_chap(chap, base_path, basetmpdir, yamlfile, ispart=nil) + def template_name + if @producer.params["htmlversion"].to_i == 5 + './html/layout-html5.html.erb' + else + './html/layout-xhtml1.html.erb' + end + end + + def build_chap(chap, base_path, basetmpdir, ispart) filename = "" chaptype = "body" - if ispart.present? + if ispart chaptype = "part" elsif chap.on_PREDEF? chaptype = "pre" elsif chap.on_APPENDIX? chaptype = "post" @@ -267,31 +318,36 @@ htmlfile = "#{id}.#{@params["htmlext"]}" write_buildlogtxt(basetmpdir, htmlfile, filename) log("Create #{htmlfile} from #{filename}.") - level = @params["secnolevel"] - -# TODO: It would be nice if we can modify level in PART, PREDEF, or POSTDEF. -# But we have to care about section number reference (@<hd>) also. -# -# if !ispart.nil? -# level = @params["part_secnolevel"] -# else -# level = @params["pre_secnolevel"] if chap.on_PREDEF? -# level = @params["post_secnolevel"] if chap.on_APPENDIX? -# end - - stylesheet = "" - if @params["stylesheet"].size > 0 - stylesheet = "--stylesheet=#{@params["stylesheet"].join(",")}" + if @params["params"].present? + warn "'params:' in config.yml is obsoleted." + if @params["params"] =~ /stylesheet=/ + warn "stylesheets should be defined in 'stylesheet:', not in 'params:'" + end end + begin + @converter.convert(filename, File.join(basetmpdir, htmlfile)) + write_info_body(basetmpdir, id, htmlfile, ispart, chaptype) + remove_hidden_title(basetmpdir, htmlfile) + rescue => e + @compile_errors = true + warn "compile error in #{filename} (#{e.class})" + warn e.message + end + end - ENV["REVIEWFNAME"] = filename - system("#{ReVIEW::MakerHelper.bindir}/review-compile --yaml=#{yamlfile} --target=html --level=#{level} --htmlversion=#{@params["htmlversion"]} --epubversion=#{@params["epubversion"]} #{stylesheet} #{@params["params"]} #{filename} > \"#{basetmpdir}/#{htmlfile}\"") - - write_info_body(basetmpdir, id, htmlfile, ispart, chaptype) + def remove_hidden_title(basetmpdir, htmlfile) + File.open("#{basetmpdir}/#{htmlfile}", "r+") do |f| + body = f.read. + gsub(/<h\d .*?hidden=['"]true['"].*?>.*?<\/h\d>\n/, ''). + gsub(/(<h\d .*?)\s*notoc=['"]true['"]\s*(.*?>.*?<\/h\d>\n)/, '\1\2') + f.rewind + f.print body + f.truncate(f.tell) + end end def detect_properties(path) properties = [] File.open(path) do |f| @@ -306,11 +362,10 @@ properties end def write_info_body(basetmpdir, id, filename, ispart=nil, chaptype=nil) headlines = [] - # FIXME:nonumを修正する必要あり path = File.join(basetmpdir, filename) Document.parse_stream(File.new(path), ReVIEWHeaderListener.new(headlines)) properties = detect_properties(path) prop_str = "" if properties.present? @@ -318,55 +373,34 @@ end first = true headlines.each do |headline| headline["level"] = 0 if ispart.present? && headline["level"] == 1 if first.nil? - write_tochtmltxt(basetmpdir, "#{headline["level"]}\t#{filename}##{headline["id"]}\t#{headline["title"]}\tchaptype=#{chaptype}") + @htmltoc.add_item(headline["level"], filename+"#"+headline["id"], headline["title"], {:chaptype => chaptype, :notoc => headline["notoc"]}) else - write_tochtmltxt(basetmpdir, "#{headline["level"]}\t#{filename}\t#{headline["title"]}\tforce_include=true,chaptype=#{chaptype}#{prop_str}") + @htmltoc.add_item(headline["level"], filename, headline["title"], {:force_include => true, :chaptype => chaptype+prop_str, :notoc => headline["notoc"]}) first = nil end end end def push_contents(basetmpdir) - File.open("#{basetmpdir}/#{@tochtmltxt}") do |f| - f.each_line do |l| - force_include = nil - customid = nil - chaptype = nil - properties = nil - level, file, title, custom = l.chomp.split("\t") - if custom.present? - # custom setting - vars = custom.split(/,\s*/) - vars.each do |var| - k, v = var.split("=") - case k - when "id" - customid = v - when "force_include" - force_include = true - when "chaptype" - chaptype = v - when "properties" - properties = v - end - end - end - next if level.to_i > @params["toclevel"] && force_include.nil? - log("Push #{file} to ePUB contents.") + @htmltoc.each_item do |level, file, title, args| + next if level.to_i > @params["toclevel"] && args[:force_include].nil? + log("Push #{file} to ePUB contents.") - hash = {"file" => file, "level" => level.to_i, "title" => title, "chaptype" => chaptype} - if customid.present? - hash["id"] = customid - end - if properties.present? - hash["properties"] = properties.split(" ") - end - @producer.contents.push(Content.new(hash)) + hash = {"file" => file, "level" => level.to_i, "title" => title, "chaptype" => args[:chaptype]} + if args[:id].present? + hash["id"] = args[:id] end + if args[:properties].present? + hash["properties"] = args[:properties].split(" ") + end + if args[:notoc].present? + hash["notoc"] = args[:notoc] + end + @producer.contents.push(Content.new(hash)) end end def copy_stylesheet(basetmpdir) if @params["stylesheet"].size > 0 @@ -384,131 +418,79 @@ if @params["titlefile"].nil? build_titlepage(basetmpdir, "titlepage.#{@params["htmlext"]}") else FileUtils.cp(@params["titlefile"], "#{basetmpdir}/titlepage.#{@params["htmlext"]}") end - write_tochtmltxt(basetmpdir, "1\ttitlepage.#{@params["htmlext"]}\t#{@producer.res.v("titlepagetitle")}\tchaptype=pre") + @htmltoc.add_item(1, "titlepage.#{@params['htmlext']}", @producer.res.v("titlepagetitle"), {:chaptype => "pre"}) end if @params["originaltitlefile"].present? && File.exist?(@params["originaltitlefile"]) FileUtils.cp(@params["originaltitlefile"], "#{basetmpdir}/#{File.basename(@params["originaltitlefile"])}") - write_tochtmltxt(basetmpdir, "1\t#{File.basename(@params["originaltitlefile"])}\t#{@producer.res.v("originaltitle")}\tchaptype=pre") + @htmltoc.add_item(1, File.basename(@params["originaltitlefile"]), @producer.res.v("originaltitle"), {:chaptype => "pre"}) end if @params["creditfile"].present? && File.exist?(@params["creditfile"]) FileUtils.cp(@params["creditfile"], "#{basetmpdir}/#{File.basename(@params["creditfile"])}") - write_tochtmltxt(basetmpdir, "1\t#{File.basename(@params["creditfile"])}\t#{@producer.res.v("credittitle")}\tchaptype=pre") + @htmltoc.add_item(1, File.basename(@params["creditfile"]), @producer.res.v("credittitle"), {:chaptype => "pre"}) end end def build_titlepage(basetmpdir, htmlfile) + # TODO: should be created via epubcommon + @title = CGI.escapeHTML(@params.name_of("booktitle")) File.open("#{basetmpdir}/#{htmlfile}", "w") do |f| - f.puts header(CGI.escapeHTML(@params["booktitle"])) - f.puts <<EOT -<div class="titlepage"> -<h1 class="tp-title">#{CGI.escapeHTML(@params["booktitle"])}</h1> -EOT - + @body = "" + @body << "<div class=\"titlepage\">\n" + @body << "<h1 class=\"tp-title\">#{CGI.escapeHTML(@params.name_of("booktitle"))}</h1>\n" if @params["aut"] - f.puts <<EOT -<h2 class="tp-author">#{@params["aut"].join(", ")}</h2> -EOT + @body << "<h2 class=\"tp-author\">#{CGI.escapeHTML(@params.names_of("aut").join(ReVIEW::I18n.t("names_splitter")))}</h2>\n" end if @params["prt"] - f.puts <<EOT -<h3 class="tp-publisher">#{@params["prt"].join(", ")}</h3> -EOT + @body << "<h3 class=\"tp-publisher\">#{CGI.escapeHTML(@params.names_of("prt").join(ReVIEW::I18n.t("names_splitter")))}</h3>\n" end + @body << "</div>" - f.puts <<EOT -</div> -EOT - f.puts footer + @language = @producer.params['language'] + @stylesheets = @producer.params["stylesheet"] + tmplfile = File.expand_path(template_name, ReVIEW::Template::TEMPLATE_DIR) + tmpl = ReVIEW::Template.load(tmplfile) + f.write tmpl.result(binding) end end def copy_backmatter(basetmpdir) if @params["profile"] FileUtils.cp(@params["profile"], "#{basetmpdir}/#{File.basename(@params["profile"])}") - write_tochtmltxt(basetmpdir, "1\t#{File.basename(@params["profile"])}\t#{@producer.res.v("profiletitle")}\tchaptype=post") + @htmltoc.add_item(1, File.basename(@params["profile"]), @producer.res.v("profiletitle"), {:chaptype => "post"}) end if @params["advfile"] FileUtils.cp(@params["advfile"], "#{basetmpdir}/#{File.basename(@params["advfile"])}") - write_tochtmltxt(basetmpdir, "1\t#{File.basename(@params["advfile"])}\t#{@producer.res.v("advtitle")}\tchaptype=post") + @htmltoc.add_item(1, File.basename(@params["advfile"]), @producer.res.v("advtitle"), {:chaptype => "post"}) end if @params["colophon"] - if @params["colophon"].instance_of?(String) # FIXME:このやり方はやめる? + if @params["colophon"].kind_of?(String) # FIXME:このやり方はやめる? FileUtils.cp(@params["colophon"], "#{basetmpdir}/colophon.#{@params["htmlext"]}") else File.open("#{basetmpdir}/colophon.#{@params["htmlext"]}", "w") {|f| @producer.colophon(f) } end - write_tochtmltxt(basetmpdir, "1\tcolophon.#{@params["htmlext"]}\t#{@producer.res.v("colophontitle")}\tchaptype=post") + @htmltoc.add_item(1, "colophon.#{@params["htmlext"]}", @producer.res.v("colophontitle"), {:chaptype => "post"}) end if @params["backcover"] FileUtils.cp(@params["backcover"], "#{basetmpdir}/#{File.basename(@params["backcover"])}") - write_tochtmltxt(basetmpdir, "1\t#{File.basename(@params["backcover"])}\t#{@producer.res.v("backcovertitle")}\tchaptype=post") + @htmltoc.add_item(1, File.basename(@params["backcover"]), @producer.res.v("backcovertitle"), {:chaptype => "post"}) end end - def write_tochtmltxt(basetmpdir, s) - File.open("#{basetmpdir}/#{@tochtmltxt}", "a") do |f| - f.puts s - end - end - def write_buildlogtxt(basetmpdir, htmlfile, reviewfile) File.open("#{basetmpdir}/#{@buildlogtxt}", "a") do |f| f.puts "#{htmlfile},#{reviewfile}" end end - def header(title) - # titleはすでにエスケープ済みと想定 - s = <<EOT -<?xml version="1.0" encoding="UTF-8"?> -EOT - if @params["htmlversion"] == 5 - s << <<EOT -<!DOCTYPE html> -<html xml:lang='ja' xmlns:ops='http://www.idpf.org/2007/ops' xmlns='http://www.w3.org/1999/xhtml'> -<head> - <meta charset="UTF-8" /> -EOT - else - s << <<EOT -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> -<html xml:lang='ja' xmlns:ops='http://www.idpf.org/2007/ops' xmlns='http://www.w3.org/1999/xhtml'> -<head> - <meta http-equiv='Content-Type' content='text/html;charset=UTF-8' /> - <meta http-equiv='Content-Style-Type' content='text/css' /> -EOT - end - if @params["stylesheet"].size > 0 - @params["stylesheet"].each do |sfile| - s << <<EOT - <link rel='stylesheet' type='text/css' href='#{sfile}' /> -EOT - end - end - s << <<EOT - <meta content='Re:VIEW' name='generator'/> - <title>#{title}</title> -</head> -<body> -EOT - end - - def footer - <<EOT -</body> -</html> -EOT - end - class ReVIEWHeaderListener include REXML::StreamListener def initialize(headlines) @level = nil @content = "" @@ -520,10 +502,11 @@ if @level.present? raise "#{name}, #{attrs}" end @level = $1.to_i @id = attrs["id"] if attrs["id"].present? + @notoc = attrs["notoc"] if attrs["notoc"].present? elsif !@level.nil? if name == "img" && attrs["alt"].present? @content << attrs["alt"] elsif name == "a" && attrs["id"].present? @id = attrs["id"] @@ -531,13 +514,14 @@ end end def tag_end(name) if name =~ /\Ah\d+/ - @headlines.push({"level" => @level, "id" => @id, "title" => @content}) if @id.present? + @headlines.push({"level" => @level, "id" => @id, "title" => @content, "notoc" => @notoc}) if @id.present? @content = "" @level = nil @id = nil + @notoc = nil end end def text(text) if @level.present?