asciidoctor-epub3 1.0.0.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.adoc +22 -0
- data/NOTICE.adoc +53 -0
- data/README.adoc +744 -0
- data/Rakefile +78 -0
- data/bin/adb-push-ebook +25 -0
- data/bin/asciidoctor-epub3 +15 -0
- data/data/fonts/assorted-icons.ttf +0 -0
- data/data/fonts/fontawesome-icons.ttf +0 -0
- data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bolditalic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
- data/data/fonts/mplus1p-bold-latin-cyrillic.ttf +0 -0
- data/data/fonts/mplus1p-bold-latin-ext.ttf +0 -0
- data/data/fonts/mplus1p-bold-latin.ttf +0 -0
- data/data/fonts/mplus1p-bold-multilingual.ttf +0 -0
- data/data/fonts/mplus1p-light-latin-cyrillic.ttf +0 -0
- data/data/fonts/mplus1p-light-latin-ext.ttf +0 -0
- data/data/fonts/mplus1p-light-latin.ttf +0 -0
- data/data/fonts/mplus1p-light-multilingual.ttf +0 -0
- data/data/fonts/mplus1p-regular-latin-cyrillic.ttf +0 -0
- data/data/fonts/mplus1p-regular-latin-ext.ttf +0 -0
- data/data/fonts/mplus1p-regular-latin.ttf +0 -0
- data/data/fonts/mplus1p-regular-multilingual.ttf +0 -0
- data/data/fonts/notoserif-bold-latin-cyrillic.ttf +0 -0
- data/data/fonts/notoserif-bold-latin-ext.ttf +0 -0
- data/data/fonts/notoserif-bold-latin.ttf +0 -0
- data/data/fonts/notoserif-bold-multilingual.ttf +0 -0
- data/data/fonts/notoserif-bolditalic-latin-cyrillic.ttf +0 -0
- data/data/fonts/notoserif-bolditalic-latin-ext.ttf +0 -0
- data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
- data/data/fonts/notoserif-bolditalic-multilingual.ttf +0 -0
- data/data/fonts/notoserif-italic-latin-cyrillic.ttf +0 -0
- data/data/fonts/notoserif-italic-latin-ext.ttf +0 -0
- data/data/fonts/notoserif-italic-latin.ttf +0 -0
- data/data/fonts/notoserif-italic-multilingual.ttf +0 -0
- data/data/fonts/notoserif-regular-latin-cyrillic.ttf +0 -0
- data/data/fonts/notoserif-regular-latin-ext.ttf +0 -0
- data/data/fonts/notoserif-regular-latin.ttf +0 -0
- data/data/fonts/notoserif-regular-multilingual.ttf +0 -0
- data/data/images/default-avatar.jpg +0 -0
- data/data/images/default-avatar.png +0 -0
- data/data/images/default-avatar.svg +67 -0
- data/data/images/default-cover-large.png +0 -0
- data/data/images/default-cover.png +0 -0
- data/data/images/default-cover.svg +53 -0
- data/data/images/default-headshot.jpg +0 -0
- data/data/images/default-headshot.png +0 -0
- data/data/samples/asciidoctor-epub3-readme.adoc +744 -0
- data/data/samples/asciidoctor-js-extension.adoc +46 -0
- data/data/samples/asciidoctor-js-introduction.adoc +91 -0
- data/data/samples/i18n.adoc +161 -0
- data/data/samples/images/asciidoctor-js-chrome-extension.png +0 -0
- data/data/samples/images/avatars/graphitefriction.png +0 -0
- data/data/samples/images/avatars/mogztter.png +0 -0
- data/data/samples/images/avatars/mojavelinux.png +0 -0
- data/data/samples/images/correct-text-justification.png +0 -0
- data/data/samples/images/incorrect-text-justification.png +0 -0
- data/data/samples/images/screenshots/chapter-title-day.png +0 -0
- data/data/samples/images/screenshots/chapter-title.png +0 -0
- data/data/samples/images/screenshots/figure-admonition.png +0 -0
- data/data/samples/images/screenshots/section-title-paragraph.png +0 -0
- data/data/samples/images/screenshots/sidebar.png +0 -0
- data/data/samples/images/screenshots/table.png +0 -0
- data/data/samples/images/screenshots/text.png +0 -0
- data/data/samples/sample-book.adoc +20 -0
- data/data/samples/sample-content.adoc +168 -0
- data/data/styles/color-palette.css +28 -0
- data/data/styles/epub3-css3-only.css +161 -0
- data/data/styles/epub3-fonts.css +94 -0
- data/data/styles/epub3.css +1293 -0
- data/lib/asciidoctor-epub3.rb +5 -0
- data/lib/asciidoctor-epub3/converter.rb +859 -0
- data/lib/asciidoctor-epub3/core_ext/string.rb +7 -0
- data/lib/asciidoctor-epub3/font_icon_map.rb +376 -0
- data/lib/asciidoctor-epub3/packager.rb +466 -0
- data/lib/asciidoctor-epub3/spine_item_processor.rb +72 -0
- data/lib/asciidoctor-epub3/version.rb +5 -0
- data/scripts/generate-font-subsets.pe +225 -0
- metadata +192 -0
| @@ -0,0 +1,859 @@ | |
| 1 | 
            +
            # encoding: UTF-8
         | 
| 2 | 
            +
            require_relative 'spine_item_processor'
         | 
| 3 | 
            +
            require_relative 'font_icon_map'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Asciidoctor
         | 
| 6 | 
            +
            module Epub3
         | 
| 7 | 
            +
            #WordJoiner = [8288].pack 'U*'
         | 
| 8 | 
            +
            WordJoiner = [65279].pack 'U*'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            # Public: The main converter for the epub3 backend that handles packaging the
         | 
| 11 | 
            +
            # EPUB3 or KF8 publication file.
         | 
| 12 | 
            +
            class Converter
         | 
| 13 | 
            +
              include ::Asciidoctor::Converter
         | 
| 14 | 
            +
              include ::Asciidoctor::Writer
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              register_for 'epub3'
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def initialize backend, opts
         | 
| 19 | 
            +
                super
         | 
| 20 | 
            +
                basebackend 'html'
         | 
| 21 | 
            +
                outfilesuffix '.epub' # dummy outfilesuffix since it may be .mobi
         | 
| 22 | 
            +
                htmlsyntax 'xml'
         | 
| 23 | 
            +
                @validate = false
         | 
| 24 | 
            +
                @extract = false
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def convert spine_doc, name = nil
         | 
| 28 | 
            +
                @validate = true if spine_doc.attr? 'ebook-validate'
         | 
| 29 | 
            +
                @extract = true if spine_doc.attr? 'ebook-extract'
         | 
| 30 | 
            +
                Packager.new spine_doc, (spine_doc.references[:spine_items] || [spine_doc]), spine_doc.attributes['ebook-format'].to_sym
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              # FIXME we have to package in write because we don't have access to target before this point
         | 
| 34 | 
            +
              def write packager, target
         | 
| 35 | 
            +
                # NOTE we use dirname of target since filename is calculated automatically
         | 
| 36 | 
            +
                packager.package validate: @validate, extract: @extract, to_dir: (::File.dirname target)
         | 
| 37 | 
            +
                nil
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            # Public: The converter for the epub3 backend that converts the individual
         | 
| 42 | 
            +
            # content documents in an EPUB3 publication.
         | 
| 43 | 
            +
            class ContentConverter
         | 
| 44 | 
            +
              include ::Asciidoctor::Converter
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              register_for 'epub3-xhtml5'
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              WordJoiner = Epub3::WordJoiner
         | 
| 49 | 
            +
              EOL = "\n"
         | 
| 50 | 
            +
              NoBreakSpace = ' '
         | 
| 51 | 
            +
              ThinNoBreakSpace = ' '
         | 
| 52 | 
            +
              RightAngleQuote = '›'
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              XmlElementRx = /<\/?.+?>/
         | 
| 55 | 
            +
              CharEntityRx = /&#(\d{2,5});/
         | 
| 56 | 
            +
              NamedEntityRx = /&([A-Z]+);/
         | 
| 57 | 
            +
              UppercaseTagRx = /<(\/)?([A-Z]+)>/
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              FromHtmlSpecialCharsMap = {
         | 
| 60 | 
            +
                '<' => '<',
         | 
| 61 | 
            +
                '>' => '>',
         | 
| 62 | 
            +
                '&' => '&'
         | 
| 63 | 
            +
              }
         | 
| 64 | 
            +
             | 
| 65 | 
            +
              FromHtmlSpecialCharsRx = /(?:#{FromHtmlSpecialCharsMap.keys * '|'})/
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              ToHtmlSpecialCharsMap = {
         | 
| 68 | 
            +
                '&' => '&',
         | 
| 69 | 
            +
                '<' => '<',
         | 
| 70 | 
            +
                '>' => '>'
         | 
| 71 | 
            +
              }
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              ToHtmlSpecialCharsRx = /[#{ToHtmlSpecialCharsMap.keys.join}]/
         | 
| 74 | 
            +
             | 
| 75 | 
            +
              OpenParagraphTagRx = /^<p>/
         | 
| 76 | 
            +
              CloseParagraphTagRx = /<\/p>$/
         | 
| 77 | 
            +
             | 
| 78 | 
            +
              def initialize backend, opts
         | 
| 79 | 
            +
                super
         | 
| 80 | 
            +
                basebackend 'html'
         | 
| 81 | 
            +
                outfilesuffix '.xhtml'
         | 
| 82 | 
            +
                htmlsyntax 'xml'
         | 
| 83 | 
            +
                @xrefs_used = ::Set.new
         | 
| 84 | 
            +
                @icon_names = []
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
              def convert node, name = nil
         | 
| 88 | 
            +
                if respond_to?(name ||= node.node_name)
         | 
| 89 | 
            +
                  send name, node
         | 
| 90 | 
            +
                else
         | 
| 91 | 
            +
                  warn %(conversion missing in epub3 backend for #{name})
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
              # TODO aggregate authors of spine document into authors attribute(s) on main document
         | 
| 96 | 
            +
              def navigation_document node, spine
         | 
| 97 | 
            +
                doctitle_sanitized = ((node.doctitle sanitize: true) || (node.attr 'untitled-label')).gsub WordJoiner, ''
         | 
| 98 | 
            +
                lines = [%(<!DOCTYPE html>
         | 
| 99 | 
            +
            <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = (node.attr 'lang', 'en')}" lang="#{lang}">
         | 
| 100 | 
            +
            <head>
         | 
| 101 | 
            +
            <meta charset="UTF-8"/>
         | 
| 102 | 
            +
            <title>#{doctitle_sanitized}</title>
         | 
| 103 | 
            +
            <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
         | 
| 104 | 
            +
            <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
         | 
| 105 | 
            +
            </head>
         | 
| 106 | 
            +
            <body>
         | 
| 107 | 
            +
            <h1>#{doctitle_sanitized}</h1>
         | 
| 108 | 
            +
            <nav epub:type="toc" id="toc">
         | 
| 109 | 
            +
            <h2>#{node.attr 'toc-title'}</h2>
         | 
| 110 | 
            +
            <ol>)]
         | 
| 111 | 
            +
                spine.each do |item|
         | 
| 112 | 
            +
                  lines << %(<li><a href="#{item.id || (item.attr 'docname')}.xhtml">#{((item.doctitle sanitize: true) || (item.attr 'untitled-label')).gsub WordJoiner, ''}</a></li>)
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
                lines << %(</ol>
         | 
| 115 | 
            +
            </nav>
         | 
| 116 | 
            +
            </body>
         | 
| 117 | 
            +
            </html>)
         | 
| 118 | 
            +
                lines * EOL
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
              def document node
         | 
| 122 | 
            +
                docid = node.id
         | 
| 123 | 
            +
                if (doctitle = node.doctitle)
         | 
| 124 | 
            +
                  doctitle_sanitized = (node.doctitle sanitize: :sgml).gsub WordJoiner, ''
         | 
| 125 | 
            +
                  if doctitle.include? ': '
         | 
| 126 | 
            +
                    title, _, subtitle = doctitle.rpartition ': '
         | 
| 127 | 
            +
                  else
         | 
| 128 | 
            +
                    # HACK until we get proper handling of title-only in CSS
         | 
| 129 | 
            +
                    title = ''
         | 
| 130 | 
            +
                    subtitle = doctitle
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
                else
         | 
| 133 | 
            +
                  # HACK until we get proper handling of title-only in CSS
         | 
| 134 | 
            +
                  title = ''
         | 
| 135 | 
            +
                  subtitle = node.attr 'untitled-label'
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
                subtitle_formatted = subtitle.gsub(WordJoiner, '').split(' ').map {|w| %(<b>#{w}</b>) } * ' '
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                title_upper = title.upcase
         | 
| 140 | 
            +
                # FIXME make this uppercase routine more intelligent, less fragile
         | 
| 141 | 
            +
                subtitle_formatted_upper = subtitle_formatted.upcase
         | 
| 142 | 
            +
                    .gsub(UppercaseTagRx) { %(<#{$1}#{$2.downcase}>) }
         | 
| 143 | 
            +
                    .gsub(NamedEntityRx) { %(&#{$1.downcase};) }
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                author = node.attr 'author'
         | 
| 146 | 
            +
                username = node.attr 'username', 'default'
         | 
| 147 | 
            +
                # FIXME needs to resolve to the imagesdir of the spine document, not this document
         | 
| 148 | 
            +
                #imagesdir = (node.attr 'imagesdir', '.').chomp '/'
         | 
| 149 | 
            +
                #imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
         | 
| 150 | 
            +
                imagesdir = 'images/'
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                mark_last_paragraph node
         | 
| 153 | 
            +
                content = node.content
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                # NOTE must run after content is resolved
         | 
| 156 | 
            +
                # NOTE pubtree requires icon CSS to be repeated inside <body> (or in a linked stylesheet); perhaps create dynamic CSS file?
         | 
| 157 | 
            +
                icon_css = unless @icon_names.empty?
         | 
| 158 | 
            +
                  icon_defs = @icon_names.map {|name|
         | 
| 159 | 
            +
                    %(.i-#{name}::before { content: "#{FontIconMap[name.tr('-', '_').to_sym]}"; })
         | 
| 160 | 
            +
                  } * EOL
         | 
| 161 | 
            +
                  %(<style>
         | 
| 162 | 
            +
            #{icon_defs}
         | 
| 163 | 
            +
            </style>
         | 
| 164 | 
            +
            )
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                # NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
         | 
| 168 | 
            +
                lines = [%(<!DOCTYPE html>
         | 
| 169 | 
            +
            <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = (node.attr 'lang', 'en')}" lang="#{lang}">
         | 
| 170 | 
            +
            <head>
         | 
| 171 | 
            +
            <meta charset="UTF-8"/>
         | 
| 172 | 
            +
            <title>#{doctitle_sanitized}</title>
         | 
| 173 | 
            +
            <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
         | 
| 174 | 
            +
            <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
         | 
| 175 | 
            +
            #{icon_css}<script type="text/javascript">
         | 
| 176 | 
            +
            document.addEventListener('DOMContentLoaded', function(event) {
         | 
| 177 | 
            +
              var epubReader = navigator.epubReadingSystem;
         | 
| 178 | 
            +
              if (!epubReader) {
         | 
| 179 | 
            +
                if (window.parent == window || !(epubReader = window.parent.navigator.epubReadingSystem)) {
         | 
| 180 | 
            +
                  return;
         | 
| 181 | 
            +
                }
         | 
| 182 | 
            +
              }
         | 
| 183 | 
            +
              document.body.setAttribute('class', epubReader.name.toLowerCase().replace(/ /g, '-'));
         | 
| 184 | 
            +
            });
         | 
| 185 | 
            +
            </script>
         | 
| 186 | 
            +
            </head>
         | 
| 187 | 
            +
            <body>
         | 
| 188 | 
            +
            <section class="chapter" title="#{doctitle_sanitized.gsub '"', '"'}" epub:type="chapter" id="#{docid}">
         | 
| 189 | 
            +
            #{icon_css && (icon_css.sub '<style>', '<style scoped="scoped">')}<header>
         | 
| 190 | 
            +
            <div class="chapter-header">
         | 
| 191 | 
            +
            <p class="byline"><img src="#{imagesdir}avatars/#{username}.jpg"/> <b class="author">#{author}</b></p>
         | 
| 192 | 
            +
            <h1 class="chapter-title">#{title_upper}#{subtitle ? %[ <small class="subtitle">#{subtitle_formatted_upper}</small>] : nil}</h1>
         | 
| 193 | 
            +
            </div>
         | 
| 194 | 
            +
            </header>
         | 
| 195 | 
            +
            #{content})]
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                if node.footnotes?
         | 
| 198 | 
            +
                  # NOTE kindlegen seems to mangle the <footer> element, so we wrap its content in a div
         | 
| 199 | 
            +
                  lines << '<footer>
         | 
| 200 | 
            +
            <div class="chapter-footer">
         | 
| 201 | 
            +
            <div class="footnotes">'
         | 
| 202 | 
            +
                  node.footnotes.each do |footnote|
         | 
| 203 | 
            +
                    lines << %(<aside id="note-#{footnote.index}" epub:type="footnote">
         | 
| 204 | 
            +
            <p><sup class="noteref"><a href="#noteref-#{footnote.index}">#{footnote.index}</a></sup> #{footnote.text}</p>
         | 
| 205 | 
            +
            </aside>)
         | 
| 206 | 
            +
                  end
         | 
| 207 | 
            +
                  lines << '</div>
         | 
| 208 | 
            +
            </div>
         | 
| 209 | 
            +
            </footer>'
         | 
| 210 | 
            +
                end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                lines << '</section>
         | 
| 213 | 
            +
            </body>
         | 
| 214 | 
            +
            </html>'
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                lines * EOL
         | 
| 217 | 
            +
              end
         | 
| 218 | 
            +
             | 
| 219 | 
            +
              def section node
         | 
| 220 | 
            +
                hlevel = node.level + 1
         | 
| 221 | 
            +
                epub_type_attr = node.special ? %( epub:type="#{node.sectname}") : nil
         | 
| 222 | 
            +
                div_classes = [%(sect#{node.level}), node.role].compact
         | 
| 223 | 
            +
                title = node.title
         | 
| 224 | 
            +
                title_sanitized = xml_sanitize title
         | 
| 225 | 
            +
                if node.document.header? || node.level != 1 || node != node.document.first_section
         | 
| 226 | 
            +
                  %(<section class="#{div_classes * ' '}" title="#{title_sanitized}"#{epub_type_attr}>
         | 
| 227 | 
            +
            <h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? nil : %[
         | 
| 228 | 
            +
            #{content}]}
         | 
| 229 | 
            +
            </section>)
         | 
| 230 | 
            +
                else
         | 
| 231 | 
            +
                  # document has no level-0 heading and this heading serves as the document title
         | 
| 232 | 
            +
                  node.content
         | 
| 233 | 
            +
                end
         | 
| 234 | 
            +
              end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
              # TODO support use of quote block as abstract
         | 
| 237 | 
            +
              def preamble node
         | 
| 238 | 
            +
                if (first_block = node.blocks[0]) && first_block.style == 'abstract'
         | 
| 239 | 
            +
                  abstract first_block
         | 
| 240 | 
            +
                # REVIEW should we treat the preamble as an abstract in general?
         | 
| 241 | 
            +
                elsif first_block && node.blocks.size == 1
         | 
| 242 | 
            +
                  abstract first_block
         | 
| 243 | 
            +
                else
         | 
| 244 | 
            +
                  node.content
         | 
| 245 | 
            +
                end
         | 
| 246 | 
            +
              end
         | 
| 247 | 
            +
             | 
| 248 | 
            +
              # QUESTION use convert_content?
         | 
| 249 | 
            +
              def open node
         | 
| 250 | 
            +
                node.content
         | 
| 251 | 
            +
              end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
              def abstract node
         | 
| 254 | 
            +
                %(<div class="abstract" epub:type="preamble">
         | 
| 255 | 
            +
            #{convert_content node}
         | 
| 256 | 
            +
            </div>)
         | 
| 257 | 
            +
              end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
              def paragraph node
         | 
| 260 | 
            +
                role = node.role
         | 
| 261 | 
            +
                # stack-head is the alternative to the default, inline-head (where inline means "run-in")
         | 
| 262 | 
            +
                head_stop = node.attr 'head-stop', (role && (node.has_role? 'stack-head') ? nil : '.')
         | 
| 263 | 
            +
                head = node.title? ? %(<strong class="head">#{title = node.title}#{head_stop && title !~ /[[:punct:]]$/ ? head_stop : nil}</strong> ) : nil
         | 
| 264 | 
            +
                if role
         | 
| 265 | 
            +
                  if node.has_role? 'signature'
         | 
| 266 | 
            +
                    node.set_option 'hardbreaks'
         | 
| 267 | 
            +
                  end
         | 
| 268 | 
            +
                  %(<p class="#{role}">#{head}#{node.content}</p>)
         | 
| 269 | 
            +
                else
         | 
| 270 | 
            +
                  %(<p>#{head}#{node.content}</p>)
         | 
| 271 | 
            +
                end
         | 
| 272 | 
            +
              end
         | 
| 273 | 
            +
             | 
| 274 | 
            +
              def pass node
         | 
| 275 | 
            +
                content = node.content
         | 
| 276 | 
            +
                if content == '<?hard-pagebreak?>'
         | 
| 277 | 
            +
                  '<hr epub:type="pagebreak" class="pagebreak"/>'
         | 
| 278 | 
            +
                else
         | 
| 279 | 
            +
                  content
         | 
| 280 | 
            +
                end
         | 
| 281 | 
            +
              end
         | 
| 282 | 
            +
             | 
| 283 | 
            +
              def admonition node
         | 
| 284 | 
            +
                if node.title?
         | 
| 285 | 
            +
                  title = node.title
         | 
| 286 | 
            +
                  title_sanitized = xml_sanitize title
         | 
| 287 | 
            +
                  title_attr = %( title="#{node.caption}: #{title_sanitized}")
         | 
| 288 | 
            +
                  title_el = %(<h2>#{title}</h2>
         | 
| 289 | 
            +
            )
         | 
| 290 | 
            +
                else
         | 
| 291 | 
            +
                  title_attr = %( title="#{node.caption}")
         | 
| 292 | 
            +
                  title_el = nil
         | 
| 293 | 
            +
                end
         | 
| 294 | 
            +
             | 
| 295 | 
            +
                type = node.attr 'name'
         | 
| 296 | 
            +
                epub_type = case type
         | 
| 297 | 
            +
                when 'tip'
         | 
| 298 | 
            +
                  'help'
         | 
| 299 | 
            +
                when 'note'
         | 
| 300 | 
            +
                  'note'
         | 
| 301 | 
            +
                when 'important', 'warning', 'caution'
         | 
| 302 | 
            +
                  'warning'
         | 
| 303 | 
            +
                end
         | 
| 304 | 
            +
                %(<aside class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
         | 
| 305 | 
            +
            #{title_el}<div class="content">
         | 
| 306 | 
            +
            #{convert_content node}
         | 
| 307 | 
            +
            </div>
         | 
| 308 | 
            +
            </aside>)
         | 
| 309 | 
            +
              end
         | 
| 310 | 
            +
             | 
| 311 | 
            +
              def example node
         | 
| 312 | 
            +
                title_div = node.title? ? %(<div class="example-title">#{node.title}</div>
         | 
| 313 | 
            +
            ) : nil
         | 
| 314 | 
            +
                %(<div class="example">
         | 
| 315 | 
            +
            #{title_div}<div class="example-content">
         | 
| 316 | 
            +
            #{convert_content node}
         | 
| 317 | 
            +
            </div>
         | 
| 318 | 
            +
            </div>)
         | 
| 319 | 
            +
              end
         | 
| 320 | 
            +
             | 
| 321 | 
            +
              def listing node
         | 
| 322 | 
            +
                figure_classes = ['listing']
         | 
| 323 | 
            +
                figure_classes << 'coalesce' if node.option? 'unbreakable'
         | 
| 324 | 
            +
                pre_classes = if node.style == 'source'
         | 
| 325 | 
            +
                  ['source', %(language-#{node.attr 'language'})]
         | 
| 326 | 
            +
                else
         | 
| 327 | 
            +
                  ['screen']
         | 
| 328 | 
            +
                end
         | 
| 329 | 
            +
                title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>
         | 
| 330 | 
            +
            ) : nil
         | 
| 331 | 
            +
                # patches conums to fix extra or missing leading space
         | 
| 332 | 
            +
                # TODO apply this patch upstream to Asciidoctor
         | 
| 333 | 
            +
                %(<figure class="#{figure_classes * ' '}">
         | 
| 334 | 
            +
            #{title_div}<pre class="#{pre_classes * ' '}"><code>#{node.content.gsub(/(?<! )<i class="conum"| +<i class="conum"/, ' <i class="conum"')}</code></pre>
         | 
| 335 | 
            +
            </figure>)
         | 
| 336 | 
            +
              end
         | 
| 337 | 
            +
             | 
| 338 | 
            +
              # QUESTION should we wrap the <pre> in either <div> or <figure>?
         | 
| 339 | 
            +
              def literal node
         | 
| 340 | 
            +
                %(<pre class="screen">#{node.content}</pre>)
         | 
| 341 | 
            +
              end
         | 
| 342 | 
            +
             | 
| 343 | 
            +
              def page_break node
         | 
| 344 | 
            +
                '<hr epub:type="pagebreak" class="pagebreak"/>'
         | 
| 345 | 
            +
              end
         | 
| 346 | 
            +
             | 
| 347 | 
            +
              def thematic_break node
         | 
| 348 | 
            +
                '<hr class="thematicbreak"/>'
         | 
| 349 | 
            +
              end
         | 
| 350 | 
            +
             | 
| 351 | 
            +
              def quote node
         | 
| 352 | 
            +
                footer_content = []
         | 
| 353 | 
            +
                if attribution = (node.attr 'attribution')
         | 
| 354 | 
            +
                  footer_content << attribution  
         | 
| 355 | 
            +
                end
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                if citetitle = (node.attr 'citetitle')
         | 
| 358 | 
            +
                  citetitle_sanitized = xml_sanitize citetitle
         | 
| 359 | 
            +
                  footer_content << %(<cite title="#{citetitle_sanitized}">#{citetitle}</cite>)
         | 
| 360 | 
            +
                end
         | 
| 361 | 
            +
             | 
| 362 | 
            +
                if node.title?
         | 
| 363 | 
            +
                  footer_content << %(<span class="context">#{node.title}</span>)
         | 
| 364 | 
            +
                end
         | 
| 365 | 
            +
             | 
| 366 | 
            +
                footer_tag = footer_content.empty? ? nil : %(
         | 
| 367 | 
            +
            <footer>~ #{footer_content * ' '}</footer>)
         | 
| 368 | 
            +
                content = (convert_content node).strip.
         | 
| 369 | 
            +
                  sub(OpenParagraphTagRx, '<p><span class="open-quote">“</span>').
         | 
| 370 | 
            +
                  sub(CloseParagraphTagRx, '<span class="close-quote">”</span></p>')
         | 
| 371 | 
            +
                %(<div class="blockquote">
         | 
| 372 | 
            +
            <blockquote>
         | 
| 373 | 
            +
            #{content}#{footer_tag}
         | 
| 374 | 
            +
            </blockquote>
         | 
| 375 | 
            +
            </div>)
         | 
| 376 | 
            +
              end
         | 
| 377 | 
            +
             | 
| 378 | 
            +
              def verse node
         | 
| 379 | 
            +
                footer_content = []
         | 
| 380 | 
            +
                if attribution = (node.attr 'attribution')
         | 
| 381 | 
            +
                  footer_content << attribution  
         | 
| 382 | 
            +
                end
         | 
| 383 | 
            +
             | 
| 384 | 
            +
                if citetitle = (node.attr 'citetitle')
         | 
| 385 | 
            +
                  citetitle_sanitized = xml_sanitize citetitle
         | 
| 386 | 
            +
                  footer_content << %(<cite title="#{citetitle_sanitized}">#{citetitle}</cite>)
         | 
| 387 | 
            +
                end
         | 
| 388 | 
            +
             | 
| 389 | 
            +
                footer_tag = footer_content.size > 0 ? %(
         | 
| 390 | 
            +
            <span class="attribution">~ #{footer_content * ', '}</span>) : nil
         | 
| 391 | 
            +
                %(<div class="verse">
         | 
| 392 | 
            +
            <pre>#{node.content}#{footer_tag}</pre>
         | 
| 393 | 
            +
            </div>)
         | 
| 394 | 
            +
              end
         | 
| 395 | 
            +
             | 
| 396 | 
            +
              def sidebar node
         | 
| 397 | 
            +
                classes = ['sidebar']
         | 
| 398 | 
            +
                if node.title?
         | 
| 399 | 
            +
                  classes << 'titled'
         | 
| 400 | 
            +
                  title = node.title
         | 
| 401 | 
            +
                  title_sanitized = xml_sanitize title
         | 
| 402 | 
            +
                  title_attr = %( title="#{title_sanitized}")
         | 
| 403 | 
            +
                  title_upper = title.upcase.gsub(NamedEntityRx) { %(&#{$1.downcase};) }
         | 
| 404 | 
            +
                  title_el = %(<h2>#{title_upper}</h2>
         | 
| 405 | 
            +
            )
         | 
| 406 | 
            +
                else
         | 
| 407 | 
            +
                  title_attr = nil
         | 
| 408 | 
            +
                  title_el = nil
         | 
| 409 | 
            +
                end
         | 
| 410 | 
            +
             | 
| 411 | 
            +
                %(<aside class="#{classes * ' '}"#{title_attr} epub:type="sidebar">
         | 
| 412 | 
            +
            #{title_el}<div class="content">
         | 
| 413 | 
            +
            #{convert_content node}
         | 
| 414 | 
            +
            </div>
         | 
| 415 | 
            +
            </aside>)
         | 
| 416 | 
            +
              end
         | 
| 417 | 
            +
             | 
| 418 | 
            +
              def table node
         | 
| 419 | 
            +
                lines = [%(<div class="table">)]
         | 
| 420 | 
            +
                lines << %(<div class="content">)
         | 
| 421 | 
            +
                table_id_attr = node.id ? %( id="#{node.id}") : nil
         | 
| 422 | 
            +
                frame_class = {
         | 
| 423 | 
            +
                  'all' => 'table-framed',
         | 
| 424 | 
            +
                  'topbot' => 'table-framed-topbot',
         | 
| 425 | 
            +
                  'sides' => 'table-framed-sides'
         | 
| 426 | 
            +
                }
         | 
| 427 | 
            +
                grid_class = {
         | 
| 428 | 
            +
                  'all' => 'table-grid',
         | 
| 429 | 
            +
                  'rows' => 'table-grid-rows',
         | 
| 430 | 
            +
                  'cols' => 'table-grid-cols'
         | 
| 431 | 
            +
                }
         | 
| 432 | 
            +
                table_classes = %W(table #{frame_class[(node.attr 'frame')] || frame_class['topbot']} #{grid_class[(node.attr 'grid')] || grid_class['rows']})
         | 
| 433 | 
            +
                if (role = node.role)
         | 
| 434 | 
            +
                  table_classes << role
         | 
| 435 | 
            +
                end
         | 
| 436 | 
            +
                table_class_attr = %( class="#{table_classes * ' '}")
         | 
| 437 | 
            +
                table_styles = []
         | 
| 438 | 
            +
                unless node.option? 'autowidth'
         | 
| 439 | 
            +
                  table_styles << %(width: #{node.attr 'tablepcwidth'}%;)
         | 
| 440 | 
            +
                end
         | 
| 441 | 
            +
                table_style_attr = table_styles.size > 0 ? %( style="#{table_styles * ' '}") : nil
         | 
| 442 | 
            +
             | 
| 443 | 
            +
                lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>)
         | 
| 444 | 
            +
                lines << %(<caption>#{node.captioned_title}</caption>) if node.title?
         | 
| 445 | 
            +
                if (node.attr 'rowcount') > 0
         | 
| 446 | 
            +
                  lines << '<colgroup>'
         | 
| 447 | 
            +
                  #if node.option? 'autowidth'
         | 
| 448 | 
            +
                    tag = %(<col/>)
         | 
| 449 | 
            +
                    node.columns.size.times do
         | 
| 450 | 
            +
                      lines << tag
         | 
| 451 | 
            +
                    end
         | 
| 452 | 
            +
                  #else
         | 
| 453 | 
            +
                  #  node.columns.each do |col|
         | 
| 454 | 
            +
                  #    lines << %(<col style="width: #{col.attr 'colpcwidth'}%;"/>)
         | 
| 455 | 
            +
                  #  end
         | 
| 456 | 
            +
                  #end
         | 
| 457 | 
            +
                  lines << '</colgroup>'
         | 
| 458 | 
            +
                  [:head, :foot, :body].select {|tsec| !node.rows[tsec].empty? }.each do |tsec|
         | 
| 459 | 
            +
                    lines << %(<t#{tsec}>)
         | 
| 460 | 
            +
                    node.rows[tsec].each do |row|
         | 
| 461 | 
            +
                      lines << '<tr>'
         | 
| 462 | 
            +
                      row.each do |cell|
         | 
| 463 | 
            +
                        if tsec == :head
         | 
| 464 | 
            +
                          cell_content = cell.text
         | 
| 465 | 
            +
                        else
         | 
| 466 | 
            +
                          case cell.style
         | 
| 467 | 
            +
                          when :asciidoc
         | 
| 468 | 
            +
                            cell_content = %(<div>#{cell.content}</div>)
         | 
| 469 | 
            +
                          when :verse
         | 
| 470 | 
            +
                            cell_content = %(<div class="verse">#{cell.text}</div>)
         | 
| 471 | 
            +
                          when :literal
         | 
| 472 | 
            +
                            cell_content = %(<div class="literal"><pre>#{cell.text}</pre></div>)
         | 
| 473 | 
            +
                          else
         | 
| 474 | 
            +
                            cell_content = ''
         | 
| 475 | 
            +
                            cell.content.each do |text|
         | 
| 476 | 
            +
                              cell_content = %(#{cell_content}<p>#{text}</p>)
         | 
| 477 | 
            +
                            end
         | 
| 478 | 
            +
                          end
         | 
| 479 | 
            +
                        end
         | 
| 480 | 
            +
             | 
| 481 | 
            +
                        cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
         | 
| 482 | 
            +
                        cell_classes = []
         | 
| 483 | 
            +
                        if (halign = cell.attr 'halign') && halign != 'left'
         | 
| 484 | 
            +
                          cell_classes << 'halign-left'
         | 
| 485 | 
            +
                        end
         | 
| 486 | 
            +
                        if (halign = cell.attr 'valign') && halign != 'top'
         | 
| 487 | 
            +
                          cell_classes << 'valign-top'
         | 
| 488 | 
            +
                        end
         | 
| 489 | 
            +
                        cell_class_attr = cell_classes.size > 0 ? %( class="#{cell_classes * ' '}") : nil
         | 
| 490 | 
            +
                        cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : nil
         | 
| 491 | 
            +
                        cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : nil
         | 
| 492 | 
            +
                        cell_style_attr = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : nil
         | 
| 493 | 
            +
                        lines << %(<#{cell_tag_name}#{cell_class_attr}#{cell_colspan_attr}#{cell_rowspan_attr}#{cell_style_attr}>#{cell_content}</#{cell_tag_name}>)
         | 
| 494 | 
            +
                      end
         | 
| 495 | 
            +
                      lines << '</tr>'
         | 
| 496 | 
            +
                    end
         | 
| 497 | 
            +
                    lines << %(</t#{tsec}>)
         | 
| 498 | 
            +
                  end
         | 
| 499 | 
            +
                end
         | 
| 500 | 
            +
                lines << '</table>
         | 
| 501 | 
            +
            </div>
         | 
| 502 | 
            +
            </div>'
         | 
| 503 | 
            +
                lines * EOL
         | 
| 504 | 
            +
              end
         | 
| 505 | 
            +
             | 
| 506 | 
            +
              def colist node
         | 
| 507 | 
            +
                lines = ['<div class="callout-list">
         | 
| 508 | 
            +
            <ol>']
         | 
| 509 | 
            +
                num = "\u2460"
         | 
| 510 | 
            +
                node.items.each_with_index do |item, i|
         | 
| 511 | 
            +
                  lines << %(<li><i class="conum" data-value="#{i + 1}">#{num}</i> #{item.text}</li>)
         | 
| 512 | 
            +
                  num = num.next
         | 
| 513 | 
            +
                end
         | 
| 514 | 
            +
                lines << '</ol>
         | 
| 515 | 
            +
            </div>'
         | 
| 516 | 
            +
              end
         | 
| 517 | 
            +
             | 
| 518 | 
            +
              # TODO add complex class if list has nested blocks
         | 
| 519 | 
            +
              def dlist node
         | 
| 520 | 
            +
                lines = []
         | 
| 521 | 
            +
                case (style = node.style)
         | 
| 522 | 
            +
                when 'itemized', 'ordered'
         | 
| 523 | 
            +
                  list_tag_name = (style == 'itemized' ? 'ul' : 'ol')
         | 
| 524 | 
            +
                  role = node.role
         | 
| 525 | 
            +
                  subject_stop = node.attr 'subject-stop', (role && (node.has_role? 'stack') ? nil : ':')
         | 
| 526 | 
            +
                  # QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
         | 
| 527 | 
            +
                  div_classes = [%(#{style}-list), role].compact
         | 
| 528 | 
            +
                  list_class_attr = (node.option? 'brief') ? ' class="brief"' : nil
         | 
| 529 | 
            +
                  lines << %(<div class="#{div_classes * ' '}">
         | 
| 530 | 
            +
            <#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : nil}>)
         | 
| 531 | 
            +
                  node.items.each do |subjects, dd|
         | 
| 532 | 
            +
                    # consists of one term (a subject) and supporting content
         | 
| 533 | 
            +
                    subject = [*subjects].first.text
         | 
| 534 | 
            +
                    subject_plain = xml_sanitize subject, :plain
         | 
| 535 | 
            +
                    subject_element = %(<strong class="subject">#{subject}#{subject_stop && subject_plain !~ /[[:punct:]]$/ ? subject_stop : nil}</strong>)
         | 
| 536 | 
            +
                    lines << '<li>'
         | 
| 537 | 
            +
                    if dd
         | 
| 538 | 
            +
                      # NOTE: must wrap remaining text in a span to help webkit justify the text properly
         | 
| 539 | 
            +
                      lines << %(<span class="principal">#{subject_element}#{dd.text? ? %[ <span class="supporting">#{dd.text}</span>] : nil}</span>) 
         | 
| 540 | 
            +
                      lines << dd.content if dd.blocks?
         | 
| 541 | 
            +
                    else
         | 
| 542 | 
            +
                      lines << %(<span class="principal">#{subject_element}</span>)
         | 
| 543 | 
            +
                    end
         | 
| 544 | 
            +
                    lines << '</li>'
         | 
| 545 | 
            +
                  end
         | 
| 546 | 
            +
                  lines << %(</#{list_tag_name}>
         | 
| 547 | 
            +
            </div>)
         | 
| 548 | 
            +
                else
         | 
| 549 | 
            +
                  lines << '<div class="description-list">
         | 
| 550 | 
            +
            <dl>'
         | 
| 551 | 
            +
                  node.items.each do |terms, dd|
         | 
| 552 | 
            +
                    [*terms].each do |dt|
         | 
| 553 | 
            +
                      lines << %(<dt>
         | 
| 554 | 
            +
            <span class="term">#{dt.text}</span>
         | 
| 555 | 
            +
            </dt>)
         | 
| 556 | 
            +
                    end
         | 
| 557 | 
            +
                    if dd
         | 
| 558 | 
            +
                      lines << '<dd>'
         | 
| 559 | 
            +
                      if dd.blocks?
         | 
| 560 | 
            +
                        lines << %(<span class="principal">#{dd.text}</span>) if dd.text?
         | 
| 561 | 
            +
                        lines << dd.content
         | 
| 562 | 
            +
                      else
         | 
| 563 | 
            +
                        lines << dd.text
         | 
| 564 | 
            +
                      end
         | 
| 565 | 
            +
                      lines << '</dd>'
         | 
| 566 | 
            +
                    end
         | 
| 567 | 
            +
                  end
         | 
| 568 | 
            +
                  lines << '</dl>
         | 
| 569 | 
            +
            </div>'
         | 
| 570 | 
            +
                end
         | 
| 571 | 
            +
                lines * EOL
         | 
| 572 | 
            +
              end
         | 
| 573 | 
            +
             | 
| 574 | 
            +
              # TODO support start attribute
         | 
| 575 | 
            +
              def olist node
         | 
| 576 | 
            +
                complex = false
         | 
| 577 | 
            +
                div_classes = ['ordered-list', node.style, node.role].compact
         | 
| 578 | 
            +
                ol_classes = [node.style, ((node.option? 'brief') ? 'brief' : nil)].compact
         | 
| 579 | 
            +
                ol_class_attr = ol_classes.empty? ? nil : %( class="#{ol_classes * ' '}")
         | 
| 580 | 
            +
                id_attribute = node.id ? %( id="#{node.id}") : nil
         | 
| 581 | 
            +
                lines = [%(<div#{id_attribute} class="#{div_classes * ' '}">)]
         | 
| 582 | 
            +
                lines << %(<h3>#{node.title}</h3>) if node.title?
         | 
| 583 | 
            +
                lines << %(<ol#{ol_class_attr}#{(node.option? 'reversed') ? ' reversed="reversed"' : nil}>)
         | 
| 584 | 
            +
                node.items.each do |item|
         | 
| 585 | 
            +
                  lines << %(<li>
         | 
| 586 | 
            +
            <span class="principal">#{item.text}</span>)
         | 
| 587 | 
            +
                  if item.blocks?
         | 
| 588 | 
            +
                    lines << item.content
         | 
| 589 | 
            +
                    complex = true unless item.blocks.size == 1 && ::Asciidoctor::List === item.blocks[0]
         | 
| 590 | 
            +
                  end
         | 
| 591 | 
            +
                  lines << '</li>'
         | 
| 592 | 
            +
                end
         | 
| 593 | 
            +
                if complex
         | 
| 594 | 
            +
                  div_classes << 'complex'
         | 
| 595 | 
            +
                  lines[0] = %(<div class="#{div_classes * ' '}">)
         | 
| 596 | 
            +
                end
         | 
| 597 | 
            +
                lines << '</ol>
         | 
| 598 | 
            +
            </div>'
         | 
| 599 | 
            +
                lines * EOL
         | 
| 600 | 
            +
              end
         | 
| 601 | 
            +
             | 
| 602 | 
            +
              def ulist node
         | 
| 603 | 
            +
                complex = false
         | 
| 604 | 
            +
                div_classes = ['itemized-list', node.style, node.role].compact
         | 
| 605 | 
            +
                # TODO could strip WordJoiner if brief since not using justify
         | 
| 606 | 
            +
                ul_classes = [node.style, ((node.option? 'brief') ? 'brief' : nil)].compact
         | 
| 607 | 
            +
                ul_class_attr = ul_classes.empty? ? nil : %( class="#{ul_classes * ' '}")
         | 
| 608 | 
            +
                id_attribute = node.id ? %( id="#{node.id}") : nil
         | 
| 609 | 
            +
                lines = [%(<div#{id_attribute} class="#{div_classes * ' '}">)]
         | 
| 610 | 
            +
                lines << %(<h3>#{node.title}</h3>) if node.title?
         | 
| 611 | 
            +
                lines << %(<ul#{ul_class_attr}>)
         | 
| 612 | 
            +
                node.items.each do |item|
         | 
| 613 | 
            +
                  lines << %(<li>
         | 
| 614 | 
            +
            <span class="principal">#{item.text}</span>)
         | 
| 615 | 
            +
                  if item.blocks?
         | 
| 616 | 
            +
                    lines << item.content
         | 
| 617 | 
            +
                    complex = true unless item.blocks.size == 1 && ::Asciidoctor::List === item.blocks[0]
         | 
| 618 | 
            +
                  end
         | 
| 619 | 
            +
                  lines << '</li>'
         | 
| 620 | 
            +
                end
         | 
| 621 | 
            +
                if complex
         | 
| 622 | 
            +
                  div_classes << 'complex'
         | 
| 623 | 
            +
                  lines[0] = %(<div class="#{div_classes * ' '}">)
         | 
| 624 | 
            +
                end
         | 
| 625 | 
            +
                lines << '</ul>
         | 
| 626 | 
            +
            </div>'
         | 
| 627 | 
            +
                lines * EOL
         | 
| 628 | 
            +
              end
         | 
| 629 | 
            +
             | 
| 630 | 
            +
              def image node
         | 
| 631 | 
            +
                target = node.attr 'target'
         | 
| 632 | 
            +
                type = (::File.extname target)[1..-1]
         | 
| 633 | 
            +
                img_attrs = [%(alt="#{node.attr 'alt'}")]
         | 
| 634 | 
            +
                case type
         | 
| 635 | 
            +
                when 'svg'
         | 
| 636 | 
            +
                  img_attrs << %(style="width: #{node.attr 'scaledwidth', '100%'};")
         | 
| 637 | 
            +
                  # TODO make this a convenience method on document
         | 
| 638 | 
            +
                  epub_properties = (node.document.attr 'epub-properties') || []
         | 
| 639 | 
            +
                  unless epub_properties.include? 'svg'
         | 
| 640 | 
            +
                    epub_properties << 'svg'
         | 
| 641 | 
            +
                    node.document.attributes['epub-properties'] = epub_properties
         | 
| 642 | 
            +
                  end
         | 
| 643 | 
            +
                else
         | 
| 644 | 
            +
                  if node.attr? 'scaledwidth'
         | 
| 645 | 
            +
                    img_attrs << %(style="width: #{node.attr 'scaledwidth'};")
         | 
| 646 | 
            +
                  end
         | 
| 647 | 
            +
                end
         | 
| 648 | 
            +
            =begin
         | 
| 649 | 
            +
                # NOTE to set actual width and height, use CSS width and height
         | 
| 650 | 
            +
                if type == 'svg'
         | 
| 651 | 
            +
                  if node.attr? 'scaledwidth'
         | 
| 652 | 
            +
                    img_attrs << %(width="#{node.attr 'scaledwidth'}")
         | 
| 653 | 
            +
                  # Kindle
         | 
| 654 | 
            +
                  #elsif node.attr? 'scaledheight'
         | 
| 655 | 
            +
                  #  img_attrs << %(width="#{node.attr 'scaledheight'}" height="#{node.attr 'scaledheight'}")
         | 
| 656 | 
            +
                  # ePub3
         | 
| 657 | 
            +
                  elsif node.attr? 'scaledheight'
         | 
| 658 | 
            +
                    img_attrs << %(height="#{node.attr 'scaledheight'}" style="max-height: #{node.attr 'scaledheight'} !important;")
         | 
| 659 | 
            +
                  else
         | 
| 660 | 
            +
                    # Aldiko doesn't not scale width to 100% by default
         | 
| 661 | 
            +
                    img_attrs << %(width="100%")
         | 
| 662 | 
            +
                  end
         | 
| 663 | 
            +
                end
         | 
| 664 | 
            +
            =end
         | 
| 665 | 
            +
                %(<figure class="image">
         | 
| 666 | 
            +
            <div class="content">
         | 
| 667 | 
            +
            <img src="#{node.image_uri node.attr('target')}" #{img_attrs * ' '}/>
         | 
| 668 | 
            +
            </div>#{node.title? ? %[
         | 
| 669 | 
            +
            <figcaption>#{node.captioned_title}</figcaption>] : nil}
         | 
| 670 | 
            +
            </figure>)
         | 
| 671 | 
            +
              end
         | 
| 672 | 
            +
             | 
| 673 | 
            +
              def inline_anchor node
         | 
| 674 | 
            +
                target = node.target
         | 
| 675 | 
            +
                case node.type
         | 
| 676 | 
            +
                when :xref
         | 
| 677 | 
            +
                  refid = (node.attr 'refid') || target
         | 
| 678 | 
            +
                  id_attr = unless @xrefs_used.include? refid
         | 
| 679 | 
            +
                    @xrefs_used << refid
         | 
| 680 | 
            +
                    %( id="xref-#{refid}")
         | 
| 681 | 
            +
                  end
         | 
| 682 | 
            +
                  # FIXME seems like text should be prepared already
         | 
| 683 | 
            +
                  # FIXME would be nice to know what type the target is (e.g., bibref)
         | 
| 684 | 
            +
                  text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
         | 
| 685 | 
            +
                  %(<a#{id_attr} href="#{target}" class="xref">#{text}</a>#{WordJoiner})
         | 
| 686 | 
            +
                when :ref
         | 
| 687 | 
            +
                  %(<a id="#{target}"></a>)
         | 
| 688 | 
            +
                when :link
         | 
| 689 | 
            +
                  %(<a href="#{target}" class="link">#{node.text}</a>#{WordJoiner})
         | 
| 690 | 
            +
                when :bibref
         | 
| 691 | 
            +
                  %(<a id="#{target}" href="#xref-#{target}">[#{target}]</a>#{WordJoiner})
         | 
| 692 | 
            +
                end
         | 
| 693 | 
            +
              end
         | 
| 694 | 
            +
             | 
| 695 | 
            +
              def inline_break node
         | 
| 696 | 
            +
                %(#{node.text}<br/>)
         | 
| 697 | 
            +
              end
         | 
| 698 | 
            +
             | 
| 699 | 
            +
              def inline_button node
         | 
| 700 | 
            +
                %(<b class="button">[<span class="label">#{node.text}</span>]</b>#{WordJoiner})
         | 
| 701 | 
            +
              end
         | 
| 702 | 
            +
             | 
| 703 | 
            +
              def inline_callout node
         | 
| 704 | 
            +
                num = "\u2460"
         | 
| 705 | 
            +
                int_num = node.text.to_i
         | 
| 706 | 
            +
                (int_num - 1).times { num = num.next }
         | 
| 707 | 
            +
                %(<i class="conum" data-value="#{int_num}">#{num}</i>)
         | 
| 708 | 
            +
              end
         | 
| 709 | 
            +
             | 
| 710 | 
            +
              def inline_footnote node
         | 
| 711 | 
            +
                if (index = node.attr 'index')
         | 
| 712 | 
            +
                  %(<sup class="noteref">[<a id="noteref-#{index}" href="#note-#{index}" epub:type="noteref">#{index}</a>]</sup>)
         | 
| 713 | 
            +
                elsif node.type == :xref
         | 
| 714 | 
            +
                  %(<mark class="noteref" title="Unresolved note reference">#{node.text}</mark>)
         | 
| 715 | 
            +
                end
         | 
| 716 | 
            +
              end
         | 
| 717 | 
            +
             | 
| 718 | 
            +
              def inline_image node
         | 
| 719 | 
            +
                if (type = node.type) == 'icon'
         | 
| 720 | 
            +
                  @icon_names << (icon_name = node.target)
         | 
| 721 | 
            +
                  i_classes = ['icon', %(i-#{icon_name})]
         | 
| 722 | 
            +
                  i_classes << %(icon-#{node.attr 'size'}) if node.attr? 'size'
         | 
| 723 | 
            +
                  i_classes << %(icon-flip-#{(node.attr 'flip')[0]}) if node.attr? 'flip'
         | 
| 724 | 
            +
                  i_classes << %(icon-rotate-#{node.attr 'rotate'}) if node.attr? 'rotate'
         | 
| 725 | 
            +
                  i_classes << node.role if node.role?
         | 
| 726 | 
            +
                  %(<i class="#{i_classes * ' '}"></i>)
         | 
| 727 | 
            +
                else
         | 
| 728 | 
            +
                  target = node.image_uri node.target
         | 
| 729 | 
            +
                  class_attr = %( class="#{node.role}") if node.role?
         | 
| 730 | 
            +
                  %(<img src="#{target}" alt="#{node.attr 'alt'}"#{class_attr}/>)
         | 
| 731 | 
            +
                end
         | 
| 732 | 
            +
              end
         | 
| 733 | 
            +
             | 
| 734 | 
            +
              def inline_indexterm node
         | 
| 735 | 
            +
                node.type == :visible ? node.text : ''
         | 
| 736 | 
            +
              end
         | 
| 737 | 
            +
             | 
| 738 | 
            +
              def inline_kbd node
         | 
| 739 | 
            +
                if (keys = node.attr 'keys').size == 1
         | 
| 740 | 
            +
                  %(<kbd>#{keys[0]}</kbd>)
         | 
| 741 | 
            +
                else
         | 
| 742 | 
            +
                  key_combo = keys.map {|key| %(<kbd>#{key}</kbd>+) }.join.chop
         | 
| 743 | 
            +
                  %(<span class="keyseq">#{key_combo}</span>)
         | 
| 744 | 
            +
                end
         | 
| 745 | 
            +
              end
         | 
| 746 | 
            +
             | 
| 747 | 
            +
              def inline_menu node
         | 
| 748 | 
            +
                menu = node.attr 'menu'
         | 
| 749 | 
            +
                # NOTE we swap right angle quote with chevron right from FontAwesome using CSS
         | 
| 750 | 
            +
                caret = %(#{NoBreakSpace}<span class="caret">#{RightAngleQuote}</span> )
         | 
| 751 | 
            +
                if !(submenus = node.attr 'submenus').empty?
         | 
| 752 | 
            +
                  submenu_path = submenus.map {|submenu| %(<span class="submenu">#{submenu}</span>#{caret}) }.join.chop
         | 
| 753 | 
            +
                  %(<span class="menuseq"><span class="menu">#{menu}</span>#{caret}#{submenu_path} <span class="menuitem">#{node.attr 'menuitem'}</span></span>)
         | 
| 754 | 
            +
                elsif (menuitem = node.attr 'menuitem')
         | 
| 755 | 
            +
                  %(<span class="menuseq"><span class="menu">#{menu}</span>#{caret}<span class="menuitem">#{menuitem}</span></span>)
         | 
| 756 | 
            +
                else
         | 
| 757 | 
            +
                  %(<span class="menu">#{menu}</span>)
         | 
| 758 | 
            +
                end
         | 
| 759 | 
            +
              end
         | 
| 760 | 
            +
             | 
| 761 | 
            +
              def inline_quoted node
         | 
| 762 | 
            +
                case node.type
         | 
| 763 | 
            +
                when :strong
         | 
| 764 | 
            +
                  %(<strong>#{node.text}</strong>#{WordJoiner})
         | 
| 765 | 
            +
                when :emphasis
         | 
| 766 | 
            +
                  %(<em>#{node.text}</em>#{WordJoiner})
         | 
| 767 | 
            +
                when :monospaced
         | 
| 768 | 
            +
                  %(<code class="literal">#{node.text}</code>#{WordJoiner})
         | 
| 769 | 
            +
                when :double
         | 
| 770 | 
            +
                  #%(“#{node.text}”)
         | 
| 771 | 
            +
                  %(“#{node.text}”)
         | 
| 772 | 
            +
                when :single
         | 
| 773 | 
            +
                  #%(‘#{node.text}’)
         | 
| 774 | 
            +
                  %(‘#{node.text}’)
         | 
| 775 | 
            +
                when :superscript
         | 
| 776 | 
            +
                  %(<sup>#{node.text}</sup>#{WordJoiner})
         | 
| 777 | 
            +
                when :subscript
         | 
| 778 | 
            +
                  %(<sub>#{node.text}</sub>#{WordJoiner})
         | 
| 779 | 
            +
                else
         | 
| 780 | 
            +
                  node.text
         | 
| 781 | 
            +
                end
         | 
| 782 | 
            +
              end
         | 
| 783 | 
            +
             | 
| 784 | 
            +
              def convert_content node
         | 
| 785 | 
            +
                if node.content_model == :simple
         | 
| 786 | 
            +
                  %(<p>#{node.content}</p>)
         | 
| 787 | 
            +
                else
         | 
| 788 | 
            +
                  node.content
         | 
| 789 | 
            +
                end
         | 
| 790 | 
            +
              end
         | 
| 791 | 
            +
             | 
| 792 | 
            +
              def xml_sanitize value, target = :attribute
         | 
| 793 | 
            +
                sanitized = (value.include? '<') ? value.gsub(XmlElementRx, '').tr_s(' ', ' ').strip : value
         | 
| 794 | 
            +
                if target == :plain && (sanitized.include? ';')
         | 
| 795 | 
            +
                  sanitized = sanitized.gsub(CharEntityRx) { [$1.to_i].pack('U*') }.gsub(FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap)
         | 
| 796 | 
            +
                elsif target == :attribute
         | 
| 797 | 
            +
                  sanitized = sanitized.gsub(WordJoiner, '').gsub('"', '"')
         | 
| 798 | 
            +
                end
         | 
| 799 | 
            +
                sanitized
         | 
| 800 | 
            +
              end
         | 
| 801 | 
            +
             | 
| 802 | 
            +
              # TODO make check for last content paragraph a feature of Asciidoctor
         | 
| 803 | 
            +
              def mark_last_paragraph root
         | 
| 804 | 
            +
                return unless (last_block = root.blocks[-1])
         | 
| 805 | 
            +
                while last_block.context == :section && last_block.blocks?
         | 
| 806 | 
            +
                  last_block = last_block.blocks[-1]
         | 
| 807 | 
            +
                end
         | 
| 808 | 
            +
                if last_block.context == :paragraph
         | 
| 809 | 
            +
                  last_block.attributes['role'] = last_block.role? ? %(#{last_block.role} last) : 'last'
         | 
| 810 | 
            +
                end
         | 
| 811 | 
            +
                nil
         | 
| 812 | 
            +
              end
         | 
| 813 | 
            +
            end
         | 
| 814 | 
            +
             | 
| 815 | 
            +
            class DocumentIdGenerator
         | 
| 816 | 
            +
              class << self
         | 
| 817 | 
            +
                def generate_id doc
         | 
| 818 | 
            +
                  unless (id = doc.id)
         | 
| 819 | 
            +
                    id = if doc.header?
         | 
| 820 | 
            +
                      doc.doctitle(sanitize: :sgml).gsub(WordJoiner, '').downcase.delete(':').tr_s(' ', '-').tr_s('-', '-')
         | 
| 821 | 
            +
                    elsif (first_section = doc.first_section)
         | 
| 822 | 
            +
                      first_section.id
         | 
| 823 | 
            +
                    else
         | 
| 824 | 
            +
                      %(document-#{doc.object_id})
         | 
| 825 | 
            +
                    end
         | 
| 826 | 
            +
                  end
         | 
| 827 | 
            +
                  id
         | 
| 828 | 
            +
                end
         | 
| 829 | 
            +
              end
         | 
| 830 | 
            +
            end
         | 
| 831 | 
            +
             | 
| 832 | 
            +
            require_relative 'packager'
         | 
| 833 | 
            +
             | 
| 834 | 
            +
            Extensions.register do
         | 
| 835 | 
            +
              if (document = @document).backend == 'epub3'
         | 
| 836 | 
            +
                document.attributes['spine'] = ''
         | 
| 837 | 
            +
                document.set_attribute 'listing-caption', 'Listing'
         | 
| 838 | 
            +
                if !(defined? ::AsciidoctorJ) && (::Gem::try_activate 'pygments.rb')
         | 
| 839 | 
            +
                  if document.set_attribute 'source-highlighter', 'pygments'
         | 
| 840 | 
            +
                    document.set_attribute 'pygments-css', 'style'
         | 
| 841 | 
            +
                    document.set_attribute 'pygments-style', 'bw'
         | 
| 842 | 
            +
                  end
         | 
| 843 | 
            +
                end
         | 
| 844 | 
            +
                case (ebook_format = document.attributes['ebook-format'])
         | 
| 845 | 
            +
                when 'epub3', 'kf8'
         | 
| 846 | 
            +
                  # all good
         | 
| 847 | 
            +
                when 'mobi'
         | 
| 848 | 
            +
                  document.attributes['ebook-format'] = 'kf8'
         | 
| 849 | 
            +
                else
         | 
| 850 | 
            +
                  document.attributes['ebook-format'] = 'epub3'
         | 
| 851 | 
            +
                end
         | 
| 852 | 
            +
                document.attributes[%(ebook-format-#{ebook_format})] = ''
         | 
| 853 | 
            +
                # Only fire SpineItemProcessor for top-level include directives
         | 
| 854 | 
            +
                include_processor SpineItemProcessor.new(document)
         | 
| 855 | 
            +
                treeprocessor { process {|doc| doc.id = DocumentIdGenerator.generate_id doc } }
         | 
| 856 | 
            +
              end
         | 
| 857 | 
            +
            end
         | 
| 858 | 
            +
            end
         | 
| 859 | 
            +
            end
         |