)
else
lines << (nav_level child_sections, depth - 1, state)
lines << ''
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)
item_id = %(nav_#{index})
if item.context == :document
item_label = sanitize_doctitle_xml item, :cdata
item_href = (state[:content_doc_href] = %(#{item.id || (item.attr 'docname')}.xhtml))
else
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 {|item|
if item.attr? 'keywords'
(item.attr 'keywords').split CsvDelimiterRx
else
[]
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
[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
if doc.attr? 'authors'
authors = (doc.attr 'authors').split(GepubBuilderMixin::CsvDelimiterRx).concat(spine.map {|item| item.attr 'author' }.compact).uniq
else
authors = []
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'
elsif doc.attr? 'producer'
# NOTE Use producer as both publisher and producer if publisher isn't specified
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'
elsif doc.attr? 'author'
# NOTE Use author as creator if both publisher or producer are absent
# marc role: Author (see http://www.loc.gov/marc/relators/relaterm.html)
creator doc.attr('author'), 'aut'
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
description doc.attr('description') if doc.attr? 'description'
(collect_keywords doc, spine).each do |s|
subject s
end
source doc.attr('source') if doc.attr? 'source'
rights doc.attr('copyright') if doc.attr? 'copyright'
#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) do |_input, output, _wait_thr|
output.each {|line| puts line } unless $VERBOSE.nil?
end
puts %(Wrote MOBI to #{::File.join ::File.dirname(epub_file), mobi_file}) if $VERBOSE
end
def validate_epub epub_file
if ::File.executable? EPUBCHECK
argv = [EPUBCHECK]
else
argv = [::Gem.ruby, ::Gem.bin_path('epubcheck-ruby', 'epubcheck')]
end
if $VERBOSE.nil?
argv << '-q'
else
argv << '-w'
end
argv << epub_file
::Open3.popen2e ::Shellwords.join(argv) do |_input, output, _wait_thr|
output.each {|line| puts line }
end
end
end
end
end