asciidoctor-epub3 1.5.0.alpha.16 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +40 -0
- data/Gemfile +3 -4
- data/LICENSE +2 -2
- data/NOTICE.adoc +1 -1
- data/README.adoc +49 -771
- data/asciidoctor-epub3.gemspec +5 -4
- data/data/fonts/awesome/fa-solid-900.ttf +0 -0
- data/data/fonts/awesome/icons.yml +873 -26
- data/data/styles/color-palette.css +7 -10
- data/data/styles/epub3-css3-only.css +0 -50
- data/data/styles/epub3.css +67 -68
- data/lib/asciidoctor-epub3/converter.rb +238 -148
- 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 +28 -13
@@ -39,9 +39,9 @@ module Asciidoctor
|
|
39
39
|
|
40
40
|
if @format == :kf8
|
41
41
|
# QUESTION shouldn't we validate this epub file too?
|
42
|
-
distill_epub_to_mobi epub_file, target, @compress
|
42
|
+
distill_epub_to_mobi epub_file, target, @compress
|
43
43
|
elsif @validate
|
44
|
-
validate_epub epub_file
|
44
|
+
validate_epub epub_file
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -105,7 +105,7 @@ module Asciidoctor
|
|
105
105
|
if respond_to? method_name
|
106
106
|
send method_name, node
|
107
107
|
else
|
108
|
-
logger.warn %(
|
108
|
+
logger.warn %(conversion missing in backend #{@backend} for #{name})
|
109
109
|
nil
|
110
110
|
end
|
111
111
|
end
|
@@ -143,6 +143,10 @@ module Asciidoctor
|
|
143
143
|
title
|
144
144
|
end
|
145
145
|
|
146
|
+
def icon_names
|
147
|
+
@icon_names ||= []
|
148
|
+
end
|
149
|
+
|
146
150
|
def convert_document node
|
147
151
|
@format = node.attr('ebook-format').to_sym
|
148
152
|
|
@@ -152,8 +156,7 @@ module Asciidoctor
|
|
152
156
|
@kindlegen_path = node.attr 'ebook-kindlegen-path'
|
153
157
|
@epubcheck_path = node.attr 'ebook-epubcheck-path'
|
154
158
|
@xrefs_seen = ::Set.new
|
155
|
-
@
|
156
|
-
@media_files = []
|
159
|
+
@media_files = {}
|
157
160
|
@footnotes = []
|
158
161
|
|
159
162
|
@book = GEPUB::Book.new 'EPUB/package.opf'
|
@@ -220,8 +223,27 @@ module Asciidoctor
|
|
220
223
|
series_meta.refine 'collection-type', 'series'
|
221
224
|
end
|
222
225
|
|
223
|
-
|
224
|
-
|
226
|
+
# For list of supported landmark types see
|
227
|
+
# https://idpf.github.io/epub-vocabs/structure/
|
228
|
+
landmarks = []
|
229
|
+
|
230
|
+
front_cover = add_cover_page node, 'front-cover'
|
231
|
+
landmarks << { type: 'cover', href: front_cover.href, title: 'Front Cover' } unless front_cover.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
|
225
247
|
|
226
248
|
if node.doctype == 'book'
|
227
249
|
toc_items = node.sections
|
@@ -231,24 +253,34 @@ module Asciidoctor
|
|
231
253
|
add_chapter node
|
232
254
|
end
|
233
255
|
|
234
|
-
|
235
|
-
|
256
|
+
_back_cover = add_cover_page node, 'back-cover'
|
257
|
+
# TODO: add landmark for back cover? But what epub:type?
|
258
|
+
|
259
|
+
landmarks << { type: 'bodymatter', href: %(#{get_chapter_name toc_items[0]}.xhtml), title: 'Start of Content' } unless toc_items.empty?
|
260
|
+
|
261
|
+
toc_items.each do |item|
|
262
|
+
landmarks << { type: item.style, href: %(#{get_chapter_name item}.xhtml), title: item.title } if %w(appendix bibliography glossary index preface).include? item.style
|
263
|
+
end
|
264
|
+
|
265
|
+
nav_item.add_content postprocess_xhtml(nav_doc(node, toc_items, landmarks, outlinelevels))
|
266
|
+
# User is not supposed to see landmarks, so pass empty array here
|
267
|
+
toc_item&.add_content postprocess_xhtml(nav_doc(node, toc_items, [], toclevels))
|
236
268
|
|
237
269
|
# NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
|
238
|
-
toc_ncx = ncx_doc node, toc_items
|
270
|
+
toc_ncx = ncx_doc node, toc_items, outlinelevels
|
239
271
|
@book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
|
240
272
|
|
241
273
|
docimagesdir = (node.attr 'imagesdir', '.').chomp '/'
|
242
274
|
docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
|
243
275
|
|
244
|
-
@media_files.each do |file|
|
245
|
-
if
|
246
|
-
logger.warn %(path is reserved for cover artwork: #{
|
247
|
-
elsif
|
248
|
-
mime_types = MIME::Types.type_for
|
276
|
+
@media_files.each do |name, file|
|
277
|
+
if name.start_with? %(#{docimagesdir}jacket/cover.)
|
278
|
+
logger.warn %(path is reserved for cover artwork: #{name}; skipping file found in content)
|
279
|
+
elsif file[:path].nil? || File.readable?(file[:path])
|
280
|
+
mime_types = MIME::Types.type_for name
|
249
281
|
mime_types.delete_if {|x| x.media_type != file[:media_type] }
|
250
282
|
preferred_mime_type = mime_types.empty? ? nil : mime_types[0].content_type
|
251
|
-
@book.add_item
|
283
|
+
@book.add_item name, content: file[:path], media_type: preferred_mime_type
|
252
284
|
else
|
253
285
|
logger.error %(#{File.basename node.attr('docfile')}: media file not found or not readable: #{file[:path]})
|
254
286
|
end
|
@@ -301,25 +333,22 @@ module Asciidoctor
|
|
301
333
|
|
302
334
|
chapter_item = @book.add_ordered_item %(#{docid}.xhtml)
|
303
335
|
|
304
|
-
|
336
|
+
doctitle = node.document.doctitle partition: true, use_fallback: true
|
337
|
+
chapter_title = doctitle.combined
|
338
|
+
|
339
|
+
if node.context == :document && doctitle.subtitle?
|
305
340
|
title = %(#{doctitle.main} )
|
306
341
|
subtitle = doctitle.subtitle
|
307
342
|
elsif node.title
|
308
343
|
# HACK: until we get proper handling of title-only in CSS
|
309
344
|
title = ''
|
310
345
|
subtitle = get_numbered_title node
|
346
|
+
chapter_title = subtitle
|
311
347
|
else
|
312
348
|
title = nil
|
313
349
|
subtitle = nil
|
314
350
|
end
|
315
351
|
|
316
|
-
doctitle_sanitized = (node.document.doctitle sanitize: true, use_fallback: true).to_s
|
317
|
-
|
318
|
-
# By default, Kindle does not allow the line height to be adjusted.
|
319
|
-
# But if you float the elements, then the line height disappears and can be restored manually using margins.
|
320
|
-
# See https://github.com/asciidoctor/asciidoctor-epub3/issues/123
|
321
|
-
subtitle_formatted = subtitle ? subtitle.split.map {|w| %(<b>#{w}</b>) } * ' ' : nil
|
322
|
-
|
323
352
|
if node.document.doctype == 'book'
|
324
353
|
byline = ''
|
325
354
|
else
|
@@ -337,10 +366,10 @@ module Asciidoctor
|
|
337
366
|
|
338
367
|
# NOTE must run after content is resolved
|
339
368
|
# TODO perhaps create dynamic CSS file?
|
340
|
-
if
|
369
|
+
if icon_names.empty?
|
341
370
|
icon_css_head = ''
|
342
371
|
else
|
343
|
-
icon_defs =
|
372
|
+
icon_defs = icon_names.map {|name|
|
344
373
|
%(.i-#{name}::before { content: "#{FontIconMap.unicode name}"; })
|
345
374
|
} * LF
|
346
375
|
icon_css_head = %(<style>
|
@@ -351,19 +380,20 @@ module Asciidoctor
|
|
351
380
|
|
352
381
|
header = (title || subtitle) ? %(<header>
|
353
382
|
<div class="chapter-header">
|
354
|
-
#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{
|
383
|
+
#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle}</small>) : ''}</h1>
|
355
384
|
</div>
|
356
385
|
</header>) : ''
|
357
386
|
|
358
|
-
#
|
359
|
-
|
387
|
+
# We want highlighter CSS to be stored in a separate file
|
388
|
+
# in order to avoid style duplication across chapter files
|
389
|
+
linkcss = true
|
360
390
|
|
361
391
|
# NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
|
362
392
|
lines = [%(<!DOCTYPE html>
|
363
|
-
<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}">
|
393
|
+
<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}">
|
364
394
|
<head>
|
365
395
|
<meta charset="UTF-8"/>
|
366
|
-
<title>#{
|
396
|
+
<title>#{chapter_title}</title>
|
367
397
|
<link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
|
368
398
|
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
369
399
|
#{icon_css_head}<script type="text/javascript"><![CDATA[
|
@@ -376,13 +406,14 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
376
406
|
});
|
377
407
|
]]></script>)]
|
378
408
|
|
379
|
-
|
380
|
-
|
381
|
-
|
409
|
+
syntax_hl = node.document.syntax_highlighter
|
410
|
+
epub_type_attr = node.respond_to?(:section) && node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
|
411
|
+
|
412
|
+
lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :head
|
382
413
|
|
383
414
|
lines << %(</head>
|
384
415
|
<body>
|
385
|
-
<section class="chapter" title
|
416
|
+
<section class="chapter" title=#{chapter_title.encode xml: :attr}#{epub_type_attr} id="#{docid}">
|
386
417
|
#{header}
|
387
418
|
#{content})
|
388
419
|
|
@@ -405,7 +436,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
405
436
|
|
406
437
|
lines << '</section>'
|
407
438
|
|
408
|
-
lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl
|
439
|
+
lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :footer
|
409
440
|
|
410
441
|
lines << '</body>
|
411
442
|
</html>'
|
@@ -423,11 +454,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
423
454
|
def convert_section node
|
424
455
|
if add_chapter(node).nil?
|
425
456
|
hlevel = node.level
|
426
|
-
epub_type_attr = node.
|
457
|
+
epub_type_attr = node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
|
427
458
|
div_classes = [%(sect#{node.level}), node.role].compact
|
428
459
|
title = get_numbered_title node
|
429
|
-
|
430
|
-
%(<section class="#{div_classes * ' '}" title="#{title_sanitized}"#{epub_type_attr}>
|
460
|
+
%(<section class="#{div_classes * ' '}" title=#{title.encode xml: :attr}#{epub_type_attr}>
|
431
461
|
<h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? '' : %(
|
432
462
|
#{content})}
|
433
463
|
</section>)
|
@@ -510,14 +540,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
510
540
|
type = node.attr 'name'
|
511
541
|
epub_type = case type
|
512
542
|
when 'tip'
|
513
|
-
'
|
514
|
-
when 'note'
|
515
|
-
'
|
516
|
-
when 'important', 'warning', 'caution'
|
517
|
-
'warning'
|
543
|
+
'tip'
|
544
|
+
when 'important', 'warning', 'caution', 'note'
|
545
|
+
'notice'
|
518
546
|
else
|
519
547
|
logger.warn %(unknown admonition type: #{type})
|
520
|
-
'
|
548
|
+
'notice'
|
521
549
|
end
|
522
550
|
%(<aside#{id_attr} class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
|
523
551
|
#{title_el}<div class="content">
|
@@ -544,10 +572,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
544
572
|
end
|
545
573
|
|
546
574
|
def convert_listing node
|
575
|
+
id_attribute = node.id ? %( id="#{node.id}") : ''
|
547
576
|
nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap')
|
548
577
|
if node.style == 'source'
|
549
578
|
lang = node.attr 'language'
|
550
|
-
|
579
|
+
syntax_hl = node.document.syntax_highlighter
|
580
|
+
if syntax_hl
|
551
581
|
opts = syntax_hl.highlight? ? {
|
552
582
|
css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
|
553
583
|
style: doc_attrs[%(#{syntax_hl.name}-style)],
|
@@ -565,17 +595,41 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
565
595
|
figure_classes = ['listing']
|
566
596
|
figure_classes << 'coalesce' if node.option? 'unbreakable'
|
567
597
|
title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
568
|
-
%(<figure class="#{figure_classes * ' '}">#{title_div}
|
598
|
+
%(<figure#{id_attribute} class="#{figure_classes * ' '}">#{title_div}
|
569
599
|
#{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close}
|
570
600
|
</figure>)
|
571
601
|
end
|
572
602
|
|
573
|
-
|
574
|
-
|
603
|
+
def convert_stem node
|
604
|
+
return convert_listing node if node.style != 'asciimath' || !asciimath_available?
|
605
|
+
|
606
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
607
|
+
title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
608
|
+
equation_data = AsciiMath.parse(node.content).to_mathml 'mml:'
|
609
|
+
|
610
|
+
%(<figure#{id_attr} class="#{prepend_space node.role}">
|
611
|
+
#{title_element}
|
612
|
+
<div class="content">
|
613
|
+
#{equation_data}
|
614
|
+
</div>
|
615
|
+
</figure>)
|
616
|
+
end
|
617
|
+
|
618
|
+
def asciimath_available?
|
619
|
+
(@asciimath_status ||= load_asciimath) == :loaded
|
620
|
+
end
|
621
|
+
|
622
|
+
def load_asciimath
|
623
|
+
Helpers.require_library('asciimath', true, :warn).nil? ? :unavailable : :loaded
|
624
|
+
end
|
575
625
|
|
576
|
-
# QUESTION should we wrap the <pre> in either <div> or <figure>?
|
577
626
|
def convert_literal node
|
578
|
-
%(
|
627
|
+
id_attribute = node.id ? %( id="#{node.id}") : ''
|
628
|
+
title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
629
|
+
%(<figure#{id_attribute} class="literalblock#{prepend_space node.role}">
|
630
|
+
#{title_element}
|
631
|
+
<div class="content"><pre class="screen">#{node.content}</pre></div>
|
632
|
+
</figure>)
|
579
633
|
end
|
580
634
|
|
581
635
|
def convert_page_break _node
|
@@ -657,41 +711,34 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
657
711
|
lines = [%(<div class="table">)]
|
658
712
|
lines << %(<div class="content">)
|
659
713
|
table_id_attr = node.id ? %( id="#{node.id}") : ''
|
660
|
-
|
661
|
-
'
|
662
|
-
'
|
663
|
-
'
|
664
|
-
|
665
|
-
}
|
666
|
-
grid_class = {
|
667
|
-
'all' => 'table-grid',
|
668
|
-
'rows' => 'table-grid-rows',
|
669
|
-
'cols' => 'table-grid-cols',
|
670
|
-
'none' => '',
|
671
|
-
}
|
672
|
-
table_classes = %W[table #{frame_class[node.attr 'frame'] || frame_class['topbot']} #{grid_class[node.attr 'grid'] || grid_class['rows']}]
|
714
|
+
table_classes = [
|
715
|
+
'table',
|
716
|
+
%(table-framed-#{node.attr 'frame', 'rows', 'table-frame'}),
|
717
|
+
%(table-grid-#{node.attr 'grid', 'rows', 'table-grid'}),
|
718
|
+
]
|
673
719
|
if (role = node.role)
|
674
720
|
table_classes << role
|
675
721
|
end
|
676
|
-
table_class_attr = %( class="#{table_classes * ' '}")
|
677
722
|
table_styles = []
|
678
|
-
|
723
|
+
if (autowidth = node.option? 'autowidth') && !(node.attr? 'width')
|
724
|
+
table_classes << 'fit-content'
|
725
|
+
else
|
726
|
+
table_styles << %(width: #{node.attr 'tablepcwidth'}%;)
|
727
|
+
end
|
728
|
+
table_class_attr = %( class="#{table_classes * ' '}")
|
679
729
|
table_style_attr = !table_styles.empty? ? %( style="#{table_styles * '; '}") : ''
|
680
730
|
|
681
731
|
lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>)
|
682
732
|
lines << %(<caption>#{node.captioned_title}</caption>) if node.title?
|
683
733
|
if (node.attr 'rowcount') > 0
|
684
734
|
lines << '<colgroup>'
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
735
|
+
if autowidth
|
736
|
+
lines += (Array.new node.columns.size, %(<col/>))
|
737
|
+
else
|
738
|
+
node.columns.each do |col|
|
739
|
+
lines << ((col.option? 'autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />))
|
740
|
+
end
|
689
741
|
end
|
690
|
-
#else
|
691
|
-
# node.columns.each do |col|
|
692
|
-
# lines << %(<col style="width: #{col.attr 'colpcwidth'}%"/>)
|
693
|
-
# end
|
694
|
-
#end
|
695
742
|
lines << '</colgroup>'
|
696
743
|
[:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
|
697
744
|
lines << %(<t#{tsec}>)
|
@@ -711,19 +758,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
711
758
|
else
|
712
759
|
cell_content = ''
|
713
760
|
cell.content.each do |text|
|
714
|
-
cell_content = %(#{cell_content}<p>#{text}</p>)
|
761
|
+
cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
|
715
762
|
end
|
716
763
|
end
|
717
764
|
end
|
718
765
|
|
719
766
|
cell_tag_name = tsec == :head || cell.style == :header ? 'th' : 'td'
|
720
|
-
cell_classes = [
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
if (halign = cell.attr 'valign') && halign != 'top'
|
725
|
-
cell_classes << 'valign-top'
|
726
|
-
end
|
767
|
+
cell_classes = [
|
768
|
+
"halign-#{cell.attr 'halign'}",
|
769
|
+
"valign-#{cell.attr 'valign'}",
|
770
|
+
]
|
727
771
|
cell_class_attr = !cell_classes.empty? ? %( class="#{cell_classes * ' '}") : ''
|
728
772
|
cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : ''
|
729
773
|
cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
|
@@ -932,26 +976,40 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
932
976
|
|
933
977
|
return if target.start_with? 'data:'
|
934
978
|
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
979
|
+
if Asciidoctor::Helpers.uriish? target
|
980
|
+
# We need to add both local and remote media files to manifest
|
981
|
+
fs_path = nil
|
982
|
+
else
|
983
|
+
out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
|
984
|
+
fs_path = (::File.join out_dir, target)
|
985
|
+
unless ::File.exist? fs_path
|
986
|
+
base_dir = root_document(node.document).base_dir
|
987
|
+
fs_path = ::File.join base_dir, target
|
988
|
+
end
|
940
989
|
end
|
941
990
|
# We need *both* virtual and physical image paths. Unfortunately, references[:images] only has one of them.
|
942
|
-
@media_files
|
991
|
+
@media_files[target] ||= { path: fs_path, media_type: media_type }
|
943
992
|
end
|
944
993
|
|
945
994
|
def resolve_image_attrs node
|
946
995
|
img_attrs = []
|
947
996
|
img_attrs << %(alt="#{node.attr 'alt'}") if node.attr? 'alt'
|
948
997
|
|
949
|
-
width = node.attr 'scaledwidth'
|
950
|
-
width = node.attr 'width' if width.nil?
|
951
|
-
|
952
998
|
# Unlike browsers, Calibre/Kindle *do* scale image if only height is specified
|
953
999
|
# So, in order to match browser behavior, we just always omit height
|
954
|
-
|
1000
|
+
|
1001
|
+
if (scaledwidth = node.attr 'scaledwidth')
|
1002
|
+
img_attrs << %(style="width: #{scaledwidth}")
|
1003
|
+
elsif (width = node.attr 'width')
|
1004
|
+
# HTML5 spec (and EPUBCheck) only allows pixels in width, but browsers also accept percents
|
1005
|
+
# and there are multiple AsciiDoc files in the wild that have width=percents%
|
1006
|
+
# So, for compatibility reasons, output percentage width as a CSS style
|
1007
|
+
if width[/^\d+%$/]
|
1008
|
+
img_attrs << %(style="width: #{width}")
|
1009
|
+
else
|
1010
|
+
img_attrs << %(width="#{width}")
|
1011
|
+
end
|
1012
|
+
end
|
955
1013
|
|
956
1014
|
img_attrs
|
957
1015
|
end
|
@@ -1126,7 +1184,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1126
1184
|
|
1127
1185
|
def convert_inline_image node
|
1128
1186
|
if node.type == 'icon'
|
1129
|
-
|
1187
|
+
icon_names << (icon_name = node.target)
|
1130
1188
|
i_classes = ['icon', %(i-#{icon_name})]
|
1131
1189
|
i_classes << %(icon-#{node.attr 'size'}) if node.attr? 'size'
|
1132
1190
|
i_classes << %(icon-flip-#{(node.attr 'flip')[0]}) if node.attr? 'flip'
|
@@ -1173,25 +1231,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1173
1231
|
def convert_inline_quoted node
|
1174
1232
|
open, close, tag = QUOTE_TAGS[node.type]
|
1175
1233
|
|
1176
|
-
|
1234
|
+
if node.type == :asciimath && asciimath_available?
|
1235
|
+
content = AsciiMath.parse(node.text).to_mathml 'mml:'
|
1236
|
+
else
|
1237
|
+
content = node.text
|
1238
|
+
end
|
1239
|
+
|
1177
1240
|
node.add_role 'literal' if [:monospaced, :asciimath, :latexmath].include? node.type
|
1178
1241
|
|
1179
1242
|
if node.id
|
1180
1243
|
class_attr = class_string node
|
1181
1244
|
if tag
|
1182
|
-
%(#{open.chop} id="#{node.id}"#{class_attr}>#{
|
1245
|
+
%(#{open.chop} id="#{node.id}"#{class_attr}>#{content}#{close})
|
1183
1246
|
else
|
1184
|
-
%(<span id="#{node.id}"#{class_attr}>#{open}#{
|
1247
|
+
%(<span id="#{node.id}"#{class_attr}>#{open}#{content}#{close}</span>)
|
1185
1248
|
end
|
1186
1249
|
elsif role_valid_class? node.role
|
1187
1250
|
class_attr = class_string node
|
1188
1251
|
if tag
|
1189
|
-
%(#{open.chop}#{class_attr}>#{
|
1252
|
+
%(#{open.chop}#{class_attr}>#{content}#{close})
|
1190
1253
|
else
|
1191
|
-
%(<span#{class_attr}>#{open}#{
|
1254
|
+
%(<span#{class_attr}>#{open}#{content}#{close}</span>)
|
1192
1255
|
end
|
1193
1256
|
else
|
1194
|
-
%(#{open}#{
|
1257
|
+
%(#{open}#{content}#{close})
|
1195
1258
|
end
|
1196
1259
|
end
|
1197
1260
|
|
@@ -1255,6 +1318,19 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1255
1318
|
@book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
|
1256
1319
|
end
|
1257
1320
|
|
1321
|
+
syntax_hl = doc.syntax_highlighter
|
1322
|
+
if syntax_hl&.write_stylesheet? doc
|
1323
|
+
Dir.mktmpdir do |dir|
|
1324
|
+
syntax_hl.write_stylesheet doc, dir
|
1325
|
+
Pathname.glob(dir + '/**/*').map do |filename|
|
1326
|
+
# Workaround for https://github.com/skoji/gepub/pull/117
|
1327
|
+
filename.open do |f|
|
1328
|
+
@book.add_item filename.basename.to_s, content: f
|
1329
|
+
end if filename.file?
|
1330
|
+
end
|
1331
|
+
end
|
1332
|
+
end
|
1333
|
+
|
1258
1334
|
font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
|
1259
1335
|
@book.add_item 'styles/epub3-fonts.css', content: font_css
|
1260
1336
|
unless font_files.empty?
|
@@ -1274,38 +1350,41 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1274
1350
|
nil
|
1275
1351
|
end
|
1276
1352
|
|
1277
|
-
def
|
1278
|
-
|
1353
|
+
def add_cover_page doc, name
|
1354
|
+
image_attr_name = %(#{name}-image)
|
1355
|
+
|
1356
|
+
return nil if (image_path = doc.attr image_attr_name).nil?
|
1279
1357
|
|
1280
1358
|
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
1281
1359
|
imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
|
1282
1360
|
|
1283
1361
|
image_attrs = {}
|
1284
1362
|
if (image_path.include? ':') && image_path =~ ImageMacroRx
|
1285
|
-
logger.warn %(deprecated block macro syntax detected in
|
1363
|
+
logger.warn %(deprecated block macro syntax detected in :#{image_attr_name}: attribute) if image_path.start_with? 'image::'
|
1286
1364
|
image_path = %(#{imagesdir}#{$1})
|
1287
1365
|
(::Asciidoctor::AttributeList.new $2).parse_into image_attrs, %w(alt width height) unless $2.empty?
|
1288
1366
|
end
|
1289
1367
|
|
1290
|
-
image_href = %(#{imagesdir}jacket
|
1368
|
+
image_href = %(#{imagesdir}jacket/#{name}#{::File.extname image_path})
|
1291
1369
|
|
1292
1370
|
workdir = doc.attr 'docdir'
|
1293
1371
|
workdir = '.' if workdir.nil_or_empty?
|
1294
1372
|
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1373
|
+
begin
|
1374
|
+
@book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
|
1375
|
+
rescue => e
|
1376
|
+
logger.error %(#{::File.basename doc.attr('docfile')}: error adding cover image. Make sure that :#{image_attr_name}: attribute points to a valid image file. #{e})
|
1377
|
+
return nil
|
1298
1378
|
end
|
1299
1379
|
|
1380
|
+
return nil if @format == :kf8
|
1381
|
+
|
1300
1382
|
unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
|
1301
1383
|
width, height = 1050, 1600
|
1302
1384
|
end
|
1303
1385
|
|
1304
|
-
|
1305
|
-
|
1306
|
-
unless @format == :kf8
|
1307
|
-
# NOTE SVG wrapper maintains aspect ratio and confines image to view box
|
1308
|
-
content = %(<!DOCTYPE html>
|
1386
|
+
# NOTE SVG wrapper maintains aspect ratio and confines image to view box
|
1387
|
+
content = %(<!DOCTYPE html>
|
1309
1388
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
|
1310
1389
|
<head>
|
1311
1390
|
<meta charset="UTF-8"/>
|
@@ -1335,10 +1414,7 @@ body > svg {
|
|
1335
1414
|
</svg></body>
|
1336
1415
|
</html>).to_ios
|
1337
1416
|
|
1338
|
-
|
1339
|
-
@book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
|
1340
|
-
end
|
1341
|
-
nil
|
1417
|
+
@book.add_ordered_item %(#{name}.xhtml), content: content, id: name
|
1342
1418
|
end
|
1343
1419
|
|
1344
1420
|
def get_frontmatter_files doc, workdir
|
@@ -1368,19 +1444,22 @@ body > svg {
|
|
1368
1444
|
workdir = doc.attr 'docdir'
|
1369
1445
|
workdir = '.' if workdir.nil_or_empty?
|
1370
1446
|
|
1447
|
+
result = nil
|
1371
1448
|
get_frontmatter_files(doc, workdir).each do |front_matter|
|
1372
1449
|
front_matter_content = ::File.read front_matter
|
1373
1450
|
|
1374
1451
|
front_matter_file = File.basename front_matter, '.html'
|
1375
1452
|
item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content)
|
1376
1453
|
item.add_property 'svg' if SvgImgSniffRx =~ front_matter_content
|
1454
|
+
# Store link to first frontmatter page
|
1455
|
+
result = item if result.nil?
|
1377
1456
|
|
1378
1457
|
front_matter_content.scan ImgSrcScanRx do
|
1379
1458
|
@book.add_item $1, content: File.join(File.dirname(front_matter), $1)
|
1380
1459
|
end
|
1381
1460
|
end
|
1382
1461
|
|
1383
|
-
|
1462
|
+
result
|
1384
1463
|
end
|
1385
1464
|
|
1386
1465
|
def add_profile_images doc, usernames
|
@@ -1412,8 +1491,7 @@ body > svg {
|
|
1412
1491
|
nil
|
1413
1492
|
end
|
1414
1493
|
|
1415
|
-
|
1416
|
-
def nav_doc doc, items
|
1494
|
+
def nav_doc doc, items, landmarks, depth
|
1417
1495
|
lines = [%(<!DOCTYPE html>
|
1418
1496
|
<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}">
|
1419
1497
|
<head>
|
@@ -1423,13 +1501,29 @@ body > svg {
|
|
1423
1501
|
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
1424
1502
|
</head>
|
1425
1503
|
<body>
|
1426
|
-
<
|
1427
|
-
<
|
1428
|
-
<
|
1429
|
-
|
1430
|
-
|
1504
|
+
<section class="chapter">
|
1505
|
+
<header>
|
1506
|
+
<div class="chapter-header"><h1 class="chapter-title"><small class="subtitle">#{doc.attr 'toc-title'}</small></h1></div>
|
1507
|
+
</header>
|
1508
|
+
<nav epub:type="toc" id="toc">)]
|
1509
|
+
lines << (nav_level items, [depth, 0].max)
|
1510
|
+
lines << '</nav>'
|
1511
|
+
|
1512
|
+
unless landmarks.empty?
|
1513
|
+
lines << '
|
1514
|
+
<nav epub:type="landmarks" id="landmarks" hidden="hidden">
|
1515
|
+
<ol>'
|
1516
|
+
landmarks.each do |landmark|
|
1517
|
+
lines << %(<li><a epub:type="#{landmark[:type]}" href="#{landmark[:href]}">#{landmark[:title]}</a></li>)
|
1518
|
+
end
|
1519
|
+
lines << '
|
1520
|
+
</ol>
|
1521
|
+
</nav>'
|
1522
|
+
end
|
1523
|
+
lines << '
|
1524
|
+
</section>
|
1431
1525
|
</body>
|
1432
|
-
</html>
|
1526
|
+
</html>'
|
1433
1527
|
lines * LF
|
1434
1528
|
end
|
1435
1529
|
|
@@ -1463,7 +1557,7 @@ body > svg {
|
|
1463
1557
|
lines * LF
|
1464
1558
|
end
|
1465
1559
|
|
1466
|
-
def ncx_doc doc, items
|
1560
|
+
def ncx_doc doc, items, depth
|
1467
1561
|
# TODO: populate docAuthor element based on unique authors in work
|
1468
1562
|
lines = [%(<?xml version="1.0" encoding="utf-8"?>
|
1469
1563
|
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
|
@@ -1475,7 +1569,7 @@ body > svg {
|
|
1475
1569
|
</head>
|
1476
1570
|
<docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
|
1477
1571
|
<navMap>)]
|
1478
|
-
lines << (ncx_level items,
|
1572
|
+
lines << (ncx_level items, depth, state = {})
|
1479
1573
|
lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
|
1480
1574
|
lines << %(</navMap>
|
1481
1575
|
</ncx>)
|
@@ -1553,10 +1647,10 @@ body > svg {
|
|
1553
1647
|
.to_ios
|
1554
1648
|
end
|
1555
1649
|
|
1556
|
-
def get_kindlegen_command
|
1557
|
-
unless kindlegen_path.nil?
|
1558
|
-
logger.debug %(Using ebook-kindlegen-path attribute: #{kindlegen_path})
|
1559
|
-
return [kindlegen_path]
|
1650
|
+
def get_kindlegen_command
|
1651
|
+
unless @kindlegen_path.nil?
|
1652
|
+
logger.debug %(Using ebook-kindlegen-path attribute: #{@kindlegen_path})
|
1653
|
+
return [@kindlegen_path]
|
1560
1654
|
end
|
1561
1655
|
|
1562
1656
|
unless (result = ENV['KINDLEGEN']).nil?
|
@@ -1575,11 +1669,11 @@ body > svg {
|
|
1575
1669
|
end
|
1576
1670
|
end
|
1577
1671
|
|
1578
|
-
def distill_epub_to_mobi epub_file, target, compress
|
1672
|
+
def distill_epub_to_mobi epub_file, target, compress
|
1579
1673
|
mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
|
1580
1674
|
compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
|
1581
1675
|
|
1582
|
-
argv = get_kindlegen_command
|
1676
|
+
argv = get_kindlegen_command + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
|
1583
1677
|
begin
|
1584
1678
|
# This duplicates Kindlegen.run, but we want to override executable
|
1585
1679
|
out, err, res = Open3.capture3(*argv) do |r|
|
@@ -1604,10 +1698,10 @@ body > svg {
|
|
1604
1698
|
end
|
1605
1699
|
end
|
1606
1700
|
|
1607
|
-
def get_epubcheck_command
|
1608
|
-
unless epubcheck_path.nil?
|
1609
|
-
logger.debug %(Using ebook-epubcheck-path attribute: #{epubcheck_path})
|
1610
|
-
return [epubcheck_path]
|
1701
|
+
def get_epubcheck_command
|
1702
|
+
unless @epubcheck_path.nil?
|
1703
|
+
logger.debug %(Using ebook-epubcheck-path attribute: #{@epubcheck_path})
|
1704
|
+
return [@epubcheck_path]
|
1611
1705
|
end
|
1612
1706
|
|
1613
1707
|
unless (result = ENV['EPUBCHECK']).nil?
|
@@ -1625,8 +1719,8 @@ body > svg {
|
|
1625
1719
|
end
|
1626
1720
|
end
|
1627
1721
|
|
1628
|
-
def validate_epub epub_file
|
1629
|
-
argv = get_epubcheck_command
|
1722
|
+
def validate_epub epub_file
|
1723
|
+
argv = get_epubcheck_command + ['-w', epub_file]
|
1630
1724
|
begin
|
1631
1725
|
out, err, res = Open3.capture3(*argv)
|
1632
1726
|
rescue Errno::ENOENT => e
|
@@ -1671,14 +1765,6 @@ body > svg {
|
|
1671
1765
|
def role_valid_class? role
|
1672
1766
|
role.is_a? String
|
1673
1767
|
end
|
1674
|
-
|
1675
|
-
class << self
|
1676
|
-
def supports_highlighter_docinfo?
|
1677
|
-
# Asciidoctor only got pluggable syntax highlighters since 2.0:
|
1678
|
-
# https://github.com/asciidoctor/asciidoctor/commit/23ddbaed6818025cbe74365fec7e8101f34eadca
|
1679
|
-
Asciidoctor::Document.method_defined? :syntax_highlighter
|
1680
|
-
end
|
1681
|
-
end
|
1682
1768
|
end
|
1683
1769
|
|
1684
1770
|
class DocumentIdGenerator
|
@@ -1691,6 +1777,7 @@ body > svg {
|
|
1691
1777
|
InvalidIdCharsRx = /[^[:word:]]+/
|
1692
1778
|
LeadingDigitRx = /^[[:digit:]]/
|
1693
1779
|
end
|
1780
|
+
|
1694
1781
|
class << self
|
1695
1782
|
def generate_id doc, pre = nil, sep = nil
|
1696
1783
|
synthetic = false
|
@@ -1746,7 +1833,10 @@ body > svg {
|
|
1746
1833
|
# TODO: bw theme for CodeRay
|
1747
1834
|
document.set_attribute 'pygments-style', 'bw' unless document.attr? 'pygments-style'
|
1748
1835
|
document.set_attribute 'rouge-style', 'bw' unless document.attr? 'rouge-style'
|
1749
|
-
|
1836
|
+
|
1837
|
+
# Old asciidoctor versions do not have public API for writing highlighter CSS file
|
1838
|
+
# So just use inline CSS there.
|
1839
|
+
unless Document.supports_syntax_highlighter?
|
1750
1840
|
document.set_attribute 'coderay-css', 'style'
|
1751
1841
|
document.set_attribute 'pygments-css', 'style'
|
1752
1842
|
document.set_attribute 'rouge-css', 'style'
|