lib/asciidoctor-epub3/converter.rb in asciidoctor-epub3-1.5.0.alpha.15 vs lib/asciidoctor-epub3/converter.rb in asciidoctor-epub3-1.5.0.alpha.16
- old
+ new
@@ -1,7 +1,8 @@
# frozen_string_literal: true
+require 'mime/types'
require 'open3'
require_relative 'font_icon_map'
module Asciidoctor
module Epub3
@@ -27,11 +28,11 @@
entries.each do |entry|
next unless entry.file?
unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
::FileUtils.mkdir_p entry_dir
end
- entry.extract
+ entry.extract entry.name
end
end
end
logger.debug %(Extracted #{@format.upcase} to #{extract_dir})
end
@@ -102,21 +103,23 @@
def convert node, name = nil, _opts = {}
method_name = %(convert_#{name ||= node.node_name})
if respond_to? method_name
send method_name, node
else
- logger.warn %(conversion missing in backend #{@backend} for #{name})
+ logger.warn %(#{::File.basename node.attr('docfile')}: conversion missing in backend #{@backend} for #{name})
+ nil
end
end
# See https://asciidoctor.org/docs/user-manual/#book-parts-and-chapters
def get_chapter_name node
if node.document.doctype != 'book'
return Asciidoctor::Document === node ? node.attr('docname') || node.id : nil
end
return (node.id || 'preamble') if node.context == :preamble && node.level == 0
- Asciidoctor::Section === node && node.level <= 1 ? node.id : nil
+ chapter_level = [node.document.attr('epub-chapter-level', 1).to_i, 1].max
+ Asciidoctor::Section === node && node.level <= chapter_level ? node.id : nil
end
def get_numbered_title node
doc_attrs = node.document.attributes
level = node.level
@@ -148,11 +151,11 @@
@compress = node.attr 'ebook-compress'
@kindlegen_path = node.attr 'ebook-kindlegen-path'
@epubcheck_path = node.attr 'ebook-epubcheck-path'
@xrefs_seen = ::Set.new
@icon_names = []
- @images = []
+ @media_files = []
@footnotes = []
@book = GEPUB::Book.new 'EPUB/package.opf'
@book.epub_backward_compat = @format != :kf8
@book.language node.attr('lang', 'en'), id: 'pub-language'
@@ -219,18 +222,11 @@
add_cover_image node
add_front_matter_page node
if node.doctype == 'book'
- toc_items = []
- node.sections.each do |section|
- toc_items << section
- section.sections.each do |subsection|
- next if get_chapter_name(node).nil?
- toc_items << subsection
- end
- end
+ toc_items = node.sections
node.content
else
toc_items = [node]
add_chapter node
end
@@ -243,17 +239,20 @@
@book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
docimagesdir = (node.attr 'imagesdir', '.').chomp '/'
docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
- @images.each do |image|
- if image[:name].start_with? %(#{docimagesdir}jacket/cover.)
- logger.warn %(image path is reserved for cover artwork: #{image[:name]}; skipping image found in content)
- elsif ::File.readable? image[:path]
- @book.add_item image[:name], content: image[:path]
+ @media_files.each do |file|
+ if file[:name].start_with? %(#{docimagesdir}jacket/cover.)
+ logger.warn %(path is reserved for cover artwork: #{file[:name]}; skipping file found in content)
+ elsif ::File.readable? file[:path]
+ mime_types = MIME::Types.type_for file[:name]
+ mime_types.delete_if {|x| x.media_type != file[:media_type] }
+ preferred_mime_type = mime_types.empty? ? nil : mime_types[0].content_type
+ @book.add_item file[:name], content: file[:path], media_type: preferred_mime_type
else
- logger.error %(#{File.basename node.attr('docfile')}: image not found or not readable: #{image[:path]})
+ logger.error %(#{File.basename node.attr('docfile')}: media file not found or not readable: #{file[:path]})
end
end
#add_metadata 'ibooks:specified-fonts', true
@@ -563,11 +562,11 @@
pre_close = '</pre>'
syntax_hl = nil
end
figure_classes = ['listing']
figure_classes << 'coalesce' if node.option? 'unbreakable'
- title_div = node.title? ? %(<figcaption>#{get_numbered_title node}</figcaption>) : ''
+ title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
%(<figure class="#{figure_classes * ' '}">#{title_div}
#{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close}
</figure>)
end
@@ -755,20 +754,34 @@
end
# TODO: add complex class if list has nested blocks
def convert_dlist node
lines = []
+ id_attribute = node.id ? %( id="#{node.id}") : ''
+
+ classes = case node.style
+ when 'horizontal'
+ ['hdlist', node.role]
+ when 'itemized', 'ordered'
+ # QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
+ ['dlist', %(#{node.style}-list), node.role]
+ else
+ ['description-list']
+ end.compact
+
+ class_attribute = %( class="#{classes.join ' '}")
+
+ lines << %(<div#{id_attribute}#{class_attribute}>)
+ lines << %(<div class="title">#{node.title}</div>) if node.title?
+
case (style = node.style)
when 'itemized', 'ordered'
list_tag_name = style == 'itemized' ? 'ul' : 'ol'
role = node.role
subject_stop = node.attr 'subject-stop', (role && (node.has_role? 'stack') ? nil : ':')
- # QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
- div_classes = [%(#{style}-list), role].compact
list_class_attr = (node.option? 'brief') ? ' class="brief"' : ''
- lines << %(<div class="#{div_classes * ' '}">
-<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
+ lines << %(<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
node.items.each do |subjects, dd|
# consists of one term (a subject) and supporting content
subject = [*subjects].first.text
subject_plain = xml_sanitize subject, :plain
subject_element = %(<strong class="subject">#{subject}#{subject_stop && subject_plain !~ TrailingPunctRx ? subject_stop : ''}</strong>)
@@ -780,15 +793,44 @@
else
lines << %(<span class="principal">#{subject_element}</span>)
end
lines << '</li>'
end
- lines << %(</#{list_tag_name}>
-</div>)
+ lines << %(</#{list_tag_name}>)
+ when 'horizontal'
+ lines << '<table>'
+ if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
+ lines << '<colgroup>'
+ col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
+ lines << %(<col#{col_style_attribute} />)
+ col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
+ lines << %(<col#{col_style_attribute} />)
+ lines << '</colgroup>'
+ end
+ node.items.each do |terms, dd|
+ lines << '<tr>'
+ lines << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : ''}">)
+ first_term = true
+ terms.each do |dt|
+ lines << %(<br />) unless first_term
+ lines << '<p>'
+ lines << dt.text
+ lines << '</p>'
+ first_term = nil
+ end
+ lines << '</td>'
+ lines << '<td class="hdlist2">'
+ if dd
+ lines << %(<p>#{dd.text}</p>) if dd.text?
+ lines << dd.content if dd.blocks?
+ end
+ lines << '</td>'
+ lines << '</tr>'
+ end
+ lines << '</table>'
else
- lines << '<div class="description-list">
-<dl>'
+ lines << '<dl>'
node.items.each do |terms, dd|
[*terms].each do |dt|
lines << %(<dt>
<span class="term">#{dt.text}</span>
</dt>)
@@ -801,13 +843,14 @@
else
lines << %(<span class="principal">#{dd.text}</span>)
end
lines << '</dd>'
end
- lines << '</dl>
-</div>'
+ lines << '</dl>'
end
+
+ lines << '</div>'
lines * LF
end
def convert_olist node
complex = false
@@ -877,26 +920,28 @@
def root_document document
document = document.parent_document until document.parent_document.nil?
document
end
- def register_image node, target
- if target.end_with? '.svg'
+ def register_media_file node, target, media_type
+ if target.end_with?('.svg') || target.start_with?('data:image/svg+xml')
chapter = get_enclosing_chapter node
chapter.set_attr 'epub-properties', [] unless chapter.attr? 'epub-properties'
epub_properties = chapter.attr 'epub-properties'
epub_properties << 'svg' unless epub_properties.include? 'svg'
end
+ return if target.start_with? 'data:'
+
out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
fs_path = (::File.join out_dir, target)
unless ::File.exist? fs_path
base_dir = root_document(node.document).base_dir
fs_path = ::File.join base_dir, target
end
# We need *both* virtual and physical image paths. Unfortunately, references[:images] only has one of them.
- @images << { name: target, path: fs_path }
+ @media_files << { name: target, path: fs_path, media_type: media_type }
end
def resolve_image_attrs node
img_attrs = []
img_attrs << %(alt="#{node.attr 'alt'}") if node.attr? 'alt'
@@ -909,20 +954,85 @@
img_attrs << %(width="#{width}") unless width.nil?
img_attrs
end
+ def convert_audio node
+ id_attr = node.id ? %( id="#{node.id}") : ''
+ target = node.media_uri node.attr 'target'
+ register_media_file node, target, 'audio'
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
+
+ autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
+ controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
+ loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
+
+ start_t = node.attr 'start'
+ end_t = node.attr 'end'
+ if start_t || end_t
+ time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
+ else
+ time_anchor = ''
+ end
+
+ %(<figure#{id_attr} class="audioblock#{prepend_space node.role}">#{title_element}
+<div class="content">
+<audio src="#{target}#{time_anchor}"#{autoplay_attr}#{controls_attr}#{loop_attr}>
+<div>Your Reading System does not support (this) audio.</div>
+</audio>
+</div>
+</figure>)
+ end
+
+ # TODO: Support multiple video files in different formats for a single video
+ def convert_video node
+ id_attr = node.id ? %( id="#{node.id}") : ''
+ target = node.media_uri node.attr 'target'
+ register_media_file node, target, 'video'
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
+
+ width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
+ height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : ''
+ autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
+ controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
+ loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
+
+ start_t = node.attr 'start'
+ end_t = node.attr 'end'
+ if start_t || end_t
+ time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
+ else
+ time_anchor = ''
+ end
+
+ if (poster = node.attr 'poster').nil_or_empty?
+ poster_attr = ''
+ else
+ poster = node.media_uri poster
+ register_media_file node, poster, 'image'
+ poster_attr = %( poster="#{poster}")
+ end
+
+ %(<figure#{id_attr} class="video#{prepend_space node.role}">#{title_element}
+<div class="content">
+<video src="#{target}#{time_anchor}"#{width_attr}#{height_attr}#{autoplay_attr}#{poster_attr}#{controls_attr}#{loop_attr}>
+<div>Your Reading System does not support (this) video.</div>
+</video>
+</div>
+</figure>)
+ end
+
def convert_image node
target = node.image_uri node.attr 'target'
- register_image node, target
+ register_media_file node, target, 'image'
id_attr = node.id ? %( id="#{node.id}") : ''
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
img_attrs = resolve_image_attrs node
%(<figure#{id_attr} class="image#{prepend_space node.role}">
<div class="content">
<img src="#{target}"#{prepend_space img_attrs * ' '} />
-</div>#{node.title? ? %(
-<figcaption>#{node.captioned_title}</figcaption>) : ''}
+</div>#{title_element}
</figure>)
end
def get_enclosing_chapter node
loop do
@@ -1023,10 +1133,10 @@
i_classes << %(icon-rotate-#{node.attr 'rotate'}) if node.attr? 'rotate'
i_classes << node.role if node.role?
%(<i class="#{i_classes * ' '}"></i>)
else
target = node.image_uri node.target
- register_image node, target
+ register_media_file node, target, 'image'
img_attrs = resolve_image_attrs node
img_attrs << %(class="inline#{prepend_space node.role}")
%(<img src="#{target}"#{prepend_space img_attrs * ' '}/>)
end