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

- old
+ new

@@ -1,41 +1,52 @@ # encoding: utf-8 # = epubcommon.rb -- super class for EPUBv2 and EPUBv3 # -# Copyright (c) 2010-2014 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 'epubmaker/producer' require 'review/i18n' +require 'review/template' require 'cgi' require 'shellwords' +begin + require 'zip' +rescue LoadError + ## I cannot find rubyzip library, so I use external zip command. +end module EPUBMaker # EPUBCommon is the common class for EPUB producer. class EPUBCommon # Construct object with parameter hash +params+ and message resource hash +res+. def initialize(producer) + @body_ext = '' @producer = producer + @body_ext = nil end # Return mimetype content. def mimetype "application/epub+zip" end + def opf_path + "OEBPS/#{@producer.params["bookname"]}.opf" + end + def opf_coverimage s = "" if @producer.params["coverimage"] file = nil @producer.contents.each do |item| - if item.media =~ /\Aimage/ && item.file =~ /#{@producer.params["coverimage"]}\Z/ + if item.media.start_with?('image') && item.file =~ /#{@producer.params["coverimage"]}\Z/ s << %Q[ <meta name="cover" content="#{item.id}"/>\n] file = item.file break end end @@ -43,24 +54,21 @@ end s end def ncx_isbn - if @producer.params["isbn"].nil? - %Q[ <meta name="dtb:uid" content="#{@producer.params["urnid"]}"/>\n] - else - %Q[ <meta name="dtb:uid" content="#{@producer.params["isbn"]}"/>\n] - end + uid = @producer.params["isbn"] || @producer.params["urnid"] + %Q[ <meta name="dtb:uid" content="#{uid}"/>\n] end def ncx_doctitle <<EOT <docTitle> <text>#{CGI.escapeHTML(@producer.params["title"])}</text> </docTitle> <docAuthor> - <text>#{@producer.params["aut"].nil? ? "" : CGI.escapeHTML(@producer.params["aut"].join(", "))}</text> + <text>#{@producer.params["aut"].nil? ? "" : CGI.escapeHTML(join_with_separator(@producer.params["aut"], ReVIEW::I18n.t("names_splitter")))}</text> </docAuthor> EOT end def ncx_navmap(indentarray) @@ -78,11 +86,11 @@ unless @producer.params["mytoc"].nil? s << <<EOT <navPoint id="toc" playOrder="#{nav_count}"> <navLabel> - <text>#{@producer.res.v("toctitle")}</text> + <text>#{CGI.escapeHTML(@producer.res.v("toctitle"))}</text> </navLabel> <content src="#{@producer.params["bookname"]}-toc.#{@producer.params["htmlext"]}"/> </navPoint> EOT nav_count += 1 @@ -110,204 +118,190 @@ s end # Return container content. def container - s = <<EOT -<?xml version="1.0" encoding="UTF-8"?> -<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0"> - <rootfiles> - <rootfile full-path="OEBPS/#{@producer.params["bookname"]}.opf" media-type="application/oebps-package+xml" /> - </rootfiles> -</container> -EOT - s + @opf_path = opf_path + tmplfile = File.expand_path('./xml/container.xml.erb', ReVIEW::Template::TEMPLATE_DIR) + tmpl = ReVIEW::Template.load(tmplfile) + tmpl.result(binding) end # Return cover content. def cover(type=nil) - bodyext = type.nil? ? "" : " epub:type=\"#{type}\"" + @body_ext = type.nil? ? "" : " epub:type=\"#{type}\"" - s = common_header - s << <<EOT - <title>#{CGI.escapeHTML(@producer.params["title"])}</title> -</head> -<body#{bodyext}> -EOT - if @producer.params["coverimage"].nil? - s << <<EOT -<h1 class="cover-title">#{CGI.escapeHTML(@producer.params["title"])}</h1> -EOT - else - file = nil - @producer.contents.each do |item| - if item.media =~ /\Aimage/ && item.file =~ /#{@producer.params["coverimage"]}\Z/ # / - file = item.file - break - end + if @producer.params["coverimage"] + file = @producer.coverimage + if !file + raise "coverimage #{@producer.params["coverimage"]} not found. Abort." end - raise "coverimage #{@producer.params["coverimage"]} not found. Abort." if file.nil? - s << <<EOT + @body = <<-EOT <div id="cover-image" class="cover-image"> - <img src="#{file}" alt="#{CGI.escapeHTML(@producer.params["title"])}" class="max"/> + <img src="#{file}" alt="#{CGI.escapeHTML(@producer.params.name_of("title"))}" class="max"/> </div> -EOT + EOT + else + @body = <<-EOT +<h1 class="cover-title">#{CGI.escapeHTML(@producer.params.name_of("title"))}</h1> + EOT end - s << <<EOT -</body> -</html> -EOT - s + @title = CGI.escapeHTML(@producer.params.name_of("title")) + @language = @producer.params['language'] + @stylesheets = @producer.params["stylesheet"] + if @producer.params["htmlversion"].to_i == 5 + tmplfile = File.expand_path('./html/layout-html5.html.erb', ReVIEW::Template::TEMPLATE_DIR) + else + tmplfile = File.expand_path('./html/layout-xhtml1.html.erb', ReVIEW::Template::TEMPLATE_DIR) + end + tmpl = ReVIEW::Template.load(tmplfile) + tmpl.result(binding) end # Return title (copying) content. def titlepage - s = common_header - s << <<EOT - <title>#{CGI.escapeHTML(@producer.params["title"])}</title> -</head> -<body> - <h1 class="tp-title">#{CGI.escapeHTML(@producer.params["title"])}</h1> -EOT + @title = CGI.escapeHTML(@producer.params.name_of("title")) + @body = <<EOT + <h1 class="tp-title">#{@title}</h1> +EOT if @producer.params["aut"] - s << <<EOT + @body << <<EOT <p> <br /> <br /> </p> - <h2 class="tp-author">#{CGI.escapeHTML(@producer.params["aut"].join(", "))}</h2> + <h2 class="tp-author">#{CGI.escapeHTML(join_with_separator(@producer.params.names_of("aut"), ReVIEW::I18n.t("names_splitter")))}</h2> EOT end - publisher = @producer.params["pbl"] || @producer.params["prt"] # XXX Backward Compatiblity + publisher = @producer.params.names_of("pbl") if publisher - s << <<EOT + @body << <<EOT <p> <br /> <br /> <br /> <br /> </p> - <h3 class="tp-publisher">#{CGI.escapeHTML(publisher.join(", "))}</h3> + <h3 class="tp-publisher">#{CGI.escapeHTML(join_with_separator(publisher, ReVIEW::I18n.t("names_splitter")))}</h3> EOT end - s << <<EOT -</body> -</html> -EOT - - s + @language = @producer.params['language'] + @stylesheets = @producer.params["stylesheet"] + if @producer.params["htmlversion"].to_i == 5 + tmplfile = File.expand_path('./html/layout-html5.html.erb', ReVIEW::Template::TEMPLATE_DIR) + else + tmplfile = File.expand_path('./html/layout-xhtml1.html.erb', ReVIEW::Template::TEMPLATE_DIR) + end + tmpl = ReVIEW::Template.load(tmplfile) + tmpl.result(binding) end # Return colophon content. def colophon - s = common_header - s << <<EOT - <title>#{@producer.res.v("colophontitle")}</title> -</head> -<body> + @title = CGI.escapeHTML(@producer.res.v("colophontitle")) + @body = <<EOT <div class="colophon"> EOT if @producer.params["subtitle"].nil? - s << <<EOT - <p class="title">#{CGI.escapeHTML(@producer.params["title"])}</p> + @body << <<EOT + <p class="title">#{CGI.escapeHTML(@producer.params.name_of("title"))}</p> EOT else - s << <<EOT - <p class="title">#{CGI.escapeHTML(@producer.params["title"])}<br /><span class="subtitle">#{CGI.escapeHTML(@producer.params["subtitle"])}</span></p> + @body << <<EOT + <p class="title">#{CGI.escapeHTML(@producer.params.name_of("title"))}<br /><span class="subtitle">#{CGI.escapeHTML(@producer.params.name_of("subtitle"))}</span></p> EOT end if @producer.params["date"] || @producer.params["history"] - s << %Q[ <div class="pubhistory">\n] - if @producer.params["history"] - @producer.params["history"].each_with_index do |items, edit| - items.each_with_index do |item, rev| - editstr = (edit == 0) ? ReVIEW::I18n.t("first_edition") : ReVIEW::I18n.t("nth_edition","#{edit+1}") - revstr = ReVIEW::I18n.t("nth_impression", "#{rev+1}") - if item =~ /\A\d+\-\d+\-\d+\Z/ - s << %Q[ <p>#{ReVIEW::I18n.t("published_by1", [date_to_s(item), editstr+revstr])}</p>\n] - else - # custom date with string - item.match(/\A(\d+\-\d+\-\d+)[\s ](.+)/) do |m| - s << %Q[ <p>#{ReVIEW::I18n.t("published_by3", [date_to_s(m[1]), m[2]])}</p>\n] - end - end - end - end - else - s << %Q[ <p>#{ReVIEW::I18n.t("published_by2", date_to_s(@producer.params["date"]))}</p>\n] - end - s << %Q[ </div>\n] + @body << colophon_history end - s << %Q[ <table class="colophon">\n] - s << %Q[ <tr><th>#{@producer.res.v("aut")}</th><td>#{CGI.escapeHTML(@producer.params["aut"].join(", "))}</td></tr>\n] unless @producer.params["aut"].nil? - s << %Q[ <tr><th>#{@producer.res.v("csl")}</th><td>#{CGI.escapeHTML(@producer.params["csl"].join(", "))}</td></tr>\n] unless @producer.params["csl"].nil? - s << %Q[ <tr><th>#{@producer.res.v("trl")}</th><td>#{CGI.escapeHTML(@producer.params["trl"].join(", "))}</td></tr>\n] unless @producer.params["trl"].nil? - s << %Q[ <tr><th>#{@producer.res.v("dsr")}</th><td>#{CGI.escapeHTML(@producer.params["dsr"].join(", "))}</td></tr>\n] unless @producer.params["dsr"].nil? - s << %Q[ <tr><th>#{@producer.res.v("ill")}</th><td>#{CGI.escapeHTML(@producer.params["ill"].join(", "))}</td></tr>\n] unless @producer.params["ill"].nil? - s << %Q[ <tr><th>#{@producer.res.v("edt")}</th><td>#{CGI.escapeHTML(@producer.params["edt"].join(", "))}</td></tr>\n] unless @producer.params["edt"].nil? - s << %Q[ <tr><th>#{@producer.res.v("pbl")}</th><td>#{CGI.escapeHTML(@producer.params["pbl"].join(", "))}</td></tr>\n] unless @producer.params["pbl"].nil? - s << %Q[ <tr><th>#{@producer.res.v("prt")}</th><td>#{CGI.escapeHTML(@producer.params["prt"].join(", "))}</td></tr>\n] unless @producer.params["prt"].nil? - s << %Q[ <tr><th>#{@producer.res.v("pht")}</th><td>#{CGI.escapeHTML(@producer.params["pht"].join(", "))}</td></tr>\n] unless @producer.params["pht"].nil? - if @producer.params["isbn"].to_s =~ /\A\d{10}\Z/ || @producer.params["isbn"].to_s =~ /\A\d{13}\Z/ - isbn = nil - str = @producer.params["isbn"].to_s - if str.size == 10 - isbn = "#{str[0..0]}-#{str[1..5]}-#{str[6..8]}-#{str[9..9]}" + @body << %Q[ <table class="colophon">\n] + @body << @producer.params["colophon_order"].map{ |role| + if @producer.params[role] + %Q[ <tr><th>#{CGI.escapeHTML(@producer.res.v(role))}</th><td>#{CGI.escapeHTML(join_with_separator(@producer.params.names_of(role), ReVIEW::I18n.t("names_splitter")))}</td></tr>\n] else - isbn = "#{str[0..2]}-#{str[3..3]}-#{str[4..8]}-#{str[9..11]}-#{str[12..12]}" + "" end - s << %Q[ <tr><th>ISBN</th><td>#{isbn}</td></tr>\n] + }.join("") + + if @producer.isbn_hyphen + @body << %Q[ <tr><th>ISBN</th><td>#{@producer.isbn_hyphen}</td></tr>\n] end - s << <<EOT - </table> -EOT + @body << %Q[ </table>\n] if !@producer.params["rights"].nil? && @producer.params["rights"].size > 0 - s << %Q[ <p class="copyright">#{@producer.params["rights"].join("<br />")}</p>] + @body << %Q[ <p class="copyright">#{join_with_separator(@producer.params.names_of("rights").map {|m| CGI.escapeHTML(m)}, "<br />")}</p>\n] end + @body << %Q[ </div>\n] - s << <<EOT - </div> -</body> -</html> -EOT - s + @language = @producer.params['language'] + @stylesheets = @producer.params["stylesheet"] + if @producer.params["htmlversion"].to_i == 5 + tmplfile = File.expand_path('./html/layout-html5.html.erb', ReVIEW::Template::TEMPLATE_DIR) + else + tmplfile = File.expand_path('./html/layout-xhtml1.html.erb', ReVIEW::Template::TEMPLATE_DIR) + end + tmpl = ReVIEW::Template.load(tmplfile) + tmpl.result(binding) end + def colophon_history + buf = "" + buf << %Q[ <div class="pubhistory">\n] + if @producer.params["history"] + @producer.params["history"].each_with_index do |items, edit| + items.each_with_index do |item, rev| + editstr = (edit == 0) ? ReVIEW::I18n.t("first_edition") : ReVIEW::I18n.t("nth_edition","#{edit+1}") + revstr = ReVIEW::I18n.t("nth_impression", "#{rev+1}") + if item =~ /\A\d+\-\d+\-\d+\Z/ + buf << %Q[ <p>#{ReVIEW::I18n.t("published_by1", [date_to_s(item), editstr+revstr])}</p>\n] + else + # custom date with string + item.match(/\A(\d+\-\d+\-\d+)[\s ](.+)/) do |m| + buf << %Q[ <p>#{ReVIEW::I18n.t("published_by3", [date_to_s(m[1]), m[2]])}</p>\n] + end + end + end + end + else + buf << %Q[ <p>#{ReVIEW::I18n.t("published_by2", date_to_s(@producer.params["date"]))}</p>\n] + end + buf << %Q[ </div>\n] + buf + end + def date_to_s(date) require 'date' d = Date.parse(date) d.strftime(ReVIEW::I18n.t("date_format")) end # Return own toc content. def mytoc - s = common_header - s << <<EOT - <title>#{@producer.res.v("toctitle")}</title> -</head> -<body> - <h1 class="toc-title">#{@producer.res.v("toctitle")}</h1> -EOT + @title = CGI.escapeHTML(@producer.res.v("toctitle")) + @body = %Q[ <h1 class="toc-title">#{CGI.escapeHTML(@producer.res.v("toctitle"))}</h1>\n] if @producer.params["epubmaker"]["flattoc"].nil? - s << hierarchy_ncx("ul") + @body << hierarchy_ncx("ul") else - s << flat_ncx("ul", @producer.params["epubmaker"]["flattocindent"]) + @body << flat_ncx("ul", @producer.params["epubmaker"]["flattocindent"]) end - s << <<EOT -</body> -</html> -EOT - s + @language = @producer.params['language'] + @stylesheets = @producer.params["stylesheet"] + if @producer.params["htmlversion"].to_i == 5 + tmplfile = File.expand_path('./html/layout-html5.html.erb', ReVIEW::Template::TEMPLATE_DIR) + else + tmplfile = File.expand_path('./html/layout-xhtml1.html.erb', ReVIEW::Template::TEMPLATE_DIR) + end + tmpl = ReVIEW::Template.load(tmplfile) + tmpl.result(binding) end def hierarchy_ncx(type) require 'rexml/document' level = 1 @@ -392,11 +386,11 @@ FileUtils.mkdir_p("#{tmpdir}/META-INF") File.open("#{tmpdir}/META-INF/container.xml", "w") {|f| @producer.container(f) } FileUtils.mkdir_p("#{tmpdir}/OEBPS") - File.open("#{tmpdir}/OEBPS/#{@producer.params["bookname"]}.opf", "w") {|f| @producer.opf(f) } + File.open(File.join(tmpdir, opf_path), "w") {|f| @producer.opf(f) } if File.exist?("#{basedir}/#{@producer.params["cover"]}") FileUtils.cp("#{basedir}/#{@producer.params["cover"]}", "#{tmpdir}/OEBPS") else File.open("#{tmpdir}/OEBPS/#{@producer.params["cover"]}", "w") {|f| @producer.cover(f) } @@ -410,32 +404,66 @@ FileUtils.cp(fname, "#{tmpdir}/OEBPS/#{item.file}") end end def export_zip(tmpdir, epubfile) + if defined?(Zip) + export_zip_rubyzip(tmpdir, epubfile) + else + export_zip_extcmd(tmpdir, epubfile) + end + end + + def export_zip_extcmd(tmpdir, epubfile) Dir.chdir(tmpdir) {|d| `#{@producer.params["epubmaker"]["zip_stage1"]} #{epubfile.shellescape} mimetype` } Dir.chdir(tmpdir) {|d| `#{@producer.params["epubmaker"]["zip_stage2"]} #{epubfile.shellescape} META-INF OEBPS #{@producer.params["epubmaker"]["zip_addpath"]}` } end + def export_zip_rubyzip(tmpdir, epubfile) + Dir.chdir(tmpdir) do |d| + Zip::OutputStream.open(epubfile) do |epub| + root_pathname = Pathname.new(tmpdir) + # relpath = Pathname.new(File.join(tmpdir,'mimetype')).relative_path_from(root_pathname) + epub.put_next_entry('mimetype', nil, nil, Zip::Entry::STORED) + epub << "application/epub+zip" + + export_zip_rubyzip_addpath(epub, File.join(tmpdir,'META-INF'), root_pathname) + export_zip_rubyzip_addpath(epub, File.join(tmpdir,'OEBPS'), root_pathname) + if @producer.params["zip_addpath"].present? + export_zip_rubyzip_addpath(epub, File.join(tmpdir,@producer.params["zip_addpath"]), root_pathname) + end + end + end + end + + def export_zip_rubyzip_addpath(epub, dirname, rootdir) + Dir[File.join(dirname,'**','**')].each do |path| + next if File.directory?(path) + relpath = Pathname.new(path).relative_path_from(rootdir) + epub.put_next_entry(relpath) + epub << File.binread(path) + end + end + def legacy_cover_and_title_file(loadfile, writefile) - s = common_header - s << <<EOT - <title>#{@producer.params["booktitle"]}</title> -</head> -<body> -EOT + @title = @producer.params["booktitle"] + s = "" File.open(loadfile) do |f| f.each_line do |l| s << l end end - s << <<EOT -</body> -</html> -EOT File.open(writefile, "w") do |f| f.puts s + end + end + + def join_with_separator(value, sep) + if value.kind_of? Array + value.join(sep) + else + value end end end end