lib/asciidoctor-epub3/converter.rb in asciidoctor-epub3-1.5.0.alpha.17 vs lib/asciidoctor-epub3/converter.rb in asciidoctor-epub3-1.5.0.alpha.18

- old
+ new

@@ -218,26 +218,52 @@ series_meta.refine 'dcterms:identifier', series_id unless series_id.nil? # Calibre only understands 'series' series_meta.refine 'collection-type', 'series' end - add_cover_image node - add_front_matter_page node + # For list of supported landmark types see + # https://idpf.github.io/epub-vocabs/structure/ + landmarks = [] + cover_page = add_cover_page node + landmarks << { type: 'cover', href: cover_page.href, title: 'Cover' } unless cover_page.nil? + + front_matter_page = add_front_matter_page node + landmarks << { type: 'frontmatter', href: front_matter_page.href, title: 'Front Matter' } unless front_matter_page.nil? + + nav_item = @book.add_item('nav.xhtml', id: 'nav').nav + + toclevels = [(node.attr 'toclevels', 1).to_i, 0].max + outlinelevels = [(node.attr 'outlinelevels', toclevels).to_i, 0].max + + if node.attr? 'toc' + toc_item = @book.add_ordered_item 'toc.xhtml', id: 'toc' + landmarks << { type: 'toc', href: toc_item.href, title: node.attr('toc-title') } + else + toc_item = nil + end + if node.doctype == 'book' toc_items = node.sections node.content else toc_items = [node] add_chapter node end - nav_xhtml = @book.add_item 'nav.xhtml', content: postprocess_xhtml(nav_doc(node, toc_items)), id: 'nav' - nav_xhtml.nav + landmarks << { type: 'bodymatter', href: %(#{get_chapter_name toc_items[0]}.xhtml), title: 'Start of Content' } unless toc_items.empty? + toc_items.each do |item| + landmarks << { type: item.style, href: %(#{get_chapter_name item}.xhtml), title: item.title } if %w(appendix bibliography glossary index preface).include? item.style + end + + nav_item.add_content postprocess_xhtml(nav_doc(node, toc_items, landmarks, outlinelevels)) + # User is not supposed to see landmarks, so pass empty array here + toc_item&.add_content postprocess_xhtml(nav_doc(node, toc_items, [], toclevels)) + # NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves - toc_ncx = ncx_doc node, toc_items + toc_ncx = ncx_doc node, toc_items, outlinelevels @book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx' docimagesdir = (node.attr 'imagesdir', '.').chomp '/' docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/)) @@ -299,29 +325,26 @@ docid = get_chapter_name node return nil if docid.nil? chapter_item = @book.add_ordered_item %(#{docid}.xhtml) - if node.context == :document && (doctitle = node.doctitle partition: true, use_fallback: true).subtitle? + doctitle = node.document.doctitle partition: true, use_fallback: true + doctitle_sanitized = sanitize_xml doctitle.combined, :cdata + + if node.context == :document && doctitle.subtitle? title = %(#{doctitle.main} ) subtitle = doctitle.subtitle elsif node.title # HACK: until we get proper handling of title-only in CSS title = '' subtitle = get_numbered_title node + doctitle_sanitized = sanitize_xml subtitle, :cdata else title = nil subtitle = nil end - doctitle_sanitized = (node.document.doctitle sanitize: true, use_fallback: true).to_s - - # By default, Kindle does not allow the line height to be adjusted. - # But if you float the elements, then the line height disappears and can be restored manually using margins. - # See https://github.com/asciidoctor/asciidoctor-epub3/issues/123 - subtitle_formatted = subtitle ? subtitle.split.map {|w| %(<b>#{w}</b>) } * ' ' : nil - if node.document.doctype == 'book' byline = '' else author = node.attr 'author' username = node.attr 'username', 'default' @@ -349,20 +372,21 @@ ) end header = (title || subtitle) ? %(<header> <div class="chapter-header"> -#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle_formatted}</small>) : ''}</h1> +#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle}</small>) : ''}</h1> </div> </header>) : '' - # TODO : support writing code highlighter CSS to a separate file - linkcss = false + # We want highlighter CSS to be stored in a separate file + # in order to avoid style duplication across chapter files + linkcss = true # NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div lines = [%(<!DOCTYPE html> -<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}"> +<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}"> <head> <meta charset="UTF-8"/> <title>#{doctitle_sanitized}</title> <link rel="stylesheet" type="text/css" href="styles/epub3.css"/> <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/> @@ -374,17 +398,16 @@ } document.body.setAttribute('class', reader.name.toLowerCase().replace(/ /g, '-')); }); ]]></script>)] - if self.class.supports_highlighter_docinfo? && (syntax_hl = node.document.syntax_highlighter) && (syntax_hl.docinfo? :head) - lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/') - end + syntax_hl = node.document.syntax_highlighter + lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :head lines << %(</head> <body> -<section class="chapter" title="#{doctitle_sanitized.gsub '"', '&quot;'}" epub:type="chapter" id="#{docid}"> +<section class="chapter" title="#{doctitle_sanitized}" epub:type="chapter" id="#{docid}"> #{header} #{content}) unless (fns = node.document.footnotes - @footnotes).empty? @footnotes += fns @@ -403,11 +426,11 @@ </footer>' end lines << '</section>' - lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl && (syntax_hl.docinfo? :footer) + lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :footer lines << '</body> </html>' chapter_item.add_content postprocess_xhtml lines * LF @@ -545,11 +568,12 @@ def convert_listing node nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap') if node.style == 'source' lang = node.attr 'language' - if self.class.supports_highlighter_docinfo? && (syntax_hl = node.document.syntax_highlighter) + syntax_hl = node.document.syntax_highlighter + if syntax_hl opts = syntax_hl.highlight? ? { css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym, style: doc_attrs[%(#{syntax_hl.name}-style)], } : {} opts[:nowrap] = nowrap @@ -568,13 +592,33 @@ %(<figure class="#{figure_classes * ' '}">#{title_div} #{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close} </figure>) end - # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10 - alias convert_stem convert_listing + def convert_stem node + return convert_listing node if node.style != 'asciimath' || !asciimath_available? + id_attr = node.id ? %( id="#{node.id}") : '' + title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : '' + equation_data = AsciiMath.parse(node.content).to_mathml 'mml:' + + %(<figure#{id_attr} class="#{prepend_space node.role}"> +#{title_element} +<div class="content"> +#{equation_data} +</div> +</figure>) + end + + def asciimath_available? + (@asciimath_status ||= load_asciimath) == :loaded + end + + def load_asciimath + Helpers.require_library('asciimath', true, :warn).nil? ? :unavailable : :loaded + end + # QUESTION should we wrap the <pre> in either <div> or <figure>? def convert_literal node %(<pre class="screen">#{node.content}</pre>) end @@ -655,45 +699,40 @@ def convert_table node lines = [%(<div class="table">)] lines << %(<div class="content">) table_id_attr = node.id ? %( id="#{node.id}") : '' - frame_class = { - 'all' => 'table-framed', - 'topbot' => 'table-framed-topbot', - 'sides' => 'table-framed-sides', - 'none' => '', - } - grid_class = { - 'all' => 'table-grid', - 'rows' => 'table-grid-rows', - 'cols' => 'table-grid-cols', - 'none' => '', - } - table_classes = %W[table #{frame_class[node.attr 'frame'] || frame_class['topbot']} #{grid_class[node.attr 'grid'] || grid_class['rows']}] + table_classes = [ + 'table', + %(table-framed-#{node.attr 'frame', 'rows', 'table-frame'}), + %(table-grid-#{node.attr 'grid', 'rows', 'table-grid'}), + ] if (role = node.role) table_classes << role end - table_class_attr = %( class="#{table_classes * ' '}") table_styles = [] - table_styles << %(width: #{node.attr 'tablepcwidth'}%) unless (node.option? 'autowidth') && !(node.attr? 'width', nil, false) + if (autowidth = node.option? 'autowidth') && !(node.attr? 'width') + table_classes << 'fit-content' + elsif (tablewidth = node.attr 'tablepcwidth') == 100 + table_classes << 'stretch' + else + table_styles << %(width: #{tablewidth}%;) + end + table_class_attr = %( class="#{table_classes * ' '}") table_style_attr = !table_styles.empty? ? %( style="#{table_styles * '; '}") : '' lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>) lines << %(<caption>#{node.captioned_title}</caption>) if node.title? if (node.attr 'rowcount') > 0 lines << '<colgroup>' - #if node.option? 'autowidth' - tag = %(<col/>) - node.columns.size.times do - lines << tag + if autowidth + lines += (Array.new node.columns.size, %(<col/>)) + else + node.columns.each do |col| + lines << ((col.option? 'autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />)) + end end - #else - # node.columns.each do |col| - # lines << %(<col style="width: #{col.attr 'colpcwidth'}%"/>) - # end - #end lines << '</colgroup>' [:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec| lines << %(<t#{tsec}>) node.rows[tsec].each do |row| lines << '<tr>' @@ -709,23 +748,20 @@ when :literal cell_content = %(<div class="literal"><pre>#{cell.text}</pre></div>) else cell_content = '' cell.content.each do |text| - cell_content = %(#{cell_content}<p>#{text}</p>) + cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>) end end end cell_tag_name = tsec == :head || cell.style == :header ? 'th' : 'td' - cell_classes = [] - if (halign = cell.attr 'halign') && halign != 'left' - cell_classes << 'halign-left' - end - if (halign = cell.attr 'valign') && halign != 'top' - cell_classes << 'valign-top' - end + cell_classes = [ + "halign-#{cell.attr 'halign'}", + "valign-#{cell.attr 'valign'}", + ] cell_class_attr = !cell_classes.empty? ? %( class="#{cell_classes * ' '}") : '' cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : '' cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : '' cell_style_attr = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'}") : '' lines << %(<#{cell_tag_name}#{cell_class_attr}#{cell_colspan_attr}#{cell_rowspan_attr}#{cell_style_attr}>#{cell_content}</#{cell_tag_name}>) @@ -929,11 +965,11 @@ epub_properties = chapter.attr 'epub-properties' epub_properties << 'svg' unless epub_properties.include? 'svg' end if Asciidoctor::Helpers.uriish? target - # We need to add both local and remote media files to manifect + # We need to add both local and remote media files to manifest fs_path = nil else 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 @@ -1174,29 +1210,34 @@ end def convert_inline_quoted node open, close, tag = QUOTE_TAGS[node.type] - # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10 + if node.type == :asciimath && asciimath_available? + content = AsciiMath.parse(node.text).to_mathml 'mml:' + else + content = node.text + end + node.add_role 'literal' if [:monospaced, :asciimath, :latexmath].include? node.type if node.id class_attr = class_string node if tag - %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close}) + %(#{open.chop} id="#{node.id}"#{class_attr}>#{content}#{close}) else - %(<span id="#{node.id}"#{class_attr}>#{open}#{node.text}#{close}</span>) + %(<span id="#{node.id}"#{class_attr}>#{open}#{content}#{close}</span>) end elsif role_valid_class? node.role class_attr = class_string node if tag - %(#{open.chop}#{class_attr}>#{node.text}#{close}) + %(#{open.chop}#{class_attr}>#{content}#{close}) else - %(<span#{class_attr}>#{open}#{node.text}#{close}</span>) + %(<span#{class_attr}>#{open}#{content}#{close}</span>) end else - %(#{open}#{node.text}#{close}) + %(#{open}#{content}#{close}) end end def output_content node node.content_model == :simple ? %(<p>#{node.content}</p>) : node.content @@ -1256,10 +1297,23 @@ else @book.add_item 'styles/epub3.css', content: (postprocess_css_file ::File.join(workdir, 'epub3.css'), format) @book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format) end + syntax_hl = doc.syntax_highlighter + if syntax_hl&.write_stylesheet? doc + Dir.mktmpdir do |dir| + syntax_hl.write_stylesheet doc, dir + Pathname.glob(dir + '/**/*').map do |filename| + # Workaround for https://github.com/skoji/gepub/pull/117 + filename.open do |f| + @book.add_item filename.basename.to_s, content: f + end if filename.file? + end + end + end + font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin') @book.add_item 'styles/epub3-fonts.css', content: font_css unless font_files.empty? # NOTE metadata property in oepbs package manifest doesn't work; must use proprietary iBooks file instead #(@book.metadata.add_metadata 'meta', 'true')['property'] = 'ibooks:specified-fonts' unless format == :kf8 @@ -1275,12 +1329,12 @@ end end nil end - def add_cover_image doc - return if (image_path = doc.attr 'front-cover-image').nil? + def add_cover_page doc + return nil if (image_path = doc.attr 'front-cover-image').nil? imagesdir = (doc.attr 'imagesdir', '.').chomp '/' imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/)) image_attrs = {} @@ -1293,24 +1347,25 @@ image_href = %(#{imagesdir}jacket/cover#{::File.extname image_path}) workdir = doc.attr 'docdir' workdir = '.' if workdir.nil_or_empty? - unless ::File.readable? ::File.join(workdir, image_path) - logger.error %(#{::File.basename doc.attr('docfile')}: front cover image not found or readable: #{::File.expand_path image_path, workdir}) - return + begin + @book.add_item(image_href, content: File.join(workdir, image_path)).cover_image + rescue => e + 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}) + return nil end + return nil if @format == :kf8 + unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height']) width, height = 1050, 1600 end - @book.add_item(image_href, content: File.join(workdir, image_path)).cover_image - - unless @format == :kf8 - # NOTE SVG wrapper maintains aspect ratio and confines image to view box - content = %(<!DOCTYPE html> + # NOTE SVG wrapper maintains aspect ratio and confines image to view box + content = %(<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en"> <head> <meta charset="UTF-8"/> <title>#{sanitize_doctitle_xml doc, :cdata}</title> <style type="text/css"> @@ -1336,14 +1391,11 @@ width="100%" height="100%" viewBox="0 0 #{width} #{height}" preserveAspectRatio="xMidYMid meet"> <image width="#{width}" height="#{height}" xlink:href="#{image_href}"/> </svg></body> </html>).to_ios - # Gitden expects a cover.xhtml, so add it to the spine - @book.add_ordered_item 'cover.xhtml', content: content, id: 'cover' - end - nil + @book.add_ordered_item 'cover.xhtml', content: content, id: 'cover' end def get_frontmatter_files doc, workdir if doc.attr? 'epub3-frontmatterdir' fmdir = doc.attr 'epub3-frontmatterdir' @@ -1369,23 +1421,26 @@ def add_front_matter_page doc workdir = doc.attr 'docdir' workdir = '.' if workdir.nil_or_empty? + result = nil get_frontmatter_files(doc, workdir).each do |front_matter| front_matter_content = ::File.read front_matter front_matter_file = File.basename front_matter, '.html' item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content) item.add_property 'svg' if SvgImgSniffRx =~ front_matter_content + # Store link to first frontmatter page + result = item if result.nil? front_matter_content.scan ImgSrcScanRx do @book.add_item $1, content: File.join(File.dirname(front_matter), $1) end end - nil + result end def add_profile_images doc, usernames imagesdir = (doc.attr 'imagesdir', '.').chomp '/' imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/)) @@ -1413,28 +1468,43 @@ end end nil end - # TODO: aggregate authors of chapters into authors attribute(s) on main document - def nav_doc doc, items + def nav_doc doc, items, landmarks, depth lines = [%(<!DOCTYPE html> <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}"> <head> <meta charset="UTF-8"/> <title>#{sanitize_doctitle_xml doc, :cdata}</title> <link rel="stylesheet" type="text/css" href="styles/epub3.css"/> <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/> </head> <body> -<h1>#{sanitize_doctitle_xml doc, :pcdata}</h1> -<nav epub:type="toc" id="toc"> -<h2>#{doc.attr 'toc-title'}</h2>)] - lines << (nav_level items, [(doc.attr 'toclevels', 1).to_i, 0].max) - lines << %(</nav> +<section class="chapter"> +<header> +<div class="chapter-header"><h1 class="chapter-title"><small class="subtitle">#{doc.attr 'toc-title'}</small></h1></div> +</header> +<nav epub:type="toc" id="toc">)] + lines << (nav_level items, [depth, 0].max) + lines << '</nav>' + + unless landmarks.empty? + lines << ' +<nav epub:type="landmarks" id="landmarks" hidden="hidden"> +<ol>' + landmarks.each do |landmark| + lines << %(<li><a epub:type="#{landmark[:type]}" href="#{landmark[:href]}">#{landmark[:title]}</a></li>) + end + lines << ' +</ol> +</nav>' + end + lines << ' +</section> </body> -</html>) +</html>' lines * LF end def nav_level items, depth, state = {} lines = [] @@ -1464,11 +1534,11 @@ end lines << '</ol>' lines * LF end - def ncx_doc doc, items + def ncx_doc doc, items, depth # TODO: populate docAuthor element based on unique authors in work lines = [%(<?xml version="1.0" encoding="utf-8"?> <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}"> <head> <meta name="dtb:uid" content="#{@book.identifier}"/> @@ -1476,11 +1546,11 @@ <meta name="dtb:totalPageCount" content="0"/> <meta name="dtb:maxPageNumber" content="0"/> </head> <docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle> <navMap>)] - lines << (ncx_level items, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {}) + lines << (ncx_level items, depth, state = {}) lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>) lines << %(</navMap> </ncx>) lines * LF end @@ -1672,18 +1742,10 @@ # Handles asciidoctor 1.5.6 quirk when role can be parent def role_valid_class? role role.is_a? String end - - class << self - def supports_highlighter_docinfo? - # Asciidoctor only got pluggable syntax highlighters since 2.0: - # https://github.com/asciidoctor/asciidoctor/commit/23ddbaed6818025cbe74365fec7e8101f34eadca - Asciidoctor::Document.method_defined? :syntax_highlighter - end - end end class DocumentIdGenerator ReservedIds = %w(cover nav ncx) CharRefRx = /&(?:([a-zA-Z][a-zA-Z]+\d{0,2})|#(\d\d\d{0,4})|#x([\da-fA-F][\da-fA-F][\da-fA-F]{0,3}));/ @@ -1692,10 +1754,11 @@ LeadingDigitRx = /^\p{Nd}/ else InvalidIdCharsRx = /[^[:word:]]+/ LeadingDigitRx = /^[[:digit:]]/ end + class << self def generate_id doc, pre = nil, sep = nil synthetic = false unless (id = doc.id) # NOTE we assume pre is a valid ID prefix and that pre and sep only contain valid ID chars @@ -1747,10 +1810,13 @@ document.set_attribute 'listing-caption', 'Listing' # TODO: bw theme for CodeRay document.set_attribute 'pygments-style', 'bw' unless document.attr? 'pygments-style' document.set_attribute 'rouge-style', 'bw' unless document.attr? 'rouge-style' - unless Converter.supports_highlighter_docinfo? + + # Old asciidoctor versions do not have public API for writing highlighter CSS file + # So just use inline CSS there. + unless Document.supports_syntax_highlighter? document.set_attribute 'coderay-css', 'style' document.set_attribute 'pygments-css', 'style' document.set_attribute 'rouge-css', 'style' end