persie 0.0.1.alpha1 → 0.0.1.alpha.2

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.
@@ -9,20 +9,22 @@ module Persie
9
9
 
10
10
  # Builds mobi.
11
11
  def build
12
- @ui.info '=== Build mobi ' << '=' * 57
12
+ info '=== Build mobi ' << '=' * 57
13
13
 
14
+ self.before_build
14
15
  self.check_dependency
15
16
  check_sample
16
17
  self.check_epub
17
18
  self.generate_mobi
19
+ self.after_build
18
20
 
19
- @ui.info END_LINE
21
+ info END_LINE
20
22
  end
21
23
 
22
24
  def check_dependency
23
25
  unless Dependency.kindlegen_installed?
24
- @ui.error 'kindlegen not installed, termineted!'
25
- @ui.info END_LINE
26
+ error 'kindlegen not installed, termineted!'
27
+ info END_LINE
26
28
  exit 41
27
29
  end
28
30
  end
@@ -31,8 +33,8 @@ module Persie
31
33
  def check_epub
32
34
  unless File.exist? self.epub_path
33
35
  sample = sample? ? 'sample ' : nil
34
- @ui.error "Please generate #{sample}ePub first"
35
- @ui.info END_LINE
36
+ error "Please generate #{sample}ePub first"
37
+ info END_LINE
36
38
  exit 42
37
39
  end
38
40
  end
@@ -40,7 +42,7 @@ module Persie
40
42
  # Generates mobi file.
41
43
  def generate_mobi
42
44
  FileUtils.chdir File.dirname(self.epub_path) do
43
- @ui.info 'Converting to mobi...'
45
+ info 'Converting to mobi...'
44
46
 
45
47
  system "kindlegen -c2 #{self.epub_path(true)}"
46
48
 
@@ -49,11 +51,11 @@ module Persie
49
51
  prepare_directory(self.mobi_path)
50
52
  FileUtils.mv(mobi_file, self.mobi_path)
51
53
 
52
- @ui.confirm ' mobi file created'
53
- @ui.info " Location: #{self.mobi_path(true)}"
54
+ confirm ' mobi file created'
55
+ info " Location: #{self.mobi_path(true)}"
54
56
  else
55
- @ui.error ' Can not create mobi'
56
- @ui.info END_LINE
57
+ error ' Can not create mobi'
58
+ info END_LINE
57
59
  exit 43
58
60
  end
59
61
  end
@@ -0,0 +1,87 @@
1
+ require 'fileutils'
2
+
3
+ require_relative '../builder'
4
+ require_relative '../chunkable'
5
+
6
+ module Persie
7
+ class MultipleHTMLs < Builder
8
+
9
+ include Chunkable
10
+
11
+ def initialize(book, options = {})
12
+ super
13
+ @spine_items = []
14
+ @spine_item_titles = []
15
+ end
16
+
17
+ # Builds multiple HTML files.
18
+ def build
19
+ info '=== Build Mutiple HTML ' << '=' * 49
20
+
21
+ self.before_build
22
+ self.check_sample
23
+ self.convert_to_single_html
24
+ self.generate_spine_items
25
+ self.chunk
26
+ self.copy_images
27
+ self.after_build
28
+
29
+ info 'Location: builds/html/multiple/'
30
+ info END_LINE
31
+
32
+ nil
33
+ end
34
+
35
+ def copy_images
36
+ images_dir = File.join @book.builds_dir, 'html', 'multiple', 'images'
37
+
38
+ # remove previous images first
39
+ # QUSTION: is this necessary?
40
+ FileUtils.rm_r(images_dir) if File.directory?(images_dir)
41
+
42
+ info 'Copy images...'
43
+ FileUtils.cp_r "#{@book.images_dir}/.", images_dir
44
+ confirm ' Done'
45
+ end
46
+
47
+ private
48
+
49
+ def adoc_custom_attributes
50
+ {
51
+ 'imagesdir' => 'images',
52
+ 'ebook-format' => 'html',
53
+ 'multiple-pages' => true,
54
+ 'outfilesuffix' => '.html'
55
+ }
56
+ end
57
+
58
+ # Assembles payloads for Liquid to render.
59
+ def assemble_payloads
60
+ attrs = @document.attributes
61
+ custom = {
62
+ 'title' => attrs['doctitle'],
63
+ 'generator' => "persie #{::Persie::VERSION}"
64
+ }
65
+
66
+ attrs.merge(custom)
67
+ end
68
+
69
+ # Renders ERb layouts of `single' or `multiple'.
70
+ def render_layout_of(format, payloads)
71
+ unless ['single', 'multipe'].include? format
72
+ error "ONLY can render layout for `single' or `multiple'"
73
+ info END_LINE
74
+ exit 53
75
+ end
76
+
77
+ # Site templates stored in `themes/site/' folder
78
+ path = File.join @book.themes_dir, 'site', "#{format}.html.liquid"
79
+
80
+ return nil unless File.exist? path
81
+
82
+ tpl = ::Liquid::Template.parse File.read(path)
83
+ tpl.render(payloads)
84
+ end
85
+
86
+ end
87
+ end
@@ -11,16 +11,17 @@ module Persie
11
11
 
12
12
  # Builds PDF.
13
13
  def build
14
- @ui.info '=== Build PDF ' << '=' * 58
14
+ info '=== Build PDF ' << '=' * 58
15
15
 
16
+ self.before_build
16
17
  self.check_dependency
17
18
  self.check_sample
18
-
19
19
  self.convert_to_html
20
20
  self.restart_page_number
21
21
  self.convert_to_pdf
22
+ self.after_build
22
23
 
23
- @ui.info END_LINE
24
+ info END_LINE
24
25
 
25
26
  nil
26
27
  end
@@ -28,8 +29,8 @@ module Persie
28
29
  # Checks dependency.
29
30
  def check_dependency
30
31
  unless Dependency.prince_installed?
31
- @ui.error 'Error: PrinceXML not installed'
32
- @ui.info END_LINE
32
+ error 'Error: PrinceXML not installed'
33
+ info END_LINE
33
34
  exit 22
34
35
  end
35
36
  end
@@ -54,13 +55,13 @@ module Persie
54
55
 
55
56
  # Converts AsciiDoc document to HTML, and writes to a file.
56
57
  def convert_to_html
57
- @ui.info 'Converting to HTML...'
58
+ info 'Converting to HTML...'
58
59
  html = @document.convert
59
60
  prepare_directory(self.html_path)
60
61
  File.write(self.html_path, html)
61
- @ui.confirm ' HTMl file created'
62
- @ui.info " Location: #{self.html_path(true)}"
63
- @ui.info '' # new line
62
+ confirm ' HTMl file created'
63
+ info " Location: #{self.html_path(true)}"
64
+ info '' # new line
64
65
  end
65
66
 
66
67
  # Restart PDF page number.
@@ -81,15 +82,15 @@ module Persie
81
82
 
82
83
  # Converts HTML to PDF with PrinceXML.
83
84
  def convert_to_pdf
84
- @ui.info 'Converting to PDF...'
85
+ info 'Converting to PDF...'
85
86
  prepare_directory(self.pdf_path)
86
87
  system "prince #{self.html_path} -o #{self.pdf_path}"
87
88
  if $?.to_i == 0
88
- @ui.confirm ' PDF file created'
89
- @ui.info " Location: #{self.pdf_path(true)}"
89
+ confirm ' PDF file created'
90
+ info " Location: #{self.pdf_path(true)}"
90
91
  else
91
- @ui.error ' Error: Cannot create PDF with PrinceXML'
92
- @ui.info END_LINE
92
+ error ' Error: Cannot create PDF with PrinceXML'
93
+ info END_LINE
93
94
  exit 23
94
95
  end
95
96
  end
@@ -3,43 +3,33 @@ require 'liquid'
3
3
  require_relative '../builder'
4
4
 
5
5
  module Persie
6
- class Site < Builder
6
+ class SingleHTML < Builder
7
7
 
8
8
  def initialize(book, options = {})
9
9
  super
10
10
  end
11
11
 
12
- # Builds a website.
12
+ # Builds single HTML file.
13
13
  def build
14
- @ui.info '=== Build site ' << '=' * 57
14
+ info '=== Build Single HTML ' << '=' * 50
15
15
 
16
- if @options.multiple?
17
- self.build_multiple
18
- return nil
19
- end
20
-
21
- self.build_single
16
+ self.before_build
17
+ self.check_sample
18
+ self.generate_html
19
+ self.after_build
22
20
 
23
- @ui.info END_LINE
21
+ info END_LINE
24
22
 
25
23
  nil
26
24
  end
27
25
 
28
- def build_multiple
29
- @ui.warning "Multiple pages\n"
30
-
31
- @ui.warning 'Not Implemented!'
32
- end
33
-
34
- # Builds single file website.
35
- def build_single
36
- @ui.warning "Single page\n"
37
-
38
- html_path = File.join(@book.builds_dir, 'site', 'single' ,'index.html')
26
+ # Generates single HTML file.
27
+ def generate_html
28
+ html_path = File.join(@book.builds_dir, 'html', 'single' ,'index.html')
39
29
  prepare_directory(html_path)
40
30
 
41
31
  html = @document.convert
42
- content = render_layout_of('single', assemble_payloads(html))
32
+ content = render_layout assemble_payloads(html)
43
33
 
44
34
  if content.nil?
45
35
  File.write(html_path, html)
@@ -48,11 +38,11 @@ module Persie
48
38
  end
49
39
 
50
40
  if File.exist? html_path
51
- @ui.confirm 'Site created'
52
- @ui.info "Location: site/single/index.html"
41
+ confirm 'HTML created'
42
+ info "Location: builds/html/single/index.html"
53
43
  else
54
- @ui.error 'Cannot create site'
55
- @ui.info END_LINE
44
+ error 'Cannot create HTML'
45
+ info END_LINE
56
46
  exit 52
57
47
  end
58
48
  end
@@ -61,8 +51,8 @@ module Persie
61
51
 
62
52
  def adoc_custom_attributes
63
53
  {
64
- 'ebook-format' => 'site',
65
- 'single-page' => @options.multiple? ? false : true,
54
+ 'ebook-format' => 'html',
55
+ 'single-page' => true,
66
56
  'outfilesuffix' => '.html'
67
57
  }
68
58
  end
@@ -89,16 +79,9 @@ module Persie
89
79
  attrs.merge(custom)
90
80
  end
91
81
 
92
- # Renders ERb layouts of `single' or `multiple'.
93
- def render_layout_of(format, payloads)
94
- unless ['single', 'multipe'].include? format
95
- @ui.error "ONLY can render layout for `single' or `multiple'"
96
- @ui.info END_LINE
97
- exit 53
98
- end
99
-
100
- # Site templates stored in `themes/site/' folder
101
- path = File.join @book.themes_dir, 'site', "#{format}.html.liquid"
82
+ # Renders ERb layouts.
83
+ def render_layout(payloads)
84
+ path = File.join @book.themes_dir, 'html', 'single.html.liquid'
102
85
 
103
86
  return nil unless File.exist? path
104
87
 
@@ -0,0 +1,255 @@
1
+ require 'nokogiri'
2
+
3
+ module Persie
4
+ module Chunkable
5
+
6
+ # these are not using `include' directive
7
+ SPECIAL_SPINE_ITEMS = ['cover', 'titlepage', 'nav']
8
+
9
+ # Gets/Sets spine items.
10
+ attr_accessor :spine_items
11
+
12
+ # Gets/Sets spine items's titles.
13
+ attr_accessor :spine_item_titles
14
+
15
+ # Converts to single HTML file.
16
+ def convert_to_single_html
17
+ info 'Converting to HTML...'
18
+ format = @document.attr('ebook-format')
19
+ html = @document.convert
20
+ prepare_directory self.html_path(format)
21
+ File.write self.html_path(format), html
22
+ confirm ' HTMl file created'
23
+ info " Location: #{self.html_path(format, true)}\n"
24
+ end
25
+
26
+ # Generates spine items.
27
+ def generate_spine_items
28
+ register_spine_item_processor
29
+
30
+ # Re-load the master file
31
+ doc = ::Asciidoctor.load_file(@book.master_file, adoc_options)
32
+ @spine_items.concat SPECIAL_SPINE_ITEMS
33
+ @spine_items.concat doc.references['spine_items']
34
+
35
+ # no need cover page and titlepage in HTML format
36
+ @spine_items.shift(2) if @document.attr('ebook-format') == 'html'
37
+
38
+ @spine_items
39
+ end
40
+
41
+ # Chucks single HTML file to multiple HTML files.
42
+ def chunk
43
+ info 'Chunking files...'
44
+
45
+ format = @document.attr('ebook-format')
46
+ content = File.read self.html_path(format)
47
+ root = ::Nokogiri::HTML(content)
48
+
49
+ # Adjust spint items
50
+ @has_cover = root.css('div[data-type="cover"]').size > 0
51
+ @has_toc = root.css('nav[data-type="toc"]').size > 0
52
+ self.spine_items.delete('cover') unless @has_cover
53
+ self.spine_items.delete('toc') unless @has_toc
54
+
55
+ correct_nav_href(root)
56
+
57
+ top_level_sections = resolve_top_level_sections(root)
58
+
59
+ # stupid check, incase of something went wrong
60
+ unless top_level_sections.count == self.spine_items.count
61
+ error ' Count of sections DO NOT equal to spine items count.'
62
+ error ' Terminated!'
63
+ if @options.debug?
64
+ info 'sections count: ' + top_level_sections.count
65
+ info 'spine_items: ' + self.spine_items.inspect
66
+ end
67
+ info '=' * 72
68
+ exit 31
69
+ end
70
+
71
+ sep = '<body data-type="book">'
72
+ before = content.split(sep).first
73
+ after = %(</body>\n</html>)
74
+
75
+ # Collect the first h1 heading first
76
+ top_level_sections.each_with_index do |node, i|
77
+ title = if (i == 0 && @has_cover) # cover page don't have title
78
+ @document.attr('cover-page-title', 'Cover')
79
+ else
80
+ node.css('h1:first-of-type').first.inner_text
81
+ end
82
+ @spine_item_titles << title
83
+ end
84
+
85
+ top_level_sections.each_with_index do |node, i|
86
+ # Footnotes
87
+ footnotes_div = generate_footnotes(node)
88
+
89
+ # Write to chunked file
90
+ ext = @document.attr('outfilesuffix', '.html')
91
+ to_dir = if format == 'epub'
92
+ @tmp_dir
93
+ else
94
+ File.join @book.builds_dir, 'html', 'multiple'
95
+ end
96
+ path = File.join(to_dir, "#{self.spine_items[i]}#{ext}")
97
+ prepare_directory(path)
98
+ combined = [before, sep, node.to_xhtml, footnotes_div, after] * "\n"
99
+
100
+ # use only when building multiple html files
101
+ layout = File.join @book.themes_dir, 'html', 'multiple.html.liquid'
102
+
103
+ chunked_content = if format == 'epub'
104
+ combined
105
+ elsif (format == 'html' && !File.exist?(layout))
106
+ combined
107
+ else
108
+ require 'liquid'
109
+ tpl = ::Liquid::Template.parse File.read(layout)
110
+
111
+ if i == 0
112
+ prev_url = nil
113
+ prev_title = nil
114
+ else
115
+ prev_url = "#{@spine_items[i - 1]}#{ext}"
116
+ prev_title = @spine_item_titles[i - 1]
117
+ end
118
+
119
+ if (j = i + 1) == @spine_items.count
120
+ next_url = nil
121
+ next_title = nil
122
+ else
123
+ next_url = "#{@spine_items[j]}#{ext}"
124
+ next_title = @spine_item_titles[j]
125
+ end
126
+
127
+ payloads = assemble_payloads.merge 'page_title' => @spine_item_titles[i],
128
+ 'content' => node.to_xhtml,
129
+ 'footnotes' => footnotes_div,
130
+ 'prev_url' => prev_url,
131
+ 'next_url' => next_url,
132
+ 'prev_title' => prev_title,
133
+ 'next_title' => next_title
134
+
135
+ tpl.render(payloads)
136
+ end
137
+
138
+ File.write path, chunked_content
139
+ end
140
+
141
+ confirm " Done"
142
+ end
143
+
144
+ # Gets XHTML file path.
145
+ def html_path(format, relative = false)
146
+ name = sample? ? "#{@book.slug}-sample" : @book.slug
147
+ path = File.join('tmp', format, "#{name}.html")
148
+ return path if relative
149
+
150
+ File.join(@book.base_dir, path)
151
+ end
152
+
153
+ private
154
+
155
+ # Corrects navigation items' href.
156
+ #
157
+ # Example:
158
+ # href="#id" => href="path.xhtml#id"
159
+ def correct_nav_href(node)
160
+ # return early if no table of contents
161
+ return nil unless (ols = node.css('nav[data-type="toc"]> ol')).size > 0
162
+
163
+ spine_items_dup = self.spine_items.dup
164
+ SPECIAL_SPINE_ITEMS.each { |i| spine_items_dup.delete(i) }
165
+
166
+ ext = @document.attr('outfilesuffix', '.html')
167
+ top_level_lis = ols.first.css('> li')
168
+ j = 0
169
+ top_level_lis.each do |li|
170
+ if li['data-type'] == 'part'
171
+ first_a = li.css('> a').first
172
+ first_a_href = first_a['href']
173
+ first_a['href'] = "#{spine_items_dup[j]}#{ext}#{first_a_href}"
174
+ if (li_ols = li.css('> ol')).size > 0
175
+ li_ol = li_ols.first
176
+ li_ol.css('> li').each do |lli|
177
+ j += 1
178
+ lli.css('a').each do |a|
179
+ old_href = a['href']
180
+ a['href'] = "#{spine_items_dup[j]}#{ext}#{old_href}"
181
+ end
182
+ end
183
+ j += 1
184
+ end
185
+ else
186
+ li.css('a').each do |a|
187
+ old_href = a['href']
188
+ a['href'] = "#{spine_items_dup[j]}#{ext}#{old_href}"
189
+ end
190
+ j += 1
191
+ end
192
+ end
193
+ end
194
+
195
+ # Resolves top level sections.
196
+ #
197
+ # When there are parts, takes sections within each part out.
198
+ def resolve_top_level_sections(node)
199
+ if (parts = node.css('body > div[data-type="part"]')).size > 0
200
+ parts.each do |part|
201
+ sections = part.css('> section')
202
+ sections.each do |sect|
203
+ part.delete sect
204
+ end
205
+ part.add_next_sibling(sections)
206
+ end
207
+ end
208
+
209
+ # return a nodeset, contains all top level sections
210
+ node.css('body > *')
211
+ end
212
+
213
+ # Generates footnotes for one node.
214
+ def generate_footnotes(node)
215
+ footnotes_div = nil
216
+ footnotes = node.css('span[data-type="footnote"]')
217
+ if footnotes.length > 0
218
+ footnotes_div = generate_footnotes_div(footnotes)
219
+ replace_footnote_with_sup(footnotes)
220
+ end
221
+
222
+ footnotes_div
223
+ end
224
+
225
+ # Generate a footnotes div element.
226
+ def generate_footnotes_div(footnotes)
227
+ epub_type = if @document.attr('ebook-format') == 'epub'
228
+ %( epub:type="footnote")
229
+ else
230
+ nil
231
+ end
232
+ result = ['<div class="footnotes">']
233
+ result << '<ol>'
234
+ footnotes.each_with_index do |fn, i|
235
+ index = i + 1
236
+ ref = %( <a href="#fn-ref-#{index}">&#8617;</a>)
237
+ result << %(<li id="fn-#{index}"#{epub_type}>#{fn.inner_html}#{ref}</li>)
238
+ end
239
+ result << '</ol>'
240
+ result << '</div>'
241
+
242
+ result * "\n"
243
+ end
244
+
245
+ def replace_footnote_with_sup(footnotes)
246
+ footnotes.each_with_index do |fn, i|
247
+ index = i + 1
248
+ fn.replace(%(<sup><a id="fn-ref-#{index}" href="#fn-#{index}">#{index}</a></sup>))
249
+ end
250
+
251
+ nil
252
+ end
253
+
254
+ end
255
+ end