'
else
lines[-1] = %(#{lines[-1]})
end
state.delete :content_doc_href if item.context == :document
end
lines << ''
lines * LF
end
# NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
def add_ncx_doc doc, spine_builder, spine
spine_builder.file 'toc.ncx' => (ncx_doc doc, spine).to_ios
spine_builder.id 'ncx'
nil
end
def ncx_doc doc, spine
# TODO populate docAuthor element based on unique authors in work
lines = [%(
%{depth}
#{sanitize_doctitle_xml doc, :cdata})]
lines << (ncx_level spine, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {})
lines[0] = lines[0].sub '%{depth}', %()
lines << %()
lines * LF
end
def ncx_level items, depth, state = {}
lines = []
state[:max_depth] = (state.fetch :max_depth, 0) + 1
items.each do |item|
index = (state[:index] = (state.fetch :index, 0) + 1)
if item.context == :document
item_id = %(nav_#{index})
item_label = sanitize_doctitle_xml item, :cdata
item_href = (state[:content_doc_href] = %(#{item.id || (item.attr 'docname')}.xhtml))
else
item_id = %(nav_#{index})
item_label = sanitize_xml item.title, :cdata
item_href = %(#{state[:content_doc_href]}##{item.id})
end
lines << %()
lines << %(#{item_label})
lines << %()
unless depth == 0 || (child_sections = item.sections).empty?
lines << (ncx_level child_sections, depth - 1, state)
end
lines << %()
state.delete :content_doc_href if item.context == :document
end
lines * LF
end
def collect_keywords doc, spine
([doc] + spine).map do |item|
if item.attr? 'keywords'
(item.attr 'keywords').split CsvDelimiterRx
else
[]
end
end.flatten.uniq
end
# Swap fonts in CSS based on the value of the document attribute 'scripts',
# then return the list of fonts as well as the font CSS.
def select_fonts filename, scripts = 'latin'
font_css = ::File.read(filename)
font_css = font_css.gsub(/(?<=-)latin(?=\.ttf\))/, scripts) unless scripts == 'latin'
# match CSS font urls in the forms of:
# src: url(../fonts/notoserif-regular-latin.ttf);
# src: url(../fonts/notoserif-regular-latin.ttf) format("truetype");
font_list = font_css.scan(/url\(\.\.\/([^)]+\.ttf)\)/).flatten
return [font_list, font_css.to_ios]
end
def postprocess_css_file filename, format
return filename unless format == :kf8
postprocess_css ::File.read(filename), format
end
def postprocess_css content, format
return content.to_ios unless format == :kf8
# TODO convert regular expressions to constants
content
.gsub(/^ -webkit-column-break-.*\n/, '')
.gsub(/^ max-width: .*\n/, '')
.to_ios
end
def postprocess_xhtml_file filename, format
return filename unless format == :kf8
postprocess_xhtml ::File.read(filename), format
end
# NOTE Kindle requires that
#
# be converted to
#
def postprocess_xhtml content, format
return content.to_ios unless format == :kf8
# TODO convert regular expressions to constants
content
.gsub(//, '')
.gsub(/]+) style="width: (\d\d)%;"/, '.*?<\/script>\n?/m, '')
.to_ios
end
end
module GepubResourceBuilderMixin
# Add missing method to builder to add a property to last defined item
def add_property property
@last_defined_item.add_property property
end
# Add helper method to builder to check if property is set on last defined item
def property? property
(@last_defined_item['properties'] || []).include? property
end
end
class Packager
KINDLEGEN = ENV['KINDLEGEN'] || 'kindlegen'
EPUBCHECK = ENV['EPUBCHECK'] || %(epubcheck#{::Gem.win_platform? ? '.bat' : '.sh'})
EpubExtensionRx = /\.epub$/i
KindlegenCompression = ::Hash['0', '-c0', '1', '-c1', '2', '-c2', 'none', '-c0', 'standard', '-c1', 'huffdic', '-c2']
def initialize spine_doc, spine, format = :epub3, options = {}
@document = spine_doc
@spine = spine || []
@format = format
end
def package options = {}
doc = @document
spine = @spine
fmt = @format
target = options[:target]
dest = File.dirname target
# FIXME authors should be aggregated already on parent document
authors = if doc.attr? 'authors'
(doc.attr 'authors').split(GepubBuilderMixin::CsvDelimiterRx).concat(spine.map {|item| item.attr 'author' }.compact).uniq
else
[]
end
builder = ::GEPUB::Builder.new do
extend GepubBuilderMixin
@document = doc
@spine = spine
@format = fmt
@book.epub_backward_compat = (fmt != :kf8)
language(doc.attr 'lang', 'en')
id 'pub-language'
if doc.attr? 'uuid'
unique_identifier doc.attr('uuid'), 'pub-identifier', 'uuid'
else
unique_identifier doc.id, 'pub-identifier', 'uuid'
end
# replace with next line once the attributes argument is supported
#unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
# NOTE we must use :plain_text here since gepub reencodes
title(sanitize_doctitle_xml doc, :plain_text)
id 'pub-title'
# FIXME this logic needs some work
if doc.attr? 'publisher'
publisher(publisher_name = doc.attr('publisher'))
# marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
creator doc.attr('producer', publisher_name), 'bkp'
else
# NOTE Use producer as both publisher and producer if publisher isn't specified
if doc.attr? 'producer'
producer_name = doc.attr 'producer'
publisher producer_name
# marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
creator producer_name, 'bkp'
# NOTE Use author as creator if both publisher or producer are absent
elsif doc.attr? 'author'
# marc role: Author (see http://www.loc.gov/marc/relators/relaterm.html)
creator doc.attr('author'), 'aut'
end
end
if doc.attr? 'creator'
# marc role: Creator (see http://www.loc.gov/marc/relators/relaterm.html)
creator doc.attr('creator'), 'cre'
else
# marc role: Manufacturer (see http://www.loc.gov/marc/relators/relaterm.html)
# QUESTION should this be bkp?
creator 'Asciidoctor', 'mfr'
end
# TODO getting author list should be a method on Asciidoctor API
contributors(*authors)
if doc.attr? 'revdate'
# TODO ensure this is a real date
date(doc.attr 'revdate')
else
date ::Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
end
if doc.attr? 'description'
description(doc.attr 'description')
end
(collect_keywords doc, spine).each do |s|
subject s
end
if doc.attr? 'source'
source(doc.attr 'source')
end
if doc.attr? 'copyright'
rights(doc.attr 'copyright')
end
#add_metadata 'ibooks:specified-fonts', true
add_theme_assets doc
add_cover_image doc
if (doc.attr 'publication-type', 'book') != 'book'
usernames = spine.map {|item| item.attr 'username' }.compact.uniq
add_profile_images doc, usernames
end
add_content doc
end
::FileUtils.mkdir_p dest unless ::File.directory? dest
epub_file = fmt == :kf8 ? %(#{::Asciidoctor::Helpers.rootname target}-kf8.epub) : target
builder.generate_epub epub_file
puts %(Wrote #{fmt.upcase} to #{epub_file}) if $VERBOSE
if options[:extract]
extract_dir = epub_file.sub EpubExtensionRx, ''
::FileUtils.remove_dir extract_dir if ::File.directory? extract_dir
::Dir.mkdir extract_dir
::Dir.chdir extract_dir do
::Zip::File.open epub_file do |entries|
entries.each do |entry|
next unless entry.file?
unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
::FileUtils.mkdir_p entry_dir
end
entry.extract
end
end
end
puts %(Extracted #{fmt.upcase} to #{extract_dir}) if $VERBOSE
end
if fmt == :kf8
# QUESTION shouldn't we validate this epub file too?
distill_epub_to_mobi epub_file, target, options[:compress]
elsif options[:validate]
validate_epub epub_file
end
end
def distill_epub_to_mobi epub_file, target, compress
kindlegen_cmd = KINDLEGEN
unless ::File.executable? kindlegen_cmd
require 'kindlegen' unless defined? ::Kindlegen
kindlegen_cmd = ::Kindlegen.command
end
mobi_file = ::File.basename(target.sub EpubExtensionRx, '.mobi')
compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
cmd = [kindlegen_cmd, '-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
::Open3.popen2e(::Shellwords.join cmd) {|input, output, wait_thr|
output.each {|line| puts line } unless $VERBOSE.nil?
}
puts %(Wrote MOBI to #{::File.join ::File.dirname(epub_file), mobi_file}) if $VERBOSE
end
def validate_epub epub_file
epubcheck_cmd = EPUBCHECK
unless ::File.executable? epubcheck_cmd
epubcheck_cmd = ::Gem.bin_path 'epubcheck', 'epubcheck'
end
# NOTE epubcheck gem doesn't support epubcheck command options; enable -quiet once supported
::Open3.popen2e(::Shellwords.join [epubcheck_cmd, epub_file]) {|input, output, wait_thr|
output.each {|line| puts line } unless $VERBOSE.nil?
}
end
end
end
end