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.
- checksums.yaml +4 -4
- data/lib/persie/asciidoctor_ext/htmlbook.rb +82 -79
- data/lib/persie/asciidoctor_ext/spine_item_processor.rb +4 -1
- data/lib/persie/book.rb +9 -3
- data/lib/persie/builder.rb +14 -4
- data/lib/persie/builders/epub.rb +15 -289
- data/lib/persie/builders/mobi.rb +13 -11
- data/lib/persie/builders/multiple_htmls.rb +87 -0
- data/lib/persie/builders/pdf.rb +15 -14
- data/lib/persie/builders/{site.rb → single_html.rb} +21 -38
- data/lib/persie/chunkable.rb +255 -0
- data/lib/persie/cli.rb +10 -6
- data/lib/persie/generator.rb +8 -8
- data/lib/persie/gepub_ext.rb +88 -0
- data/lib/persie/server.rb +2 -2
- data/lib/persie/ui.rb +13 -13
- data/lib/persie/version.rb +1 -1
- data/spec/build_epub_cmd_spec.rb +70 -0
- data/spec/{build_pdf_command_spec.rb → build_pdf_cmd_spec.rb} +7 -3
- data/spec/new_cmd_spec.rb +78 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/{version_command_spec.rb → version_cmd_spec.rb} +0 -0
- data/templates/{Gemfile.txt → Gemfile.erb} +1 -1
- data/templates/stylesheets/html.css +1 -0
- metadata +15 -12
- data/spec/new_command_spec.rb +0 -57
- data/spec/pdf_builder_spec.rb +0 -39
- data/templates/stylesheets/site.css +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41585c7758fb18afab82bae8efd74601ea83b192
|
4
|
+
data.tar.gz: 33669806f0da31cc015369764b1698ef63e54ec0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3a40f880ba33b0212a01db6e136e99bf38caa9aeb0b1024e7474656c722261f6bab08f93b21a13bd3725d62238a33cc7479577adf400c277e9e9ee18a5d3e4d
|
7
|
+
data.tar.gz: 03dcbd66639256a4af1f7fd5b58f9306ad6c932d3589fac3198eeb4f42f2be43986f2e0d4e100ab8c4cbad7c17e20c08ac7bff8e2bcc0eae8f0ff6aa69f3ca1d
|
@@ -45,8 +45,8 @@ module Persie
|
|
45
45
|
# In this method, node == node.document
|
46
46
|
# In other methods, you should use node.document
|
47
47
|
ebook_format = node.attr('ebook-format')
|
48
|
-
result = []
|
49
48
|
|
49
|
+
result = []
|
50
50
|
result << '<!DOCTYPE html>'
|
51
51
|
lang_attr = %(lang="#{node.attr('lang', 'en')}")
|
52
52
|
if EPUB_FORMATS.include? ebook_format
|
@@ -54,29 +54,25 @@ module Persie
|
|
54
54
|
else
|
55
55
|
result << %(<html xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.w3.org/1999/xhtml" #{lang_attr}>)
|
56
56
|
end
|
57
|
-
result <<
|
58
|
-
|
59
|
-
'application/xhtml+xml'
|
60
|
-
else
|
61
|
-
'text/html'
|
62
|
-
end
|
63
|
-
result << %(<meta charset="#{node.attr 'encoding', 'UTF-8'}"/>)
|
57
|
+
result << '<head>'
|
58
|
+
result << %(<meta charset="#{node.attr('encoding', 'UTF-8')}"/>)
|
64
59
|
result << %(<title>#{node.doctitle(:sanitize => true) || node.attr('untitled-label')}</title>)
|
65
|
-
if ebook_format === '
|
60
|
+
if ebook_format === 'html'
|
66
61
|
result << %(<meta http-equiv="X-UA-Compatible" content="IE=edge"/>)
|
67
62
|
result << %(<meta name="viewport" content="width=device-width, initial-scale=1.0"/>)
|
68
63
|
end
|
69
|
-
result << %(<meta name="generator" content="
|
64
|
+
result << %(<meta name="generator" content="persie #{node.attr 'persie-version'}"/>)
|
70
65
|
result << %(<meta name="date" content="#{Time.parse(node.revdate).iso8601}"/>) if node.attr? 'revdate'
|
71
66
|
result << %(<meta name="description" content="#{node.attr 'description'}"/>) if node.attr? 'description'
|
72
67
|
result << %(<meta name="keywords" content="#{node.attr 'keywords'}"/>) if node.attr? 'keywords'
|
73
68
|
result << %(<meta name="author" content="#{node.attr 'author'}"/>) if node.attr? 'author'
|
69
|
+
result << %(<meta name="translator" content="#{node.attr 'translator'}"/>) if node.attr? 'translator'
|
74
70
|
result << %(<meta name="copyright" content="#{node.attr 'copyright'}"/>) if node.attr? 'copyright'
|
75
71
|
|
76
72
|
stylesheet_path = case ebook_format
|
77
73
|
when 'pdf'
|
78
|
-
File.join(node.attr('themes-dir'),
|
79
|
-
when '
|
74
|
+
File.join(node.attr('themes-dir'), 'pdf', 'pdf.css')
|
75
|
+
when 'html'
|
80
76
|
'style.css'
|
81
77
|
else
|
82
78
|
"#{ebook_format}.css"
|
@@ -115,8 +111,8 @@ MathJax.Hub.Config({
|
|
115
111
|
body_attrs << %(class="sample") if node.attr? 'is-sample'
|
116
112
|
result << %(<body #{body_attrs * ' '}>)
|
117
113
|
|
118
|
-
result << cover(node)
|
119
|
-
result << titlepage(node)
|
114
|
+
result << cover(node) unless multiple_pages_html?(node)
|
115
|
+
result << titlepage(node) unless multiple_pages_html?(node)
|
120
116
|
result << toc(node)
|
121
117
|
|
122
118
|
if node.attr? 'is-sample'
|
@@ -125,10 +121,10 @@ MathJax.Hub.Config({
|
|
125
121
|
result << node.content
|
126
122
|
end
|
127
123
|
|
128
|
-
# Display footnotes in single page
|
129
|
-
if
|
124
|
+
# Display footnotes in single page html
|
125
|
+
if single_page_html?(node)
|
130
126
|
if node.footnotes? && !(node.attr? 'nofootnotes')
|
131
|
-
result <<
|
127
|
+
result << %(<div class="footnotes">\n<ol>)
|
132
128
|
node.footnotes.each do |fn|
|
133
129
|
ref = %( <a href="#fn-ref-#{fn.index}">↩</a>)
|
134
130
|
result << %(<li data-type="footnote" id="fn-#{fn.index}">#{fn.text}#{ref}</li>)
|
@@ -143,7 +139,6 @@ MathJax.Hub.Config({
|
|
143
139
|
result * "\n"
|
144
140
|
end
|
145
141
|
|
146
|
-
# FIXME: need review
|
147
142
|
def embedded(node)
|
148
143
|
result = []
|
149
144
|
if !node.notitle && node.has_header?
|
@@ -154,15 +149,11 @@ MathJax.Hub.Config({
|
|
154
149
|
result << node.content
|
155
150
|
|
156
151
|
if node.footnotes? && !(node.attr? 'nofootnotes')
|
157
|
-
result << %(<div
|
158
|
-
<hr/>)
|
152
|
+
result << %(<div class="footnotes">\n<ol>)
|
159
153
|
node.footnotes.each do |footnote|
|
160
|
-
result << %(<
|
161
|
-
<a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a> #{footnote.text}
|
162
|
-
</div>)
|
154
|
+
result << %(<li data-type="footnote" id="fn-#{footnote.index}">#{footnote.text}</li>)
|
163
155
|
end
|
164
|
-
|
165
|
-
result << '</div>'
|
156
|
+
result << "</ol>\n</div>"
|
166
157
|
end
|
167
158
|
|
168
159
|
result * "\n"
|
@@ -218,6 +209,7 @@ MathJax.Hub.Config({
|
|
218
209
|
else
|
219
210
|
slevel - 1
|
220
211
|
end
|
212
|
+
|
221
213
|
wrapper_tag = if data_type != 'part'
|
222
214
|
'section'
|
223
215
|
else
|
@@ -268,20 +260,26 @@ MathJax.Hub.Config({
|
|
268
260
|
result * "\n"
|
269
261
|
end
|
270
262
|
|
271
|
-
# fixme: not touched, need cleanup
|
272
263
|
def audio(node)
|
273
264
|
xml = node.document.attr? 'htmlsyntax', 'xml'
|
274
265
|
id_attribute = node.id ? %( id="#{node.id}") : nil
|
275
266
|
classes = ['audioblock', node.style, node.role].compact
|
276
267
|
class_attribute = %( class="#{classes * ' '}")
|
277
268
|
title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
|
278
|
-
|
279
|
-
#{
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
269
|
+
|
270
|
+
result = [%(<div#{id_attribute}#{class_attribute}>)]
|
271
|
+
result << title_element
|
272
|
+
result << '<div class="content">'
|
273
|
+
|
274
|
+
autoplay = node.option?('autoplay') ? append_boolean_attribute('autoplay', xml) : nil
|
275
|
+
controls = node.option?('nocontrols') ? nil : append_boolean_attribute('controls', xml)
|
276
|
+
looop = node.option?('loop') ? append_boolean_attribute('loop', xml) : nil
|
277
|
+
|
278
|
+
result << %(<audio src="#{node.media_uri(node.attr 'target')}"#{autoplay}#{controls}#{looop}>)
|
279
|
+
result << 'Your browser does not support the audio tag.'
|
280
|
+
result << %(</audio>\n</div>\n</div>)
|
281
|
+
|
282
|
+
result * "\n"
|
285
283
|
end
|
286
284
|
|
287
285
|
def colist(node)
|
@@ -293,13 +291,12 @@ Your browser does not support the audio tag.
|
|
293
291
|
start_attr = node.attr?('start') ? %( start="#{node.attr('start')}") : nil
|
294
292
|
|
295
293
|
result << %(<dl#{id_attr}#{class_attr}#{start_attr}>)
|
296
|
-
|
297
294
|
node.items.each_with_index do |item, i|
|
298
295
|
result << %(<dt>#{digits[i]}</dt>)
|
299
296
|
result << %(<dd><p>#{item.text}</p>#{item.block? ? item.content : nil}</dd>)
|
300
297
|
end
|
301
|
-
|
302
298
|
result << '</dl>'
|
299
|
+
|
303
300
|
result * "\n"
|
304
301
|
end
|
305
302
|
|
@@ -393,7 +390,6 @@ Your browser does not support the audio tag.
|
|
393
390
|
%(<div data-type="example"#{id_attr}#{class_attr}>#{title_element}#{node.content}</div>)
|
394
391
|
end
|
395
392
|
|
396
|
-
# FIXME: not touched, need cleanup
|
397
393
|
def floating_title(node)
|
398
394
|
tag_name = %(h#{node.level + 1})
|
399
395
|
id_attribute = node.id ? %( id="#{node.id}") : nil
|
@@ -402,15 +398,15 @@ Your browser does not support the audio tag.
|
|
402
398
|
end
|
403
399
|
|
404
400
|
def image(node)
|
405
|
-
align =
|
406
|
-
float =
|
401
|
+
align = node.attr?('align') ? node.attr('align') : nil
|
402
|
+
float = node.attr?('float') ? node.attr('float') : nil
|
407
403
|
style_attr = if align || float
|
408
404
|
styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact
|
409
405
|
%( style="#{styles * ';'}")
|
410
406
|
end
|
411
407
|
|
412
|
-
width_attr
|
413
|
-
height_attr =
|
408
|
+
width_attr = node.attr?('width') ? %( width="#{node.attr 'width'}") : nil
|
409
|
+
height_attr = node.attr?('height') ? %( height="#{node.attr 'height'}") : nil
|
414
410
|
|
415
411
|
img_element = %(<img src="#{node.image_uri node.attr('target')}" alt="#{node.attr 'alt'}"#{width_attr}#{height_attr}/>)
|
416
412
|
if (link = node.attr 'link')
|
@@ -462,11 +458,11 @@ Your browser does not support the audio tag.
|
|
462
458
|
def literal(node)
|
463
459
|
id_attr = node.id ? %( id="#{node.id}") : nil
|
464
460
|
cls = node.role ? " #{node.role}" : nil
|
465
|
-
|
466
|
-
|
467
|
-
|
461
|
+
result = [%(<div#{id_attr} class="literal#{cls}">)]
|
462
|
+
result << %(<pre>#{node.content}</pre>)
|
463
|
+
result << '</div>'
|
468
464
|
|
469
|
-
|
465
|
+
result * "\n"
|
470
466
|
end
|
471
467
|
|
472
468
|
def math(node)
|
@@ -474,7 +470,7 @@ Your browser does not support the audio tag.
|
|
474
470
|
class_attr = node.role ? %( class="#{node.role}") : nil
|
475
471
|
title_element = node.title? ? %(<h5>#{node.title}</h5>\n) : nil
|
476
472
|
open, close = ::Asciidoctor::BLOCK_MATH_DELIMITERS[node.style.to_sym]
|
477
|
-
|
473
|
+
|
478
474
|
equation = node.content.strip
|
479
475
|
if node.subs.nil_or_empty? && !(node.attr? 'subs')
|
480
476
|
equation = node.sub_specialcharacters equation
|
@@ -741,15 +737,15 @@ Your browser does not support the audio tag.
|
|
741
737
|
result * "\n"
|
742
738
|
end
|
743
739
|
|
744
|
-
# FIXME: not touched, need cleanup
|
745
740
|
def video(node)
|
746
741
|
xml = node.document.attr? 'htmlsyntax', 'xml'
|
747
742
|
id_attribute = node.id ? %( id="#{node.id}") : nil
|
748
743
|
classes = ['videoblock', node.style, node.role].compact
|
749
744
|
class_attribute = %( class="#{classes * ' '}")
|
750
745
|
title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
|
751
|
-
width_attribute =
|
752
|
-
height_attribute =
|
746
|
+
width_attribute = node.attr?('width') ? %( width="#{node.attr 'width'}") : nil
|
747
|
+
height_attribute = node.attr?('height') ? %( height="#{node.attr 'height'}") : nil
|
748
|
+
|
753
749
|
case node.attr 'poster'
|
754
750
|
when 'vimeo'
|
755
751
|
start_anchor = (node.attr? 'start') ? "#at=#{node.attr 'start'}" : nil
|
@@ -757,32 +753,36 @@ Your browser does not support the audio tag.
|
|
757
753
|
autoplay_param = (node.option? 'autoplay') ? "#{delimiter}autoplay=1" : nil
|
758
754
|
delimiter = '&' if autoplay_param
|
759
755
|
loop_param = (node.option? 'loop') ? "#{delimiter}loop=1" : nil
|
760
|
-
|
761
|
-
<div
|
762
|
-
|
763
|
-
|
764
|
-
|
756
|
+
|
757
|
+
result = %(<div#{id_attribute}#{class_attribute}>)
|
758
|
+
result << title_element
|
759
|
+
result << '<div class="content">'
|
760
|
+
result << %(<iframe#{width_attribute}#{height_attribute} src="//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{append_boolean_attribute 'webkitAllowFullScreen', xml}#{append_boolean_attribute 'mozallowfullscreen', xml}#{append_boolean_attribute 'allowFullScreen', xml}></iframe>)
|
761
|
+
result << %(</div>\n</div>)
|
762
|
+
result * "\n"
|
765
763
|
when 'youtube'
|
766
|
-
start_param =
|
767
|
-
end_param =
|
768
|
-
autoplay_param =
|
769
|
-
loop_param =
|
770
|
-
controls_param =
|
771
|
-
|
772
|
-
<div
|
773
|
-
|
774
|
-
|
775
|
-
|
764
|
+
start_param = node.attr?('start') ? "&start=#{node.attr 'start'}" : nil
|
765
|
+
end_param = node.attr?('end') ? "&end=#{node.attr 'end'}" : nil
|
766
|
+
autoplay_param = node.option?('autoplay') ? '&autoplay=1' : nil
|
767
|
+
loop_param = node.option?('loop') ? '&loop=1' : nil
|
768
|
+
controls_param = node.option?('nocontrols') ? '&controls=0' : nil
|
769
|
+
|
770
|
+
result = %(<div#{id_attribute}#{class_attribute}>)
|
771
|
+
result << title_element
|
772
|
+
result << '<div class="content">'
|
773
|
+
result << %(<iframe#{width_attribute}#{height_attribute} src="//www.youtube.com/embed/#{node.attr 'target'}?rel=0#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>)
|
774
|
+
result << %(</div>\n</div>)
|
775
|
+
result * "\n"
|
776
776
|
else
|
777
777
|
poster_attribute = %(#{poster = node.attr 'poster'}).empty? ? nil : %( poster="#{node.media_uri poster}")
|
778
778
|
time_anchor = ((node.attr? 'start') || (node.attr? 'end')) ? %(#t=#{node.attr 'start'}#{(node.attr? 'end') ? ',' : nil}#{node.attr 'end'}) : nil
|
779
|
-
%(<div#{id_attribute}#{class_attribute}
|
780
|
-
|
781
|
-
<
|
782
|
-
|
783
|
-
|
784
|
-
</div>
|
785
|
-
|
779
|
+
result = %(<div#{id_attribute}#{class_attribute}>)
|
780
|
+
result << title_element
|
781
|
+
result << '<div class="content">'
|
782
|
+
result << %(<video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : nil}>)
|
783
|
+
result << 'Your browser does not support the video tag.'
|
784
|
+
result << %(</video>\m</div>\n</div>)
|
785
|
+
result * "\n"
|
786
786
|
end
|
787
787
|
end
|
788
788
|
|
@@ -792,10 +792,9 @@ Your browser does not support the video tag.
|
|
792
792
|
|
793
793
|
case node.type
|
794
794
|
when :xref
|
795
|
-
refid =
|
796
|
-
# FIXME seems like text should be prepared already
|
795
|
+
refid = node.attr('refid') || target
|
797
796
|
text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
|
798
|
-
if (ebook_format == 'pdf' ||
|
797
|
+
if (ebook_format == 'pdf' || single_page_html?(node)) && !target.start_with?('#')
|
799
798
|
parts = target.split('#', 2)
|
800
799
|
target = "##{parts.last}"
|
801
800
|
end
|
@@ -839,7 +838,7 @@ Your browser does not support the video tag.
|
|
839
838
|
%(<a data-type="footnoteref" href="##{node.target}">#{index}</a>)
|
840
839
|
else
|
841
840
|
id_attr = node.id ? %( id="#{node.id}") : nil
|
842
|
-
if
|
841
|
+
if single_page_html?(node)
|
843
842
|
%(<sup><a href="#fn-#{index}" id="fn-ref-#{index}">#{index}</a></sup>)
|
844
843
|
else
|
845
844
|
%(<span data-type="footnote"#{id_attr}>#{node.text}</span>)
|
@@ -893,7 +892,7 @@ Your browser does not support the video tag.
|
|
893
892
|
if (keys = node.attr 'keys').size == 1
|
894
893
|
%(<kbd>#{keys[0]}</kbd>)
|
895
894
|
else
|
896
|
-
key_combo = keys.map {|key| %(<kbd>#{key}</kbd
|
895
|
+
key_combo = keys.map { |key| %(<kbd>#{key}</kbd>-) }.join.chop
|
897
896
|
%(<span class="keyseq">#{key_combo}</span>)
|
898
897
|
end
|
899
898
|
end
|
@@ -923,11 +922,16 @@ Your browser does not support the video tag.
|
|
923
922
|
|
924
923
|
private
|
925
924
|
|
926
|
-
# Generates single page
|
927
|
-
def
|
925
|
+
# Generates single page html or not.
|
926
|
+
def single_page_html?(node)
|
928
927
|
node.document.attr('single-page', false)
|
929
928
|
end
|
930
929
|
|
930
|
+
# Generates multiple pages html or not.
|
931
|
+
def multiple_pages_html?(node)
|
932
|
+
node.document.attr('multiple-pages', false)
|
933
|
+
end
|
934
|
+
|
931
935
|
# Genarate cover page
|
932
936
|
def cover(node)
|
933
937
|
doc = node.document
|
@@ -966,7 +970,7 @@ Your browser does not support the video tag.
|
|
966
970
|
result * "\n"
|
967
971
|
end
|
968
972
|
|
969
|
-
# Generate a title page
|
973
|
+
# Generate a title page
|
970
974
|
def titlepage(node)
|
971
975
|
result = [%(<section data-type="titlepage">)]
|
972
976
|
result << %(<h1>#{node.header.title}</h1>)
|
@@ -1020,7 +1024,6 @@ Your browser does not support the video tag.
|
|
1020
1024
|
result * "\n"
|
1021
1025
|
end
|
1022
1026
|
|
1023
|
-
# FIXME: after cleanup, delete this
|
1024
1027
|
def append_boolean_attribute(name, xml)
|
1025
1028
|
xml ? %( #{name}="#{name}") : %( #{name})
|
1026
1029
|
end
|
@@ -31,7 +31,10 @@ module Persie
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def handles? target
|
34
|
-
|
34
|
+
format = ['epub', 'html'].include? @document.attr('ebook-format')
|
35
|
+
ext = ::Asciidoctor::ASCIIDOC_EXTENSIONS.include? ::File.extname(target)
|
36
|
+
|
37
|
+
format && ext
|
35
38
|
end
|
36
39
|
|
37
40
|
def update_config config
|
data/lib/persie/book.rb
CHANGED
@@ -5,7 +5,9 @@ require 'asciidoctor'
|
|
5
5
|
require_relative 'builders/pdf'
|
6
6
|
require_relative 'builders/epub'
|
7
7
|
require_relative 'builders/mobi'
|
8
|
-
require_relative 'builders/
|
8
|
+
require_relative 'builders/single_html'
|
9
|
+
require_relative 'builders/multiple_htmls'
|
10
|
+
|
9
11
|
|
10
12
|
module Persie
|
11
13
|
class Book
|
@@ -52,8 +54,12 @@ module Persie
|
|
52
54
|
Mobi.new(self, options).build
|
53
55
|
end
|
54
56
|
|
55
|
-
def
|
56
|
-
|
57
|
+
def build_single_html(options = {})
|
58
|
+
SingleHTML.new(self, options).build
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_multiple_htmls(options = {})
|
62
|
+
MultipleHTMLs.new(self, options).build
|
57
63
|
end
|
58
64
|
|
59
65
|
end
|
data/lib/persie/builder.rb
CHANGED
@@ -9,6 +9,7 @@ require_relative 'asciidoctor_ext/sample'
|
|
9
9
|
|
10
10
|
module Persie
|
11
11
|
class Builder
|
12
|
+
include UI
|
12
13
|
|
13
14
|
END_LINE = '=' * 72
|
14
15
|
|
@@ -16,7 +17,6 @@ module Persie
|
|
16
17
|
attr_reader :document
|
17
18
|
|
18
19
|
def initialize(book, options = {})
|
19
|
-
@ui = UI.new(options)
|
20
20
|
@book = book
|
21
21
|
@options = options
|
22
22
|
@document = ::Asciidoctor.load_file(@book.master_file, adoc_options)
|
@@ -28,15 +28,25 @@ module Persie
|
|
28
28
|
raise ::NotImplementedError
|
29
29
|
end
|
30
30
|
|
31
|
+
# Do nothing here.
|
32
|
+
# Implement in subclass or plugin.
|
33
|
+
def before_build
|
34
|
+
end
|
35
|
+
|
36
|
+
# Do nothing here.
|
37
|
+
# Implement in subclass or plugin.
|
38
|
+
def after_build
|
39
|
+
end
|
40
|
+
|
31
41
|
# If in sample mode, show an indicator in command line.
|
32
42
|
def check_sample
|
33
43
|
if sample?
|
34
44
|
if @document.sample_sections.size == 0
|
35
|
-
|
36
|
-
|
45
|
+
error 'Not setting sample, terminated!'
|
46
|
+
info END_LINE
|
37
47
|
exit
|
38
48
|
end
|
39
|
-
|
49
|
+
warning "Sample only", true
|
40
50
|
end
|
41
51
|
end
|
42
52
|
|
data/lib/persie/builders/epub.rb
CHANGED
@@ -1,107 +1,14 @@
|
|
1
|
-
require 'nokogiri'
|
2
|
-
|
3
1
|
require 'time'
|
4
2
|
|
3
|
+
require_relative '../gepub_ext'
|
5
4
|
require_relative '../builder'
|
5
|
+
require_relative '../chunkable'
|
6
6
|
|
7
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
|
-
resources(workdir: @theme_dir) do
|
66
|
-
cover_image image if File.exist? image
|
67
|
-
end
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
def add_images
|
72
|
-
resources(workdir: @base_dir) do
|
73
|
-
glob 'images/*.*'
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def add_content
|
78
|
-
builder = self
|
79
|
-
spine_items = @spine_items
|
80
|
-
spine_item_titles = @spine_item_titles
|
81
|
-
resources(workdir: @tmp_dir) do
|
82
|
-
nav 'nav.xhtml' if @has_toc
|
83
|
-
|
84
|
-
ordered do
|
85
|
-
spine_items.each_with_index do |item, i|
|
86
|
-
file "#{item}.xhtml"
|
87
|
-
heading builder.sanitized_title(spine_item_titles[i])
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
end
|
94
8
|
|
95
9
|
class Epub < Builder
|
96
10
|
|
97
|
-
|
98
|
-
SPECIAL_SPINE_ITEMS = ['cover', 'titlepage', 'nav']
|
99
|
-
|
100
|
-
# Gets/Sets spine items.
|
101
|
-
attr_accessor :spine_items
|
102
|
-
|
103
|
-
# Gets/Sets spine items's titles.
|
104
|
-
attr_accessor :spine_item_titles
|
11
|
+
include Chunkable
|
105
12
|
|
106
13
|
def initialize(book, options = {})
|
107
14
|
super
|
@@ -114,97 +21,18 @@ module Persie
|
|
114
21
|
|
115
22
|
# Builds ePub.
|
116
23
|
def build
|
117
|
-
|
24
|
+
info '=== Build ePub ' << '=' * 57
|
118
25
|
|
26
|
+
self.before_build
|
119
27
|
self.check_sample
|
120
|
-
self.
|
28
|
+
self.convert_to_single_html
|
121
29
|
self.generate_spine_items
|
122
30
|
self.chunk
|
123
31
|
self.generate_epub
|
124
32
|
self.validate
|
33
|
+
self.after_build
|
125
34
|
|
126
|
-
|
127
|
-
end
|
128
|
-
|
129
|
-
# Converts to single XHTML file.
|
130
|
-
def convert_to_single_xhtml
|
131
|
-
@ui.info 'Converting to XHTML...'
|
132
|
-
xhtml = @document.convert
|
133
|
-
prepare_directory(self.xhtml_path)
|
134
|
-
File.write(self.xhtml_path, xhtml)
|
135
|
-
@ui.confirm ' XHTMl file created'
|
136
|
-
@ui.info " Location: #{self.xhtml_path(true)}\n"
|
137
|
-
end
|
138
|
-
|
139
|
-
# Generates spine items.
|
140
|
-
def generate_spine_items
|
141
|
-
register_spine_item_processor
|
142
|
-
|
143
|
-
# Re-loading the master file
|
144
|
-
doc = ::Asciidoctor.load_file(@book.master_file, adoc_options)
|
145
|
-
@spine_items.concat SPECIAL_SPINE_ITEMS
|
146
|
-
@spine_items.concat doc.references['spine_items']
|
147
|
-
|
148
|
-
@spine_items
|
149
|
-
end
|
150
|
-
|
151
|
-
# Chucks single XHTML file to multiple XHTML files.
|
152
|
-
def chunk
|
153
|
-
@ui.info 'Chunking files...'
|
154
|
-
|
155
|
-
content = File.read(self.xhtml_path)
|
156
|
-
root = ::Nokogiri::HTML(content)
|
157
|
-
|
158
|
-
# Adjust spint items
|
159
|
-
@has_cover = root.css('div[data-type="cover"]').size > 0
|
160
|
-
@has_toc = root.css('nav[data-type="toc"]').size > 0
|
161
|
-
self.spine_items.delete('cover') unless @has_cover
|
162
|
-
self.spine_items.delete('toc') unless @has_toc
|
163
|
-
|
164
|
-
correct_nav_href(root)
|
165
|
-
|
166
|
-
top_level_sections = resolve_top_level_sections(root)
|
167
|
-
|
168
|
-
# stupid check, incase of something went wrong
|
169
|
-
unless top_level_sections.count == self.spine_items.count
|
170
|
-
@ui.error ' Count of sections DO NOT equal to spine items count.'
|
171
|
-
@ui.error ' Terminated!'
|
172
|
-
if @options.debug?
|
173
|
-
@ui.info 'sections count: ' + top_level_sections.count
|
174
|
-
@ui.info 'spine_items: ' + self.spine_items.inspect
|
175
|
-
end
|
176
|
-
@ui.info END_LINE
|
177
|
-
exit 31
|
178
|
-
end
|
179
|
-
|
180
|
-
sep = '<body data-type="book">'
|
181
|
-
tpl_before = content.split(sep).first
|
182
|
-
tpl_after = %(</body>\n</html>)
|
183
|
-
|
184
|
-
top_level_sections.each_with_index do |node, i|
|
185
|
-
# Collect the first h1 heading
|
186
|
-
title = if (i == 0 && @has_cover) # cover page don't have title
|
187
|
-
@document.attr('cover-page-title', 'Cover')
|
188
|
-
else
|
189
|
-
node.css('h1:first-of-type').first.inner_text
|
190
|
-
end
|
191
|
-
@spine_item_titles << title
|
192
|
-
|
193
|
-
# Footnotes
|
194
|
-
footnotes_div = generate_footnotes(node)
|
195
|
-
|
196
|
-
# Write to chunked file
|
197
|
-
path = File.join(@tmp_dir, "#{self.spine_items[i]}.xhtml")
|
198
|
-
File.open(path, 'w') do |f|
|
199
|
-
f.puts tpl_before
|
200
|
-
f.puts sep
|
201
|
-
f.puts node.to_xhtml
|
202
|
-
f.puts footnotes_div
|
203
|
-
f.puts tpl_after
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
@ui.confirm ' Done\n'
|
35
|
+
info END_LINE
|
208
36
|
end
|
209
37
|
|
210
38
|
# Generates ePub file.
|
@@ -216,7 +44,7 @@ module Persie
|
|
216
44
|
spine_items = self.spine_items
|
217
45
|
spine_item_titles = self.spine_item_titles
|
218
46
|
|
219
|
-
|
47
|
+
info 'Building ePub...'
|
220
48
|
|
221
49
|
builder = ::GEPUB::Builder.new do
|
222
50
|
extend GepubBuilderMixin
|
@@ -289,36 +117,27 @@ module Persie
|
|
289
117
|
|
290
118
|
prepare_directory(self.epub_path)
|
291
119
|
builder.generate_epub(self.epub_path)
|
292
|
-
|
293
|
-
|
120
|
+
confirm ' ePub file created'
|
121
|
+
info " Location: #{self.epub_path(true)}"
|
294
122
|
end
|
295
123
|
|
296
124
|
# Validates ePub file, optionally.
|
297
125
|
def validate
|
298
126
|
if @options.validate?
|
299
|
-
|
127
|
+
info "Validating..."
|
300
128
|
if Dependency.epubcheck_installed?
|
301
129
|
system "epubcheck #{epub_path}"
|
302
130
|
if $?.to_i == 0
|
303
|
-
|
131
|
+
confirm ' PASS'
|
304
132
|
else
|
305
|
-
|
133
|
+
error ' ERROR'
|
306
134
|
end
|
307
135
|
else
|
308
|
-
|
136
|
+
warning ' epubcheck not installed, skip validation'
|
309
137
|
end
|
310
138
|
end
|
311
139
|
end
|
312
140
|
|
313
|
-
# Gets XHTML file path.
|
314
|
-
def xhtml_path(relative = false)
|
315
|
-
name = sample? ? "#{@book.slug}-sample" : @book.slug
|
316
|
-
path = File.join('tmp', 'epub', "#{name}.html")
|
317
|
-
return path if relative
|
318
|
-
|
319
|
-
File.join(@book.base_dir, path)
|
320
|
-
end
|
321
|
-
|
322
141
|
# Gets ePub file path.
|
323
142
|
def epub_path(relative = false)
|
324
143
|
name = sample? ? "#{@book.slug}-sample" : @book.slug
|
@@ -338,98 +157,5 @@ module Persie
|
|
338
157
|
}
|
339
158
|
end
|
340
159
|
|
341
|
-
# Corrects navigation items' href.
|
342
|
-
#
|
343
|
-
# Example:
|
344
|
-
# href="#id" => href="path.xhtml#id"
|
345
|
-
def correct_nav_href(node)
|
346
|
-
return unless (ols = node.css('nav[data-type="toc"]> ol')).size > 0
|
347
|
-
|
348
|
-
spine_items_dup = self.spine_items.dup
|
349
|
-
SPECIAL_SPINE_ITEMS.each { |i| spine_items_dup.delete(i) }
|
350
|
-
|
351
|
-
top_level_lis = ols.first.css('> li')
|
352
|
-
j = 0
|
353
|
-
top_level_lis.each do |li|
|
354
|
-
if li['data-type'] == 'part'
|
355
|
-
first_a = li.css('> a').first
|
356
|
-
first_a_href = first_a['href']
|
357
|
-
first_a['href'] = "#{spine_items_dup[j]}.xhtml#{first_a_href}"
|
358
|
-
if (li_ols = li.css('> ol')).size > 0
|
359
|
-
li_ol = li_ols.first
|
360
|
-
li_ol.css('> li').each do |lli|
|
361
|
-
j += 1
|
362
|
-
lli.css('a').each do |a|
|
363
|
-
old_href = a['href']
|
364
|
-
a['href'] = "#{spine_items_dup[j]}.xhtml#{old_href}"
|
365
|
-
end
|
366
|
-
end
|
367
|
-
j += 1
|
368
|
-
end
|
369
|
-
else
|
370
|
-
li.css('a').each do |a|
|
371
|
-
old_href = a['href']
|
372
|
-
a['href'] = "#{spine_items_dup[j]}.xhtml#{old_href}"
|
373
|
-
end
|
374
|
-
j += 1
|
375
|
-
end
|
376
|
-
end
|
377
|
-
end
|
378
|
-
|
379
|
-
|
380
|
-
# Resolves top level sections.
|
381
|
-
#
|
382
|
-
# When there are parts, takes sections within each part out.
|
383
|
-
def resolve_top_level_sections(node)
|
384
|
-
if (parts = node.css('body > div[data-type="part"]')).size > 0
|
385
|
-
parts.each do |part|
|
386
|
-
sections = part.css('> section')
|
387
|
-
sections.each do |sect|
|
388
|
-
part.delete sect
|
389
|
-
end
|
390
|
-
part.add_next_sibling(sections)
|
391
|
-
end
|
392
|
-
end
|
393
|
-
|
394
|
-
node.css('body > *')
|
395
|
-
|
396
|
-
end
|
397
|
-
|
398
|
-
# Generates footnotes for one node.
|
399
|
-
def generate_footnotes(node)
|
400
|
-
footnotes_div = nil
|
401
|
-
footnotes = node.css('span[data-type="footnote"]')
|
402
|
-
if footnotes.length > 0
|
403
|
-
footnotes_div = generate_footnotes_div(footnotes)
|
404
|
-
replace_footnote_with_sup(footnotes)
|
405
|
-
end
|
406
|
-
|
407
|
-
footnotes_div
|
408
|
-
end
|
409
|
-
|
410
|
-
# Generate a footnotes div element.
|
411
|
-
def generate_footnotes_div(footnotes)
|
412
|
-
result = ['<div class="footnotes">']
|
413
|
-
result << '<ol>'
|
414
|
-
footnotes.each_with_index do |fn, i|
|
415
|
-
index = i + 1
|
416
|
-
result << %(<li id="fn-#{index}" epub:type="footnote">#{fn.inner_text}</li>)
|
417
|
-
end
|
418
|
-
result << '</ol>'
|
419
|
-
result << '</div>'
|
420
|
-
|
421
|
-
result * "\n"
|
422
|
-
end
|
423
|
-
|
424
|
-
def replace_footnote_with_sup(footnotes)
|
425
|
-
footnotes.each_with_index do |fn, i|
|
426
|
-
index = i + 1
|
427
|
-
fn.replace(%(<sup><a href="#fn-#{index}">#{index}</a></sup>))
|
428
|
-
end
|
429
|
-
|
430
|
-
nil
|
431
|
-
end
|
432
|
-
|
433
160
|
end
|
434
|
-
|
435
161
|
end
|