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