asciidoctor-epub3 1.0.0.alpha.1

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.
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