asciidoctor-epub3 1.5.0.alpha.13 → 1.5.0.alpha.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,298 @@
1
+ area-chart:
2
+ name: chart-area
3
+ arrow-circle-o-down:
4
+ name: arrow-alt-circle-down
5
+ prefix: far
6
+ arrow-circle-o-left:
7
+ name: arrow-alt-circle-left
8
+ prefix: far
9
+ arrow-circle-o-right:
10
+ name: arrow-alt-circle-right
11
+ prefix: far
12
+ arrow-circle-o-up:
13
+ name: arrow-alt-circle-up
14
+ prefix: far
15
+ arrows:
16
+ name: arrows-alt
17
+ arrows-alt:
18
+ name: expand-arrows-alt
19
+ arrows-h:
20
+ name: arrows-alt-h
21
+ arrows-v:
22
+ name: arrows-alt-v
23
+ bar-chart:
24
+ name: chart-bar
25
+ prefix: far
26
+ bitbucket-square:
27
+ name: bitbucket
28
+ prefix: fab
29
+ calendar:
30
+ name: calendar-alt
31
+ calendar-o:
32
+ name: calendar
33
+ prefix: far
34
+ caret-square-o-down:
35
+ name: caret-square-down
36
+ prefix: far
37
+ caret-square-o-left:
38
+ name: caret-square-left
39
+ prefix: far
40
+ caret-square-o-right:
41
+ name: caret-square-right
42
+ prefix: far
43
+ caret-square-o-up:
44
+ name: caret-square-up
45
+ prefix: far
46
+ cc:
47
+ name: closed-captioning
48
+ prefix: far
49
+ chain-broken:
50
+ name: unlink
51
+ circle-o-notch:
52
+ name: circle-notch
53
+ circle-thin:
54
+ name: circle
55
+ prefix: far
56
+ clipboard:
57
+ prefix: far
58
+ clone:
59
+ prefix: far
60
+ cloud-download:
61
+ name: cloud-download-alt
62
+ cloud-upload:
63
+ name: cloud-upload-alt
64
+ code-fork:
65
+ name: code-branch
66
+ comment-alt:
67
+ name: comment-dots
68
+ prefix: far
69
+ commenting:
70
+ name: comment-dots
71
+ compass:
72
+ prefix: far
73
+ compress:
74
+ name: compress-alt
75
+ copyright:
76
+ prefix: far
77
+ creative-commons:
78
+ prefix: fab
79
+ credit-card:
80
+ prefix: far
81
+ credit-card-alt:
82
+ name: credit-card
83
+ cutlery:
84
+ name: utensils
85
+ diamond:
86
+ name: gem
87
+ prefix: far
88
+ eercast:
89
+ name: sellcast
90
+ prefix: fab
91
+ eur:
92
+ name: euro-sign
93
+ exchange:
94
+ name: exchange-alt
95
+ expand:
96
+ name: expand-alt
97
+ external-link:
98
+ name: external-link-alt
99
+ external-link-square:
100
+ name: external-link-square-alt
101
+ eye:
102
+ prefix: far
103
+ eye-dropper:
104
+ name: eye-dropper
105
+ prefix: far
106
+ eye-slash:
107
+ prefix: far
108
+ eyedropper:
109
+ name: eye-dropper
110
+ facebook:
111
+ name: facebook-f
112
+ prefix: fab
113
+ facebook-official:
114
+ name: facebook
115
+ prefix: fab
116
+ file-text:
117
+ name: file-alt
118
+ files-o:
119
+ name: copy
120
+ prefix: far
121
+ floppy-o:
122
+ name: save
123
+ prefix: far
124
+ gbp:
125
+ name: pound-sign
126
+ glass:
127
+ name: glass-martini
128
+ google-plus:
129
+ name: google-plus-g
130
+ prefix: fab
131
+ google-plus-circle:
132
+ name: google-plus
133
+ prefix: fab
134
+ google-plus-official:
135
+ name: google-plus
136
+ prefix: fab
137
+ hand-o-down:
138
+ name: hand-point-down
139
+ prefix: far
140
+ hand-o-left:
141
+ name: hand-point-left
142
+ prefix: far
143
+ hand-o-right:
144
+ name: hand-point-right
145
+ prefix: far
146
+ hand-o-up:
147
+ name: hand-point-up
148
+ prefix: far
149
+ header:
150
+ name: heading
151
+ id-badge:
152
+ prefix: far
153
+ ils:
154
+ name: shekel-sign
155
+ inr:
156
+ name: rupee-sign
157
+ intersex:
158
+ name: transgender
159
+ jpy:
160
+ name: yen-sign
161
+ krw:
162
+ name: won-sign
163
+ level-down:
164
+ name: level-down-alt
165
+ level-up:
166
+ name: level-up-alt
167
+ life-ring:
168
+ prefix: far
169
+ line-chart:
170
+ name: chart-line
171
+ linkedin:
172
+ name: linkedin-in
173
+ prefix: fab
174
+ linkedin-square:
175
+ name: linkedin
176
+ prefix: fab
177
+ list-alt:
178
+ prefix: far
179
+ long-arrow-down:
180
+ name: long-arrow-alt-down
181
+ long-arrow-left:
182
+ name: long-arrow-alt-left
183
+ long-arrow-right:
184
+ name: long-arrow-alt-right
185
+ long-arrow-up:
186
+ name: long-arrow-alt-up
187
+ map-marker:
188
+ name: map-marker-alt
189
+ meanpath:
190
+ name: font-awesome
191
+ prefix: fab
192
+ mobile:
193
+ name: mobile-alt
194
+ money:
195
+ name: money-bill-alt
196
+ prefix: far
197
+ object-group:
198
+ prefix: far
199
+ object-ungroup:
200
+ prefix: far
201
+ paste:
202
+ prefix: far
203
+ pencil:
204
+ name: pencil-alt
205
+ pencil-square:
206
+ name: pen-square
207
+ pencil-square-o:
208
+ name: edit
209
+ prefix: far
210
+ picture:
211
+ name: image
212
+ pie-chart:
213
+ name: chart-pie
214
+ refresh:
215
+ name: sync
216
+ registered:
217
+ prefix: far
218
+ repeat:
219
+ name: redo
220
+ rub:
221
+ name: ruble-sign
222
+ scissors:
223
+ name: cut
224
+ shield:
225
+ name: shield-alt
226
+ sign-in:
227
+ name: sign-in-alt
228
+ sign-out:
229
+ name: sign-out-alt
230
+ sliders:
231
+ name: sliders-h
232
+ sort-alpha-asc:
233
+ name: sort-alpha-down
234
+ sort-alpha-desc:
235
+ name: sort-alpha-down-alt
236
+ sort-amount-asc:
237
+ name: sort-amount-down
238
+ sort-amount-desc:
239
+ name: sort-amount-down-alt
240
+ sort-asc:
241
+ name: sort-up
242
+ sort-desc:
243
+ name: sort-down
244
+ sort-numeric-asc:
245
+ name: sort-numeric-down
246
+ sort-numeric-desc:
247
+ name: sort-numeric-down-alt
248
+ spoon:
249
+ name: utensil-spoon
250
+ star-half-empty:
251
+ name: star-half
252
+ star-half-full:
253
+ name: star-half
254
+ support:
255
+ name: life-ring
256
+ prefix: far
257
+ tablet:
258
+ name: tablet-alt
259
+ tachometer:
260
+ name: tachometer-alt
261
+ television:
262
+ name: tv
263
+ thumb-tack:
264
+ name: thumbtack
265
+ thumbs-o-down:
266
+ name: thumbs-down
267
+ prefix: far
268
+ thumbs-o-up:
269
+ name: thumbs-up
270
+ prefix: far
271
+ ticket:
272
+ name: ticket-alt
273
+ trash:
274
+ name: trash-alt
275
+ trash-o:
276
+ name: trash-alt
277
+ prefix: far
278
+ try:
279
+ name: lira-sign
280
+ usd:
281
+ name: dollar-sign
282
+ video-camera:
283
+ name: video
284
+ vimeo:
285
+ name: vimeo-v
286
+ prefix: fab
287
+ volume-control-phone:
288
+ name: phone-volume
289
+ wheelchair-alt:
290
+ name: accessible-icon
291
+ prefix: fab
292
+ window-maximize:
293
+ prefix: far
294
+ window-restore:
295
+ prefix: far
296
+ youtube-play:
297
+ name: youtube
298
+ prefix: fab
@@ -83,7 +83,7 @@
83
83
  font-family: "FontAwesome";
84
84
  font-style: normal;
85
85
  font-weight: normal;
86
- src: url(../fonts/fontawesome-icons.ttf);
86
+ src: url(../fonts/awesome/fa-solid-900.ttf);
87
87
  }
88
88
 
89
89
  @font-face {
@@ -1023,7 +1023,7 @@ aside[class~="admonition"] > div[class~="content"] {
1023
1023
  }
1024
1024
 
1025
1025
  aside.note::before {
1026
- content: "\f040"; /* fa-pencil */
1026
+ content: "\f303"; /* fa-pencil-alt */
1027
1027
  color: #FFC14F;
1028
1028
  }
1029
1029
 
@@ -1033,7 +1033,7 @@ aside[class~="note"] > div[class~="content"] {
1033
1033
  }
1034
1034
 
1035
1035
  aside.tip::before {
1036
- content: "\f0eb"; /* fa-lightbulb-o */
1036
+ content: "\f0eb"; /* fa-lightbulb */
1037
1037
  color: #40403E;
1038
1038
  }
1039
1039
 
@@ -5,7 +5,6 @@ require 'asciidoctor/extensions'
5
5
  require 'gepub'
6
6
  require_relative 'asciidoctor-epub3/ext'
7
7
  require_relative 'asciidoctor-epub3/converter'
8
- require_relative 'asciidoctor-epub3/packager'
9
8
 
10
9
  # We need to be able to write files with unicode names. See https://github.com/asciidoctor/asciidoctor-epub3/issues/217
11
10
  ::Zip.unicode_names = true
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'spine_item_processor'
3
+ require 'open3'
4
4
  require_relative 'font_icon_map'
5
5
 
6
6
  module Asciidoctor
@@ -14,56 +14,45 @@ module Asciidoctor
14
14
 
15
15
  register_for 'epub3'
16
16
 
17
- def initialize backend, opts
18
- super
19
- basebackend 'html'
20
- outfilesuffix '.epub' # dummy outfilesuffix since it may be .mobi
21
- htmlsyntax 'xml'
22
- @validate = false
23
- @extract = false
24
- @kindlegen_path = nil
25
- @epubcheck_path = nil
26
- end
27
-
28
- def convert node, name = nil
29
- if (name ||= node.node_name) == 'document'
30
- @validate = node.attr? 'ebook-validate'
31
- @extract = node.attr? 'ebook-extract'
32
- @compress = node.attr 'ebook-compress'
33
- @kindlegen_path = node.attr 'ebook-kindlegen-path'
34
- @epubcheck_path = node.attr 'ebook-epubcheck-path'
35
- spine_items = node.references[:spine_items]
36
- if spine_items.nil?
37
- logger.error %(#{::File.basename node.document.attr('docfile')}: failed to find spine items, produced file will be invalid)
38
- spine_items = []
39
- end
40
- Packager.new node, spine_items, node.attributes['ebook-format'].to_sym
41
- # converting an element from the spine document, such as an inline node in the doctitle
42
- elsif name.start_with? 'inline_'
43
- (@content_converter ||= ::Asciidoctor::Converter::Factory.default.create 'epub3-xhtml5').convert node, name
44
- else
45
- raise ::ArgumentError, %(Encountered unexpected node in epub3 package converter: #{name})
17
+ def write output, target
18
+ epub_file = @format == :kf8 ? %(#{::Asciidoctor::Helpers.rootname target}-kf8.epub) : target
19
+ output.generate_epub epub_file
20
+ logger.debug %(Wrote #{@format.upcase} to #{epub_file})
21
+ if @extract
22
+ extract_dir = epub_file.sub EpubExtensionRx, ''
23
+ ::FileUtils.remove_dir extract_dir if ::File.directory? extract_dir
24
+ ::Dir.mkdir extract_dir
25
+ ::Dir.chdir extract_dir do
26
+ ::Zip::File.open epub_file do |entries|
27
+ entries.each do |entry|
28
+ next unless entry.file?
29
+ unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
30
+ ::FileUtils.mkdir_p entry_dir
31
+ end
32
+ entry.extract
33
+ end
34
+ end
35
+ end
36
+ logger.debug %(Extracted #{@format.upcase} to #{extract_dir})
46
37
  end
47
- end
48
38
 
49
- # FIXME: we have to package in write because we don't have access to target before this point
50
- def write packager, target
51
- packager.package validate: @validate, extract: @extract, compress: @compress, kindlegen_path: @kindlegen_path, epubcheck_path: @epubcheck_path, target: target
52
- nil
39
+ if @format == :kf8
40
+ # QUESTION shouldn't we validate this epub file too?
41
+ distill_epub_to_mobi epub_file, target, @compress, @kindlegen_path
42
+ elsif @validate
43
+ validate_epub epub_file, @epubcheck_path
44
+ end
53
45
  end
54
- end
55
46
 
56
- # Public: The converter for the epub3 backend that converts the individual
57
- # content documents in an EPUB3 publication.
58
- class ContentConverter
59
- include ::Asciidoctor::Converter
60
- include ::Asciidoctor::Logging
47
+ CsvDelimiterRx = /\s*,\s*/
61
48
 
62
- register_for 'epub3-xhtml5'
49
+ DATA_DIR = ::File.expand_path ::File.join(__dir__, '..', '..', 'data')
50
+ ImageMacroRx = /^image::?(.*?)\[(.*?)\]$/
51
+ ImgSrcScanRx = /<img src="(.+?)"/
52
+ SvgImgSniffRx = /<img src=".+?\.svg"/
63
53
 
64
54
  LF = ?\n
65
55
  NoBreakSpace = '&#xa0;'
66
- ThinNoBreakSpace = '&#x202f;'
67
56
  RightAngleQuote = '&#x203a;'
68
57
  CalloutStartNum = %(\u2460)
69
58
 
@@ -87,13 +76,27 @@ module Asciidoctor
87
76
 
88
77
  ToHtmlSpecialCharsRx = /[#{ToHtmlSpecialCharsMap.keys.join}]/
89
78
 
90
- def initialize backend, opts
79
+ EpubExtensionRx = /\.epub$/i
80
+ KindlegenCompression = ::Hash['0', '-c0', '1', '-c1', '2', '-c2', 'none', '-c0', 'standard', '-c1', 'huffdic', '-c2']
81
+
82
+ (QUOTE_TAGS = {
83
+ monospaced: ['<code>', '</code>', true],
84
+ emphasis: ['<em>', '</em>', true],
85
+ strong: ['<strong>', '</strong>', true],
86
+ double: ['“', '”'],
87
+ single: ['‘', '’'],
88
+ mark: ['<mark>', '</mark>', true],
89
+ superscript: ['<sup>', '</sup>', true],
90
+ subscript: ['<sub>', '</sub>', true],
91
+ asciimath: ['<code>', '</code>', true],
92
+ latexmath: ['<code>', '</code>', true],
93
+ }).default = ['', '']
94
+
95
+ def initialize backend, opts = {}
91
96
  super
92
97
  basebackend 'html'
93
- outfilesuffix '.xhtml'
98
+ outfilesuffix '.epub' # dummy outfilesuffix since it may be .mobi
94
99
  htmlsyntax 'xml'
95
- @xrefs_seen = ::Set.new
96
- @icon_names = []
97
100
  end
98
101
 
99
102
  def convert node, name = nil, _opts = {}
@@ -105,33 +108,197 @@ module Asciidoctor
105
108
  end
106
109
  end
107
110
 
111
+ # See https://asciidoctor.org/docs/user-manual/#book-parts-and-chapters
112
+ def get_chapter_name node
113
+ if node.document.doctype != 'book'
114
+ return Asciidoctor::Document === node ? node.attr('docname') : nil
115
+ end
116
+ return (node.id || 'preamble') if node.context == :preamble && node.level == 0
117
+ Asciidoctor::Section === node && node.level <= 1 ? node.id : nil
118
+ end
119
+
108
120
  def convert_document node
109
- docid = node.id
110
- pubtype = node.attr 'publication-type', 'book'
121
+ @format = node.attr('ebook-format').to_sym
122
+
123
+ @validate = node.attr? 'ebook-validate'
124
+ @extract = node.attr? 'ebook-extract'
125
+ @compress = node.attr 'ebook-compress'
126
+ @kindlegen_path = node.attr 'ebook-kindlegen-path'
127
+ @epubcheck_path = node.attr 'ebook-epubcheck-path'
128
+ @xrefs_seen = ::Set.new
129
+ @icon_names = []
130
+ @images = []
131
+ @footnotes = []
132
+
133
+ @book = GEPUB::Book.new 'EPUB/package.opf'
134
+ @book.epub_backward_compat = @format != :kf8
135
+ @book.language node.attr('lang', 'en'), id: 'pub-language'
136
+
137
+ if node.attr? 'uuid'
138
+ @book.primary_identifier node.attr('uuid'), 'pub-identifier', 'uuid'
139
+ else
140
+ @book.primary_identifier node.id, 'pub-identifier', 'uuid'
141
+ end
142
+ # replace with next line once the attributes argument is supported
143
+ #unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
144
+
145
+ # NOTE we must use :plain_text here since gepub reencodes
146
+ @book.add_title sanitize_doctitle_xml(node, :plain_text), id: 'pub-title'
147
+
148
+ # see https://www.w3.org/publishing/epub3/epub-packages.html#sec-opf-dccreator
149
+ (1..(node.attr 'authorcount', 1).to_i).map do |idx|
150
+ author = node.attr(idx == 1 ? 'author' : %(author_#{idx}))
151
+ @book.add_creator author, role: 'aut' unless author.nil_or_empty?
152
+ end
153
+
154
+ publisher = node.attr 'publisher'
155
+ # NOTE Use producer as both publisher and producer if publisher isn't specified
156
+ publisher = node.attr 'producer' if publisher.nil_or_empty?
157
+ @book.publisher = publisher unless publisher.nil_or_empty?
158
+
159
+ if node.attr? 'reproducible'
160
+ # We need to set lastmodified to some fixed value. Otherwise, gepub will set it to current date.
161
+ @book.lastmodified = (::Time.at 0).utc
162
+ # Is it correct that we do not populate dc:date when 'reproducible' is set?
163
+ else
164
+ if node.attr? 'revdate'
165
+ begin
166
+ @book.date = node.attr 'revdate'
167
+ rescue ArgumentError => e
168
+ logger.error %(#{::File.basename node.attr('docfile')}: failed to parse revdate: #{e})
169
+ @book.date = node.attr 'docdatetime'
170
+ end
171
+ else
172
+ @book.date = node.attr 'docdatetime'
173
+ end
174
+ @book.lastmodified = node.attr 'localdatetime'
175
+ end
176
+
177
+ @book.description = node.attr 'description' if node.attr? 'description'
178
+ @book.source = node.attr 'source' if node.attr? 'source'
179
+ @book.rights = node.attr 'copyright' if node.attr? 'copyright'
111
180
 
112
- if (doctitle = node.doctitle partition: true, use_fallback: true).subtitle?
181
+ (node.attr 'keywords', '').split(CsvDelimiterRx).each do |s|
182
+ @book.metadata.add_metadata 'subject', s
183
+ end
184
+
185
+ add_cover_image node
186
+ add_front_matter_page node
187
+
188
+ if node.doctype == 'book'
189
+ toc_items = []
190
+ node.sections.each do |section|
191
+ toc_items << section
192
+ section.sections.each do |subsection|
193
+ next if get_chapter_name(node).nil?
194
+ toc_items << subsection
195
+ end
196
+ end
197
+ node.content
198
+ else
199
+ toc_items = [node]
200
+ add_chapter node
201
+ end
202
+
203
+ nav_xhtml = @book.add_item 'nav.xhtml', content: postprocess_xhtml(nav_doc(node, toc_items)), id: 'nav'
204
+ nav_xhtml.nav
205
+
206
+ # NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
207
+ toc_ncx = ncx_doc node, toc_items
208
+ @book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
209
+
210
+ docimagesdir = (node.attr 'imagesdir', '.').chomp '/'
211
+ docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
212
+
213
+ @images.each do |image|
214
+ if image[:name].start_with? %(#{docimagesdir}jacket/cover.)
215
+ logger.warn %(image path is reserved for cover artwork: #{image[:name]}; skipping image found in content)
216
+ elsif ::File.readable? image[:path]
217
+ @book.add_item image[:name], content: image[:path]
218
+ else
219
+ logger.error %(#{File.basename node.attr('docfile')}: image not found or not readable: #{image[:path]})
220
+ end
221
+ end
222
+
223
+ #add_metadata 'ibooks:specified-fonts', true
224
+
225
+ add_theme_assets node
226
+ if node.doctype != 'book'
227
+ usernames = [node].map {|item| item.attr 'username' }.compact.uniq
228
+ add_profile_images node, usernames
229
+ end
230
+
231
+ @book
232
+ end
233
+
234
+ # FIXME: move to Asciidoctor::Helpers
235
+ def sanitize_doctitle_xml doc, content_spec
236
+ doctitle = doc.doctitle use_fallback: true
237
+ sanitize_xml doctitle, content_spec
238
+ end
239
+
240
+ # FIXME: move to Asciidoctor::Helpers
241
+ def sanitize_xml content, content_spec
242
+ if content_spec != :pcdata && (content.include? '<')
243
+ if (content = (content.gsub XmlElementRx, '').strip).include? ' '
244
+ content = content.tr_s ' ', ' '
245
+ end
246
+ end
247
+
248
+ case content_spec
249
+ when :attribute_cdata
250
+ content = content.gsub '"', '&quot;' if content.include? '"'
251
+ when :cdata, :pcdata
252
+ # noop
253
+ when :plain_text
254
+ if content.include? ';'
255
+ content = content.gsub(CharEntityRx) { [$1.to_i].pack 'U*' } if content.include? '&#'
256
+ content = content.gsub FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap
257
+ end
258
+ else
259
+ raise ::ArgumentError, %(Unknown content spec: #{content_spec})
260
+ end
261
+ content
262
+ end
263
+
264
+ def add_chapter node
265
+ docid = get_chapter_name node
266
+ return nil if docid.nil?
267
+
268
+ chapter_item = @book.add_ordered_item %(#{docid}.xhtml)
269
+
270
+ if node.context == :document && (doctitle = node.doctitle partition: true, use_fallback: true).subtitle?
113
271
  title = %(#{doctitle.main} )
114
272
  subtitle = doctitle.subtitle
115
- else
273
+ elsif node.title
116
274
  # HACK: until we get proper handling of title-only in CSS
117
275
  title = ''
118
- subtitle = doctitle.combined
276
+ subtitle = node.title
277
+ else
278
+ title = nil
279
+ subtitle = nil
119
280
  end
120
281
 
121
- doctitle_sanitized = (node.doctitle sanitize: true, use_fallback: true).to_s
122
- subtitle_formatted = subtitle.split.map {|w| %(<b>#{w}</b>) } * ' '
282
+ doctitle_sanitized = (node.document.doctitle sanitize: true, use_fallback: true).to_s
283
+
284
+ # By default, Kindle does not allow the line height to be adjusted.
285
+ # But if you float the elements, then the line height disappears and can be restored manually using margins.
286
+ # See https://github.com/asciidoctor/asciidoctor-epub3/issues/123
287
+ subtitle_formatted = subtitle ? subtitle.split.map {|w| %(<b>#{w}</b>) } * ' ' : nil
123
288
 
124
- if pubtype == 'book'
289
+ if node.document.doctype == 'book'
125
290
  byline = ''
126
291
  else
127
292
  author = node.attr 'author'
128
293
  username = node.attr 'username', 'default'
129
- imagesdir = (node.references[:spine].attr 'imagesdir', '.').chomp '/'
294
+ imagesdir = (node.document.attr 'imagesdir', '.').chomp '/'
130
295
  imagesdir = imagesdir == '.' ? '' : %(#{imagesdir}/)
131
296
  byline = %(<p class="byline"><img src="#{imagesdir}avatars/#{username}.jpg"/> <b class="author">#{author}</b></p>#{LF})
132
297
  end
133
298
 
134
- mark_last_paragraph node unless pubtype == 'book'
299
+ mark_last_paragraph node unless node.document.doctype == 'book'
300
+
301
+ @xrefs_seen.clear
135
302
  content = node.content
136
303
 
137
304
  # NOTE must run after content is resolved
@@ -140,7 +307,7 @@ module Asciidoctor
140
307
  icon_css_head = ''
141
308
  else
142
309
  icon_defs = @icon_names.map {|name|
143
- %(.i-#{name}::before { content: "#{FontIconMap[name.tr('-', '_').to_sym]}"; })
310
+ %(.i-#{name}::before { content: "#{FontIconMap.unicode name}"; })
144
311
  } * LF
145
312
  icon_css_head = %(<style>
146
313
  #{icon_defs}
@@ -148,9 +315,15 @@ module Asciidoctor
148
315
  )
149
316
  end
150
317
 
318
+ header = (title || subtitle) ? %(<header>
319
+ <div class="chapter-header">
320
+ #{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle_formatted}</small>) : ''}</h1>
321
+ </div>
322
+ </header>) : ''
323
+
151
324
  # NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
152
325
  lines = [%(<!DOCTYPE html>
153
- <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = node.attr 'lang', 'en'}" lang="#{lang}">
326
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = node.document.attr 'lang', 'en'}" lang="#{lang}">
154
327
  <head>
155
328
  <meta charset="UTF-8"/>
156
329
  <title>#{doctitle_sanitized}</title>
@@ -168,19 +341,17 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
168
341
  </head>
169
342
  <body>
170
343
  <section class="chapter" title="#{doctitle_sanitized.gsub '"', '&quot;'}" epub:type="chapter" id="#{docid}">
171
- <header>
172
- <div class="chapter-header">
173
- #{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle_formatted}</small>) : ''}</h1>
174
- </div>
175
- </header>
344
+ #{header}
176
345
  #{content})]
177
346
 
178
- if node.footnotes?
347
+ unless (fns = node.document.footnotes - @footnotes).empty?
348
+ @footnotes += fns
349
+
179
350
  # NOTE kindlegen seems to mangle the <footer> element, so we wrap its content in a div
180
351
  lines << '<footer>
181
352
  <div class="chapter-footer">
182
353
  <div class="footnotes">'
183
- node.footnotes.each do |footnote|
354
+ fns.each do |footnote|
184
355
  lines << %(<aside id="note-#{footnote.index}" epub:type="footnote">
185
356
  <p><sup class="noteref"><a href="#noteref-#{footnote.index}">#{footnote.index}</a></sup> #{footnote.text}</p>
186
357
  </aside>)
@@ -194,40 +365,46 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
194
365
  </body>
195
366
  </html>'
196
367
 
197
- lines * LF
198
- end
368
+ chapter_item.add_content postprocess_xhtml lines * LF
369
+ epub_properties = node.attr 'epub-properties'
370
+ chapter_item.add_property 'svg' if epub_properties&.include? 'svg'
199
371
 
200
- # NOTE embedded is used for AsciiDoc table cell content
201
- def convert_embedded node
202
- node.content
372
+ # # QUESTION reenable?
373
+ # #linear 'yes' if i == 0
374
+
375
+ chapter_item
203
376
  end
204
377
 
205
378
  def convert_section node
206
- hlevel = node.level + 1
207
- epub_type_attr = node.special ? %( epub:type="#{node.sectname}") : ''
208
- div_classes = [%(sect#{node.level}), node.role].compact
209
- title = node.title
210
- title_sanitized = xml_sanitize title
211
- if node.document.header? || node.level != 1 || node != node.document.first_section
379
+ if add_chapter(node).nil?
380
+ hlevel = node.level
381
+ epub_type_attr = node.special ? %( epub:type="#{node.sectname}") : ''
382
+ div_classes = [%(sect#{node.level}), node.role].compact
383
+ title = node.title
384
+ title_sanitized = xml_sanitize title
212
385
  %(<section class="#{div_classes * ' '}" title="#{title_sanitized}"#{epub_type_attr}>
213
386
  <h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? '' : %(
214
387
  #{content})}
215
388
  </section>)
216
- else
217
- # document has no level-0 heading and this heading serves as the document title
218
- node.content
219
389
  end
220
390
  end
221
391
 
392
+ # NOTE embedded is used for AsciiDoc table cell content
393
+ def convert_embedded node
394
+ node.content
395
+ end
396
+
222
397
  # TODO: support use of quote block as abstract
223
398
  def convert_preamble node
224
- if (first_block = node.blocks[0]) && first_block.style == 'abstract'
225
- convert_abstract first_block
226
- # REVIEW: should we treat the preamble as an abstract in general?
227
- elsif first_block && node.blocks.size == 1
228
- convert_abstract first_block
229
- else
230
- node.content
399
+ if add_chapter(node).nil?
400
+ if (first_block = node.blocks[0]) && first_block.style == 'abstract'
401
+ convert_abstract first_block
402
+ # REVIEW: should we treat the preamble as an abstract in general?
403
+ elsif first_block && node.blocks.size == 1
404
+ convert_abstract first_block
405
+ else
406
+ node.content
407
+ end
231
408
  end
232
409
  end
233
410
 
@@ -292,6 +469,9 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
292
469
  'note'
293
470
  when 'important', 'warning', 'caution'
294
471
  'warning'
472
+ else
473
+ logger.warn %(unknown admonition type: #{type})
474
+ 'note'
295
475
  end
296
476
  %(<aside#{id_attr} class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
297
477
  #{title_el}<div class="content">
@@ -323,13 +503,14 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
323
503
  pre_classes = node.style == 'source' ? ['source', %(language-#{node.attr 'language'})] : ['screen']
324
504
  title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>
325
505
  ) : ''
326
- # patches conums to fix extra or missing leading space
327
- # TODO remove patch once upgrading to Asciidoctor 1.5.6
328
506
  %(<figure class="#{figure_classes * ' '}">
329
- #{title_div}<pre class="#{pre_classes * ' '}"><code>#{(node.content || '').gsub(/(?<! )<i class="conum"| +<i class="conum"/, ' <i class="conum"')}</code></pre>
507
+ #{title_div}<pre class="#{pre_classes * ' '}"><code>#{node.content}</code></pre>
330
508
  </figure>)
331
509
  end
332
510
 
511
+ # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
512
+ alias convert_stem convert_listing
513
+
333
514
  # QUESTION should we wrap the <pre> in either <div> or <figure>?
334
515
  def convert_literal node
335
516
  %(<pre class="screen">#{node.content}</pre>)
@@ -344,7 +525,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
344
525
  end
345
526
 
346
527
  def convert_quote node
347
- id_attr = %( id="#{node.id}") if node.id
528
+ id_attr = node.id ? %( id="#{node.id}") : ''
348
529
  class_attr = (role = node.role) ? %( class="blockquote #{role}") : ' class="blockquote"'
349
530
 
350
531
  footer_content = []
@@ -370,7 +551,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
370
551
  end
371
552
 
372
553
  def convert_verse node
373
- id_attr = %( id="#{node.id}") if node.id
554
+ id_attr = node.id ? %( id="#{node.id}") : ''
374
555
  class_attr = (role = node.role) ? %( class="verse #{role}") : ' class="verse"'
375
556
 
376
557
  footer_content = []
@@ -450,7 +631,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
450
631
  # end
451
632
  #end
452
633
  lines << '</colgroup>'
453
- [:head, :foot, :body].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
634
+ [:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
454
635
  lines << %(<t#{tsec}>)
455
636
  node.rows[tsec].each do |row|
456
637
  lines << '<tr>'
@@ -620,110 +801,130 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
620
801
  lines * LF
621
802
  end
622
803
 
623
- def convert_image node
624
- target = node.attr 'target'
625
- type = (::File.extname target)[1..-1]
626
- id_attr = node.id ? %( id="#{node.id}") : ''
627
- img_attrs = [%(alt="#{node.attr 'alt'}")]
628
- case type
629
- when 'svg'
630
- img_attrs << %(style="width: #{node.attr 'scaledwidth', '100%'}")
631
- # TODO: make this a convenience method on document
632
- epub_properties = (node.document.attributes['epub-properties'] ||= [])
804
+ def doc_option document, key
805
+ loop do
806
+ value = document.options[key]
807
+ return value unless value.nil?
808
+ document = document.parent_document
809
+ break if document.nil?
810
+ end
811
+ nil
812
+ end
813
+
814
+ def root_document document
815
+ document = document.parent_document until document.parent_document.nil?
816
+ document
817
+ end
818
+
819
+ def register_image node, target
820
+ if target.end_with? '.svg'
821
+ chapter = get_enclosing_chapter node
822
+ chapter.set_attr 'epub-properties', [] unless chapter.attr? 'epub-properties'
823
+ epub_properties = chapter.attr 'epub-properties'
633
824
  epub_properties << 'svg' unless epub_properties.include? 'svg'
634
- else
635
- img_attrs << %(style="width: #{node.attr 'scaledwidth'}") if node.attr? 'scaledwidth'
636
- end
637
- =begin
638
- # NOTE to set actual width and height, use CSS width and height
639
- if type == 'svg'
640
- if node.attr? 'scaledwidth'
641
- img_attrs << %(width="#{node.attr 'scaledwidth'}")
642
- # Kindle
643
- #elsif node.attr? 'scaledheight'
644
- # img_attrs << %(width="#{node.attr 'scaledheight'}" height="#{node.attr 'scaledheight'}")
645
- # ePub3
646
- elsif node.attr? 'scaledheight'
647
- img_attrs << %(height="#{node.attr 'scaledheight'}" style="max-height: #{node.attr 'scaledheight'} !important")
648
- else
649
- # Aldiko doesn't not scale width to 100% by default
650
- img_attrs << %(width="100%")
825
+ end
826
+
827
+ out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
828
+ fs_path = (::File.join out_dir, target)
829
+ unless ::File.exist? fs_path
830
+ base_dir = root_document(node.document).base_dir
831
+ fs_path = ::File.join base_dir, target
832
+ end
833
+ # We need *both* virtual and physical image paths. Unfortunately, references[:images] only has one of them.
834
+ @images << { name: target, path: fs_path }
651
835
  end
652
- end
653
- =end
836
+
837
+ def resolve_image_attrs node
838
+ img_attrs = []
839
+ img_attrs << %(alt="#{node.attr 'alt'}") if node.attr? 'alt'
840
+
841
+ width = node.attr 'scaledwidth'
842
+ width = node.attr 'width' if width.nil?
843
+
844
+ # Unlike browsers, Calibre/Kindle *do* scale image if only height is specified
845
+ # So, in order to match browser behavior, we just always omit height
846
+ img_attrs << %(width="#{width}") unless width.nil?
847
+
848
+ img_attrs
849
+ end
850
+
851
+ def convert_image node
852
+ target = node.image_uri node.attr 'target'
853
+ register_image node, target
854
+ id_attr = node.id ? %( id="#{node.id}") : ''
855
+ img_attrs = resolve_image_attrs node
654
856
  %(<figure#{id_attr} class="image#{prepend_space node.role}">
655
857
  <div class="content">
656
- <img src="#{node.image_uri node.attr('target')}" #{img_attrs * ' '}/>
858
+ <img src="#{target}"#{prepend_space img_attrs * ' '} />
657
859
  </div>#{node.title? ? %(
658
860
  <figcaption>#{node.captioned_title}</figcaption>) : ''}
659
861
  </figure>)
660
862
  end
661
863
 
864
+ def get_enclosing_chapter node
865
+ loop do
866
+ return nil if node.nil?
867
+ return node unless get_chapter_name(node).nil?
868
+ node = node.parent
869
+ end
870
+ end
871
+
662
872
  def convert_inline_anchor node
663
- target = node.target
664
873
  case node.type
665
- when :xref # TODO: would be helpful to know what type the target is (e.g., bibref)
666
- doc, refid, text, path = node.document, ((node.attr 'refid') || target), node.text, (node.attr 'path')
667
- # NOTE if path is non-nil, we have an inter-document xref
668
- # QUESTION should we drop the id attribute for an inter-document xref?
669
- if path
670
- # ex. chapter-id#section-id
671
- if node.attr 'fragment'
672
- refdoc_id, refdoc_refid = refid.split '#', 2
673
- if refdoc_id == refdoc_refid
674
- target = target[0...(target.index '#')]
675
- id_attr = %( id="xref--#{refdoc_id}")
676
- else
677
- id_attr = %( id="xref--#{refdoc_id}--#{refdoc_refid}")
678
- end
679
- # ex. chapter-id#
680
- else
681
- refdoc_id = refdoc_refid = refid
682
- # inflate key to spine item root (e.g., transform chapter-id to chapter-id#chapter-id)
683
- refid = %(#{refid}##{refid})
684
- id_attr = %( id="xref--#{refdoc_id}")
685
- end
686
- id_attr = '' unless @xrefs_seen.add? refid
687
- refdoc = doc.references[:spine_items].find {|it| refdoc_id == (it.id || (it.attr 'docname')) }
688
- if refdoc
689
- if (refs = refdoc.references[:refs]) && ::Asciidoctor::AbstractNode === (ref = refs[refdoc_refid])
690
- text ||= ::Asciidoctor::Document === ref ? ((ref.attr 'docreftext') || ref.doctitle) : ref.xreftext((@xrefstyle ||= (doc.attr 'xrefstyle')))
691
- elsif (xreftext = refdoc.references[:ids][refdoc_refid])
692
- text ||= xreftext
874
+ when :xref
875
+ doc, refid, target, text = node.document, node.attr('refid'), node.target, node.text
876
+ id_attr = ''
877
+
878
+ if (path = node.attributes['path'])
879
+ # NOTE non-nil path indicates this is an inter-document xref that's not included in current document
880
+ text = node.text || path
881
+ elsif refid == '#'
882
+ logger.warn %(#{::File.basename doc.attr('docfile')}: <<chapter#>> xref syntax isn't supported anymore. Use either <<chapter>> or <<chapter#anchor>>)
883
+ elsif refid
884
+ ref = doc.references[:refs][refid]
885
+ our_chapter = get_enclosing_chapter node
886
+ ref_chapter = get_enclosing_chapter ref
887
+ if ref_chapter
888
+ ref_docname = get_chapter_name ref_chapter
889
+ if ref_chapter == our_chapter
890
+ # ref within same chapter file
891
+ id_attr = %( id="xref-#{refid}")
892
+ target = %(##{refid})
893
+ elsif refid == ref_docname
894
+ # ref to top section of other chapter file
895
+ id_attr = %( id="xref--#{refid}")
896
+ target = %(#{refid}.xhtml)
693
897
  else
694
- logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to unknown anchor in #{refdoc_id} chapter: #{refdoc_refid})
695
- end
696
- else
697
- logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to anchor in unknown chapter: #{refdoc_id})
698
- end
699
- else
700
- id_attr = (@xrefs_seen.add? refid) ? %( id="xref-#{refid}") : ''
701
- if (refs = doc.references[:refs])
702
- if ::Asciidoctor::AbstractNode === (ref = refs[refid])
703
- xreftext = text || ref.xreftext((@xrefstyle ||= (doc.attr 'xrefstyle')))
898
+ # ref to section within other chapter file
899
+ id_attr = %( id="xref--#{ref_docname}--#{refid}")
900
+ target = %(#{ref_docname}.xhtml##{refid})
704
901
  end
705
- else
706
- xreftext = doc.references[:ids][refid]
707
- end
708
902
 
709
- if xreftext
710
- text ||= xreftext
903
+ id_attr = '' unless @xrefs_seen.add? refid
904
+ text = (ref.xreftext node.attr('xrefstyle', nil, true))
711
905
  else
712
- # FIXME: we get false negatives for reference to bibref when using Asciidoctor < 1.5.6
713
- logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to unknown local anchor (or valid bibref): #{refid})
906
+ logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to unknown anchor: #{refid})
714
907
  end
715
908
  end
909
+
716
910
  %(<a#{id_attr} href="#{target}" class="xref">#{text || "[#{refid}]"}</a>)
717
911
  when :ref
718
- %(<a id="#{target}"></a>)
912
+ # NOTE id is used instead of target starting in Asciidoctor 2.0.0
913
+ %(<a id="#{node.target || node.id}"></a>)
719
914
  when :link
720
- %(<a href="#{target}" class="link">#{node.text}</a>)
915
+ %(<a href="#{node.target}" class="link">#{node.text}</a>)
721
916
  when :bibref
722
- if @xrefs_seen.include? target
723
- %(<a id="#{target}" href="#xref-#{target}">[#{target}]</a>)
917
+ # NOTE reftext is no longer enclosed in [] starting in Asciidoctor 2.0.0
918
+ # NOTE id is used instead of target starting in Asciidoctor 2.0.0
919
+ if (reftext = node.reftext)
920
+ reftext = %([#{reftext}]) unless reftext.start_with? '['
724
921
  else
725
- %(<a id="#{target}"></a>[#{target}])
922
+ reftext = %([#{node.target || node.id}])
726
923
  end
924
+ %(<a id="#{node.target || node.id}"></a>#{reftext})
925
+ else
926
+ logger.warn %(unknown anchor type: #{node.type.inspect})
927
+ nil
727
928
  end
728
929
  end
729
930
 
@@ -761,16 +962,11 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
761
962
  %(<i class="#{i_classes * ' '}"></i>)
762
963
  else
763
964
  target = node.image_uri node.target
764
- img_attrs = [%(alt="#{node.attr 'alt'}"), %(class="inline#{node.role? ? " #{node.role}" : ''}")]
765
- if target.end_with? '.svg'
766
- img_attrs << %(style="width: #{node.attr 'scaledwidth', '100%'}")
767
- # TODO: make this a convenience method on document
768
- epub_properties = (node.document.attributes['epub-properties'] ||= [])
769
- epub_properties << 'svg' unless epub_properties.include? 'svg'
770
- elsif node.attr? 'scaledwidth'
771
- img_attrs << %(style="width: #{node.attr 'scaledwidth'}")
772
- end
773
- %(<img src="#{target}" #{img_attrs * ' '}/>)
965
+ register_image node, target
966
+
967
+ img_attrs = resolve_image_attrs node
968
+ img_attrs << %(class="inline#{prepend_space node.role}")
969
+ %(<img src="#{target}"#{prepend_space img_attrs * ' '}/>)
774
970
  end
775
971
  end
776
972
 
@@ -802,25 +998,27 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
802
998
  end
803
999
 
804
1000
  def convert_inline_quoted node
805
- case node.type
806
- when :strong
807
- %(<strong>#{node.text}</strong>)
808
- when :emphasis
809
- %(<em>#{node.text}</em>)
810
- when :monospaced
811
- %(<code class="literal">#{node.text}</code>)
812
- when :double
813
- #%(&#x201c;#{node.text}&#x201d;)
814
- %(“#{node.text}”)
815
- when :single
816
- #%(&#x2018;#{node.text}&#x2019;)
817
- %(‘#{node.text}’)
818
- when :superscript
819
- %(<sup>#{node.text}</sup>)
820
- when :subscript
821
- %(<sub>#{node.text}</sub>)
1001
+ open, close, tag = QUOTE_TAGS[node.type]
1002
+
1003
+ # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
1004
+ node.add_role 'literal' if [:monospaced, :asciimath, :latexmath].include? node.type
1005
+
1006
+ if node.id
1007
+ class_attr = class_string node
1008
+ if tag
1009
+ %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close})
1010
+ else
1011
+ %(<span id="#{node.id}"#{class_attr}>#{open}#{node.text}#{close}</span>)
1012
+ end
1013
+ elsif role_valid_class? node.role
1014
+ class_attr = class_string node
1015
+ if tag
1016
+ %(#{open.chop}#{class_attr}>#{node.text}#{close})
1017
+ else
1018
+ %(<span#{class_attr}>#{open}#{node.text}#{close}</span>)
1019
+ end
822
1020
  else
823
- node.text
1021
+ %(#{open}#{node.text}#{close})
824
1022
  end
825
1023
  end
826
1024
 
@@ -854,6 +1052,452 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
854
1052
  def prepend_space value
855
1053
  value ? %( #{value}) : ''
856
1054
  end
1055
+
1056
+ def add_theme_assets doc
1057
+ format = @format
1058
+ workdir = if doc.attr? 'epub3-stylesdir'
1059
+ stylesdir = doc.attr 'epub3-stylesdir'
1060
+ # FIXME: make this work for Windows paths!!
1061
+ if stylesdir.start_with? '/'
1062
+ stylesdir
1063
+ else
1064
+ docdir = doc.attr 'docdir', '.'
1065
+ docdir = '.' if docdir.empty?
1066
+ ::File.join docdir, stylesdir
1067
+ end
1068
+ else
1069
+ ::File.join DATA_DIR, 'styles'
1070
+ end
1071
+
1072
+ # TODO: improve design/UX of custom theme functionality, including custom fonts
1073
+
1074
+ if format == :kf8
1075
+ # NOTE add layer of indirection so Kindle Direct Publishing (KDP) doesn't strip font-related CSS rules
1076
+ @book.add_item 'styles/epub3.css', content: '@import url("epub3-proxied.css");'.to_ios
1077
+ @book.add_item 'styles/epub3-css3-only.css', content: '@import url("epub3-css3-only-proxied.css");'.to_ios
1078
+ @book.add_item 'styles/epub3-proxied.css', content: (postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
1079
+ @book.add_item 'styles/epub3-css3-only-proxied.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
1080
+ else
1081
+ @book.add_item 'styles/epub3.css', content: (postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
1082
+ @book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
1083
+ end
1084
+
1085
+ font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
1086
+ @book.add_item 'styles/epub3-fonts.css', content: font_css
1087
+ unless font_files.empty?
1088
+ # NOTE metadata property in oepbs package manifest doesn't work; must use proprietary iBooks file instead
1089
+ #(@book.metadata.add_metadata 'meta', 'true')['property'] = 'ibooks:specified-fonts' unless format == :kf8
1090
+ @book.add_optional_file 'META-INF/com.apple.ibooks.display-options.xml', '<?xml version="1.0" encoding="UTF-8"?>
1091
+ <display_options>
1092
+ <platform name="*">
1093
+ <option name="specified-fonts">true</option>
1094
+ </platform>
1095
+ </display_options>'.to_ios unless format == :kf8
1096
+
1097
+ font_files.each do |font_file|
1098
+ @book.add_item font_file, content: File.join(DATA_DIR, font_file)
1099
+ end
1100
+ end
1101
+ nil
1102
+ end
1103
+
1104
+ def add_cover_image doc
1105
+ return if (image_path = doc.attr 'front-cover-image').nil?
1106
+
1107
+ imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
1108
+ imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
1109
+
1110
+ image_attrs = {}
1111
+ if (image_path.include? ':') && image_path =~ ImageMacroRx
1112
+ logger.warn %(deprecated block macro syntax detected in front-cover-image attribute) if image_path.start_with? 'image::'
1113
+ image_path = %(#{imagesdir}#{$1})
1114
+ (::Asciidoctor::AttributeList.new $2).parse_into image_attrs, %w(alt width height) unless $2.empty?
1115
+ end
1116
+
1117
+ image_href = %(#{imagesdir}jacket/cover#{::File.extname image_path})
1118
+
1119
+ workdir = doc.attr 'docdir'
1120
+ workdir = '.' if workdir.nil_or_empty?
1121
+
1122
+ unless ::File.readable? ::File.join(workdir, image_path)
1123
+ logger.error %(#{::File.basename doc.attr('docfile')}: front cover image not found or readable: #{::File.expand_path image_path, workdir})
1124
+ return
1125
+ end
1126
+
1127
+ unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
1128
+ width, height = 1050, 1600
1129
+ end
1130
+
1131
+ @book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
1132
+
1133
+ unless @format == :kf8
1134
+ # NOTE SVG wrapper maintains aspect ratio and confines image to view box
1135
+ content = %(<!DOCTYPE html>
1136
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
1137
+ <head>
1138
+ <meta charset="UTF-8"/>
1139
+ <title>#{sanitize_doctitle_xml doc, :cdata}</title>
1140
+ <style type="text/css">
1141
+ @page {
1142
+ margin: 0;
1143
+ }
1144
+ html {
1145
+ margin: 0 !important;
1146
+ padding: 0 !important;
1147
+ }
1148
+ body {
1149
+ margin: 0;
1150
+ padding: 0 !important;
1151
+ text-align: center;
1152
+ }
1153
+ body > svg {
1154
+ /* prevent bleed onto second page (removes descender space) */
1155
+ display: block;
1156
+ }
1157
+ </style>
1158
+ </head>
1159
+ <body epub:type="cover"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
1160
+ width="100%" height="100%" viewBox="0 0 #{width} #{height}" preserveAspectRatio="xMidYMid meet">
1161
+ <image width="#{width}" height="#{height}" xlink:href="#{image_href}"/>
1162
+ </svg></body>
1163
+ </html>).to_ios
1164
+
1165
+ # Gitden expects a cover.xhtml, so add it to the spine
1166
+ @book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
1167
+ end
1168
+ nil
1169
+ end
1170
+
1171
+ def get_frontmatter_files doc, workdir
1172
+ if doc.attr? 'epub3-frontmatterdir'
1173
+ fmdir = doc.attr 'epub3-frontmatterdir'
1174
+ fmglob = 'front-matter.*\.html'
1175
+ fm_path = File.join workdir, fmdir
1176
+ unless Dir.exist? fm_path
1177
+ logger.warn %(#{File.basename doc.attr('docfile')}: directory specified by 'epub3-frontmattderdir' doesn't exist! Ignoring ...)
1178
+ return []
1179
+ end
1180
+ fms = Dir.entries(fm_path).delete_if {|x| !x.match fmglob }.sort.map {|y| File.join fm_path, y }
1181
+ if fms && !fms.empty?
1182
+ fms
1183
+ else
1184
+ logger.warn %(#{File.basename doc.attr('docfile')}: directory specified by 'epub3-frontmattderdir' contains no suitable files! Ignoring ...)
1185
+ []
1186
+ end
1187
+ elsif File.exist? File.join workdir, 'front-matter.html'
1188
+ [File.join(workdir, 'front-matter.html')]
1189
+ else
1190
+ []
1191
+ end
1192
+ end
1193
+
1194
+ def add_front_matter_page doc
1195
+ workdir = doc.attr 'docdir'
1196
+ workdir = '.' if workdir.nil_or_empty?
1197
+
1198
+ get_frontmatter_files(doc, workdir).each do |front_matter|
1199
+ front_matter_content = ::File.read front_matter
1200
+
1201
+ front_matter_file = File.basename front_matter, '.html'
1202
+ item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content)
1203
+ item.add_property 'svg' if SvgImgSniffRx =~ front_matter_content
1204
+
1205
+ front_matter_content.scan ImgSrcScanRx do
1206
+ @book.add_item $1, content: File.join(File.dirname(front_matter), $1)
1207
+ end
1208
+ end
1209
+
1210
+ nil
1211
+ end
1212
+
1213
+ def add_profile_images doc, usernames
1214
+ imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
1215
+ imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
1216
+
1217
+ @book.add_item %(#{imagesdir}avatars/default.jpg), content: ::File.join(DATA_DIR, 'images/default-avatar.jpg')
1218
+ @book.add_item %(#{imagesdir}headshots/default.jpg), content: ::File.join(DATA_DIR, 'images/default-headshot.jpg')
1219
+
1220
+ workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
1221
+
1222
+ usernames.each do |username|
1223
+ avatar = %(#{imagesdir}avatars/#{username}.jpg)
1224
+ if ::File.readable? (resolved_avatar = (::File.join workdir, avatar))
1225
+ @book.add_item avatar, content: resolved_avatar
1226
+ else
1227
+ logger.error %(avatar for #{username} not found or readable: #{avatar}; falling back to default avatar)
1228
+ @book.add_item avatar, content: ::File.join(DATA_DIR, 'images/default-avatar.jpg')
1229
+ end
1230
+
1231
+ headshot = %(#{imagesdir}headshots/#{username}.jpg)
1232
+ if ::File.readable? (resolved_headshot = (::File.join workdir, headshot))
1233
+ @book.add_item headshot, content: resolved_headshot
1234
+ elsif doc.attr? 'builder', 'editions'
1235
+ logger.error %(headshot for #{username} not found or readable: #{headshot}; falling back to default headshot)
1236
+ @book.add_item headshot, content: ::File.join(DATA_DIR, 'images/default-headshot.jpg')
1237
+ end
1238
+ end
1239
+ nil
1240
+ end
1241
+
1242
+ # TODO: aggregate authors of chapters into authors attribute(s) on main document
1243
+ def nav_doc doc, items
1244
+ lines = [%(<!DOCTYPE html>
1245
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = doc.attr 'lang', 'en'}" lang="#{lang}">
1246
+ <head>
1247
+ <meta charset="UTF-8"/>
1248
+ <title>#{sanitize_doctitle_xml doc, :cdata}</title>
1249
+ <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
1250
+ <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
1251
+ </head>
1252
+ <body>
1253
+ <h1>#{sanitize_doctitle_xml doc, :pcdata}</h1>
1254
+ <nav epub:type="toc" id="toc">
1255
+ <h2>#{doc.attr 'toc-title'}</h2>)]
1256
+ lines << (nav_level items, [(doc.attr 'toclevels', 1).to_i, 0].max)
1257
+ lines << %(</nav>
1258
+ </body>
1259
+ </html>)
1260
+ lines * LF
1261
+ end
1262
+
1263
+ def nav_level items, depth, state = {}
1264
+ lines = []
1265
+ lines << '<ol>'
1266
+ items.each do |item|
1267
+ #index = (state[:index] = (state.fetch :index, 0) + 1)
1268
+ if (chapter_name = get_chapter_name item).nil?
1269
+ item_label = sanitize_xml item.title, :pcdata
1270
+ item_href = %(#{state[:content_doc_href]}##{item.id})
1271
+ else
1272
+ # NOTE we sanitize the chapter titles because we use formatting to control layout
1273
+ if item.context == :document
1274
+ item_label = sanitize_doctitle_xml item, :cdata
1275
+ else
1276
+ item_label = sanitize_xml item.title, :cdata
1277
+ end
1278
+ item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
1279
+ end
1280
+ lines << %(<li><a href="#{item_href}">#{item_label}</a>)
1281
+ if depth == 0 || (child_sections = item.sections).empty?
1282
+ lines[-1] = %(#{lines[-1]}</li>)
1283
+ else
1284
+ lines << (nav_level child_sections, depth - 1, state)
1285
+ lines << '</li>'
1286
+ end
1287
+ state.delete :content_doc_href unless chapter_name.nil?
1288
+ end
1289
+ lines << '</ol>'
1290
+ lines * LF
1291
+ end
1292
+
1293
+ def ncx_doc doc, items
1294
+ # TODO: populate docAuthor element based on unique authors in work
1295
+ lines = [%(<?xml version="1.0" encoding="utf-8"?>
1296
+ <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
1297
+ <head>
1298
+ <meta name="dtb:uid" content="#{@book.identifier}"/>
1299
+ %{depth}
1300
+ <meta name="dtb:totalPageCount" content="0"/>
1301
+ <meta name="dtb:maxPageNumber" content="0"/>
1302
+ </head>
1303
+ <docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
1304
+ <navMap>)]
1305
+ lines << (ncx_level items, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {})
1306
+ lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
1307
+ lines << %(</navMap>
1308
+ </ncx>)
1309
+ lines * LF
1310
+ end
1311
+
1312
+ def ncx_level items, depth, state = {}
1313
+ lines = []
1314
+ state[:max_depth] = (state.fetch :max_depth, 0) + 1
1315
+ items.each do |item|
1316
+ index = (state[:index] = (state.fetch :index, 0) + 1)
1317
+ item_id = %(nav_#{index})
1318
+ if (chapter_name = get_chapter_name item).nil?
1319
+ item_label = sanitize_xml item.title, :cdata
1320
+ item_href = %(#{state[:content_doc_href]}##{item.id})
1321
+ else
1322
+ if item.context == :document
1323
+ item_label = sanitize_doctitle_xml item, :cdata
1324
+ else
1325
+ item_label = sanitize_xml item.title, :cdata
1326
+ end
1327
+ item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
1328
+ end
1329
+ lines << %(<navPoint id="#{item_id}" playOrder="#{index}">)
1330
+ lines << %(<navLabel><text>#{item_label}</text></navLabel>)
1331
+ lines << %(<content src="#{item_href}"/>)
1332
+ unless depth == 0 || (child_sections = item.sections).empty?
1333
+ lines << (ncx_level child_sections, depth - 1, state)
1334
+ end
1335
+ lines << %(</navPoint>)
1336
+ state.delete :content_doc_href unless chapter_name.nil?
1337
+ end
1338
+ lines * LF
1339
+ end
1340
+
1341
+ # Swap fonts in CSS based on the value of the document attribute 'scripts',
1342
+ # then return the list of fonts as well as the font CSS.
1343
+ def select_fonts filename, scripts = 'latin'
1344
+ font_css = ::File.read filename
1345
+ font_css = font_css.gsub(/(?<=-)latin(?=\.ttf\))/, scripts) unless scripts == 'latin'
1346
+
1347
+ # match CSS font urls in the forms of:
1348
+ # src: url(../fonts/notoserif-regular-latin.ttf);
1349
+ # src: url(../fonts/notoserif-regular-latin.ttf) format("truetype");
1350
+ font_list = font_css.scan(/url\(\.\.\/([^)]+\.ttf)\)/).flatten
1351
+
1352
+ [font_list, font_css.to_ios]
1353
+ end
1354
+
1355
+ def postprocess_css_file filename, format
1356
+ return filename unless format == :kf8
1357
+ postprocess_css ::File.read(filename), format
1358
+ end
1359
+
1360
+ def postprocess_css content, format
1361
+ return content.to_ios unless format == :kf8
1362
+ # TODO: convert regular expressions to constants
1363
+ content
1364
+ .gsub(/^ -webkit-column-break-.*\n/, '')
1365
+ .gsub(/^ max-width: .*\n/, '')
1366
+ .to_ios
1367
+ end
1368
+
1369
+ # NOTE Kindle requires that
1370
+ # <meta charset="utf-8"/>
1371
+ # be converted to
1372
+ # <meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
1373
+ def postprocess_xhtml content
1374
+ return content.to_ios unless @format == :kf8
1375
+ # TODO: convert regular expressions to constants
1376
+ content
1377
+ .gsub(/<meta charset="(.+?)"\/>/, '<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=\1"/>')
1378
+ .gsub(/<img([^>]+) style="width: (\d\d)%;"/, '<img\1 style="width: \2%; height: \2%;"')
1379
+ .gsub(/<script type="text\/javascript">.*?<\/script>\n?/m, '')
1380
+ .to_ios
1381
+ end
1382
+
1383
+ def get_kindlegen_command kindlegen_path
1384
+ unless kindlegen_path.nil?
1385
+ logger.debug %(Using ebook-kindlegen-path attribute: #{kindlegen_path})
1386
+ return [kindlegen_path]
1387
+ end
1388
+
1389
+ unless (result = ENV['KINDLEGEN']).nil?
1390
+ logger.debug %(Using KINDLEGEN env variable: #{result})
1391
+ return [result]
1392
+ end
1393
+
1394
+ begin
1395
+ require 'kindlegen' unless defined? ::Kindlegen
1396
+ result = ::Kindlegen.command.to_s
1397
+ logger.debug %(Using KindleGen from gem: #{result})
1398
+ [result]
1399
+ rescue LoadError => e
1400
+ logger.debug %(#{e}; Using KindleGen from PATH)
1401
+ [%(kindlegen#{::Gem.win_platform? ? '.exe' : ''})]
1402
+ end
1403
+ end
1404
+
1405
+ def distill_epub_to_mobi epub_file, target, compress, kindlegen_path
1406
+ mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
1407
+ compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
1408
+
1409
+ argv = get_kindlegen_command(kindlegen_path) + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
1410
+ begin
1411
+ # This duplicates Kindlegen.run, but we want to override executable
1412
+ out, err, res = Open3.capture3(*argv) do |r|
1413
+ r.force_encoding 'UTF-8' if ::Gem.win_platform? && r.respond_to?(:force_encoding)
1414
+ end
1415
+ rescue Errno::ENOENT => e
1416
+ raise 'Unable to run KindleGen. Either install the kindlegen gem or place `kindlegen` executable on PATH or set KINDLEGEN environment variable with path to it', cause: e
1417
+ end
1418
+
1419
+ out.each_line do |line|
1420
+ log_line line
1421
+ end
1422
+ err.each_line do |line|
1423
+ log_line line
1424
+ end
1425
+
1426
+ output_file = ::File.join ::File.dirname(epub_file), mobi_file
1427
+ if res.success?
1428
+ logger.debug %(Wrote MOBI to #{output_file})
1429
+ else
1430
+ logger.error %(KindleGen failed to write MOBI to #{output_file})
1431
+ end
1432
+ end
1433
+
1434
+ def get_epubcheck_command epubcheck_path
1435
+ unless epubcheck_path.nil?
1436
+ logger.debug %(Using ebook-epubcheck-path attribute: #{epubcheck_path})
1437
+ return [epubcheck_path]
1438
+ end
1439
+
1440
+ unless (result = ENV['EPUBCHECK']).nil?
1441
+ logger.debug %(Using EPUBCHECK env variable: #{result})
1442
+ return [result]
1443
+ end
1444
+
1445
+ begin
1446
+ result = ::Gem.bin_path 'epubcheck-ruby', 'epubcheck'
1447
+ logger.debug %(Using EPUBCheck from gem: #{result})
1448
+ [::Gem.ruby, result]
1449
+ rescue ::Gem::Exception => e
1450
+ logger.debug %(#{e}; Using EPUBCheck from PATH)
1451
+ ['epubcheck']
1452
+ end
1453
+ end
1454
+
1455
+ def validate_epub epub_file, epubcheck_path
1456
+ argv = get_epubcheck_command(epubcheck_path) + ['-w', epub_file]
1457
+ begin
1458
+ out, err, res = Open3.capture3(*argv)
1459
+ rescue Errno::ENOENT => e
1460
+ raise 'Unable to run EPUBCheck. Either install epubcheck-ruby gem or place `epubcheck` executable on PATH or set EPUBCHECK environment variable with path to it', cause: e
1461
+ end
1462
+
1463
+ out.each_line do |line|
1464
+ logger.info line
1465
+ end
1466
+ err.each_line do |line|
1467
+ log_line line
1468
+ end
1469
+
1470
+ logger.error %(EPUB validation failed: #{epub_file}) unless res.success?
1471
+ end
1472
+
1473
+ def log_line line
1474
+ line = line.strip
1475
+
1476
+ if line =~ /^fatal/i
1477
+ logger.fatal line
1478
+ elsif line =~ /^error/i
1479
+ logger.error line
1480
+ elsif line =~ /^warning/i
1481
+ logger.warn line
1482
+ else
1483
+ logger.info line
1484
+ end
1485
+ end
1486
+
1487
+ private
1488
+
1489
+ def class_string node
1490
+ role = node.role
1491
+
1492
+ return '' unless role_valid_class? role
1493
+
1494
+ %( class="#{role}")
1495
+ end
1496
+
1497
+ # Handles asciidoctor 1.5.6 quirk when role can be parent
1498
+ def role_valid_class? role
1499
+ role.is_a? String
1500
+ end
857
1501
  end
858
1502
 
859
1503
  class DocumentIdGenerator
@@ -914,11 +1558,8 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
914
1558
  end
915
1559
  end
916
1560
 
917
- require_relative 'packager'
918
-
919
1561
  Extensions.register do
920
1562
  if (document = @document).backend == 'epub3'
921
- document.attributes['spine'] = ''
922
1563
  document.set_attribute 'listing-caption', 'Listing'
923
1564
  # pygments.rb hangs on JRuby for Windows, see https://github.com/asciidoctor/asciidoctor-epub3/issues/253
924
1565
  if !(::RUBY_ENGINE == 'jruby' && Gem.win_platform?) && (Gem.try_activate 'pygments.rb')
@@ -937,8 +1578,6 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
937
1578
  ebook_format = document.attributes['ebook-format'] = 'epub3'
938
1579
  end
939
1580
  document.attributes[%(ebook-format-#{ebook_format})] = ''
940
- # Only fire SpineItemProcessor for top-level include directives
941
- include_processor SpineItemProcessor.new(document)
942
1581
  treeprocessor do
943
1582
  process do |doc|
944
1583
  doc.id = DocumentIdGenerator.generate_id doc, (doc.attr 'idprefix'), (doc.attr 'idseparator')