asciidoctor-epub3 1.5.0.alpha.14 → 1.5.0.alpha.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +49 -0
- data/Gemfile +1 -9
- data/LICENSE +21 -0
- data/README.adoc +44 -25
- data/asciidoctor-epub3.gemspec +7 -2
- data/data/styles/color-palette.css +7 -10
- data/data/styles/epub3-css3-only.css +1 -56
- data/data/styles/epub3.css +87 -81
- data/lib/asciidoctor-epub3/converter.rb +438 -185
- data/lib/asciidoctor-epub3/ext/asciidoctor.rb +1 -0
- data/lib/asciidoctor-epub3/ext/asciidoctor/document.rb +19 -0
- data/lib/asciidoctor-epub3/version.rb +1 -1
- metadata +77 -7
- data/.yardopts +0 -12
- data/LICENSE.adoc +0 -25
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'mime/types'
|
3
4
|
require 'open3'
|
4
5
|
require_relative 'font_icon_map'
|
5
6
|
|
@@ -29,7 +30,7 @@ module Asciidoctor
|
|
29
30
|
unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
|
30
31
|
::FileUtils.mkdir_p entry_dir
|
31
32
|
end
|
32
|
-
entry.extract
|
33
|
+
entry.extract entry.name
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -38,9 +39,9 @@ module Asciidoctor
|
|
38
39
|
|
39
40
|
if @format == :kf8
|
40
41
|
# QUESTION shouldn't we validate this epub file too?
|
41
|
-
distill_epub_to_mobi epub_file, target, @compress
|
42
|
+
distill_epub_to_mobi epub_file, target, @compress
|
42
43
|
elsif @validate
|
43
|
-
validate_epub epub_file
|
44
|
+
validate_epub epub_file
|
44
45
|
end
|
45
46
|
end
|
46
47
|
|
@@ -105,16 +106,45 @@ module Asciidoctor
|
|
105
106
|
send method_name, node
|
106
107
|
else
|
107
108
|
logger.warn %(conversion missing in backend #{@backend} for #{name})
|
109
|
+
nil
|
108
110
|
end
|
109
111
|
end
|
110
112
|
|
111
113
|
# See https://asciidoctor.org/docs/user-manual/#book-parts-and-chapters
|
112
114
|
def get_chapter_name node
|
113
115
|
if node.document.doctype != 'book'
|
114
|
-
return Asciidoctor::Document === node ? node.attr('docname') : nil
|
116
|
+
return Asciidoctor::Document === node ? node.attr('docname') || node.id : nil
|
115
117
|
end
|
116
118
|
return (node.id || 'preamble') if node.context == :preamble && node.level == 0
|
117
|
-
|
119
|
+
chapter_level = [node.document.attr('epub-chapter-level', 1).to_i, 1].max
|
120
|
+
Asciidoctor::Section === node && node.level <= chapter_level ? node.id : nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_numbered_title node
|
124
|
+
doc_attrs = node.document.attributes
|
125
|
+
level = node.level
|
126
|
+
if node.caption
|
127
|
+
title = node.captioned_title
|
128
|
+
elsif node.respond_to?(:numbered) && node.numbered && level <= (doc_attrs['sectnumlevels'] || 3).to_i
|
129
|
+
if level < 2 && node.document.doctype == 'book'
|
130
|
+
if node.sectname == 'chapter'
|
131
|
+
title = %(#{(signifier = doc_attrs['chapter-signifier']) ? "#{signifier} " : ''}#{node.sectnum} #{node.title})
|
132
|
+
elsif node.sectname == 'part'
|
133
|
+
title = %(#{(signifier = doc_attrs['part-signifier']) ? "#{signifier} " : ''}#{node.sectnum nil, ':'} #{node.title})
|
134
|
+
else
|
135
|
+
title = %(#{node.sectnum} #{node.title})
|
136
|
+
end
|
137
|
+
else
|
138
|
+
title = %(#{node.sectnum} #{node.title})
|
139
|
+
end
|
140
|
+
else
|
141
|
+
title = node.title
|
142
|
+
end
|
143
|
+
title
|
144
|
+
end
|
145
|
+
|
146
|
+
def icon_names
|
147
|
+
@icon_names ||= []
|
118
148
|
end
|
119
149
|
|
120
150
|
def convert_document node
|
@@ -126,8 +156,7 @@ module Asciidoctor
|
|
126
156
|
@kindlegen_path = node.attr 'ebook-kindlegen-path'
|
127
157
|
@epubcheck_path = node.attr 'ebook-epubcheck-path'
|
128
158
|
@xrefs_seen = ::Set.new
|
129
|
-
@
|
130
|
-
@images = []
|
159
|
+
@media_files = {}
|
131
160
|
@footnotes = []
|
132
161
|
|
133
162
|
@book = GEPUB::Book.new 'EPUB/package.opf'
|
@@ -182,41 +211,75 @@ module Asciidoctor
|
|
182
211
|
@book.metadata.add_metadata 'subject', s
|
183
212
|
end
|
184
213
|
|
185
|
-
|
186
|
-
|
214
|
+
if node.attr? 'series-name'
|
215
|
+
series_name = node.attr 'series-name'
|
216
|
+
series_volume = node.attr 'series-volume', 1
|
217
|
+
series_id = node.attr 'series-id'
|
218
|
+
|
219
|
+
series_meta = @book.metadata.add_metadata 'meta', series_name, id: 'pub-collection', group_position: series_volume
|
220
|
+
series_meta['property'] = 'belongs-to-collection'
|
221
|
+
series_meta.refine 'dcterms:identifier', series_id unless series_id.nil?
|
222
|
+
# Calibre only understands 'series'
|
223
|
+
series_meta.refine 'collection-type', 'series'
|
224
|
+
end
|
225
|
+
|
226
|
+
# For list of supported landmark types see
|
227
|
+
# https://idpf.github.io/epub-vocabs/structure/
|
228
|
+
landmarks = []
|
229
|
+
|
230
|
+
cover_page = add_cover_page node
|
231
|
+
landmarks << { type: 'cover', href: cover_page.href, title: 'Cover' } unless cover_page.nil?
|
232
|
+
|
233
|
+
front_matter_page = add_front_matter_page node
|
234
|
+
landmarks << { type: 'frontmatter', href: front_matter_page.href, title: 'Front Matter' } unless front_matter_page.nil?
|
235
|
+
|
236
|
+
nav_item = @book.add_item('nav.xhtml', id: 'nav').nav
|
237
|
+
|
238
|
+
toclevels = [(node.attr 'toclevels', 1).to_i, 0].max
|
239
|
+
outlinelevels = [(node.attr 'outlinelevels', toclevels).to_i, 0].max
|
240
|
+
|
241
|
+
if node.attr? 'toc'
|
242
|
+
toc_item = @book.add_ordered_item 'toc.xhtml', id: 'toc'
|
243
|
+
landmarks << { type: 'toc', href: toc_item.href, title: node.attr('toc-title') }
|
244
|
+
else
|
245
|
+
toc_item = nil
|
246
|
+
end
|
187
247
|
|
188
248
|
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
|
249
|
+
toc_items = node.sections
|
197
250
|
node.content
|
198
251
|
else
|
199
252
|
toc_items = [node]
|
200
253
|
add_chapter node
|
201
254
|
end
|
202
255
|
|
203
|
-
|
204
|
-
|
256
|
+
landmarks << { type: 'bodymatter', href: %(#{get_chapter_name toc_items[0]}.xhtml), title: 'Start of Content' } unless toc_items.empty?
|
257
|
+
|
258
|
+
toc_items.each do |item|
|
259
|
+
landmarks << { type: item.style, href: %(#{get_chapter_name item}.xhtml), title: item.title } if %w(appendix bibliography glossary index preface).include? item.style
|
260
|
+
end
|
261
|
+
|
262
|
+
nav_item.add_content postprocess_xhtml(nav_doc(node, toc_items, landmarks, outlinelevels))
|
263
|
+
# User is not supposed to see landmarks, so pass empty array here
|
264
|
+
toc_item&.add_content postprocess_xhtml(nav_doc(node, toc_items, [], toclevels))
|
205
265
|
|
206
266
|
# NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
|
207
|
-
toc_ncx = ncx_doc node, toc_items
|
267
|
+
toc_ncx = ncx_doc node, toc_items, outlinelevels
|
208
268
|
@book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
|
209
269
|
|
210
270
|
docimagesdir = (node.attr 'imagesdir', '.').chomp '/'
|
211
271
|
docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
|
212
272
|
|
213
|
-
@
|
214
|
-
if
|
215
|
-
logger.warn %(
|
216
|
-
elsif
|
217
|
-
|
273
|
+
@media_files.each do |name, file|
|
274
|
+
if name.start_with? %(#{docimagesdir}jacket/cover.)
|
275
|
+
logger.warn %(path is reserved for cover artwork: #{name}; skipping file found in content)
|
276
|
+
elsif file[:path].nil? || File.readable?(file[:path])
|
277
|
+
mime_types = MIME::Types.type_for name
|
278
|
+
mime_types.delete_if {|x| x.media_type != file[:media_type] }
|
279
|
+
preferred_mime_type = mime_types.empty? ? nil : mime_types[0].content_type
|
280
|
+
@book.add_item name, content: file[:path], media_type: preferred_mime_type
|
218
281
|
else
|
219
|
-
logger.error %(#{File.basename node.attr('docfile')}:
|
282
|
+
logger.error %(#{File.basename node.attr('docfile')}: media file not found or not readable: #{file[:path]})
|
220
283
|
end
|
221
284
|
end
|
222
285
|
|
@@ -267,25 +330,22 @@ module Asciidoctor
|
|
267
330
|
|
268
331
|
chapter_item = @book.add_ordered_item %(#{docid}.xhtml)
|
269
332
|
|
270
|
-
|
333
|
+
doctitle = node.document.doctitle partition: true, use_fallback: true
|
334
|
+
chapter_title = doctitle.combined
|
335
|
+
|
336
|
+
if node.context == :document && doctitle.subtitle?
|
271
337
|
title = %(#{doctitle.main} )
|
272
338
|
subtitle = doctitle.subtitle
|
273
339
|
elsif node.title
|
274
340
|
# HACK: until we get proper handling of title-only in CSS
|
275
341
|
title = ''
|
276
|
-
subtitle = node
|
342
|
+
subtitle = get_numbered_title node
|
343
|
+
chapter_title = subtitle
|
277
344
|
else
|
278
345
|
title = nil
|
279
346
|
subtitle = nil
|
280
347
|
end
|
281
348
|
|
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
|
288
|
-
|
289
349
|
if node.document.doctype == 'book'
|
290
350
|
byline = ''
|
291
351
|
else
|
@@ -303,10 +363,10 @@ module Asciidoctor
|
|
303
363
|
|
304
364
|
# NOTE must run after content is resolved
|
305
365
|
# TODO perhaps create dynamic CSS file?
|
306
|
-
if
|
366
|
+
if icon_names.empty?
|
307
367
|
icon_css_head = ''
|
308
368
|
else
|
309
|
-
icon_defs =
|
369
|
+
icon_defs = icon_names.map {|name|
|
310
370
|
%(.i-#{name}::before { content: "#{FontIconMap.unicode name}"; })
|
311
371
|
} * LF
|
312
372
|
icon_css_head = %(<style>
|
@@ -317,16 +377,20 @@ module Asciidoctor
|
|
317
377
|
|
318
378
|
header = (title || subtitle) ? %(<header>
|
319
379
|
<div class="chapter-header">
|
320
|
-
#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{
|
380
|
+
#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle}</small>) : ''}</h1>
|
321
381
|
</div>
|
322
382
|
</header>) : ''
|
323
383
|
|
384
|
+
# We want highlighter CSS to be stored in a separate file
|
385
|
+
# in order to avoid style duplication across chapter files
|
386
|
+
linkcss = true
|
387
|
+
|
324
388
|
# NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
|
325
389
|
lines = [%(<!DOCTYPE html>
|
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}">
|
390
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xmlns:mml="http://www.w3.org/1998/Math/MathML" xml:lang="#{lang = node.document.attr 'lang', 'en'}" lang="#{lang}">
|
327
391
|
<head>
|
328
392
|
<meta charset="UTF-8"/>
|
329
|
-
<title>#{
|
393
|
+
<title>#{chapter_title}</title>
|
330
394
|
<link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
|
331
395
|
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
332
396
|
#{icon_css_head}<script type="text/javascript"><![CDATA[
|
@@ -337,12 +401,18 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
337
401
|
}
|
338
402
|
document.body.setAttribute('class', reader.name.toLowerCase().replace(/ /g, '-'));
|
339
403
|
});
|
340
|
-
]]></script>
|
341
|
-
|
404
|
+
]]></script>)]
|
405
|
+
|
406
|
+
syntax_hl = node.document.syntax_highlighter
|
407
|
+
epub_type_attr = node.respond_to?(:section) && node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
|
408
|
+
|
409
|
+
lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :head
|
410
|
+
|
411
|
+
lines << %(</head>
|
342
412
|
<body>
|
343
|
-
<section class="chapter" title
|
413
|
+
<section class="chapter" title=#{chapter_title.encode xml: :attr}#{epub_type_attr} id="#{docid}">
|
344
414
|
#{header}
|
345
|
-
#{content})
|
415
|
+
#{content})
|
346
416
|
|
347
417
|
unless (fns = node.document.footnotes - @footnotes).empty?
|
348
418
|
@footnotes += fns
|
@@ -361,8 +431,11 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
361
431
|
</footer>'
|
362
432
|
end
|
363
433
|
|
364
|
-
lines << '</section>
|
365
|
-
|
434
|
+
lines << '</section>'
|
435
|
+
|
436
|
+
lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :footer
|
437
|
+
|
438
|
+
lines << '</body>
|
366
439
|
</html>'
|
367
440
|
|
368
441
|
chapter_item.add_content postprocess_xhtml lines * LF
|
@@ -378,11 +451,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
378
451
|
def convert_section node
|
379
452
|
if add_chapter(node).nil?
|
380
453
|
hlevel = node.level
|
381
|
-
epub_type_attr = node.
|
454
|
+
epub_type_attr = node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
|
382
455
|
div_classes = [%(sect#{node.level}), node.role].compact
|
383
|
-
title = node
|
384
|
-
|
385
|
-
%(<section class="#{div_classes * ' '}" title="#{title_sanitized}"#{epub_type_attr}>
|
456
|
+
title = get_numbered_title node
|
457
|
+
%(<section class="#{div_classes * ' '}" title=#{title.encode xml: :attr}#{epub_type_attr}>
|
386
458
|
<h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? '' : %(
|
387
459
|
#{content})}
|
388
460
|
</section>)
|
@@ -427,15 +499,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
427
499
|
end
|
428
500
|
|
429
501
|
def convert_paragraph node
|
502
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
430
503
|
role = node.role
|
431
504
|
# stack-head is the alternative to the default, inline-head (where inline means "run-in")
|
432
505
|
head_stop = node.attr 'head-stop', (role && (node.has_role? 'stack-head') ? nil : '.')
|
433
506
|
head = node.title? ? %(<strong class="head">#{title = node.title}#{head_stop && title !~ TrailingPunctRx ? head_stop : ''}</strong> ) : ''
|
434
507
|
if role
|
435
508
|
node.set_option 'hardbreaks' if node.has_role? 'signature'
|
436
|
-
%(<p class="#{role}">#{head}#{node.content}</p>)
|
509
|
+
%(<p#{id_attr} class="#{role}">#{head}#{node.content}</p>)
|
437
510
|
else
|
438
|
-
%(<p>#{head}#{node.content}</p>)
|
511
|
+
%(<p#{id_attr}>#{head}#{node.content}</p>)
|
439
512
|
end
|
440
513
|
end
|
441
514
|
|
@@ -464,14 +537,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
464
537
|
type = node.attr 'name'
|
465
538
|
epub_type = case type
|
466
539
|
when 'tip'
|
467
|
-
'
|
468
|
-
when 'note'
|
469
|
-
'
|
470
|
-
when 'important', 'warning', 'caution'
|
471
|
-
'warning'
|
540
|
+
'tip'
|
541
|
+
when 'important', 'warning', 'caution', 'note'
|
542
|
+
'notice'
|
472
543
|
else
|
473
544
|
logger.warn %(unknown admonition type: #{type})
|
474
|
-
'
|
545
|
+
'notice'
|
475
546
|
end
|
476
547
|
%(<aside#{id_attr} class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
|
477
548
|
#{title_el}<div class="content">
|
@@ -498,22 +569,64 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
498
569
|
end
|
499
570
|
|
500
571
|
def convert_listing node
|
572
|
+
id_attribute = node.id ? %( id="#{node.id}") : ''
|
573
|
+
nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap')
|
574
|
+
if node.style == 'source'
|
575
|
+
lang = node.attr 'language'
|
576
|
+
syntax_hl = node.document.syntax_highlighter
|
577
|
+
if syntax_hl
|
578
|
+
opts = syntax_hl.highlight? ? {
|
579
|
+
css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
|
580
|
+
style: doc_attrs[%(#{syntax_hl.name}-style)],
|
581
|
+
} : {}
|
582
|
+
opts[:nowrap] = nowrap
|
583
|
+
else
|
584
|
+
pre_open = %(<pre class="highlight#{nowrap ? ' nowrap' : ''}"><code#{lang ? %( class="language-#{lang}" data-lang="#{lang}") : ''}>)
|
585
|
+
pre_close = '</code></pre>'
|
586
|
+
end
|
587
|
+
else
|
588
|
+
pre_open = %(<pre#{nowrap ? ' class="nowrap"' : ''}>)
|
589
|
+
pre_close = '</pre>'
|
590
|
+
syntax_hl = nil
|
591
|
+
end
|
501
592
|
figure_classes = ['listing']
|
502
593
|
figure_classes << 'coalesce' if node.option? 'unbreakable'
|
503
|
-
|
504
|
-
|
505
|
-
) : ''
|
506
|
-
|
507
|
-
|
594
|
+
title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
595
|
+
%(<figure#{id_attribute} class="#{figure_classes * ' '}">#{title_div}
|
596
|
+
#{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close}
|
597
|
+
</figure>)
|
598
|
+
end
|
599
|
+
|
600
|
+
def convert_stem node
|
601
|
+
return convert_listing node if node.style != 'asciimath' || !asciimath_available?
|
602
|
+
|
603
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
604
|
+
title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
605
|
+
equation_data = AsciiMath.parse(node.content).to_mathml 'mml:'
|
606
|
+
|
607
|
+
%(<figure#{id_attr} class="#{prepend_space node.role}">
|
608
|
+
#{title_element}
|
609
|
+
<div class="content">
|
610
|
+
#{equation_data}
|
611
|
+
</div>
|
508
612
|
</figure>)
|
509
613
|
end
|
510
614
|
|
511
|
-
|
512
|
-
|
615
|
+
def asciimath_available?
|
616
|
+
(@asciimath_status ||= load_asciimath) == :loaded
|
617
|
+
end
|
618
|
+
|
619
|
+
def load_asciimath
|
620
|
+
Helpers.require_library('asciimath', true, :warn).nil? ? :unavailable : :loaded
|
621
|
+
end
|
513
622
|
|
514
|
-
# QUESTION should we wrap the <pre> in either <div> or <figure>?
|
515
623
|
def convert_literal node
|
516
|
-
%(
|
624
|
+
id_attribute = node.id ? %( id="#{node.id}") : ''
|
625
|
+
title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
626
|
+
%(<figure#{id_attribute} class="literalblock#{prepend_space node.role}">
|
627
|
+
#{title_element}
|
628
|
+
<div class="content"><pre class="screen">#{node.content}</pre></div>
|
629
|
+
</figure>)
|
517
630
|
end
|
518
631
|
|
519
632
|
def convert_page_break _node
|
@@ -595,41 +708,34 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
595
708
|
lines = [%(<div class="table">)]
|
596
709
|
lines << %(<div class="content">)
|
597
710
|
table_id_attr = node.id ? %( id="#{node.id}") : ''
|
598
|
-
|
599
|
-
'
|
600
|
-
'
|
601
|
-
'
|
602
|
-
|
603
|
-
}
|
604
|
-
grid_class = {
|
605
|
-
'all' => 'table-grid',
|
606
|
-
'rows' => 'table-grid-rows',
|
607
|
-
'cols' => 'table-grid-cols',
|
608
|
-
'none' => '',
|
609
|
-
}
|
610
|
-
table_classes = %W[table #{frame_class[node.attr 'frame'] || frame_class['topbot']} #{grid_class[node.attr 'grid'] || grid_class['rows']}]
|
711
|
+
table_classes = [
|
712
|
+
'table',
|
713
|
+
%(table-framed-#{node.attr 'frame', 'rows', 'table-frame'}),
|
714
|
+
%(table-grid-#{node.attr 'grid', 'rows', 'table-grid'}),
|
715
|
+
]
|
611
716
|
if (role = node.role)
|
612
717
|
table_classes << role
|
613
718
|
end
|
614
|
-
table_class_attr = %( class="#{table_classes * ' '}")
|
615
719
|
table_styles = []
|
616
|
-
|
720
|
+
if (autowidth = node.option? 'autowidth') && !(node.attr? 'width')
|
721
|
+
table_classes << 'fit-content'
|
722
|
+
else
|
723
|
+
table_styles << %(width: #{node.attr 'tablepcwidth'}%;)
|
724
|
+
end
|
725
|
+
table_class_attr = %( class="#{table_classes * ' '}")
|
617
726
|
table_style_attr = !table_styles.empty? ? %( style="#{table_styles * '; '}") : ''
|
618
727
|
|
619
728
|
lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>)
|
620
729
|
lines << %(<caption>#{node.captioned_title}</caption>) if node.title?
|
621
730
|
if (node.attr 'rowcount') > 0
|
622
731
|
lines << '<colgroup>'
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
732
|
+
if autowidth
|
733
|
+
lines += (Array.new node.columns.size, %(<col/>))
|
734
|
+
else
|
735
|
+
node.columns.each do |col|
|
736
|
+
lines << ((col.option? 'autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />))
|
737
|
+
end
|
627
738
|
end
|
628
|
-
#else
|
629
|
-
# node.columns.each do |col|
|
630
|
-
# lines << %(<col style="width: #{col.attr 'colpcwidth'}%"/>)
|
631
|
-
# end
|
632
|
-
#end
|
633
739
|
lines << '</colgroup>'
|
634
740
|
[:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
|
635
741
|
lines << %(<t#{tsec}>)
|
@@ -649,19 +755,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
649
755
|
else
|
650
756
|
cell_content = ''
|
651
757
|
cell.content.each do |text|
|
652
|
-
cell_content = %(#{cell_content}<p>#{text}</p>)
|
758
|
+
cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
|
653
759
|
end
|
654
760
|
end
|
655
761
|
end
|
656
762
|
|
657
763
|
cell_tag_name = tsec == :head || cell.style == :header ? 'th' : 'td'
|
658
|
-
cell_classes = [
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
if (halign = cell.attr 'valign') && halign != 'top'
|
663
|
-
cell_classes << 'valign-top'
|
664
|
-
end
|
764
|
+
cell_classes = [
|
765
|
+
"halign-#{cell.attr 'halign'}",
|
766
|
+
"valign-#{cell.attr 'valign'}",
|
767
|
+
]
|
665
768
|
cell_class_attr = !cell_classes.empty? ? %( class="#{cell_classes * ' '}") : ''
|
666
769
|
cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : ''
|
667
770
|
cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
|
@@ -694,16 +797,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
694
797
|
# TODO: add complex class if list has nested blocks
|
695
798
|
def convert_dlist node
|
696
799
|
lines = []
|
800
|
+
id_attribute = node.id ? %( id="#{node.id}") : ''
|
801
|
+
|
802
|
+
classes = case node.style
|
803
|
+
when 'horizontal'
|
804
|
+
['hdlist', node.role]
|
805
|
+
when 'itemized', 'ordered'
|
806
|
+
# QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
|
807
|
+
['dlist', %(#{node.style}-list), node.role]
|
808
|
+
else
|
809
|
+
['description-list']
|
810
|
+
end.compact
|
811
|
+
|
812
|
+
class_attribute = %( class="#{classes.join ' '}")
|
813
|
+
|
814
|
+
lines << %(<div#{id_attribute}#{class_attribute}>)
|
815
|
+
lines << %(<div class="title">#{node.title}</div>) if node.title?
|
816
|
+
|
697
817
|
case (style = node.style)
|
698
818
|
when 'itemized', 'ordered'
|
699
819
|
list_tag_name = style == 'itemized' ? 'ul' : 'ol'
|
700
820
|
role = node.role
|
701
821
|
subject_stop = node.attr 'subject-stop', (role && (node.has_role? 'stack') ? nil : ':')
|
702
|
-
# QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
|
703
|
-
div_classes = [%(#{style}-list), role].compact
|
704
822
|
list_class_attr = (node.option? 'brief') ? ' class="brief"' : ''
|
705
|
-
lines << %(
|
706
|
-
<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
|
823
|
+
lines << %(<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
|
707
824
|
node.items.each do |subjects, dd|
|
708
825
|
# consists of one term (a subject) and supporting content
|
709
826
|
subject = [*subjects].first.text
|
@@ -719,11 +836,40 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
719
836
|
end
|
720
837
|
lines << '</li>'
|
721
838
|
end
|
722
|
-
lines << %(</#{list_tag_name}>
|
723
|
-
|
839
|
+
lines << %(</#{list_tag_name}>)
|
840
|
+
when 'horizontal'
|
841
|
+
lines << '<table>'
|
842
|
+
if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
|
843
|
+
lines << '<colgroup>'
|
844
|
+
col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
|
845
|
+
lines << %(<col#{col_style_attribute} />)
|
846
|
+
col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
|
847
|
+
lines << %(<col#{col_style_attribute} />)
|
848
|
+
lines << '</colgroup>'
|
849
|
+
end
|
850
|
+
node.items.each do |terms, dd|
|
851
|
+
lines << '<tr>'
|
852
|
+
lines << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : ''}">)
|
853
|
+
first_term = true
|
854
|
+
terms.each do |dt|
|
855
|
+
lines << %(<br />) unless first_term
|
856
|
+
lines << '<p>'
|
857
|
+
lines << dt.text
|
858
|
+
lines << '</p>'
|
859
|
+
first_term = nil
|
860
|
+
end
|
861
|
+
lines << '</td>'
|
862
|
+
lines << '<td class="hdlist2">'
|
863
|
+
if dd
|
864
|
+
lines << %(<p>#{dd.text}</p>) if dd.text?
|
865
|
+
lines << dd.content if dd.blocks?
|
866
|
+
end
|
867
|
+
lines << '</td>'
|
868
|
+
lines << '</tr>'
|
869
|
+
end
|
870
|
+
lines << '</table>'
|
724
871
|
else
|
725
|
-
lines << '<
|
726
|
-
<dl>'
|
872
|
+
lines << '<dl>'
|
727
873
|
node.items.each do |terms, dd|
|
728
874
|
[*terms].each do |dt|
|
729
875
|
lines << %(<dt>
|
@@ -740,9 +886,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
740
886
|
end
|
741
887
|
lines << '</dd>'
|
742
888
|
end
|
743
|
-
lines << '</dl>
|
744
|
-
</div>'
|
889
|
+
lines << '</dl>'
|
745
890
|
end
|
891
|
+
|
892
|
+
lines << '</div>'
|
746
893
|
lines * LF
|
747
894
|
end
|
748
895
|
|
@@ -816,22 +963,29 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
816
963
|
document
|
817
964
|
end
|
818
965
|
|
819
|
-
def
|
820
|
-
if target.end_with?
|
966
|
+
def register_media_file node, target, media_type
|
967
|
+
if target.end_with?('.svg') || target.start_with?('data:image/svg+xml')
|
821
968
|
chapter = get_enclosing_chapter node
|
822
969
|
chapter.set_attr 'epub-properties', [] unless chapter.attr? 'epub-properties'
|
823
970
|
epub_properties = chapter.attr 'epub-properties'
|
824
971
|
epub_properties << 'svg' unless epub_properties.include? 'svg'
|
825
972
|
end
|
826
973
|
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
fs_path =
|
974
|
+
return if target.start_with? 'data:'
|
975
|
+
|
976
|
+
if Asciidoctor::Helpers.uriish? target
|
977
|
+
# We need to add both local and remote media files to manifest
|
978
|
+
fs_path = nil
|
979
|
+
else
|
980
|
+
out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
|
981
|
+
fs_path = (::File.join out_dir, target)
|
982
|
+
unless ::File.exist? fs_path
|
983
|
+
base_dir = root_document(node.document).base_dir
|
984
|
+
fs_path = ::File.join base_dir, target
|
985
|
+
end
|
832
986
|
end
|
833
987
|
# We need *both* virtual and physical image paths. Unfortunately, references[:images] only has one of them.
|
834
|
-
@
|
988
|
+
@media_files[target] ||= { path: fs_path, media_type: media_type }
|
835
989
|
end
|
836
990
|
|
837
991
|
def resolve_image_attrs node
|
@@ -848,16 +1002,81 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
848
1002
|
img_attrs
|
849
1003
|
end
|
850
1004
|
|
1005
|
+
def convert_audio node
|
1006
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
1007
|
+
target = node.media_uri node.attr 'target'
|
1008
|
+
register_media_file node, target, 'audio'
|
1009
|
+
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
1010
|
+
|
1011
|
+
autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
|
1012
|
+
controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
|
1013
|
+
loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
|
1014
|
+
|
1015
|
+
start_t = node.attr 'start'
|
1016
|
+
end_t = node.attr 'end'
|
1017
|
+
if start_t || end_t
|
1018
|
+
time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
|
1019
|
+
else
|
1020
|
+
time_anchor = ''
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
%(<figure#{id_attr} class="audioblock#{prepend_space node.role}">#{title_element}
|
1024
|
+
<div class="content">
|
1025
|
+
<audio src="#{target}#{time_anchor}"#{autoplay_attr}#{controls_attr}#{loop_attr}>
|
1026
|
+
<div>Your Reading System does not support (this) audio.</div>
|
1027
|
+
</audio>
|
1028
|
+
</div>
|
1029
|
+
</figure>)
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
# TODO: Support multiple video files in different formats for a single video
|
1033
|
+
def convert_video node
|
1034
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
1035
|
+
target = node.media_uri node.attr 'target'
|
1036
|
+
register_media_file node, target, 'video'
|
1037
|
+
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
1038
|
+
|
1039
|
+
width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
|
1040
|
+
height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : ''
|
1041
|
+
autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
|
1042
|
+
controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
|
1043
|
+
loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
|
1044
|
+
|
1045
|
+
start_t = node.attr 'start'
|
1046
|
+
end_t = node.attr 'end'
|
1047
|
+
if start_t || end_t
|
1048
|
+
time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
|
1049
|
+
else
|
1050
|
+
time_anchor = ''
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
if (poster = node.attr 'poster').nil_or_empty?
|
1054
|
+
poster_attr = ''
|
1055
|
+
else
|
1056
|
+
poster = node.media_uri poster
|
1057
|
+
register_media_file node, poster, 'image'
|
1058
|
+
poster_attr = %( poster="#{poster}")
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
%(<figure#{id_attr} class="video#{prepend_space node.role}">#{title_element}
|
1062
|
+
<div class="content">
|
1063
|
+
<video src="#{target}#{time_anchor}"#{width_attr}#{height_attr}#{autoplay_attr}#{poster_attr}#{controls_attr}#{loop_attr}>
|
1064
|
+
<div>Your Reading System does not support (this) video.</div>
|
1065
|
+
</video>
|
1066
|
+
</div>
|
1067
|
+
</figure>)
|
1068
|
+
end
|
1069
|
+
|
851
1070
|
def convert_image node
|
852
1071
|
target = node.image_uri node.attr 'target'
|
853
|
-
|
1072
|
+
register_media_file node, target, 'image'
|
854
1073
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
1074
|
+
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
855
1075
|
img_attrs = resolve_image_attrs node
|
856
1076
|
%(<figure#{id_attr} class="image#{prepend_space node.role}">
|
857
1077
|
<div class="content">
|
858
1078
|
<img src="#{target}"#{prepend_space img_attrs * ' '} />
|
859
|
-
</div>#{
|
860
|
-
<figcaption>#{node.captioned_title}</figcaption>) : ''}
|
1079
|
+
</div>#{title_element}
|
861
1080
|
</figure>)
|
862
1081
|
end
|
863
1082
|
|
@@ -901,7 +1120,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
901
1120
|
end
|
902
1121
|
|
903
1122
|
id_attr = '' unless @xrefs_seen.add? refid
|
904
|
-
text
|
1123
|
+
text ||= (ref.xreftext node.attr('xrefstyle', nil, true))
|
905
1124
|
else
|
906
1125
|
logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to unknown anchor: #{refid})
|
907
1126
|
end
|
@@ -953,7 +1172,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
953
1172
|
|
954
1173
|
def convert_inline_image node
|
955
1174
|
if node.type == 'icon'
|
956
|
-
|
1175
|
+
icon_names << (icon_name = node.target)
|
957
1176
|
i_classes = ['icon', %(i-#{icon_name})]
|
958
1177
|
i_classes << %(icon-#{node.attr 'size'}) if node.attr? 'size'
|
959
1178
|
i_classes << %(icon-flip-#{(node.attr 'flip')[0]}) if node.attr? 'flip'
|
@@ -962,7 +1181,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
962
1181
|
%(<i class="#{i_classes * ' '}"></i>)
|
963
1182
|
else
|
964
1183
|
target = node.image_uri node.target
|
965
|
-
|
1184
|
+
register_media_file node, target, 'image'
|
966
1185
|
|
967
1186
|
img_attrs = resolve_image_attrs node
|
968
1187
|
img_attrs << %(class="inline#{prepend_space node.role}")
|
@@ -1000,25 +1219,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1000
1219
|
def convert_inline_quoted node
|
1001
1220
|
open, close, tag = QUOTE_TAGS[node.type]
|
1002
1221
|
|
1003
|
-
|
1222
|
+
if node.type == :asciimath && asciimath_available?
|
1223
|
+
content = AsciiMath.parse(node.text).to_mathml 'mml:'
|
1224
|
+
else
|
1225
|
+
content = node.text
|
1226
|
+
end
|
1227
|
+
|
1004
1228
|
node.add_role 'literal' if [:monospaced, :asciimath, :latexmath].include? node.type
|
1005
1229
|
|
1006
1230
|
if node.id
|
1007
1231
|
class_attr = class_string node
|
1008
1232
|
if tag
|
1009
|
-
%(#{open.chop} id="#{node.id}"#{class_attr}>#{
|
1233
|
+
%(#{open.chop} id="#{node.id}"#{class_attr}>#{content}#{close})
|
1010
1234
|
else
|
1011
|
-
%(<span id="#{node.id}"#{class_attr}>#{open}#{
|
1235
|
+
%(<span id="#{node.id}"#{class_attr}>#{open}#{content}#{close}</span>)
|
1012
1236
|
end
|
1013
1237
|
elsif role_valid_class? node.role
|
1014
1238
|
class_attr = class_string node
|
1015
1239
|
if tag
|
1016
|
-
%(#{open.chop}#{class_attr}>#{
|
1240
|
+
%(#{open.chop}#{class_attr}>#{content}#{close})
|
1017
1241
|
else
|
1018
|
-
%(<span#{class_attr}>#{open}#{
|
1242
|
+
%(<span#{class_attr}>#{open}#{content}#{close}</span>)
|
1019
1243
|
end
|
1020
1244
|
else
|
1021
|
-
%(#{open}#{
|
1245
|
+
%(#{open}#{content}#{close})
|
1022
1246
|
end
|
1023
1247
|
end
|
1024
1248
|
|
@@ -1082,6 +1306,19 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1082
1306
|
@book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
|
1083
1307
|
end
|
1084
1308
|
|
1309
|
+
syntax_hl = doc.syntax_highlighter
|
1310
|
+
if syntax_hl&.write_stylesheet? doc
|
1311
|
+
Dir.mktmpdir do |dir|
|
1312
|
+
syntax_hl.write_stylesheet doc, dir
|
1313
|
+
Pathname.glob(dir + '/**/*').map do |filename|
|
1314
|
+
# Workaround for https://github.com/skoji/gepub/pull/117
|
1315
|
+
filename.open do |f|
|
1316
|
+
@book.add_item filename.basename.to_s, content: f
|
1317
|
+
end if filename.file?
|
1318
|
+
end
|
1319
|
+
end
|
1320
|
+
end
|
1321
|
+
|
1085
1322
|
font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
|
1086
1323
|
@book.add_item 'styles/epub3-fonts.css', content: font_css
|
1087
1324
|
unless font_files.empty?
|
@@ -1101,8 +1338,8 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1101
1338
|
nil
|
1102
1339
|
end
|
1103
1340
|
|
1104
|
-
def
|
1105
|
-
return if (image_path = doc.attr 'front-cover-image').nil?
|
1341
|
+
def add_cover_page doc
|
1342
|
+
return nil if (image_path = doc.attr 'front-cover-image').nil?
|
1106
1343
|
|
1107
1344
|
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
1108
1345
|
imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
|
@@ -1119,20 +1356,21 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1119
1356
|
workdir = doc.attr 'docdir'
|
1120
1357
|
workdir = '.' if workdir.nil_or_empty?
|
1121
1358
|
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1359
|
+
begin
|
1360
|
+
@book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
|
1361
|
+
rescue => e
|
1362
|
+
logger.error %(#{::File.basename doc.attr('docfile')}: error adding front cover image. Make sure that :front-cover-image: attribute points to a valid image file. #{e})
|
1363
|
+
return nil
|
1125
1364
|
end
|
1126
1365
|
|
1366
|
+
return nil if @format == :kf8
|
1367
|
+
|
1127
1368
|
unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
|
1128
1369
|
width, height = 1050, 1600
|
1129
1370
|
end
|
1130
1371
|
|
1131
|
-
|
1132
|
-
|
1133
|
-
unless @format == :kf8
|
1134
|
-
# NOTE SVG wrapper maintains aspect ratio and confines image to view box
|
1135
|
-
content = %(<!DOCTYPE html>
|
1372
|
+
# NOTE SVG wrapper maintains aspect ratio and confines image to view box
|
1373
|
+
content = %(<!DOCTYPE html>
|
1136
1374
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
|
1137
1375
|
<head>
|
1138
1376
|
<meta charset="UTF-8"/>
|
@@ -1162,10 +1400,7 @@ body > svg {
|
|
1162
1400
|
</svg></body>
|
1163
1401
|
</html>).to_ios
|
1164
1402
|
|
1165
|
-
|
1166
|
-
@book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
|
1167
|
-
end
|
1168
|
-
nil
|
1403
|
+
@book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
|
1169
1404
|
end
|
1170
1405
|
|
1171
1406
|
def get_frontmatter_files doc, workdir
|
@@ -1195,19 +1430,22 @@ body > svg {
|
|
1195
1430
|
workdir = doc.attr 'docdir'
|
1196
1431
|
workdir = '.' if workdir.nil_or_empty?
|
1197
1432
|
|
1433
|
+
result = nil
|
1198
1434
|
get_frontmatter_files(doc, workdir).each do |front_matter|
|
1199
1435
|
front_matter_content = ::File.read front_matter
|
1200
1436
|
|
1201
1437
|
front_matter_file = File.basename front_matter, '.html'
|
1202
1438
|
item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content)
|
1203
1439
|
item.add_property 'svg' if SvgImgSniffRx =~ front_matter_content
|
1440
|
+
# Store link to first frontmatter page
|
1441
|
+
result = item if result.nil?
|
1204
1442
|
|
1205
1443
|
front_matter_content.scan ImgSrcScanRx do
|
1206
1444
|
@book.add_item $1, content: File.join(File.dirname(front_matter), $1)
|
1207
1445
|
end
|
1208
1446
|
end
|
1209
1447
|
|
1210
|
-
|
1448
|
+
result
|
1211
1449
|
end
|
1212
1450
|
|
1213
1451
|
def add_profile_images doc, usernames
|
@@ -1239,8 +1477,7 @@ body > svg {
|
|
1239
1477
|
nil
|
1240
1478
|
end
|
1241
1479
|
|
1242
|
-
|
1243
|
-
def nav_doc doc, items
|
1480
|
+
def nav_doc doc, items, landmarks, depth
|
1244
1481
|
lines = [%(<!DOCTYPE html>
|
1245
1482
|
<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
1483
|
<head>
|
@@ -1250,13 +1487,29 @@ body > svg {
|
|
1250
1487
|
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
1251
1488
|
</head>
|
1252
1489
|
<body>
|
1253
|
-
<
|
1254
|
-
<
|
1255
|
-
<
|
1256
|
-
|
1257
|
-
|
1490
|
+
<section class="chapter">
|
1491
|
+
<header>
|
1492
|
+
<div class="chapter-header"><h1 class="chapter-title"><small class="subtitle">#{doc.attr 'toc-title'}</small></h1></div>
|
1493
|
+
</header>
|
1494
|
+
<nav epub:type="toc" id="toc">)]
|
1495
|
+
lines << (nav_level items, [depth, 0].max)
|
1496
|
+
lines << '</nav>'
|
1497
|
+
|
1498
|
+
unless landmarks.empty?
|
1499
|
+
lines << '
|
1500
|
+
<nav epub:type="landmarks" id="landmarks" hidden="hidden">
|
1501
|
+
<ol>'
|
1502
|
+
landmarks.each do |landmark|
|
1503
|
+
lines << %(<li><a epub:type="#{landmark[:type]}" href="#{landmark[:href]}">#{landmark[:title]}</a></li>)
|
1504
|
+
end
|
1505
|
+
lines << '
|
1506
|
+
</ol>
|
1507
|
+
</nav>'
|
1508
|
+
end
|
1509
|
+
lines << '
|
1510
|
+
</section>
|
1258
1511
|
</body>
|
1259
|
-
</html>
|
1512
|
+
</html>'
|
1260
1513
|
lines * LF
|
1261
1514
|
end
|
1262
1515
|
|
@@ -1266,14 +1519,14 @@ body > svg {
|
|
1266
1519
|
items.each do |item|
|
1267
1520
|
#index = (state[:index] = (state.fetch :index, 0) + 1)
|
1268
1521
|
if (chapter_name = get_chapter_name item).nil?
|
1269
|
-
item_label = sanitize_xml item
|
1522
|
+
item_label = sanitize_xml get_numbered_title(item), :pcdata
|
1270
1523
|
item_href = %(#{state[:content_doc_href]}##{item.id})
|
1271
1524
|
else
|
1272
1525
|
# NOTE we sanitize the chapter titles because we use formatting to control layout
|
1273
1526
|
if item.context == :document
|
1274
1527
|
item_label = sanitize_doctitle_xml item, :cdata
|
1275
1528
|
else
|
1276
|
-
item_label = sanitize_xml item
|
1529
|
+
item_label = sanitize_xml get_numbered_title(item), :cdata
|
1277
1530
|
end
|
1278
1531
|
item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
|
1279
1532
|
end
|
@@ -1290,7 +1543,7 @@ body > svg {
|
|
1290
1543
|
lines * LF
|
1291
1544
|
end
|
1292
1545
|
|
1293
|
-
def ncx_doc doc, items
|
1546
|
+
def ncx_doc doc, items, depth
|
1294
1547
|
# TODO: populate docAuthor element based on unique authors in work
|
1295
1548
|
lines = [%(<?xml version="1.0" encoding="utf-8"?>
|
1296
1549
|
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
|
@@ -1302,7 +1555,7 @@ body > svg {
|
|
1302
1555
|
</head>
|
1303
1556
|
<docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
|
1304
1557
|
<navMap>)]
|
1305
|
-
lines << (ncx_level items,
|
1558
|
+
lines << (ncx_level items, depth, state = {})
|
1306
1559
|
lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
|
1307
1560
|
lines << %(</navMap>
|
1308
1561
|
</ncx>)
|
@@ -1316,13 +1569,13 @@ body > svg {
|
|
1316
1569
|
index = (state[:index] = (state.fetch :index, 0) + 1)
|
1317
1570
|
item_id = %(nav_#{index})
|
1318
1571
|
if (chapter_name = get_chapter_name item).nil?
|
1319
|
-
item_label = sanitize_xml item
|
1572
|
+
item_label = sanitize_xml get_numbered_title(item), :cdata
|
1320
1573
|
item_href = %(#{state[:content_doc_href]}##{item.id})
|
1321
1574
|
else
|
1322
1575
|
if item.context == :document
|
1323
1576
|
item_label = sanitize_doctitle_xml item, :cdata
|
1324
1577
|
else
|
1325
|
-
item_label = sanitize_xml item
|
1578
|
+
item_label = sanitize_xml get_numbered_title(item), :cdata
|
1326
1579
|
end
|
1327
1580
|
item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
|
1328
1581
|
end
|
@@ -1380,10 +1633,10 @@ body > svg {
|
|
1380
1633
|
.to_ios
|
1381
1634
|
end
|
1382
1635
|
|
1383
|
-
def get_kindlegen_command
|
1384
|
-
unless kindlegen_path.nil?
|
1385
|
-
logger.debug %(Using ebook-kindlegen-path attribute: #{kindlegen_path})
|
1386
|
-
return [kindlegen_path]
|
1636
|
+
def get_kindlegen_command
|
1637
|
+
unless @kindlegen_path.nil?
|
1638
|
+
logger.debug %(Using ebook-kindlegen-path attribute: #{@kindlegen_path})
|
1639
|
+
return [@kindlegen_path]
|
1387
1640
|
end
|
1388
1641
|
|
1389
1642
|
unless (result = ENV['KINDLEGEN']).nil?
|
@@ -1391,22 +1644,15 @@ body > svg {
|
|
1391
1644
|
return [result]
|
1392
1645
|
end
|
1393
1646
|
|
1394
|
-
|
1395
|
-
|
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
|
1647
|
+
logger.debug 'Using KindleGen from PATH'
|
1648
|
+
[%(kindlegen#{::Gem.win_platform? ? '.exe' : ''})]
|
1403
1649
|
end
|
1404
1650
|
|
1405
|
-
def distill_epub_to_mobi epub_file, target, compress
|
1651
|
+
def distill_epub_to_mobi epub_file, target, compress
|
1406
1652
|
mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
|
1407
1653
|
compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
|
1408
1654
|
|
1409
|
-
argv = get_kindlegen_command
|
1655
|
+
argv = get_kindlegen_command + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
|
1410
1656
|
begin
|
1411
1657
|
# This duplicates Kindlegen.run, but we want to override executable
|
1412
1658
|
out, err, res = Open3.capture3(*argv) do |r|
|
@@ -1431,10 +1677,10 @@ body > svg {
|
|
1431
1677
|
end
|
1432
1678
|
end
|
1433
1679
|
|
1434
|
-
def get_epubcheck_command
|
1435
|
-
unless epubcheck_path.nil?
|
1436
|
-
logger.debug %(Using ebook-epubcheck-path attribute: #{epubcheck_path})
|
1437
|
-
return [epubcheck_path]
|
1680
|
+
def get_epubcheck_command
|
1681
|
+
unless @epubcheck_path.nil?
|
1682
|
+
logger.debug %(Using ebook-epubcheck-path attribute: #{@epubcheck_path})
|
1683
|
+
return [@epubcheck_path]
|
1438
1684
|
end
|
1439
1685
|
|
1440
1686
|
unless (result = ENV['EPUBCHECK']).nil?
|
@@ -1452,8 +1698,8 @@ body > svg {
|
|
1452
1698
|
end
|
1453
1699
|
end
|
1454
1700
|
|
1455
|
-
def validate_epub epub_file
|
1456
|
-
argv = get_epubcheck_command
|
1701
|
+
def validate_epub epub_file
|
1702
|
+
argv = get_epubcheck_command + ['-w', epub_file]
|
1457
1703
|
begin
|
1458
1704
|
out, err, res = Open3.capture3(*argv)
|
1459
1705
|
rescue Errno::ENOENT => e
|
@@ -1510,6 +1756,7 @@ body > svg {
|
|
1510
1756
|
InvalidIdCharsRx = /[^[:word:]]+/
|
1511
1757
|
LeadingDigitRx = /^[[:digit:]]/
|
1512
1758
|
end
|
1759
|
+
|
1513
1760
|
class << self
|
1514
1761
|
def generate_id doc, pre = nil, sep = nil
|
1515
1762
|
synthetic = false
|
@@ -1561,13 +1808,19 @@ body > svg {
|
|
1561
1808
|
Extensions.register do
|
1562
1809
|
if (document = @document).backend == 'epub3'
|
1563
1810
|
document.set_attribute 'listing-caption', 'Listing'
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1811
|
+
|
1812
|
+
# TODO: bw theme for CodeRay
|
1813
|
+
document.set_attribute 'pygments-style', 'bw' unless document.attr? 'pygments-style'
|
1814
|
+
document.set_attribute 'rouge-style', 'bw' unless document.attr? 'rouge-style'
|
1815
|
+
|
1816
|
+
# Old asciidoctor versions do not have public API for writing highlighter CSS file
|
1817
|
+
# So just use inline CSS there.
|
1818
|
+
unless Document.supports_syntax_highlighter?
|
1819
|
+
document.set_attribute 'coderay-css', 'style'
|
1820
|
+
document.set_attribute 'pygments-css', 'style'
|
1821
|
+
document.set_attribute 'rouge-css', 'style'
|
1570
1822
|
end
|
1823
|
+
|
1571
1824
|
case (ebook_format = document.attributes['ebook-format'])
|
1572
1825
|
when 'epub3', 'kf8'
|
1573
1826
|
# all good
|