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?