persie 0.0.1.alpha

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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +21 -0
  6. data/README.md +52 -0
  7. data/Rakefile +85 -0
  8. data/bin/persie +12 -0
  9. data/lib/persie/asciidoctor_ext/htmlbook.rb +1120 -0
  10. data/lib/persie/asciidoctor_ext/sample.rb +54 -0
  11. data/lib/persie/asciidoctor_ext/spine_item_processor.rb +43 -0
  12. data/lib/persie/book.rb +60 -0
  13. data/lib/persie/builder.rb +110 -0
  14. data/lib/persie/builders/epub.rb +434 -0
  15. data/lib/persie/builders/mobi.rb +80 -0
  16. data/lib/persie/builders/pdf.rb +113 -0
  17. data/lib/persie/builders/site.rb +110 -0
  18. data/lib/persie/cli.rb +106 -0
  19. data/lib/persie/dependency.rb +26 -0
  20. data/lib/persie/generator.rb +68 -0
  21. data/lib/persie/server.rb +27 -0
  22. data/lib/persie/ui.rb +27 -0
  23. data/lib/persie/version.rb +3 -0
  24. data/lib/persie.rb +32 -0
  25. data/persie.gemspec +41 -0
  26. data/spec/build_pdf_command_spec.rb +25 -0
  27. data/spec/fixtures/a-book/.gitignore +2 -0
  28. data/spec/fixtures/a-book/Gemfile +3 -0
  29. data/spec/fixtures/a-book/book.adoc +31 -0
  30. data/spec/fixtures/a-book/manuscript/chapter1.adoc +5 -0
  31. data/spec/fixtures/a-book/manuscript/chapter2.adoc +3 -0
  32. data/spec/fixtures/a-book/manuscript/preface.adoc +6 -0
  33. data/spec/fixtures/a-book/themes/pdf/pdf.css +1 -0
  34. data/spec/fixtures/a-book-with-parts/.gitignore +2 -0
  35. data/spec/fixtures/a-book-with-parts/Gemfile +3 -0
  36. data/spec/fixtures/a-book-with-parts/book.adoc +39 -0
  37. data/spec/fixtures/a-book-with-parts/manuscript/chapter1.adoc +4 -0
  38. data/spec/fixtures/a-book-with-parts/manuscript/chapter2.adoc +4 -0
  39. data/spec/fixtures/a-book-with-parts/manuscript/chapter3.adoc +3 -0
  40. data/spec/fixtures/a-book-with-parts/manuscript/chapter4.adoc +3 -0
  41. data/spec/fixtures/a-book-with-parts/manuscript/part1.adoc +3 -0
  42. data/spec/fixtures/a-book-with-parts/manuscript/part2.adoc +3 -0
  43. data/spec/fixtures/a-book-with-parts/manuscript/preface.adoc +4 -0
  44. data/spec/htmlbook_spec.rb +29 -0
  45. data/spec/new_command_spec.rb +57 -0
  46. data/spec/pdf_builder_spec.rb +39 -0
  47. data/spec/spec_helper.rb +14 -0
  48. data/spec/version_command_spec.rb +8 -0
  49. data/templates/Gemfile.txt +3 -0
  50. data/templates/book.adoc.erb +35 -0
  51. data/templates/chapter1.adoc +3 -0
  52. data/templates/chapter2.adoc +3 -0
  53. data/templates/gitignore.txt +2 -0
  54. data/templates/preface.adoc +4 -0
  55. data/workflow.png +0 -0
  56. metadata +278 -0
@@ -0,0 +1,1120 @@
1
+ require 'asciidoctor'
2
+ require 'rouge'
3
+
4
+ require 'time'
5
+
6
+ module Persie
7
+ # A custom Asciidoctor backend, convert AsciiDoc to O'Reilly HTMLBook.
8
+ class HTMLBook
9
+ include ::Asciidoctor::Converter
10
+
11
+ register_for 'htmlbook'
12
+
13
+ EPUB_FORMATS = ['epub', 'duokan']
14
+
15
+ QUOTE_TAGS = {
16
+ :emphasis => ['<em>', '</em>', true],
17
+ :strong => ['<strong>', '</strong>', true],
18
+ :monospaced => ['<code>', '</code>', true],
19
+ :superscript => ['<sup>', '</sup>', true],
20
+ :subscript => ['<sub>', '</sub>', true],
21
+ :double => ['&#8220;', '&#8221;', false],
22
+ :single => ['&#8216;', '&#8217;', false],
23
+ :asciimath => ['\\$', '\\$', false],
24
+ :latexmath => ['\\(', '\\)', false]
25
+ # Opal can't resolve these constants when referenced here
26
+ #:asciimath => INLINE_MATH_DELIMITERS[:asciimath] + [false],
27
+ #:latexmath => INLINE_MATH_DELIMITERS[:latexmath] + [false]
28
+ }
29
+ QUOTE_TAGS.default = [nil, nil, nil]
30
+
31
+ def initialize(backend, opts={})
32
+ super
33
+
34
+ # These two vars are used to auto-numbering figures, listing, etc
35
+ @reset_num = nil
36
+ @nums = Hash.new(0)
37
+ end
38
+
39
+ def convert(node, transform = nil)
40
+ transform ||= node.node_name
41
+ send(transform, node)
42
+ end
43
+
44
+ def document(node)
45
+ # In this method, node == node.document
46
+ # In other methods, you should use node.document
47
+ ebook_format = node.attr('ebook-format')
48
+ result = []
49
+
50
+ result << '<!DOCTYPE html>'
51
+ lang_attr = %(lang="#{node.attr('lang', 'en')}")
52
+ if EPUB_FORMATS.include? ebook_format
53
+ result << %(<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:#{lang_attr} #{lang_attr}>)
54
+ else
55
+ result << %(<html xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.w3.org/1999/xhtml" #{lang_attr}>)
56
+ end
57
+ result << %(<head>)
58
+ content_type = if EPUB_FORMATS.include? ebook_format
59
+ 'application/xhtml+xml'
60
+ else
61
+ 'text/html'
62
+ end
63
+ result << %(<meta charset="#{node.attr 'encoding', 'UTF-8'}"/>)
64
+ result << %(<title>#{node.doctitle(:sanitize => true) || node.attr('untitled-label')}</title>)
65
+ if ebook_format === 'site'
66
+ result << %(<meta http-equiv="X-UA-Compatible" content="IE=edge"/>)
67
+ result << %(<meta name="viewport" content="width=device-width, initial-scale=1.0"/>)
68
+ end
69
+ result << %(<meta name="generator" content="Persie #{node.attr 'persie-version'}"/>)
70
+ result << %(<meta name="date" content="#{Time.parse(node.revdate).iso8601}"/>) if node.attr? 'revdate'
71
+ result << %(<meta name="description" content="#{node.attr 'description'}"/>) if node.attr? 'description'
72
+ result << %(<meta name="keywords" content="#{node.attr 'keywords'}"/>) if node.attr? 'keywords'
73
+ result << %(<meta name="author" content="#{node.attr 'author'}"/>) if node.attr? 'author'
74
+ result << %(<meta name="copyright" content="#{node.attr 'copyright'}"/>) if node.attr? 'copyright'
75
+
76
+ stylesheet_path = case ebook_format
77
+ when 'pdf'
78
+ File.join(node.attr('themes-dir'), ebook_format, 'pdf.css')
79
+ when 'site'
80
+ 'style.css'
81
+ else
82
+ "#{ebook_format}.css"
83
+ end
84
+ result << %(<link rel="stylesheet" href="#{stylesheet_path}"/>)
85
+
86
+ # FIXME: cleanup
87
+ if node.attr? 'math'
88
+ result << %(<script type="text/x-mathjax-config">
89
+ MathJax.Hub.Config({
90
+ tex2jax: {
91
+ inlineMath: [#{::Asciidoctor::INLINE_MATH_DELIMITERS[:latexmath]}],
92
+ displayMath: [#{::Asciidoctor::BLOCK_MATH_DELIMITERS[:latexmath]}],
93
+ ignoreClass: "nomath|nolatexmath"
94
+ },
95
+ asciimath2jax: {
96
+ delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath]}],
97
+ ignoreClass: "nomath|noasciimath"
98
+ }
99
+ });
100
+ </script>
101
+ <script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>
102
+ <script>document.addEventListener('DOMContentLoaded', MathJax.Hub.TypeSet)</script>)
103
+ end
104
+
105
+ # Use seperate docinfo file
106
+ unless (docinfo_content = node.docinfo).empty?
107
+ result << docinfo_content
108
+ end
109
+
110
+ result << '</head>'
111
+
112
+ body_attrs = []
113
+ body_attrs << %(data-type="book")
114
+ body_attrs << %(id="#{node.id}") if node.id
115
+ body_attrs << %(class="sample") if node.attr? 'is-sample'
116
+ result << %(<body #{body_attrs * ' '}>)
117
+
118
+ result << cover(node)
119
+ result << titlepage(node)
120
+ result << toc(node)
121
+
122
+ if node.attr? 'is-sample'
123
+ result << node.sample_content
124
+ else
125
+ result << node.content
126
+ end
127
+
128
+ # Display footnotes in single page site
129
+ if single_page_site?(node)
130
+ if node.footnotes? && !(node.attr? 'nofootnotes')
131
+ result << "<div class=\"footnotes\">\n<ol>"
132
+ node.footnotes.each do |fn|
133
+ ref = %( <a href="#fn-ref-#{fn.index}">&#8617;</a>)
134
+ result << %(<li data-type="footnote" id="fn-#{fn.index}">#{fn.text}#{ref}</li>)
135
+ end
136
+ result << "</ol>\n</div>"
137
+ end
138
+ end
139
+
140
+ result << '</body>'
141
+ result << '</html>'
142
+
143
+ result * "\n"
144
+ end
145
+
146
+ # FIXME: need review
147
+ def embedded(node)
148
+ result = []
149
+ if !node.notitle && node.has_header?
150
+ id_attr = node.id ? %( id="#{node.id}") : nil
151
+ result << %(<h1#{id_attr}>#{node.header.title}</h1>)
152
+ end
153
+
154
+ result << node.content
155
+
156
+ if node.footnotes? && !(node.attr? 'nofootnotes')
157
+ result << %(<div id="footnotes">
158
+ <hr/>)
159
+ node.footnotes.each do |footnote|
160
+ result << %(<div class="footnote" id="_footnote_#{footnote.index}">
161
+ <a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a> #{footnote.text}
162
+ </div>)
163
+ end
164
+
165
+ result << '</div>'
166
+ end
167
+
168
+ result * "\n"
169
+ end
170
+
171
+ # Generate an outline for use in toc page
172
+ def outline(node, opts = {})
173
+ return if node.sections.empty?
174
+
175
+ sectnumlevels = opts[:sectnumlevels] || (node.document.attr 'sectnumlevels', 3).to_i
176
+ toclevels = opts[:toclevels] || (node.document.attr 'toclevels', 2).to_i
177
+ result = ['<ol>']
178
+
179
+ sections = node.attr?('is-sample') ? node.sample_sections : node.sections
180
+ sections.each do |section|
181
+ data_type = data_type_of(section)
182
+ data_type_attr = %( data-type="#{data_type}")
183
+ section_num = (section.numbered && !section.caption && section.level <= sectnumlevels) ? %(#{section.sectnum} ) : nil
184
+ result << %(<li#{data_type_attr}>)
185
+ before_title = caption_before_title_of(section, section_num)
186
+ label = if before_title.nil?
187
+ nil
188
+ else
189
+ %(<span class="title-label">#{before_title}</span> )
190
+ end
191
+ result << %(<a href="##{section.id}">#{label}#{section.title}</a>)
192
+ if section.level < toclevels && (child_toc_level = outline section, :toclevels => toclevels, :secnumlevels => sectnumlevels)
193
+ result << child_toc_level
194
+ end
195
+ result << '</li>'
196
+ end
197
+ result << '</ol>'
198
+
199
+ result * "\n"
200
+ end
201
+
202
+ def section(node)
203
+ slevel = node.level
204
+ slevel = 1 if slevel == 0 && node.special
205
+ data_type = data_type_of(node)
206
+ id_attr = node.id ? %( id="#{node.id}") : nil
207
+ class_attr = node.role ? %( class="#{node.role}") : nil
208
+ sectnum = if node.numbered && !node.caption && slevel <= (node.document.attr 'sectnumlevels', 3).to_i && slevel != 0
209
+ node.sectnum
210
+ else
211
+ nil
212
+ end
213
+
214
+ h_level = if slevel == 0
215
+ 1
216
+ elsif slevel == 1
217
+ 1
218
+ else
219
+ slevel - 1
220
+ end
221
+ wrapper_tag = if data_type != 'part'
222
+ 'section'
223
+ else
224
+ 'div'
225
+ end
226
+
227
+ result = [%(<#{wrapper_tag} data-type="#{data_type}"#{id_attr}#{class_attr}>)]
228
+
229
+ before_title = caption_before_title_of(node, sectnum)
230
+ label = if before_title.nil?
231
+ nil
232
+ else
233
+ %(<span class="title-label">#{before_title}</span> )
234
+ end
235
+
236
+ result << %(<h#{h_level}>#{label}#{node.title}</h#{h_level}>)
237
+ result << node.content
238
+ result << %(</#{wrapper_tag}>)
239
+
240
+ result * "\n"
241
+ end
242
+
243
+ def admonition(node)
244
+ ebook_format = node.document.attr('ebook-format')
245
+ id_attr = node.id ? %( id="#{node.id}") : nil
246
+ class_attr = node.role ? %( class="#{node.role}") : nil
247
+ epub_type_attr = nil
248
+ name = node.attr 'name'
249
+ title_element = node.title? ? %(<h1>#{node.title}</h1>\n) : nil
250
+
251
+ if EPUB_FORMATS.include? ebook_format
252
+ epub_type = case node.attr('name')
253
+ when 'tip'
254
+ 'help'
255
+ when 'note'
256
+ 'note'
257
+ when 'important', 'warning', 'caution'
258
+ 'warning'
259
+ end
260
+ epub_type_attr = %( epub:type="#{epub_type}")
261
+ end
262
+
263
+ result = [%(<div data-type="#{name}"#{epub_type_attr}#{id_attr}#{class_attr}>)]
264
+ result << title_element
265
+ result << node.content
266
+ result << '</div>'
267
+
268
+ result * "\n"
269
+ end
270
+
271
+ # fixme: not touched, need cleanup
272
+ def audio(node)
273
+ xml = node.document.attr? 'htmlsyntax', 'xml'
274
+ id_attribute = node.id ? %( id="#{node.id}") : nil
275
+ classes = ['audioblock', node.style, node.role].compact
276
+ class_attribute = %( class="#{classes * ' '}")
277
+ title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
278
+ %(<div#{id_attribute}#{class_attribute}>
279
+ #{title_element}<div class="content">
280
+ <audio src="#{node.media_uri(node.attr 'target')}"#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : nil}>
281
+ Your browser does not support the audio tag.
282
+ </audio>
283
+ </div>
284
+ </div>)
285
+ end
286
+
287
+ def colist(node)
288
+ result = []
289
+ digits = ['&#x278a;', '&#x278b;', '&#x278c;', '&#x278d;', '&#x278e;', '&#x278f;', '&#x2790;', '&#x279a;', '&#x2792;', '&#x2793;']
290
+ id_attr = node.id ? %( id="#{node.id}") : nil
291
+ classes = ['calloutlist', node.style, node.role].compact
292
+ class_attr = %( class="#{classes * ' '}")
293
+ start_attr = node.attr?('start') ? %( start="#{node.attr('start')}") : nil
294
+
295
+ result << %(<dl#{id_attr}#{class_attr}#{start_attr}>)
296
+
297
+ node.items.each_with_index do |item, i|
298
+ result << %(<dt>#{digits[i]}</dt>)
299
+ result << %(<dd><p>#{item.text}</p>#{item.block? ? item.content : nil}</dd>)
300
+ end
301
+
302
+ result << '</dl>'
303
+ result * "\n"
304
+ end
305
+
306
+ def dlist(node)
307
+ result = []
308
+ id_attribute = node.id ? %( id="#{node.id}") : nil
309
+
310
+ classes = case node.style
311
+ when 'qanda'
312
+ ['qlist', 'qanda', node.role]
313
+ when 'horizontal'
314
+ ['hdlist', node.role]
315
+ else
316
+ ['dlist', node.style, node.role]
317
+ end.compact
318
+
319
+ class_attribute = %( class="#{classes * ' '}")
320
+
321
+ result << %(<div#{id_attribute}#{class_attribute}>)
322
+ result << %(<div class="title">#{node.title}</div>) if node.title?
323
+ case node.style
324
+ when 'qanda'
325
+ result << '<ol>'
326
+ node.items.each do |terms, dd|
327
+ result << '<li>'
328
+ [*terms].each do |dt|
329
+ result << %(<p><em>#{dt.text}</em></p>)
330
+ end
331
+ if dd
332
+ result << %(<p>#{dd.text}</p>) if dd.text?
333
+ result << dd.content if dd.blocks?
334
+ end
335
+ result << '</li>'
336
+ end
337
+ result << '</ol>'
338
+ when 'horizontal'
339
+ result << '<table>'
340
+ if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
341
+ result << '<colgroup>'
342
+ col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : nil
343
+ result << %(<col#{col_style_attribute}/>)
344
+ col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : nil
345
+ result << %(<col#{col_style_attribute}/>)
346
+ result << '</colgroup>'
347
+ end
348
+ node.items.each do |terms, dd|
349
+ result << '<tr>'
350
+ result << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : nil}">)
351
+ terms_array = [*terms]
352
+ last_term = terms_array[-1]
353
+ terms_array.each do |dt|
354
+ result << dt.text
355
+ result << '<br/>' if dt != last_term
356
+ end
357
+ result << '</td>'
358
+ result << '<td class="hdlist2">'
359
+ if dd
360
+ result << %(<p>#{dd.text}</p>) if dd.text?
361
+ result << dd.content if dd.blocks?
362
+ end
363
+ result << '</td>'
364
+ result << '</tr>'
365
+ end
366
+ result << '</table>'
367
+ else
368
+ result << '<dl>'
369
+ dt_style_attribute = node.style ? nil : ' class="hdlist1"'
370
+ node.items.each do |terms, dd|
371
+ [*terms].each do |dt|
372
+ result << %(<dt#{dt_style_attribute}>#{dt.text}</dt>)
373
+ end
374
+ if dd
375
+ result << '<dd>'
376
+ result << %(<p>#{dd.text}</p>) if dd.text?
377
+ result << dd.content if dd.blocks?
378
+ result << '</dd>'
379
+ end
380
+ end
381
+ result << '</dl>'
382
+ end
383
+
384
+ result << '</div>'
385
+ result * "\n"
386
+ end
387
+
388
+ def example(node)
389
+ id_attr = node.id ? %( id="#{node.id}") : nil
390
+ class_attr = node.role ? %( class="#{node.role}") : nil
391
+ title_element = node.title? ? %(<h5>#{node.captioned_title}</h5>\n) : nil
392
+
393
+ %(<div data-type="example"#{id_attr}#{class_attr}>#{title_element}#{node.content}</div>)
394
+ end
395
+
396
+ # FIXME: not touched, need cleanup
397
+ def floating_title(node)
398
+ tag_name = %(h#{node.level + 1})
399
+ id_attribute = node.id ? %( id="#{node.id}") : nil
400
+ classes = [node.style, node.role].compact
401
+ %(<#{tag_name}#{id_attribute} class="#{classes * ' '}">#{node.title}</#{tag_name}>)
402
+ end
403
+
404
+ def image(node)
405
+ align = (node.attr? 'align') ? (node.attr 'align') : nil
406
+ float = (node.attr? 'float') ? (node.attr 'float') : nil
407
+ style_attr = if align || float
408
+ styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact
409
+ %( style="#{styles * ';'}")
410
+ end
411
+
412
+ width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
413
+ height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
414
+
415
+ img_element = %(<img src="#{node.image_uri node.attr('target')}" alt="#{node.attr 'alt'}"#{width_attr}#{height_attr}/>)
416
+ if (link = node.attr 'link')
417
+ img_element = %(<a href="#{link}">#{img_element}</a>)
418
+ end
419
+ id_attr = node.id ? %( id="#{node.id}") : nil
420
+ classes = ['image', node.style, node.role].compact
421
+ class_attr = %( class="#{classes * ' '}")
422
+ title_element = node.title? ? %(<figcaption>#{captioned_title_mod_of(node)}</figcaption>) : nil
423
+
424
+ %(<figure#{id_attr}#{class_attr}#{style_attr}>#{img_element}#{title_element}</figure>)
425
+ end
426
+
427
+ # Use rouge to highlight source code
428
+ # You can set `:hightlight:' document attribute to trun on highlight
429
+ # You can set `linenums' block attribute to turn on line numbers for specific source block
430
+ def listing(node)
431
+ if node.style == 'source'
432
+ language = node.attr('language') # will fall back to global language attribute
433
+ highlight = node.document.attr?('highlight')
434
+ linenums = node.attr?('linenums')
435
+
436
+ if highlight
437
+ classes = "highlight language-#{language}"
438
+ pre_element = rouge_highlight(node.content, language, classes, linenums)
439
+ else
440
+ if linenums
441
+ pre_element = rouge_highlight(node.content, 'plaintext', '', true)
442
+ else
443
+ pre_element = %(<pre><code class="language-#{language}">#{node.content}</code></pre>)
444
+ end
445
+ end
446
+ else
447
+ pre_element = %(<pre>#{node.content}</pre>)
448
+ end
449
+
450
+ id_attr = node.id ? %( id="#{node.id}") : nil
451
+ class_attr = node.role ? %( class="#{node.role}") : nil
452
+ title_element = node.title? ? %(<h5>#{captioned_title_mod_of(node)}</h5>\n) : nil
453
+
454
+ result = [%(<div#{id_attr} data-type="listing"#{class_attr}>)]
455
+ result << title_element
456
+ result << pre_element
457
+ result << '</div>'
458
+
459
+ result * "\n"
460
+ end
461
+
462
+ def literal(node)
463
+ id_attr = node.id ? %( id="#{node.id}") : nil
464
+ cls = node.role ? " #{node.role}" : nil
465
+ output = [%(<div#{id_attr} class="literal#{cls}">)]
466
+ output << %(<pre>#{node.content}</pre>)
467
+ output << '</div>'
468
+
469
+ output * "\n"
470
+ end
471
+
472
+ def math(node)
473
+ id_attr = node.id ? %( id="#{node.id}") : nil
474
+ class_attr = node.role ? %( class="#{node.role}") : nil
475
+ title_element = node.title? ? %(<h5>#{node.title}</h5>\n) : nil
476
+ open, close = ::Asciidoctor::BLOCK_MATH_DELIMITERS[node.style.to_sym]
477
+ # QUESTION should the content be stripped already?
478
+ equation = node.content.strip
479
+ if node.subs.nil_or_empty? && !(node.attr? 'subs')
480
+ equation = node.sub_specialcharacters equation
481
+ end
482
+
483
+ unless (equation.start_with? open) && (equation.end_with? close)
484
+ equation = %(#{open}#{equation}#{close})
485
+ end
486
+
487
+ result = [%(<div data-type="equation"#{id_attr}#{class_attr}>)]
488
+ result << title_element
489
+ result << %(<p data-type="tex">#{equation}</p>)
490
+ result << '</div>'
491
+
492
+ result * "\n"
493
+ end
494
+
495
+ def olist(node)
496
+ result = []
497
+ id_attr = node.id ? %( id="#{node.id}") : nil
498
+ classes = [node.style, node.role].compact
499
+ class_attr = %( class="#{classes * ' '}")
500
+ type_attr = (keyword = node.list_marker_keyword) ? %( type="#{keyword}") : nil
501
+ start_attr = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : nil
502
+ result << %(<ol#{id_attr}#{class_attr}#{type_attr}#{start_attr}>)
503
+
504
+ node.items.each do |item|
505
+ result << '<li>'
506
+ result << %(<p>#{item.text}</p>)
507
+ result << item.content if item.blocks?
508
+ result << '</li>'
509
+ end
510
+
511
+ result << '</ol>'
512
+
513
+ result * "\n"
514
+ end
515
+
516
+ def open(node)
517
+ if (style = node.style) == 'abstract'
518
+ if node.parent == node.document && node.document.doctype == 'book'
519
+ warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
520
+ ''
521
+ else
522
+ id_attr = node.id ? %( id="#{node.id}") : nil
523
+ title_el = node.title? ? %(<div class="title">#{node.title}</div>) : nil
524
+ result = [%(<div#{id_attr} class="quoteblock abstract#{(role = node.role) && " #{role}"}">)]
525
+ result << title_el
526
+ result << %(<blockquote>#{node.content}</blockquote>)
527
+ result << '</div>'
528
+ result * "\n"
529
+ end
530
+ elsif style == 'partintro' && (node.level != 0 || node.parent.context != :section || node.document.doctype != 'book')
531
+ warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.'
532
+ ''
533
+ else
534
+ id_attr = node.id ? %( id="#{node.id}") : nil
535
+ title_el = node.title? ? %(<div class="title">#{node.title}</div>) : nil
536
+ result = [%(<div#{id_attr} class="openblock#{style && style != 'open' ? " #{style}" : ''}#{(role = node.role) && " #{role}"}">)]
537
+ result << title_el
538
+ result << node.content
539
+ result << '</div>'
540
+ result * "\n"
541
+ end
542
+ end
543
+
544
+ def page_break(node)
545
+ ebook_format = node.document.attr('ebook-format')
546
+ if EPUB_FORMATS.include? ebook_format
547
+ '<hr epub:type="pagebreak"/>'
548
+ else
549
+ '<div style="page-break-after:always;"></div>'
550
+ end
551
+ end
552
+
553
+ def paragraph(node)
554
+ id_attr = node.id ? %( id="#{node.id}") : nil
555
+ class_attr = node.role ? %( class="#{node.role}") : nil
556
+
557
+ %(<p#{id_attr}#{class_attr}>#{node.content}</p>)
558
+ end
559
+
560
+ def preamble(node)
561
+ ebook_format = node.document.attr('ebook-format')
562
+ epub_type = EPUB_FORMATS.include?(ebook_format) ? %( epub:type="preamble") : nil
563
+ preamble_title = node.document.attr('preamble-title', 'Preamble')
564
+ result = [%(<section data-type="preamble"#{epub_type}>)]
565
+ result << %(<h1>#{preamble_title}</h1>)
566
+ result << node.content
567
+ result << '</section>'
568
+ result * "\n"
569
+ end
570
+
571
+ def quote(node)
572
+ id_attr = node.id ? %( id="#{node.id}") : nil
573
+ class_attr = node.role ? %( class="#{node.role}") : nil
574
+ attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
575
+ citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
576
+
577
+ if attribution || citetitle
578
+ cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
579
+ attribution_text = attribution ? %(#{citetitle ? "<br/>\n" : nil}&#8212; #{attribution}) : nil
580
+ attribution_element = %(<p data-type="attribution">#{cite_element}#{attribution_text}</p>)
581
+ else
582
+ attribution_element = nil
583
+ end
584
+
585
+ %(<blockquote#{id_attr}#{class_attr}>#{node.content}#{attribution_element}</blockquote>)
586
+ end
587
+
588
+ def thematic_break(node)
589
+ '<hr/>'
590
+ end
591
+
592
+ def sidebar(node)
593
+ ebook_format = node.document.attr('ebook-format')
594
+ id_attr = node.id ? %( id="#{node.id}") : nil
595
+ role = node.role ? %( #{node.role}) : nil
596
+ class_attr = %( class="sidebar#{role}")
597
+ title_element = node.title? ? %(<h5>#{node.title}</h5>) : nil
598
+ epub_type_attr = if EPUB_FORMATS.include?(ebook_format)
599
+ ' epub:type="sidebar"'
600
+ else
601
+ nil
602
+ end
603
+
604
+ result = [%(<aside data-type="sidebar"#{epub_type_attr}#{id_attr}#{class_attr}>)]
605
+ result << title_element
606
+ result << node.content
607
+ result << '</aside>'
608
+
609
+ result * "\n"
610
+ end
611
+
612
+ def table(node)
613
+ result = []
614
+ id_attribute = node.id ? %( id="#{node.id}") : nil
615
+ classes = ['tableblock', %(frame-#{node.attr 'frame', 'all'}), %(grid-#{node.attr 'grid', 'all'})]
616
+ if (role_class = node.role)
617
+ classes << role_class
618
+ end
619
+ class_attribute = %( class="#{classes * ' '}")
620
+ styles = [(node.option? 'autowidth') ? nil : %(width: #{node.attr 'tablepcwidth'}%;), (node.attr? 'float') ? %(float: #{node.attr 'float'};) : nil].compact
621
+ style_attribute = styles.size > 0 ? %( style="#{styles * ' '}") : nil
622
+
623
+ result << %(<table#{id_attribute}#{class_attribute}#{style_attribute}>)
624
+ result << %(<caption>#{captioned_title_mod_of(node)}</caption>) if node.title?
625
+ if (node.attr 'rowcount') > 0
626
+ slash = '/'
627
+ result << '<colgroup>'
628
+ if node.option? 'autowidth'
629
+ tag = %(<col#{slash}>)
630
+ node.columns.size.times do
631
+ result << tag
632
+ end
633
+ else
634
+ node.columns.each do |col|
635
+ result << %(<col style="width: #{col.attr 'colpcwidth'}%;"#{slash}>)
636
+ end
637
+ end
638
+ result << '</colgroup>'
639
+ [:head, :foot, :body].select {|tsec| !node.rows[tsec].empty? }.each do |tsec|
640
+ result << %(<t#{tsec}>)
641
+ node.rows[tsec].each do |row|
642
+ result << '<tr>'
643
+ row.each do |cell|
644
+ if tsec == :head
645
+ cell_content = cell.text
646
+ else
647
+ case cell.style
648
+ when :asciidoc
649
+ cell_content = %(<div>#{cell.content}</div>)
650
+ when :verse
651
+ cell_content = %(<div class="verse">#{cell.text}</div>)
652
+ when :literal
653
+ cell_content = %(<div class="literal"><pre>#{cell.text}</pre></div>)
654
+ else
655
+ cell_content = ''
656
+ cell.content.each do |text|
657
+ cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
658
+ end
659
+ end
660
+ end
661
+
662
+ cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
663
+ cell_class_attribute = %( class="tableblock halign-#{cell.attr 'halign'} valign-#{cell.attr 'valign'}")
664
+ cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : nil
665
+ cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : nil
666
+ cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : nil
667
+ result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}</#{cell_tag_name}>)
668
+ end
669
+ result << '</tr>'
670
+ end
671
+ result << %(</t#{tsec}>)
672
+ end
673
+ end
674
+ result << '</table>'
675
+ result * "\n"
676
+ end
677
+
678
+ def ulist(node)
679
+ result = []
680
+ id_attr = node.id ? %( id="#{node.id}") : nil
681
+ classes = [node.style, node.role].compact
682
+ marker_checked = nil
683
+ marker_unchecked = nil
684
+ if (checklist = node.option? 'checklist')
685
+ classes.insert 0, 'checklist'
686
+ if node.option? 'interactive'
687
+ if node.document.attr? 'htmlsyntax', 'xml'
688
+ marker_checked = '<input type="checkbox" data-item-complete="1" checked="checked"/> '
689
+ marker_unchecked = '<input type="checkbox" data-item-complete="0"/> '
690
+ else
691
+ marker_checked = '<input type="checkbox" data-item-complete="1" checked> '
692
+ marker_unchecked = '<input type="checkbox" data-item-complete="0"> '
693
+ end
694
+ else
695
+ if node.document.attr? 'icons', 'font'
696
+ marker_checked = '<i class="icon-check"></i> '
697
+ marker_unchecked = '<i class="icon-check-empty"></i> '
698
+ else
699
+ marker_checked = '&#10003; '
700
+ marker_unchecked = '&#10063; '
701
+ end
702
+ end
703
+ end
704
+ class_attr = classes.empty? ? nil : %( class="#{classes * ' '}")
705
+ result << %(<ul#{id_attr}#{class_attr}>)
706
+
707
+ node.items.each do |item|
708
+ result << '<li>'
709
+ if checklist && (item.attr? 'checkbox')
710
+ result << %(<p>#{(item.attr? 'checked') ? marker_checked : marker_unchecked}#{item.text}</p>)
711
+ else
712
+ result << %(<p>#{item.text}</p>)
713
+ end
714
+ result << item.content if item.blocks?
715
+ result << '</li>'
716
+ end
717
+
718
+ result << '</ul>'
719
+ result * "\n"
720
+ end
721
+
722
+ def verse(node)
723
+ id_attr = node.id ? %( id="#{node.id}") : nil
724
+ classes = ['verse', node.role].compact
725
+ class_attr = %( class="#{classes * ' '}")
726
+ title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : nil
727
+ attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
728
+ citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
729
+ if attribution || citetitle
730
+ cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
731
+ attribution_text = attribution ? %(#{citetitle ? "<br/>\n" : nil}&#8212; #{attribution}) : nil
732
+ attribution_element = %(\n<p data-type="attribution">\n#{cite_element}#{attribution_text}\n</p>)
733
+ else
734
+ attribution_element = nil
735
+ end
736
+
737
+ result = [%(<blockquote data-type="epigraph"#{id_attr}#{class_attr}>)]
738
+ result << %(<pre>#{node.content.chomp}</pre>)
739
+ result << attribution_element
740
+ result << '</blockquote>'
741
+ result * "\n"
742
+ end
743
+
744
+ # FIXME: not touched, need cleanup
745
+ def video(node)
746
+ xml = node.document.attr? 'htmlsyntax', 'xml'
747
+ id_attribute = node.id ? %( id="#{node.id}") : nil
748
+ classes = ['videoblock', node.style, node.role].compact
749
+ class_attribute = %( class="#{classes * ' '}")
750
+ title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
751
+ width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
752
+ height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
753
+ case node.attr 'poster'
754
+ when 'vimeo'
755
+ start_anchor = (node.attr? 'start') ? "#at=#{node.attr 'start'}" : nil
756
+ delimiter = '?'
757
+ autoplay_param = (node.option? 'autoplay') ? "#{delimiter}autoplay=1" : nil
758
+ delimiter = '&amp;' if autoplay_param
759
+ loop_param = (node.option? 'loop') ? "#{delimiter}loop=1" : nil
760
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
761
+ <div class="content">
762
+ <iframe#{width_attribute}#{height_attribute} src="//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{append_boolean_attribute 'webkitAllowFullScreen', xml}#{append_boolean_attribute 'mozallowfullscreen', xml}#{append_boolean_attribute 'allowFullScreen', xml}></iframe>
763
+ </div>
764
+ </div>)
765
+ when 'youtube'
766
+ start_param = (node.attr? 'start') ? "&amp;start=#{node.attr 'start'}" : nil
767
+ end_param = (node.attr? 'end') ? "&amp;end=#{node.attr 'end'}" : nil
768
+ autoplay_param = (node.option? 'autoplay') ? '&amp;autoplay=1' : nil
769
+ loop_param = (node.option? 'loop') ? '&amp;loop=1' : nil
770
+ controls_param = (node.option? 'nocontrols') ? '&amp;controls=0' : nil
771
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
772
+ <div class="content">
773
+ <iframe#{width_attribute}#{height_attribute} src="//www.youtube.com/embed/#{node.attr 'target'}?rel=0#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>
774
+ </div>
775
+ </div>)
776
+ else
777
+ poster_attribute = %(#{poster = node.attr 'poster'}).empty? ? nil : %( poster="#{node.media_uri poster}")
778
+ time_anchor = ((node.attr? 'start') || (node.attr? 'end')) ? %(#t=#{node.attr 'start'}#{(node.attr? 'end') ? ',' : nil}#{node.attr 'end'}) : nil
779
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
780
+ <div class="content">
781
+ <video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : nil}>
782
+ Your browser does not support the video tag.
783
+ </video>
784
+ </div>
785
+ </div>)
786
+ end
787
+ end
788
+
789
+ def inline_anchor(node)
790
+ ebook_format = node.document.attr('ebook-format')
791
+ target = node.target
792
+
793
+ case node.type
794
+ when :xref
795
+ refid = (node.attr 'refid') || target
796
+ # FIXME seems like text should be prepared already
797
+ text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
798
+ if (ebook_format == 'pdf' || single_page_site?(node)) && !target.start_with?('#')
799
+ parts = target.split('#', 2)
800
+ target = "##{parts.last}"
801
+ end
802
+ %(<a href="#{target}">#{text}</a>)
803
+ when :ref
804
+ %(<a id="#{target}"></a>)
805
+ when :link
806
+ class_attr = (role = node.role) ? %( class="#{role}") : nil
807
+ id_attr = (node.attr? 'id') ? %( id="#{node.attr 'id'}") : nil
808
+ window_attr = (node.attr? 'window') ? %( target="#{node.attr 'window'}") : nil
809
+ %(<a href="#{target}"#{id_attr}#{class_attr}#{window_attr}>#{node.text}</a>)
810
+ when :bibref
811
+ %(<a id="#{target}"></a>[#{target}])
812
+ else
813
+ warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
814
+ end
815
+ end
816
+
817
+ def inline_break(node)
818
+ %(#{node.text}<br/>)
819
+ end
820
+
821
+ def inline_button(node)
822
+ %(<b class="button">#{node.text}</b>)
823
+ end
824
+
825
+ def inline_callout(node)
826
+ if node.document.attr? 'icons', 'font'
827
+ %(<i class="conum" data-value="#{node.text}"></i><b>(#{node.text})</b>)
828
+ elsif node.document.attr? 'icons'
829
+ src = node.icon_uri("callouts/#{node.text}")
830
+ %(<img src="#{src}" alt="#{node.text}"/>)
831
+ else
832
+ %(<b class="conum">(#{node.text})</b>)
833
+ end
834
+ end
835
+
836
+ def inline_footnote(node)
837
+ index = node.attr('index')
838
+ if node.type == :xref
839
+ %(<a data-type="footnoteref" href="##{node.target}">#{index}</a>)
840
+ else
841
+ id_attr = node.id ? %( id="#{node.id}") : nil
842
+ if single_page_site?(node)
843
+ %(<sup><a href="#fn-#{index}" id="fn-ref-#{index}">#{index}</a></sup>)
844
+ else
845
+ %(<span data-type="footnote"#{id_attr}>#{node.text}</span>)
846
+ end
847
+ end
848
+ end
849
+
850
+ def inline_image(node)
851
+ if (type = node.type) == 'icon' && (node.document.attr? 'icons', 'font')
852
+ style_class = "icon-#{node.target}"
853
+ if node.attr? 'size'
854
+ style_class = %(#{style_class} icon-#{node.attr 'size'})
855
+ end
856
+ if node.attr? 'rotate'
857
+ style_class = %(#{style_class} icon-rotate-#{node.attr 'rotate'})
858
+ end
859
+ if node.attr? 'flip'
860
+ style_class = %(#{style_class} icon-flip-#{node.attr 'flip'})
861
+ end
862
+ title_attribute = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : nil
863
+ img = %(<i class="#{style_class}"#{title_attribute}></i>)
864
+ elsif type == 'icon' && !(node.document.attr? 'icons')
865
+ img = %([#{node.attr 'alt'}])
866
+ else
867
+ resolved_target = (type == 'icon') ? (node.icon_uri node.target) : (node.image_uri node.target)
868
+
869
+ attrs = ['alt', 'width', 'height', 'title'].map {|name|
870
+ (node.attr? name) ? %( #{name}="#{node.attr name}") : nil
871
+ }.join
872
+
873
+ img = %(<img src="#{resolved_target}"#{attrs}/>)
874
+ end
875
+
876
+ if node.attr? 'link'
877
+ window_attr = (node.attr? 'window') ? %( target="#{node.attr 'window'}") : nil
878
+ img = %(<a class="image" href="#{node.attr 'link'}"#{window_attr}>#{img}</a>)
879
+ end
880
+
881
+ style_classes = (role = node.role) ? %(image #{type} #{role}) : type
882
+ style_attr = (node.attr? 'float') ? %( style="float: #{node.attr 'float'}") : nil
883
+
884
+ %(<span class="#{style_classes}"#{style_attr}>#{img}</span>)
885
+ end
886
+
887
+ # FIXME: need expanded asccording to asccidoctor_html
888
+ def inline_indexterm(node)
889
+ node.type == :visible ? node.text : ''
890
+ end
891
+
892
+ def inline_kbd(node)
893
+ if (keys = node.attr 'keys').size == 1
894
+ %(<kbd>#{keys[0]}</kbd>)
895
+ else
896
+ key_combo = keys.map {|key| %(<kbd>#{key}</kbd>+) }.join.chop
897
+ %(<span class="keyseq">#{key_combo}</span>)
898
+ end
899
+ end
900
+
901
+ def inline_menu(node)
902
+ menu = node.attr 'menu'
903
+ if !(submenus = node.attr 'submenus').empty?
904
+ submenu_path = submenus.map {|submenu| %(<span class="submenu">#{submenu}</span>&#160;&#9656; ) }.join.chop
905
+ %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; #{submenu_path} <span class="menuitem">#{node.attr 'menuitem'}</span></span>)
906
+ elsif (menuitem = node.attr 'menuitem')
907
+ %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; <span class="menuitem">#{menuitem}</span></span>)
908
+ else
909
+ %(<span class="menu">#{menu}</span>)
910
+ end
911
+ end
912
+
913
+ def inline_quoted(node)
914
+ open, close, is_tag = QUOTE_TAGS[node.type]
915
+ quoted_text = if (role = node.role)
916
+ is_tag ? %(#{open.chop} class="#{role}">#{node.text}#{close}) : %(<span class="#{role}">#{open}#{node.text}#{close}</span>)
917
+ else
918
+ %(#{open}#{node.text}#{close})
919
+ end
920
+
921
+ node.id ? %(<a id="#{node.id}"></a>#{quoted_text}) : quoted_text
922
+ end
923
+
924
+ private
925
+
926
+ # Generates single page site or not.
927
+ def single_page_site?(node)
928
+ node.document.attr('single-page', false)
929
+ end
930
+
931
+ # Genarate cover page
932
+ def cover(node)
933
+ doc = node.document
934
+ ebook_format = doc.attr('ebook-format')
935
+ cover_attr = "#{ebook_format}-cover-image"
936
+ image = File.basename doc.attr(cover_attr, 'cover.png')
937
+ path = File.join doc.attr('themes-dir'), ebook_format, image
938
+ result = []
939
+
940
+ if File.exist? path
941
+ result << %(<div data-type="cover">)
942
+ src = if ebook_format == 'pdf'
943
+ path
944
+ else
945
+ image
946
+ end
947
+ result << %(<img src="#{src}" style="max-width:100%"/>)
948
+ result << %(</div>)
949
+ end
950
+
951
+ result * "\n"
952
+ end
953
+
954
+ # Generate table of contents
955
+ def toc(node)
956
+ doc = node.document
957
+ return nil unless doc.attr?('toc')
958
+
959
+ result = [%(<nav data-type="toc" class="#{doc.attr 'toc-class', 'toc'}">)]
960
+ result << %(<h1>#{doc.attr 'toc-title'}</h1>)
961
+ result << outline(node)
962
+ result << '</nav>'
963
+
964
+ reset_numbers_of_chapter_appendix_part_for(node)
965
+
966
+ result * "\n"
967
+ end
968
+
969
+ # Generate a title page for PDF format
970
+ def titlepage(node)
971
+ result = [%(<section data-type="titlepage">)]
972
+ result << %(<h1>#{node.header.title}</h1>)
973
+ result << %(<h2>#{node.attr :subtitle}</h2>) if node.attr? :subtitle
974
+ result << %(<p data-type="edition">#{node.attr :edition}</p>) if node.attr? :edition
975
+
976
+ if node.attr? 'author'
977
+ result << '<p data-type="author">'
978
+ author_text = if node.attr?('author-label')
979
+ node.attr('author-label')
980
+ else
981
+ node.attr 'author'
982
+ end
983
+ result << author_text
984
+ result << '</p>'
985
+ end
986
+
987
+ if node.attr? 'translator'
988
+ result << '<p data-type="translator">'
989
+ translator_text = if node.attr?('translator-label')
990
+ node.attr('translator-label')
991
+ else
992
+ node.attr('translator')
993
+ end
994
+ result << translator_text
995
+ result << '</p>'
996
+ end
997
+
998
+ has_revnumber = node.attr?('revnumber')
999
+ has_revdate = node.attr?('revdate')
1000
+ has_revremark = node.attr?('revremark')
1001
+ has_rev_info = [has_revnumber, has_revdate, has_revremark].any?
1002
+
1003
+ if has_rev_info
1004
+ result << %(<div class="revinfo">)
1005
+ end
1006
+ if has_revnumber
1007
+ result << %(<span data-type="revnumber">#{((node.attr 'version-label') || '').downcase} #{node.attr 'revnumber'}#{has_revdate ? ',' : ''}</span>)
1008
+ end
1009
+ if has_revdate
1010
+ result << %(<span data-type="revdate">#{node.attr 'revdate'}</span>)
1011
+ end
1012
+ if has_revremark
1013
+ result << %(<br/><span data-type="revremark">#{node.attr 'revremark'}</span>)
1014
+ end
1015
+ if has_rev_info
1016
+ result << %(</div>)
1017
+ end
1018
+
1019
+ result << %(</section>)
1020
+ result * "\n"
1021
+ end
1022
+
1023
+ # FIXME: after cleanup, delete this
1024
+ def append_boolean_attribute(name, xml)
1025
+ xml ? %( #{name}="#{name}") : %( #{name})
1026
+ end
1027
+
1028
+ # Find out the data type of a node
1029
+ def data_type_of(node)
1030
+ slevel = node.level
1031
+ if slevel == 0
1032
+ 'part'
1033
+ elsif slevel == 1
1034
+ if node.sectname == 'sect1'
1035
+ 'chapter'
1036
+ else
1037
+ node.sectname
1038
+ end
1039
+ else
1040
+ "sect#{slevel - 1}"
1041
+ end
1042
+ end
1043
+
1044
+ # Genarate auto-numbered caption to titles for chapter, appendix, part, etc.
1045
+ #
1046
+ # QUESTION: Cannot get appendix number from `sectnum'?
1047
+ def caption_before_title_of(node, sectnum)
1048
+ data_type = data_type_of(node)
1049
+
1050
+ if !sectnum.nil? && data_type == 'chapter' && node.document.attr?('chapter-caption')
1051
+ num = sectnum.split('.').first
1052
+ output = node.document.attr('chapter-caption').sub('%NUM%', num)
1053
+ elsif data_type == 'appendix' && node.document.attr?('appendix-caption')
1054
+ num = node.document.counter('appendix-number', 'A').to_s
1055
+ output =node.document.attr('appendix-caption').sub('%NUM%', num)
1056
+ else
1057
+ output = sectnum
1058
+ end
1059
+
1060
+ output
1061
+ end
1062
+
1063
+ # Cause we would call `caption_before_title_of' many times,
1064
+ # so we should reset the numbers after one call.
1065
+ def reset_numbers_of_chapter_appendix_part_for(node)
1066
+ attrs = node.document.attributes
1067
+ ['chapter-number', 'appendix-number'].each do |num|
1068
+ attrs[num] = nil
1069
+ end
1070
+ end
1071
+
1072
+ # Add auto-numbered lable to images, tables and listings
1073
+ def captioned_title_mod_of(node)
1074
+
1075
+ unless (caption = node.document.attr "#{node.context}-caption")
1076
+ return node.captioned_title
1077
+ end
1078
+
1079
+ ctx = node.context
1080
+ # FIXME maybe node.parent is not correct
1081
+ level_1_num = node.parent.sectnum.split('.', 2).first
1082
+ @reset_num ||= level_1_num
1083
+
1084
+ if @reset_num != level_1_num
1085
+ @nums.each_key { |k| @nums[k] = 0 }
1086
+ @nums["#{ctx}"] += 1
1087
+ @reset_num = nil
1088
+ else
1089
+ @nums["#{ctx}"] += 1
1090
+ end
1091
+
1092
+ caption_replaced = caption.sub('%NUM%', level_1_num.to_s)
1093
+ .sub('%SUBNUM%', @nums["#{ctx}"].to_s)
1094
+ label= if caption_replaced.strip.length > 0
1095
+ %(<span class="title-label">#{caption_replaced}</span>)
1096
+ else
1097
+ nil
1098
+ end
1099
+
1100
+ space = if node.document.attr? 'caption-append-space'
1101
+ ' '
1102
+ else
1103
+ nil
1104
+ end
1105
+
1106
+ %(#{label}#{space}#{node.title})
1107
+ end
1108
+
1109
+ # Use rouge to highlight source code
1110
+ def rouge_highlight(text, lexer, classes, linenums, &b)
1111
+ lexer = ::Rouge::Lexer.find(lexer) unless lexer.respond_to? :lex
1112
+ lexer = ::Rouge::Lexers::PlainText unless lexer
1113
+
1114
+ formatter = ::Rouge::Formatters::HTML.new(css_class: classes, line_numbers: linenums)
1115
+
1116
+ formatter.format(lexer.lex(text), &b)
1117
+ end
1118
+
1119
+ end
1120
+ end