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
|