asciidoctor-epub3 1.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.adoc +22 -0
  3. data/NOTICE.adoc +53 -0
  4. data/README.adoc +744 -0
  5. data/Rakefile +78 -0
  6. data/bin/adb-push-ebook +25 -0
  7. data/bin/asciidoctor-epub3 +15 -0
  8. data/data/fonts/assorted-icons.ttf +0 -0
  9. data/data/fonts/fontawesome-icons.ttf +0 -0
  10. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  11. data/data/fonts/mplus1mn-bolditalic-ascii.ttf +0 -0
  12. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  13. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  14. data/data/fonts/mplus1p-bold-latin-cyrillic.ttf +0 -0
  15. data/data/fonts/mplus1p-bold-latin-ext.ttf +0 -0
  16. data/data/fonts/mplus1p-bold-latin.ttf +0 -0
  17. data/data/fonts/mplus1p-bold-multilingual.ttf +0 -0
  18. data/data/fonts/mplus1p-light-latin-cyrillic.ttf +0 -0
  19. data/data/fonts/mplus1p-light-latin-ext.ttf +0 -0
  20. data/data/fonts/mplus1p-light-latin.ttf +0 -0
  21. data/data/fonts/mplus1p-light-multilingual.ttf +0 -0
  22. data/data/fonts/mplus1p-regular-latin-cyrillic.ttf +0 -0
  23. data/data/fonts/mplus1p-regular-latin-ext.ttf +0 -0
  24. data/data/fonts/mplus1p-regular-latin.ttf +0 -0
  25. data/data/fonts/mplus1p-regular-multilingual.ttf +0 -0
  26. data/data/fonts/notoserif-bold-latin-cyrillic.ttf +0 -0
  27. data/data/fonts/notoserif-bold-latin-ext.ttf +0 -0
  28. data/data/fonts/notoserif-bold-latin.ttf +0 -0
  29. data/data/fonts/notoserif-bold-multilingual.ttf +0 -0
  30. data/data/fonts/notoserif-bolditalic-latin-cyrillic.ttf +0 -0
  31. data/data/fonts/notoserif-bolditalic-latin-ext.ttf +0 -0
  32. data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
  33. data/data/fonts/notoserif-bolditalic-multilingual.ttf +0 -0
  34. data/data/fonts/notoserif-italic-latin-cyrillic.ttf +0 -0
  35. data/data/fonts/notoserif-italic-latin-ext.ttf +0 -0
  36. data/data/fonts/notoserif-italic-latin.ttf +0 -0
  37. data/data/fonts/notoserif-italic-multilingual.ttf +0 -0
  38. data/data/fonts/notoserif-regular-latin-cyrillic.ttf +0 -0
  39. data/data/fonts/notoserif-regular-latin-ext.ttf +0 -0
  40. data/data/fonts/notoserif-regular-latin.ttf +0 -0
  41. data/data/fonts/notoserif-regular-multilingual.ttf +0 -0
  42. data/data/images/default-avatar.jpg +0 -0
  43. data/data/images/default-avatar.png +0 -0
  44. data/data/images/default-avatar.svg +67 -0
  45. data/data/images/default-cover-large.png +0 -0
  46. data/data/images/default-cover.png +0 -0
  47. data/data/images/default-cover.svg +53 -0
  48. data/data/images/default-headshot.jpg +0 -0
  49. data/data/images/default-headshot.png +0 -0
  50. data/data/samples/asciidoctor-epub3-readme.adoc +744 -0
  51. data/data/samples/asciidoctor-js-extension.adoc +46 -0
  52. data/data/samples/asciidoctor-js-introduction.adoc +91 -0
  53. data/data/samples/i18n.adoc +161 -0
  54. data/data/samples/images/asciidoctor-js-chrome-extension.png +0 -0
  55. data/data/samples/images/avatars/graphitefriction.png +0 -0
  56. data/data/samples/images/avatars/mogztter.png +0 -0
  57. data/data/samples/images/avatars/mojavelinux.png +0 -0
  58. data/data/samples/images/correct-text-justification.png +0 -0
  59. data/data/samples/images/incorrect-text-justification.png +0 -0
  60. data/data/samples/images/screenshots/chapter-title-day.png +0 -0
  61. data/data/samples/images/screenshots/chapter-title.png +0 -0
  62. data/data/samples/images/screenshots/figure-admonition.png +0 -0
  63. data/data/samples/images/screenshots/section-title-paragraph.png +0 -0
  64. data/data/samples/images/screenshots/sidebar.png +0 -0
  65. data/data/samples/images/screenshots/table.png +0 -0
  66. data/data/samples/images/screenshots/text.png +0 -0
  67. data/data/samples/sample-book.adoc +20 -0
  68. data/data/samples/sample-content.adoc +168 -0
  69. data/data/styles/color-palette.css +28 -0
  70. data/data/styles/epub3-css3-only.css +161 -0
  71. data/data/styles/epub3-fonts.css +94 -0
  72. data/data/styles/epub3.css +1293 -0
  73. data/lib/asciidoctor-epub3.rb +5 -0
  74. data/lib/asciidoctor-epub3/converter.rb +859 -0
  75. data/lib/asciidoctor-epub3/core_ext/string.rb +7 -0
  76. data/lib/asciidoctor-epub3/font_icon_map.rb +376 -0
  77. data/lib/asciidoctor-epub3/packager.rb +466 -0
  78. data/lib/asciidoctor-epub3/spine_item_processor.rb +72 -0
  79. data/lib/asciidoctor-epub3/version.rb +5 -0
  80. data/scripts/generate-font-subsets.pe +225 -0
  81. metadata +192 -0
@@ -0,0 +1,7 @@
1
+ require 'stringio' unless defined? StringIO
2
+
3
+ class String
4
+ def to_ios
5
+ StringIO.new self
6
+ end unless String.respond_to? :to_ios
7
+ end
@@ -0,0 +1,376 @@
1
+ module Asciidoctor
2
+ module Epub3
3
+ # Map of Font Awesome icon names to unicode characters
4
+ FontIconMap = {
5
+ glass: '\f000',
6
+ music: '\f001',
7
+ search: '\f002',
8
+ envelope_o: '\f003',
9
+ heart: '\f004',
10
+ star: '\f005',
11
+ star_o: '\f006',
12
+ user: '\f007',
13
+ film: '\f008',
14
+ th_large: '\f009',
15
+ th: '\f00a',
16
+ th_list: '\f00b',
17
+ check: '\f00c',
18
+ times: '\f00d',
19
+ search_plus: '\f00e',
20
+ search_minus: '\f010',
21
+ power_off: '\f011',
22
+ signal: '\f012',
23
+ cog: '\f013',
24
+ trash_o: '\f014',
25
+ home: '\f015',
26
+ file_o: '\f016',
27
+ clock_o: '\f017',
28
+ road: '\f018',
29
+ download: '\f019',
30
+ arrow_circle_o_down: '\f01a',
31
+ arrow_circle_o_up: '\f01b',
32
+ inbox: '\f01c',
33
+ play_circle_o: '\f01d',
34
+ repeat: '\f01e',
35
+ refresh: '\f021',
36
+ list_alt: '\f022',
37
+ lock: '\f023',
38
+ flag: '\f024',
39
+ headphones: '\f025',
40
+ volume_off: '\f026',
41
+ volume_down: '\f027',
42
+ volume_up: '\f028',
43
+ qrcode: '\f029',
44
+ barcode: '\f02a',
45
+ tag: '\f02b',
46
+ tags: '\f02c',
47
+ book: '\f02d',
48
+ bookmark: '\f02e',
49
+ print: '\f02f',
50
+ camera: '\f030',
51
+ font: '\f031',
52
+ bold: '\f032',
53
+ italic: '\f033',
54
+ text_height: '\f034',
55
+ text_width: '\f035',
56
+ align_left: '\f036',
57
+ align_center: '\f037',
58
+ align_right: '\f038',
59
+ align_justify: '\f039',
60
+ list: '\f03a',
61
+ outdent: '\f03b',
62
+ indent: '\f03c',
63
+ video_camera: '\f03d',
64
+ picture_o: '\f03e',
65
+ pencil: '\f040',
66
+ map_marker: '\f041',
67
+ adjust: '\f042',
68
+ tint: '\f043',
69
+ pencil_square_o: '\f044',
70
+ share_square_o: '\f045',
71
+ check_square_o: '\f046',
72
+ arrows: '\f047',
73
+ step_backward: '\f048',
74
+ fast_backward: '\f049',
75
+ backward: '\f04a',
76
+ play: '\f04b',
77
+ pause: '\f04c',
78
+ stop: '\f04d',
79
+ forward: '\f04e',
80
+ fast_forward: '\f050',
81
+ step_forward: '\f051',
82
+ eject: '\f052',
83
+ chevron_left: '\f053',
84
+ chevron_right: '\f054',
85
+ plus_circle: '\f055',
86
+ minus_circle: '\f056',
87
+ times_circle: '\f057',
88
+ check_circle: '\f058',
89
+ question_circle: '\f059',
90
+ info_circle: '\f05a',
91
+ crosshairs: '\f05b',
92
+ times_circle_o: '\f05c',
93
+ check_circle_o: '\f05d',
94
+ ban: '\f05e',
95
+ arrow_left: '\f060',
96
+ arrow_right: '\f061',
97
+ arrow_up: '\f062',
98
+ arrow_down: '\f063',
99
+ share: '\f064',
100
+ expand: '\f065',
101
+ compress: '\f066',
102
+ plus: '\f067',
103
+ minus: '\f068',
104
+ asterisk: '\f069',
105
+ exclamation_circle: '\f06a',
106
+ gift: '\f06b',
107
+ leaf: '\f06c',
108
+ fire: '\f06d',
109
+ eye: '\f06e',
110
+ eye_slash: '\f070',
111
+ exclamation_triangle: '\f071',
112
+ plane: '\f072',
113
+ calendar: '\f073',
114
+ random: '\f074',
115
+ comment: '\f075',
116
+ magnet: '\f076',
117
+ chevron_up: '\f077',
118
+ chevron_down: '\f078',
119
+ retweet: '\f079',
120
+ shopping_cart: '\f07a',
121
+ folder: '\f07b',
122
+ folder_open: '\f07c',
123
+ arrows_v: '\f07d',
124
+ arrows_h: '\f07e',
125
+ bar_chart_o: '\f080',
126
+ twitter_square: '\f081',
127
+ facebook_square: '\f082',
128
+ camera_retro: '\f083',
129
+ key: '\f084',
130
+ cogs: '\f085',
131
+ comments: '\f086',
132
+ thumbs_o_up: '\f087',
133
+ thumbs_o_down: '\f088',
134
+ star_half: '\f089',
135
+ heart_o: '\f08a',
136
+ sign_out: '\f08b',
137
+ linkedin_square: '\f08c',
138
+ thumb_tack: '\f08d',
139
+ external_link: '\f08e',
140
+ sign_in: '\f090',
141
+ trophy: '\f091',
142
+ github_square: '\f092',
143
+ upload: '\f093',
144
+ lemon_o: '\f094',
145
+ phone: '\f095',
146
+ square_o: '\f096',
147
+ bookmark_o: '\f097',
148
+ phone_square: '\f098',
149
+ twitter: '\f099',
150
+ facebook: '\f09a',
151
+ github: '\f09b',
152
+ unlock: '\f09c',
153
+ credit_card: '\f09d',
154
+ rss: '\f09e',
155
+ hdd_o: '\f0a0',
156
+ bullhorn: '\f0a1',
157
+ bell: '\f0f3',
158
+ certificate: '\f0a3',
159
+ hand_o_right: '\f0a4',
160
+ hand_o_left: '\f0a5',
161
+ hand_o_up: '\f0a6',
162
+ hand_o_down: '\f0a7',
163
+ arrow_circle_left: '\f0a8',
164
+ arrow_circle_right: '\f0a9',
165
+ arrow_circle_up: '\f0aa',
166
+ arrow_circle_down: '\f0ab',
167
+ globe: '\f0ac',
168
+ wrench: '\f0ad',
169
+ tasks: '\f0ae',
170
+ filter: '\f0b0',
171
+ briefcase: '\f0b1',
172
+ arrows_alt: '\f0b2',
173
+ users: '\f0c0',
174
+ link: '\f0c1',
175
+ cloud: '\f0c2',
176
+ flask: '\f0c3',
177
+ scissors: '\f0c4',
178
+ files_o: '\f0c5',
179
+ paperclip: '\f0c6',
180
+ floppy_o: '\f0c7',
181
+ square: '\f0c8',
182
+ bars: '\f0c9',
183
+ list_ul: '\f0ca',
184
+ list_ol: '\f0cb',
185
+ strikethrough: '\f0cc',
186
+ underline: '\f0cd',
187
+ table: '\f0ce',
188
+ magic: '\f0d0',
189
+ truck: '\f0d1',
190
+ pinterest: '\f0d2',
191
+ pinterest_square: '\f0d3',
192
+ google_plus_square: '\f0d4',
193
+ google_plus: '\f0d5',
194
+ money: '\f0d6',
195
+ caret_down: '\f0d7',
196
+ caret_up: '\f0d8',
197
+ caret_left: '\f0d9',
198
+ caret_right: '\f0da',
199
+ columns: '\f0db',
200
+ sort: '\f0dc',
201
+ sort_asc: '\f0dd',
202
+ sort_desc: '\f0de',
203
+ envelope: '\f0e0',
204
+ linkedin: '\f0e1',
205
+ undo: '\f0e2',
206
+ gavel: '\f0e3',
207
+ tachometer: '\f0e4',
208
+ comment_o: '\f0e5',
209
+ comments_o: '\f0e6',
210
+ bolt: '\f0e7',
211
+ sitemap: '\f0e8',
212
+ umbrella: '\f0e9',
213
+ clipboard: '\f0ea',
214
+ lightbulb_o: '\f0eb',
215
+ exchange: '\f0ec',
216
+ cloud_download: '\f0ed',
217
+ cloud_upload: '\f0ee',
218
+ user_md: '\f0f0',
219
+ stethoscope: '\f0f1',
220
+ suitcase: '\f0f2',
221
+ bell_o: '\f0a2',
222
+ coffee: '\f0f4',
223
+ cutlery: '\f0f5',
224
+ file_text_o: '\f0f6',
225
+ building_o: '\f0f7',
226
+ hospital_o: '\f0f8',
227
+ ambulance: '\f0f9',
228
+ medkit: '\f0fa',
229
+ fighter_jet: '\f0fb',
230
+ beer: '\f0fc',
231
+ h_square: '\f0fd',
232
+ plus_square: '\f0fe',
233
+ angle_double_left: '\f100',
234
+ angle_double_right: '\f101',
235
+ angle_double_up: '\f102',
236
+ angle_double_down: '\f103',
237
+ angle_left: '\f104',
238
+ angle_right: '\f105',
239
+ angle_up: '\f106',
240
+ angle_down: '\f107',
241
+ desktop: '\f108',
242
+ laptop: '\f109',
243
+ tablet: '\f10a',
244
+ mobile: '\f10b',
245
+ circle_o: '\f10c',
246
+ quote_left: '\f10d',
247
+ quote_right: '\f10e',
248
+ spinner: '\f110',
249
+ circle: '\f111',
250
+ reply: '\f112',
251
+ github_alt: '\f113',
252
+ folder_o: '\f114',
253
+ folder_open_o: '\f115',
254
+ smile_o: '\f118',
255
+ frown_o: '\f119',
256
+ meh_o: '\f11a',
257
+ gamepad: '\f11b',
258
+ keyboard_o: '\f11c',
259
+ flag_o: '\f11d',
260
+ flag_checkered: '\f11e',
261
+ terminal: '\f120',
262
+ code: '\f121',
263
+ reply_all: '\f122',
264
+ mail_reply_all: '\f122',
265
+ star_half_o: '\f123',
266
+ location_arrow: '\f124',
267
+ crop: '\f125',
268
+ code_fork: '\f126',
269
+ chain_broken: '\f127',
270
+ question: '\f128',
271
+ info: '\f129',
272
+ exclamation: '\f12a',
273
+ superscript: '\f12b',
274
+ subscript: '\f12c',
275
+ eraser: '\f12d',
276
+ puzzle_piece: '\f12e',
277
+ microphone: '\f130',
278
+ microphone_slash: '\f131',
279
+ shield: '\f132',
280
+ calendar_o: '\f133',
281
+ fire_extinguisher: '\f134',
282
+ rocket: '\f135',
283
+ maxcdn: '\f136',
284
+ chevron_circle_left: '\f137',
285
+ chevron_circle_right: '\f138',
286
+ chevron_circle_up: '\f139',
287
+ chevron_circle_down: '\f13a',
288
+ html5: '\f13b',
289
+ css3: '\f13c',
290
+ anchor: '\f13d',
291
+ unlock_alt: '\f13e',
292
+ bullseye: '\f140',
293
+ ellipsis_h: '\f141',
294
+ ellipsis_v: '\f142',
295
+ rss_square: '\f143',
296
+ play_circle: '\f144',
297
+ ticket: '\f145',
298
+ minus_square: '\f146',
299
+ minus_square_o: '\f147',
300
+ level_up: '\f148',
301
+ level_down: '\f149',
302
+ check_square: '\f14a',
303
+ pencil_square: '\f14b',
304
+ external_link_square: '\f14c',
305
+ share_square: '\f14d',
306
+ compass: '\f14e',
307
+ caret_square_o_down: '\f150',
308
+ caret_square_o_up: '\f151',
309
+ caret_square_o_right: '\f152',
310
+ eur: '\f153',
311
+ gbp: '\f154',
312
+ usd: '\f155',
313
+ inr: '\f156',
314
+ jpy: '\f157',
315
+ rub: '\f158',
316
+ krw: '\f159',
317
+ btc: '\f15a',
318
+ file: '\f15b',
319
+ file_text: '\f15c',
320
+ sort_alpha_asc: '\f15d',
321
+ sort_alpha_desc: '\f15e',
322
+ sort_amount_asc: '\f160',
323
+ sort_amount_desc: '\f161',
324
+ sort_numeric_asc: '\f162',
325
+ sort_numeric_desc: '\f163',
326
+ thumbs_up: '\f164',
327
+ thumbs_down: '\f165',
328
+ youtube_square: '\f166',
329
+ youtube: '\f167',
330
+ xing: '\f168',
331
+ xing_square: '\f169',
332
+ youtube_play: '\f16a',
333
+ dropbox: '\f16b',
334
+ stack_overflow: '\f16c',
335
+ instagram: '\f16d',
336
+ flickr: '\f16e',
337
+ adn: '\f170',
338
+ bitbucket: '\f171',
339
+ bitbucket_square: '\f172',
340
+ tumblr: '\f173',
341
+ tumblr_square: '\f174',
342
+ long_arrow_down: '\f175',
343
+ long_arrow_up: '\f176',
344
+ long_arrow_left: '\f177',
345
+ long_arrow_right: '\f178',
346
+ apple: '\f179',
347
+ windows: '\f17a',
348
+ android: '\f17b',
349
+ linux: '\f17c',
350
+ dribbble: '\f17d',
351
+ skype: '\f17e',
352
+ foursquare: '\f180',
353
+ trello: '\f181',
354
+ female: '\f182',
355
+ male: '\f183',
356
+ gittip: '\f184',
357
+ sun_o: '\f185',
358
+ moon_o: '\f186',
359
+ archive: '\f187',
360
+ bug: '\f188',
361
+ vk: '\f189',
362
+ weibo: '\f18a',
363
+ renren: '\f18b',
364
+ pagelines: '\f18c',
365
+ stack_exchange: '\f18d',
366
+ arrow_circle_o_right: '\f18e',
367
+ arrow_circle_o_left: '\f190',
368
+ caret_square_o_left: '\f191',
369
+ dot_circle_o: '\f192',
370
+ wheelchair: '\f193',
371
+ vimeo_square: '\f194',
372
+ try: '\f195',
373
+ plus_square_o: '\f196'
374
+ }
375
+ end
376
+ end
@@ -0,0 +1,466 @@
1
+ require_relative 'core_ext/string'
2
+ autoload :FileUtils, 'fileutils'
3
+ autoload :Open3, 'open3'
4
+ autoload :Shellwords, 'shellwords'
5
+
6
+ module Asciidoctor
7
+ module Epub3
8
+ module GepubBuilderMixin
9
+ DATA_DIR = ::File.expand_path(::File.join ::File.dirname(__FILE__), '..', '..', 'data')
10
+ SAMPLES_DIR = ::File.join DATA_DIR, 'samples'
11
+ WordJoiner = Epub3::WordJoiner
12
+ FromHtmlSpecialCharsMap = ContentConverter::FromHtmlSpecialCharsMap
13
+ FromHtmlSpecialCharsRx = ContentConverter::FromHtmlSpecialCharsRx
14
+ CsvDelimiterRx = /\s*,\s*/
15
+ DefaultCoverImage = 'images/default-cover.png'
16
+ InlineImageMacroRx = /^image:(.*?)\[(.*?)\]$/
17
+
18
+ def sanitized_doctitle doc, target = :plain
19
+ return (doc.attr 'untitled-label') unless doc.header?
20
+ title = case target
21
+ when :attribute_cdata
22
+ doc.doctitle(sanitize: true).gsub('"', '"')
23
+ when :element_cdata
24
+ doc.doctitle sanitize: true
25
+ when :pcdata
26
+ doc.doctitle
27
+ when :plain
28
+ doc.doctitle(sanitize: true).gsub(FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap)
29
+ end
30
+ title.gsub WordJoiner, ''
31
+ end
32
+
33
+ def add_theme_assets doc
34
+ builder = self
35
+ format = @format
36
+ workdir = if doc.attr? 'epub3-stylesdir'
37
+ ::File.join doc.attr('docdir'), doc.attr('epub3-stylesdir')
38
+ else
39
+ ::File.join DATA_DIR, 'styles'
40
+ end
41
+
42
+ # TODO improve design/UX of custom theme functionality, including custom fonts
43
+ resources workdir: workdir do
44
+ file 'styles/epub3.css' => (builder.postprocess_css_file 'epub3.css', format)
45
+ file 'styles/epub3-css3-only.css' => (builder.postprocess_css_file 'epub3-css3-only.css', format)
46
+ end
47
+
48
+ resources workdir: DATA_DIR do
49
+ #file 'styles/epub3.css' => (builder.postprocess_css_file 'styles/epub3.css', format)
50
+ #file 'styles/epub3-css3-only.css' => (builder.postprocess_css_file 'styles/epub3-css3-only.css', format)
51
+ font_list, font_css = builder.select_fonts 'styles/epub3-fonts.css', (doc.attr 'scripts', 'latin')
52
+ file 'styles/epub3-fonts.css' => font_css
53
+ with_media_type 'application/x-font-ttf' do
54
+ files(*font_list)
55
+ end
56
+ end
57
+ end
58
+
59
+ def add_cover_image doc
60
+ imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
61
+ imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
62
+
63
+ if (front_cover_image = doc.attr 'front-cover-image')
64
+ if front_cover_image =~ InlineImageMacroRx
65
+ front_cover_image = %(#{imagesdir}#{$1})
66
+ end
67
+ workdir = doc.attr 'docdir'
68
+ else
69
+ front_cover_image = DefaultCoverImage
70
+ workdir = DATA_DIR
71
+ end
72
+
73
+ resources workdir: workdir do
74
+ cover_image %(#{imagesdir}jacket/cover#{::File.extname front_cover_image}) => front_cover_image
75
+ end
76
+ end
77
+
78
+ # NOTE must be called within the ordered block
79
+ def add_cover_page doc, spine_builder, book
80
+ imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
81
+ imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
82
+
83
+ img = (doc.attr 'front-cover-image') || DefaultCoverImage
84
+
85
+ if img =~ InlineImageMacroRx
86
+ img = %(#{imagesdir}#{$1})
87
+ # TODO use proper attribute parser
88
+ _, w, h = $2.split ',', 3
89
+ end
90
+
91
+ w ||= 1050
92
+ h ||= 1600
93
+ img_path = %(#{imagesdir}jacket/cover#{::File.extname img})
94
+ # NOTE SVG wrapper maintains aspect ratio and confines image to view box
95
+ content = %(<!DOCTYPE html>
96
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
97
+ <head>
98
+ <meta charset="UTF-8"/>
99
+ <title>#{sanitized_doctitle doc, :element_cdata}</title>
100
+ <style type="text/css">
101
+ @page {
102
+ margin: 0;
103
+ }
104
+ html {
105
+ margin: 0 !important;
106
+ padding: 0 !important;
107
+ }
108
+ body {
109
+ margin: 0;
110
+ padding: 0 !important;
111
+ text-align: center;
112
+ }
113
+ body > svg {
114
+ /* prevent bleed onto second page (removes descender space) */
115
+ display: block;
116
+ }
117
+ </style>
118
+ </head>
119
+ <body epub:type="cover"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
120
+ width="100%" height="100%" viewBox="0 0 #{w} #{h}" preserveAspectRatio="xMidYMid meet">
121
+ <image width="#{w}" height="#{h}" xlink:href="#{img_path}"/>
122
+ </svg></body>
123
+ </html>).to_ios
124
+ # GitDen expects a cover.xhtml, so add it to the spine
125
+ spine_builder.file 'cover.xhtml' => content
126
+ spine_builder.id 'cover'
127
+ # clearly a deficiency of gepub that it does not match the id correctly
128
+ book.spine.itemref_by_id['item_cover1'].idref = 'cover'
129
+ end
130
+
131
+ def add_images_from_front_matter
132
+ if ::File.exist? 'front-matter.html'
133
+ ::File.read('front-matter.html').scan(/<img src="(.+?)"/) do
134
+ resources do
135
+ file $1
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def add_front_matter_page doc, spine_builder, builder, format
142
+ if ::File.exist? 'front-matter.html'
143
+ spine_builder.file 'front-matter.html' => (builder.postprocess_xhtml_file 'front-matter.html', format)
144
+ (spine_builder.instance_variable_get :@last_defined_item).properties << 'svg'
145
+ end
146
+ end
147
+
148
+ # FIXME don't add same image more than once
149
+ # FIXME add inline images
150
+ def add_content_images doc, images
151
+ docimagesdir = (doc.attr 'imagesdir', '.').chomp '/'
152
+ docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
153
+
154
+ resources(workdir: (doc.attr 'docdir')) do
155
+ images.each do |image|
156
+ imagesdir = (image.document.attr 'imagesdir', '.').chomp '/'
157
+ imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
158
+ image_path = %(#{imagesdir}#{image.attr 'target'})
159
+ if image_path.start_with? %(#{docimagesdir}jacket/cover.)
160
+ warn %(The image path #{image_path} is reserved for the cover artwork. Ignoring conflicting image from content.)
161
+ elsif ::File.readable? image_path
162
+ file image_path
163
+ else
164
+ warn %(Image not found or not readable: #{image_path})
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ def add_profile_images doc, usernames
171
+ spine = @spine
172
+ imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
173
+ imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
174
+
175
+ resources workdir: DATA_DIR do
176
+ file %(#{imagesdir}avatars/default.jpg) => %(images/default-avatar.jpg)
177
+ file %(#{imagesdir}headshots/default.jpg) => %(images/default-headshot.jpg)
178
+ end
179
+
180
+ resources(workdir: (doc.attr 'docdir')) do
181
+ usernames.each do |username|
182
+ if ::File.readable?(avatar = %(#{imagesdir}avatars/#{username}.jpg))
183
+ file avatar
184
+ else
185
+ warn %(Avatar #{avatar} not found or readable. Falling back to default avatar for #{username}.)
186
+ ::Dir.chdir DATA_DIR do
187
+ file avatar => %(images/default-avatar.jpg)
188
+ end
189
+ end
190
+
191
+ if ::File.readable? (headshot = %(#{imagesdir}headshots/#{username}.jpg))
192
+ file headshot
193
+ else
194
+ warn %(Headshot #{headshot} not found or readable. Falling back to default headshot for #{username}.)
195
+ ::Dir.chdir DATA_DIR do
196
+ file headshot => %(images/default-headshot.jpg)
197
+ end
198
+ end
199
+ end
200
+ =begin
201
+ spine.each do |item|
202
+ username = (item.attr 'username') || 'default'
203
+ avatar_target = %(#{imagesdir}avatars/#{username}.jpg)
204
+ if ::File.readable?(avatar = %(#{item.attr 'docname'}/avatar.jpg))
205
+ file avatar_target => avatar
206
+ else
207
+ warn %(Avatar #{avatar} not found or not readable. Falling back to default avatar for #{username}.)
208
+ ::Dir.chdir DATA_DIR do
209
+ file avatar_target => %(images/default-avatar.jpg)
210
+ end
211
+ end
212
+ if ::File.readable? (headshot = %(#{item.attr 'docname'}/headshot.jpg))
213
+ file headshot
214
+ # TODO default headshot?
215
+ end
216
+ end
217
+ =end
218
+ end
219
+ end
220
+
221
+ def add_content doc
222
+ builder = self
223
+ spine = @spine
224
+ format = @format
225
+ resources(workdir: (doc.attr 'docdir')) do
226
+ builder.add_images_from_front_matter
227
+ # QUESTION should we move navigation_document to the Packager class? seems to make sense
228
+ #nav 'nav.xhtml' => (builder.postprocess_xhtml doc.converter.navigation_document(doc, spine), format)
229
+ nav 'nav.xhtml' => (builder.postprocess_xhtml ::Asciidoctor::Converter::Factory.default.create('epub3-xhtml5').navigation_document(doc, spine), format)
230
+ ordered do
231
+ builder.add_cover_page doc, self, @book unless format == :kf8
232
+ builder.add_front_matter_page doc, self, builder, format
233
+ spine.each_with_index do |item, i|
234
+ content_path = %(#{item.id || (item.attr 'docname')}.xhtml)
235
+ file content_path => (builder.postprocess_xhtml item.convert, format)
236
+ # NOTE heading for ePub2 navigation file; toc.ncx requires headings to be plain text
237
+ heading builder.sanitized_doctitle(item)
238
+ @last_defined_item.properties << 'svg' if ((item.attr 'epub-properties') || []).include? 'svg'
239
+ #linear 'yes' if i == 0
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ def collect_keywords doc, spine
246
+ ([doc] + spine).map do |item|
247
+ if item.attr? 'keywords'
248
+ (item.attr 'keywords').split CsvDelimiterRx
249
+ else
250
+ []
251
+ end
252
+ end.flatten.uniq
253
+ end
254
+
255
+ # Swap fonts in CSS based on the value of the document attribute 'scripts',
256
+ # then return the list of fonts as well as the font CSS.
257
+ def select_fonts filename, scripts = 'latin'
258
+ font_css = ::File.read(filename)
259
+ font_css = font_css.gsub(/(?<=-)latin(?=\.ttf\))/, scripts) unless scripts == 'latin'
260
+ font_list = font_css.scan(/url\(\.\.\/(.+\.ttf)\);$/).flatten
261
+ return [font_list, font_css.to_ios]
262
+ end
263
+
264
+ def postprocess_css_file filename, format
265
+ return filename unless format == :kf8
266
+ postprocess_css ::File.read(filename), format
267
+ end
268
+
269
+ def postprocess_css content, format
270
+ return content.to_ios unless format == :kf8
271
+ # TODO convert regular expressions to constants
272
+ content
273
+ .gsub(/^ -webkit-column-break-.*\n/, '')
274
+ .gsub(/^ max-width: .*\n/, '')
275
+ .to_ios
276
+ end
277
+
278
+ def postprocess_xhtml_file filename, format
279
+ return filename unless format == :kf8
280
+ postprocess_xhtml ::File.read(filename), format
281
+ end
282
+
283
+ # NOTE Kindle requires that
284
+ # <meta charset="utf-8"/>
285
+ # be converted to
286
+ # <meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
287
+ def postprocess_xhtml content, format
288
+ return content.to_ios unless format == :kf8
289
+ # TODO convert regular expressions to constants
290
+ content
291
+ .gsub(/<meta charset="(.+?)"\/>/, '<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=\1"/>')
292
+ .gsub(/<img([^>]+) style="width: (\d\d)%;"/, '<img\1 style="width: \2%; height: \2%;"')
293
+ .gsub(/<script type="text\/javascript">.*?<\/script>\n?/m, '')
294
+ .to_ios
295
+ end
296
+ end
297
+
298
+ class Packager
299
+ KINDLEGEN = ENV['KINDLEGEN'] || 'kindlegen'
300
+ EPUBCHECK = ENV['EPUBCHECK'] || %(epubcheck#{::Gem.win_platform? ? '.bat' : '.sh'})
301
+ EpubExtensionRx = /\.epub$/
302
+ Kf8ExtensionRx = /-kf8\.epub$/
303
+
304
+ def initialize spine_doc, spine, format = :epub3, options = {}
305
+ @document = spine_doc
306
+ @spine = spine || []
307
+ @format = format
308
+ end
309
+
310
+ def package options = {}
311
+ doc = @document
312
+ spine = @spine
313
+ fmt = @format
314
+ dest = @dest_dir = options[:to_dir] || (File.expand_path ::Dir.pwd)
315
+
316
+ images = spine.map {|item| (item.find_by context: :image) || [] }.flatten
317
+ usernames = spine.map {|item| item.attr 'username' }.compact.uniq
318
+ # FIXME authors should be aggregated already on parent document
319
+ authors = if doc.attr? 'authors'
320
+ (doc.attr 'authors').split(GepubBuilderMixin::CsvDelimiterRx).concat(spine.map {|item| item.attr 'author' }).uniq
321
+ else
322
+ []
323
+ end
324
+
325
+ builder = ::GEPUB::Builder.new do
326
+ extend GepubBuilderMixin
327
+ @document = doc
328
+ @spine = spine
329
+ @format = fmt
330
+ @book.epub_backward_compat = (fmt != :kf8)
331
+
332
+ language(doc.attr 'lang', 'en')
333
+ id 'pub-language'
334
+
335
+ if doc.attr? 'uuid'
336
+ unique_identifier doc.attr('uuid'), 'pub-identifier', 'uuid'
337
+ else
338
+ unique_identifier doc.id, 'pub-identifier', 'uuid'
339
+ end
340
+ # replace with next line once the attributes argument is supported
341
+ #unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
342
+
343
+ title sanitized_doctitle(doc)
344
+ id 'pub-title'
345
+
346
+ # FIXME this logic needs some work
347
+ if doc.attr? 'publisher'
348
+ publisher(publisher_name = doc.attr('publisher'))
349
+ # marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
350
+ creator doc.attr('producer', publisher_name), 'bkp'
351
+ else
352
+ # NOTE Use producer as both publisher and producer if publisher isn't specified
353
+ if doc.attr? 'producer'
354
+ producer_name = doc.attr 'producer'
355
+ publisher producer_name
356
+ # marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
357
+ creator producer_name, 'bkp'
358
+ # NOTE Use author as creator if both publisher or producer are absent
359
+ elsif doc.attr? 'author'
360
+ # marc role: Author (see http://www.loc.gov/marc/relators/relaterm.html)
361
+ creator doc.attr('author'), 'aut'
362
+ end
363
+ end
364
+
365
+ if doc.attr? 'creator'
366
+ # marc role: Creator (see http://www.loc.gov/marc/relators/relaterm.html)
367
+ creator doc.attr('creator'), 'cre'
368
+ else
369
+ # marc role: Manufacturer (see http://www.loc.gov/marc/relators/relaterm.html)
370
+ # QUESTION should this be bkp?
371
+ creator 'Asciidoctor', 'mfr'
372
+ end
373
+
374
+ # TODO getting author list should be a method on Asciidoctor API
375
+ contributors(*authors)
376
+
377
+ if doc.attr? 'revdate'
378
+ # TODO ensure this is a real date
379
+ date(doc.attr 'revdate')
380
+ else
381
+ date ::Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
382
+ end
383
+
384
+ if doc.attr? 'description'
385
+ description(doc.attr 'description')
386
+ end
387
+
388
+ (collect_keywords doc, spine).each do |s|
389
+ subject s
390
+ end
391
+
392
+ if doc.attr? 'source'
393
+ source(doc.attr 'source')
394
+ end
395
+
396
+ if doc.attr? 'copyright'
397
+ rights(doc.attr 'copyright')
398
+ end
399
+
400
+ #add_metadata 'ibooks:specified-fonts', true
401
+
402
+ add_theme_assets doc
403
+ add_cover_image doc
404
+ add_profile_images doc, usernames
405
+ # QUESTION move add_content_images to add_content method?
406
+ add_content_images doc, images
407
+ add_content doc
408
+ end
409
+
410
+ ::FileUtils.mkdir_p dest unless ::File.directory? dest
411
+
412
+ epub_file = ::File.expand_path %(#{doc.attr 'docname'}#{fmt == :kf8 ? '-kf8' : nil}.epub), dest
413
+ builder.generate_epub epub_file
414
+ puts %(Wrote #{fmt.upcase} to #{epub_file})
415
+ if options[:extract]
416
+ extract_dir = epub_file.sub EpubExtensionRx, ''
417
+ ::FileUtils.remove_dir extract_dir if ::File.directory? extract_dir
418
+ ::Dir.mkdir extract_dir
419
+ ::Dir.chdir extract_dir do
420
+ ::Zip::File.open epub_file do |entries|
421
+ entries.each do |entry|
422
+ next unless entry.file?
423
+ unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
424
+ ::FileUtils.mkdir_p entry_dir
425
+ end
426
+ entry.extract
427
+ end
428
+ end
429
+ end
430
+ puts %(Extracted #{fmt.upcase} to #{extract_dir})
431
+ end
432
+
433
+ if fmt == :kf8
434
+ distill_epub_to_mobi epub_file
435
+ elsif options[:validate]
436
+ validate_epub epub_file
437
+ end
438
+ end
439
+
440
+ # QUESTION how to enable the -c2 flag? (enables ~3-5% compression)
441
+ def distill_epub_to_mobi epub_file
442
+ kindlegen_cmd = KINDLEGEN
443
+ unless ::File.executable? kindlegen_cmd
444
+ require 'kindlegen' unless defined? ::Kindlegen
445
+ kindlegen_cmd = ::Kindlegen.command
446
+ end
447
+ mobi_file = ::File.basename(epub_file).sub Kf8ExtensionRx, '.mobi'
448
+ ::Open3.popen2e(::Shellwords.join [kindlegen_cmd, '-o', mobi_file, epub_file]) {|input, output, wait_thr|
449
+ output.each {|line| puts line }
450
+ }
451
+ puts %(Wrote MOBI to #{::File.join ::File.dirname(epub_file), mobi_file})
452
+ end
453
+
454
+ def validate_epub epub_file
455
+ epubcheck_cmd = EPUBCHECK
456
+ unless ::File.executable? epubcheck_cmd
457
+ epubcheck_cmd = ::Gem.bin_path 'epubcheck', 'epubcheck'
458
+ end
459
+ # NOTE epubcheck gem doesn't support epubcheck command options; enable -quiet once supported
460
+ ::Open3.popen2e(::Shellwords.join [epubcheck_cmd, epub_file]) {|input, output, wait_thr|
461
+ output.each {|line| puts line }
462
+ }
463
+ end
464
+ end
465
+ end
466
+ end