persie 0.0.1.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +52 -0
- data/Rakefile +85 -0
- data/bin/persie +12 -0
- data/lib/persie/asciidoctor_ext/htmlbook.rb +1120 -0
- data/lib/persie/asciidoctor_ext/sample.rb +54 -0
- data/lib/persie/asciidoctor_ext/spine_item_processor.rb +43 -0
- data/lib/persie/book.rb +60 -0
- data/lib/persie/builder.rb +110 -0
- data/lib/persie/builders/epub.rb +434 -0
- data/lib/persie/builders/mobi.rb +80 -0
- data/lib/persie/builders/pdf.rb +113 -0
- data/lib/persie/builders/site.rb +110 -0
- data/lib/persie/cli.rb +106 -0
- data/lib/persie/dependency.rb +26 -0
- data/lib/persie/generator.rb +68 -0
- data/lib/persie/server.rb +27 -0
- data/lib/persie/ui.rb +27 -0
- data/lib/persie/version.rb +3 -0
- data/lib/persie.rb +32 -0
- data/persie.gemspec +41 -0
- data/spec/build_pdf_command_spec.rb +25 -0
- data/spec/fixtures/a-book/.gitignore +2 -0
- data/spec/fixtures/a-book/Gemfile +3 -0
- data/spec/fixtures/a-book/book.adoc +31 -0
- data/spec/fixtures/a-book/manuscript/chapter1.adoc +5 -0
- data/spec/fixtures/a-book/manuscript/chapter2.adoc +3 -0
- data/spec/fixtures/a-book/manuscript/preface.adoc +6 -0
- data/spec/fixtures/a-book/themes/pdf/pdf.css +1 -0
- data/spec/fixtures/a-book-with-parts/.gitignore +2 -0
- data/spec/fixtures/a-book-with-parts/Gemfile +3 -0
- data/spec/fixtures/a-book-with-parts/book.adoc +39 -0
- data/spec/fixtures/a-book-with-parts/manuscript/chapter1.adoc +4 -0
- data/spec/fixtures/a-book-with-parts/manuscript/chapter2.adoc +4 -0
- data/spec/fixtures/a-book-with-parts/manuscript/chapter3.adoc +3 -0
- data/spec/fixtures/a-book-with-parts/manuscript/chapter4.adoc +3 -0
- data/spec/fixtures/a-book-with-parts/manuscript/part1.adoc +3 -0
- data/spec/fixtures/a-book-with-parts/manuscript/part2.adoc +3 -0
- data/spec/fixtures/a-book-with-parts/manuscript/preface.adoc +4 -0
- data/spec/htmlbook_spec.rb +29 -0
- data/spec/new_command_spec.rb +57 -0
- data/spec/pdf_builder_spec.rb +39 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/version_command_spec.rb +8 -0
- data/templates/Gemfile.txt +3 -0
- data/templates/book.adoc.erb +35 -0
- data/templates/chapter1.adoc +3 -0
- data/templates/chapter2.adoc +3 -0
- data/templates/gitignore.txt +2 -0
- data/templates/preface.adoc +4 -0
- data/workflow.png +0 -0
- metadata +278 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
# For sample generation, do some dirty hacks.
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
|
5
|
+
class AbstractBlock
|
6
|
+
# Get an array of sample sections.
|
7
|
+
def sample_sections
|
8
|
+
@blocks.reject! { |b| b.context == :section && !b.sample? }
|
9
|
+
@blocks.select { |b| b.context == :section && b.sample? }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Document
|
14
|
+
# Get converted sample contents.
|
15
|
+
def sample_content
|
16
|
+
@attributes.delete('title')
|
17
|
+
self.sample_sections.map { |s| s.convert } * "\n"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Section
|
22
|
+
|
23
|
+
def sample=(bool)
|
24
|
+
@sample = bool
|
25
|
+
end
|
26
|
+
|
27
|
+
# Whether this section is in sample.
|
28
|
+
def sample?
|
29
|
+
if self.attributes.has_key?(:attribute_entries)
|
30
|
+
self.attributes[:attribute_entries].each do |entry|
|
31
|
+
if entry.name == 'sample' && entry.value != nil
|
32
|
+
self.sample = true
|
33
|
+
# Not down to top level sections in a part
|
34
|
+
downto_subsections(self.sections) unless self.level == 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@sample
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def downto_subsections(sections)
|
45
|
+
if sections.size > 0
|
46
|
+
sections.each do |s|
|
47
|
+
s.sample = true
|
48
|
+
downto_subsections(s.sections)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'asciidoctor/extensions'
|
2
|
+
|
3
|
+
module Persie
|
4
|
+
class SpineItemProcessor < ::Asciidoctor::Extensions::IncludeProcessor
|
5
|
+
def initialize(document, sample = false)
|
6
|
+
@document = document
|
7
|
+
@sample = sample
|
8
|
+
end
|
9
|
+
|
10
|
+
def process(doc, reader, target, attributes)
|
11
|
+
include_file = doc.normalize_system_path(target, reader.dir, nil, target_name: 'include file')
|
12
|
+
unless ::File.exist? include_file
|
13
|
+
warn %(asciidoctor: WARNING: #{reader.line_info}: include file not found: #{include_file})
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
doc.references['spine_items'] ||= []
|
18
|
+
basename = File.basename(include_file).split('.')[0..-2].join('.')
|
19
|
+
|
20
|
+
if @sample
|
21
|
+
meta = ::Asciidoctor.load_file include_file,
|
22
|
+
safe: doc.safe,
|
23
|
+
doctype: :article,
|
24
|
+
parse_header_only: true
|
25
|
+
|
26
|
+
sample_attr = meta.attributes['sample']
|
27
|
+
doc.references['spine_items'] << basename unless sample_attr.nil?
|
28
|
+
else
|
29
|
+
doc.references['spine_items'] << basename
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def handles? target
|
34
|
+
(@document.attr('ebook-format') == 'epub') && (::Asciidoctor::ASCIIDOC_EXTENSIONS.include? ::File.extname(target))
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_config config
|
38
|
+
(@config ||= {}).update config
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
data/lib/persie/book.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'gepub'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'asciidoctor'
|
4
|
+
|
5
|
+
require_relative 'builders/pdf'
|
6
|
+
require_relative 'builders/epub'
|
7
|
+
require_relative 'builders/mobi'
|
8
|
+
require_relative 'builders/site'
|
9
|
+
|
10
|
+
module Persie
|
11
|
+
class Book
|
12
|
+
|
13
|
+
# Gets base directory.
|
14
|
+
attr_reader :base_dir
|
15
|
+
|
16
|
+
# Gets builds directory path.
|
17
|
+
attr_reader :builds_dir
|
18
|
+
|
19
|
+
# Gets themes directory path.
|
20
|
+
attr_reader :themes_dir
|
21
|
+
|
22
|
+
# Gets images directory path.
|
23
|
+
attr_reader :images_dir
|
24
|
+
|
25
|
+
# Gets tmp directory path.
|
26
|
+
attr_reader :tmp_dir
|
27
|
+
|
28
|
+
# Gets master file path.
|
29
|
+
attr_reader :master_file
|
30
|
+
|
31
|
+
# Gets/Sets book slug.
|
32
|
+
attr_accessor :slug
|
33
|
+
|
34
|
+
def initialize(dir)
|
35
|
+
@base_dir = File.expand_path(dir)
|
36
|
+
@tmp_dir = File.join(@base_dir, 'tmp')
|
37
|
+
@builds_dir = File.join(@base_dir, 'builds')
|
38
|
+
@themes_dir = File.join(@base_dir, 'themes')
|
39
|
+
@images_dir = File.join(@base_dir, 'images')
|
40
|
+
@master_file = File.join(@base_dir, 'book.adoc')
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_pdf(options = {})
|
44
|
+
PDF.new(self, options).build
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_epub(options = {})
|
48
|
+
Epub.new(self, options).build
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_mobi(options = {})
|
52
|
+
Mobi.new(self, options).build
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_site(options = {})
|
56
|
+
Site.new(self, options).build
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'asciidoctor'
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
require_relative 'ui'
|
6
|
+
require_relative 'asciidoctor_ext/htmlbook'
|
7
|
+
require_relative 'dependency'
|
8
|
+
require_relative 'asciidoctor_ext/sample'
|
9
|
+
|
10
|
+
module Persie
|
11
|
+
class Builder
|
12
|
+
|
13
|
+
END_LINE = '=' * 72
|
14
|
+
|
15
|
+
# Gets the AsciiDoctor::Document object.
|
16
|
+
attr_reader :document
|
17
|
+
|
18
|
+
def initialize(book, options = {})
|
19
|
+
@ui = UI.new(options)
|
20
|
+
@book = book
|
21
|
+
@options = options
|
22
|
+
@document = ::Asciidoctor.load_file(@book.master_file, adoc_options)
|
23
|
+
@book.slug = @document.attr('slug', File.basename(@book.base_dir))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Should implement in subclass.
|
27
|
+
def build
|
28
|
+
raise ::NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
# If in sample mode, show an indicator in command line.
|
32
|
+
def check_sample
|
33
|
+
if sample?
|
34
|
+
if @document.sample_sections.size == 0
|
35
|
+
@ui.error 'Not setting sample, terminated!'
|
36
|
+
@ui.info END_LINE
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
@ui.warning "Sample only\n"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Generates sample or not.
|
46
|
+
def sample?
|
47
|
+
return true if @options.has_key? 'sample'
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
# Filts contents, only keep samples if in sample mode.
|
52
|
+
def register_spine_item_processor
|
53
|
+
require_relative 'asciidoctor_ext/spine_item_processor'
|
54
|
+
|
55
|
+
sample = sample?
|
56
|
+
::Asciidoctor::Extensions.register do
|
57
|
+
include_processor SpineItemProcessor.new(@document, sample)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Options passed into AsciiDoctor loader.
|
62
|
+
def adoc_options
|
63
|
+
{
|
64
|
+
safe: 1,
|
65
|
+
backend: 'htmlbook',
|
66
|
+
doctype: 'book',
|
67
|
+
header_footer: true,
|
68
|
+
attributes: adoc_attributes
|
69
|
+
}.merge(adoc_custom_options)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Custom Asciidoctor options in subclass.
|
73
|
+
def adoc_custom_options
|
74
|
+
{}
|
75
|
+
end
|
76
|
+
|
77
|
+
# Attributes as in AsciiDoctor loader option.
|
78
|
+
def adoc_attributes
|
79
|
+
attrs = {
|
80
|
+
'persie-version' => VERSION,
|
81
|
+
'builds-dir' => @book.builds_dir,
|
82
|
+
'themes-dir' => @book.themes_dir,
|
83
|
+
'imagesdir' => @book.images_dir
|
84
|
+
}
|
85
|
+
|
86
|
+
attrs['is-sample'] = true if sample?
|
87
|
+
|
88
|
+
attrs.merge(adoc_custom_attributes)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Custom Asciidoctor attributes in subclass.
|
92
|
+
def adoc_custom_attributes
|
93
|
+
{}
|
94
|
+
end
|
95
|
+
|
96
|
+
# Creates directory if not exists.
|
97
|
+
def prepare_directory(path)
|
98
|
+
dir = File.dirname(path)
|
99
|
+
unless File.exist? dir
|
100
|
+
FileUtils.mkdir_p dir
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Checks if in test mode.
|
105
|
+
def test_mode?
|
106
|
+
@options[:test] === true
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,434 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
require_relative '../builder'
|
6
|
+
|
7
|
+
module Persie
|
8
|
+
module GepubBuilderMixin
|
9
|
+
|
10
|
+
FromHtmlSpecialCharsMap = {
|
11
|
+
'<' => '<',
|
12
|
+
'>' => '>',
|
13
|
+
'&' => '&'
|
14
|
+
}
|
15
|
+
FromHtmlSpecialCharsRx = /(?:#{FromHtmlSpecialCharsMap.keys * '|'})/
|
16
|
+
WordJoinerRx = [65279].pack 'U*'
|
17
|
+
CsvDelimiterRx = /\s*,\s*/
|
18
|
+
|
19
|
+
def sanitized_title(title, target = :plain)
|
20
|
+
return (@doc.attr 'untitled-label') unless @doc.header?
|
21
|
+
|
22
|
+
builder = self
|
23
|
+
|
24
|
+
title = case target
|
25
|
+
when :attribute_cdata
|
26
|
+
builder.sanitize(title).gsub('"', '"')
|
27
|
+
when :element_cdata
|
28
|
+
builder.sanitize(title)
|
29
|
+
when :pcdata
|
30
|
+
title
|
31
|
+
when :plain
|
32
|
+
builder.sanitize(title).gsub(FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap)
|
33
|
+
end
|
34
|
+
|
35
|
+
title.gsub WordJoinerRx, ''
|
36
|
+
end
|
37
|
+
|
38
|
+
def sanitize(text)
|
39
|
+
if text.include?('<')
|
40
|
+
text.gsub(::Asciidoctor::XmlSanitizeRx, '').tr_s(' ', ' ').strip
|
41
|
+
else
|
42
|
+
text
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def authors
|
47
|
+
if (auts = @doc.attr 'authors')
|
48
|
+
auts.split(CsvDelimiterRx)
|
49
|
+
else
|
50
|
+
[]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_theme_assets
|
55
|
+
resources(workdir: @theme_dir) do
|
56
|
+
file 'epub.css' if File.exist?('epub.css')
|
57
|
+
glob 'fonts/*.*'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_cover_image
|
62
|
+
image = @doc.attr('epub-cover-image', 'cover.png')
|
63
|
+
image = File.basename(image) # incase you set this a path
|
64
|
+
|
65
|
+
if File.exist? image
|
66
|
+
resources(workdir: @theme_dir) do
|
67
|
+
cover_image image
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_images
|
73
|
+
resources(workdir: @images_dir) do
|
74
|
+
glob '*.*'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_content
|
79
|
+
builder = self
|
80
|
+
spine_items = @spine_items
|
81
|
+
spine_item_titles = @spine_item_titles
|
82
|
+
resources(workdir: @tmp_dir) do
|
83
|
+
nav 'nav.xhtml' if @has_toc
|
84
|
+
|
85
|
+
ordered do
|
86
|
+
spine_items.each_with_index do |item, i|
|
87
|
+
file "#{item}.xhtml"
|
88
|
+
heading builder.sanitized_title(spine_item_titles[i])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
class Epub < Builder
|
97
|
+
|
98
|
+
# these are not using `include' directive
|
99
|
+
SPECIAL_SPINE_ITEMS = ['cover', 'titlepage', 'nav']
|
100
|
+
|
101
|
+
# Gets/Sets spine items.
|
102
|
+
attr_accessor :spine_items
|
103
|
+
|
104
|
+
# Gets/Sets spine items's titles.
|
105
|
+
attr_accessor :spine_item_titles
|
106
|
+
|
107
|
+
def initialize(book, options = {})
|
108
|
+
super
|
109
|
+
@tmp_dir = File.join(book.tmp_dir, 'epub')
|
110
|
+
@theme_dir = File.join(book.themes_dir, 'epub')
|
111
|
+
@build_dir = File.join(book.builds_dir, 'epub')
|
112
|
+
@spine_items = []
|
113
|
+
@spine_item_titles = []
|
114
|
+
end
|
115
|
+
|
116
|
+
# Builds ePub.
|
117
|
+
def build
|
118
|
+
@ui.info '=== Build ePub ' << '=' * 57
|
119
|
+
|
120
|
+
self.check_sample
|
121
|
+
self.convert_to_single_xhtml
|
122
|
+
self.generate_spine_items
|
123
|
+
self.chunk
|
124
|
+
self.generate_epub
|
125
|
+
self.validate
|
126
|
+
|
127
|
+
@ui.info END_LINE
|
128
|
+
end
|
129
|
+
|
130
|
+
# Converts to single XHTML file.
|
131
|
+
def convert_to_single_xhtml
|
132
|
+
@ui.info 'Converting to XHTML...'
|
133
|
+
xhtml = @document.convert
|
134
|
+
prepare_directory(self.xhtml_path)
|
135
|
+
File.write(self.xhtml_path, xhtml)
|
136
|
+
@ui.confirm ' XHTMl file created'
|
137
|
+
@ui.info " Location: #{self.xhtml_path(true)}\n"
|
138
|
+
end
|
139
|
+
|
140
|
+
# Generates spine items.
|
141
|
+
def generate_spine_items
|
142
|
+
register_spine_item_processor
|
143
|
+
|
144
|
+
# Re-loading the master file
|
145
|
+
doc = ::Asciidoctor.load_file(@book.master_file, adoc_options)
|
146
|
+
@spine_items.concat SPECIAL_SPINE_ITEMS
|
147
|
+
@spine_items.concat doc.references['spine_items']
|
148
|
+
|
149
|
+
@spine_items
|
150
|
+
end
|
151
|
+
|
152
|
+
# Chucks single XHTML file to multiple XHTML files.
|
153
|
+
def chunk
|
154
|
+
@ui.info 'Chunking files...'
|
155
|
+
|
156
|
+
content = File.read(self.xhtml_path)
|
157
|
+
root = ::Nokogiri::HTML(content)
|
158
|
+
|
159
|
+
# Adjust spint items
|
160
|
+
@has_cover = root.css('div[data-type="cover"]').size > 0
|
161
|
+
@has_toc = root.css('nav[data-type="toc"]').size > 0
|
162
|
+
self.spine_items.delete('cover') unless @has_cover
|
163
|
+
self.spine_items.delete('toc') unless @has_toc
|
164
|
+
|
165
|
+
correct_nav_href(root)
|
166
|
+
|
167
|
+
top_level_sections = resolve_top_level_sections(root)
|
168
|
+
|
169
|
+
# stupid check, incase of something went wrong
|
170
|
+
unless top_level_sections.count == self.spine_items.count
|
171
|
+
@ui.error ' Count of sections DO NOT equal to spine items count.'
|
172
|
+
@ui.error ' Terminated!'
|
173
|
+
if @options.debug?
|
174
|
+
@ui.info 'sections count: ' + top_level_sections.count
|
175
|
+
@ui.info 'spine_items: ' + self.spine_items.inspect
|
176
|
+
end
|
177
|
+
@ui.info END_LINE
|
178
|
+
exit 31
|
179
|
+
end
|
180
|
+
|
181
|
+
sep = '<body data-type="book">'
|
182
|
+
tpl_before = content.split(sep).first
|
183
|
+
tpl_after = %(</body>\n</html>)
|
184
|
+
|
185
|
+
top_level_sections.each_with_index do |node, i|
|
186
|
+
# Collect the first h1 heading
|
187
|
+
title = node.css('h1:first-of-type').first.inner_text
|
188
|
+
@spine_item_titles << title
|
189
|
+
|
190
|
+
# Footnotes
|
191
|
+
footnotes_div = generate_footnotes(node)
|
192
|
+
|
193
|
+
# Write to chunked file
|
194
|
+
path = File.join(@tmp_dir, "#{self.spine_items[i]}.xhtml")
|
195
|
+
File.open(path, 'w') do |f|
|
196
|
+
f.puts tpl_before
|
197
|
+
f.puts sep
|
198
|
+
f.puts node.to_xhtml
|
199
|
+
f.puts footnotes_div
|
200
|
+
f.puts tpl_after
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
@ui.confirm ' Done\n'
|
205
|
+
end
|
206
|
+
|
207
|
+
# Generates ePub file.
|
208
|
+
def generate_epub
|
209
|
+
doc = @document
|
210
|
+
tmp_dir = @tmp_dir
|
211
|
+
theme_dir = @theme_dir
|
212
|
+
images_dir = @book.images_dir
|
213
|
+
has_toc = @has_toc
|
214
|
+
spine_items = self.spine_items
|
215
|
+
spine_item_titles = self.spine_item_titles
|
216
|
+
|
217
|
+
@ui.info 'Building ePub...'
|
218
|
+
|
219
|
+
builder = ::GEPUB::Builder.new do
|
220
|
+
extend GepubBuilderMixin
|
221
|
+
|
222
|
+
@doc = doc
|
223
|
+
@tmp_dir = tmp_dir
|
224
|
+
@theme_dir = theme_dir
|
225
|
+
@images_dir = images_dir
|
226
|
+
@has_toc = has_toc
|
227
|
+
@spine_items = spine_items
|
228
|
+
@spine_item_titles = spine_item_titles
|
229
|
+
|
230
|
+
language doc.attr('lang', 'en')
|
231
|
+
id 'pub-language'
|
232
|
+
|
233
|
+
scheme = doc.attr('epub-identifier-scheme', 'uuid').downcase
|
234
|
+
scheme = 'uuid' unless ['uuid', 'isbn'].include? scheme
|
235
|
+
unique_identifier doc.attr(scheme), 'pub-identifier', scheme
|
236
|
+
|
237
|
+
title sanitized_title(doc.doctitle)
|
238
|
+
id 'pub-title'
|
239
|
+
|
240
|
+
if doc.attr? 'publisher'
|
241
|
+
publisher(publisher_name = doc.attr('publisher'))
|
242
|
+
# marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
|
243
|
+
creator doc.attr('producer', publisher_name), 'bkp'
|
244
|
+
else
|
245
|
+
# Use producer as both publisher and producer if publisher isn't specified
|
246
|
+
if doc.attr? 'producer'
|
247
|
+
producer_name = doc.attr('producer')
|
248
|
+
publisher producer_name
|
249
|
+
# marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
|
250
|
+
creator producer_name, 'bkp'
|
251
|
+
# Use author as creator if both publisher or producer are absent
|
252
|
+
elsif doc.attr? 'author'
|
253
|
+
# marc role: Author (see http://www.loc.gov/marc/relators/relaterm.html)
|
254
|
+
creator doc.attr('author'), 'aut'
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
if doc.attr? 'creator'
|
259
|
+
# marc role: Creator (see http://www.loc.gov/marc/relators/relaterm.html)
|
260
|
+
creator doc.attr('creator'), 'cre'
|
261
|
+
else
|
262
|
+
# marc role: Manufacturer (see http://www.loc.gov/marc/relators/relaterm.html)
|
263
|
+
creator 'persie', 'mfr'
|
264
|
+
end
|
265
|
+
|
266
|
+
contributors(*authors) unless authors.empty?
|
267
|
+
|
268
|
+
if doc.attr? 'revdate'
|
269
|
+
real_date = Time.parse(doc.attr 'revdate').iso8601
|
270
|
+
date real_date
|
271
|
+
else
|
272
|
+
date Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
|
273
|
+
end
|
274
|
+
|
275
|
+
if doc.attr? 'description'
|
276
|
+
description(doc.attr 'description')
|
277
|
+
end
|
278
|
+
|
279
|
+
if doc.attr? 'copyright'
|
280
|
+
rights(doc.attr 'copyright')
|
281
|
+
end
|
282
|
+
|
283
|
+
add_theme_assets
|
284
|
+
add_cover_image
|
285
|
+
add_images
|
286
|
+
add_content
|
287
|
+
end
|
288
|
+
|
289
|
+
prepare_directory(self.epub_path)
|
290
|
+
builder.generate_epub(self.epub_path)
|
291
|
+
@ui.confirm ' ePub file created'
|
292
|
+
@ui.info " Location: #{self.epub_path(true)}"
|
293
|
+
end
|
294
|
+
|
295
|
+
# Validates ePub file, optionally.
|
296
|
+
def validate
|
297
|
+
if @options.validate?
|
298
|
+
@ui.info "\nValidating..."
|
299
|
+
if Dependency.epubcheck_installed?
|
300
|
+
system "epubcheck #{epub_path}"
|
301
|
+
if $?.to_i == 0
|
302
|
+
@ui.confirm ' PASS'
|
303
|
+
else
|
304
|
+
@ui.error ' ERROR'
|
305
|
+
end
|
306
|
+
else
|
307
|
+
@ui.warning ' epubcheck not installed, skip validation'
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Gets XHTML file path.
|
313
|
+
def xhtml_path(relative = false)
|
314
|
+
name = sample? ? "#{@book.slug}-sample" : @book.slug
|
315
|
+
path = File.join('tmp', 'epub', "#{name}.html")
|
316
|
+
return path if relative
|
317
|
+
|
318
|
+
File.join(@book.base_dir, path)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Gets ePub file path.
|
322
|
+
def epub_path(relative = false)
|
323
|
+
name = sample? ? "#{@book.slug}-sample" : @book.slug
|
324
|
+
path = File.join('builds', 'epub', "#{name}.epub")
|
325
|
+
return path if relative
|
326
|
+
|
327
|
+
File.join(@book.base_dir, path)
|
328
|
+
end
|
329
|
+
|
330
|
+
private
|
331
|
+
|
332
|
+
def adoc_custom_attributes
|
333
|
+
{
|
334
|
+
'imagesdir' => 'images',
|
335
|
+
'ebook-format' => 'epub',
|
336
|
+
'outfilesuffix' => '.xhtml'
|
337
|
+
}
|
338
|
+
end
|
339
|
+
|
340
|
+
# Corrects navigation items' href.
|
341
|
+
#
|
342
|
+
# Example:
|
343
|
+
# href="#id" => href="path.xhtml#id"
|
344
|
+
def correct_nav_href(node)
|
345
|
+
return unless (ols = node.css('nav[data-type="toc"]> ol')).size > 0
|
346
|
+
|
347
|
+
spine_items_dup = self.spine_items.dup
|
348
|
+
SPECIAL_SPINE_ITEMS.each { |i| spine_items_dup.delete(i) }
|
349
|
+
|
350
|
+
top_level_lis = ols.first.css('> li')
|
351
|
+
j = 0
|
352
|
+
top_level_lis.each do |li|
|
353
|
+
if li['data-type'] == 'part'
|
354
|
+
first_a = li.css('> a').first
|
355
|
+
first_a_href = first_a['href']
|
356
|
+
first_a['href'] = "#{spine_items_dup[j]}.xhtml#{first_a_href}"
|
357
|
+
if (li_ols = li.css('> ol')).size > 0
|
358
|
+
li_ol = li_ols.first
|
359
|
+
li_ol.css('> li').each do |lli|
|
360
|
+
j += 1
|
361
|
+
lli.css('a').each do |a|
|
362
|
+
old_href = a['href']
|
363
|
+
a['href'] = "#{spine_items_dup[j]}.xhtml#{old_href}"
|
364
|
+
end
|
365
|
+
end
|
366
|
+
j += 1
|
367
|
+
end
|
368
|
+
else
|
369
|
+
li.css('a').each do |a|
|
370
|
+
old_href = a['href']
|
371
|
+
a['href'] = "#{spine_items_dup[j]}.xhtml#{old_href}"
|
372
|
+
end
|
373
|
+
j += 1
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
|
379
|
+
# Resolves top level sections.
|
380
|
+
#
|
381
|
+
# When there are parts, takes sections within each part out.
|
382
|
+
def resolve_top_level_sections(node)
|
383
|
+
if (parts = node.css('body > div[data-type="part"]')).size > 0
|
384
|
+
parts.each do |part|
|
385
|
+
sections = part.css('> section')
|
386
|
+
sections.each do |sect|
|
387
|
+
part.delete sect
|
388
|
+
end
|
389
|
+
part.add_next_sibling(sections)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
node.css('body > *')
|
394
|
+
|
395
|
+
end
|
396
|
+
|
397
|
+
# Generates footnotes for one node.
|
398
|
+
def generate_footnotes(node)
|
399
|
+
footnotes_div = nil
|
400
|
+
footnotes = node.css('span[data-type="footnote"]')
|
401
|
+
if footnotes.length > 0
|
402
|
+
footnotes_div = generate_footnotes_div(footnotes)
|
403
|
+
replace_footnote_with_sup(footnotes)
|
404
|
+
end
|
405
|
+
|
406
|
+
footnotes_div
|
407
|
+
end
|
408
|
+
|
409
|
+
# Generate a footnotes div element.
|
410
|
+
def generate_footnotes_div(footnotes)
|
411
|
+
result = ['<div class="footnotes">']
|
412
|
+
result << '<ol>'
|
413
|
+
footnotes.each_with_index do |fn, i|
|
414
|
+
index = i + 1
|
415
|
+
result << %(<li id="fn-#{index}" epub:type="footnote">#{fn.inner_text}</li>)
|
416
|
+
end
|
417
|
+
result << '</ol>'
|
418
|
+
result << '</div>'
|
419
|
+
|
420
|
+
result * "\n"
|
421
|
+
end
|
422
|
+
|
423
|
+
def replace_footnote_with_sup(footnotes)
|
424
|
+
footnotes.each_with_index do |fn, i|
|
425
|
+
index = i + 1
|
426
|
+
fn.replace(%(<sup><a href="#fn-#{index}">#{index}</a></sup>))
|
427
|
+
end
|
428
|
+
|
429
|
+
nil
|
430
|
+
end
|
431
|
+
|
432
|
+
end
|
433
|
+
|
434
|
+
end
|