asciibook 0.0.0 → 0.0.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.
Files changed (72) hide show
  1. checksums.yaml +5 -5
  2. data/README.adoc +122 -0
  3. data/book_template/asciibook.yml +5 -0
  4. data/book_template/book.adoc +3 -0
  5. data/exe/asciibook +7 -0
  6. data/lib/asciibook.rb +19 -1
  7. data/lib/asciibook/asciidoctor_ext/abstract_node.rb +5 -0
  8. data/lib/asciibook/book.rb +153 -0
  9. data/lib/asciibook/builders/base_builder.rb +24 -0
  10. data/lib/asciibook/builders/epub_builder.rb +84 -0
  11. data/lib/asciibook/builders/html_builder.rb +45 -0
  12. data/lib/asciibook/builders/mobi_builder.rb +21 -0
  13. data/lib/asciibook/builders/pdf_builder.rb +154 -0
  14. data/lib/asciibook/command.rb +85 -0
  15. data/lib/asciibook/converter.rb +303 -0
  16. data/lib/asciibook/page.rb +30 -0
  17. data/lib/asciibook/version.rb +1 -1
  18. data/templates/admonition.html +4 -0
  19. data/templates/audio.html +7 -0
  20. data/templates/colist.html +7 -0
  21. data/templates/dlist.html +12 -0
  22. data/templates/document.html +29 -0
  23. data/templates/embedded.html +20 -0
  24. data/templates/example.html +4 -0
  25. data/templates/footnotes.html +7 -0
  26. data/templates/image.html +11 -0
  27. data/templates/index.html +40 -0
  28. data/templates/inline_anchor.html +12 -0
  29. data/templates/inline_callout.html +1 -0
  30. data/templates/inline_footnote.html +1 -0
  31. data/templates/inline_image.html +3 -0
  32. data/templates/inline_indexterm.html +5 -0
  33. data/templates/inline_quoted.html +24 -0
  34. data/templates/listing.html +4 -0
  35. data/templates/literal.html +1 -0
  36. data/templates/olist.html +8 -0
  37. data/templates/page_break.html +1 -0
  38. data/templates/paragraph.html +1 -0
  39. data/templates/pass.html +1 -0
  40. data/templates/preamble.html +3 -0
  41. data/templates/quote.html +9 -0
  42. data/templates/section.html +4 -0
  43. data/templates/sidebar.html +6 -0
  44. data/templates/stem.html +9 -0
  45. data/templates/table.html +41 -0
  46. data/templates/thematic_break.html +1 -0
  47. data/templates/ulist.html +8 -0
  48. data/templates/verse.html +9 -0
  49. data/templates/video.html +10 -0
  50. data/theme/epub/epub.css +6 -0
  51. data/theme/epub/layout.html +14 -0
  52. data/theme/html/html.css +190 -0
  53. data/theme/html/html.js +29 -0
  54. data/theme/html/layout.html +91 -0
  55. data/theme/mobi/layout.html +13 -0
  56. data/theme/mobi/mobi.css +2 -0
  57. data/theme/pdf/config.yml +6 -0
  58. data/theme/pdf/footer.html +11 -0
  59. data/theme/pdf/header.html +3 -0
  60. data/theme/pdf/layout.html +14 -0
  61. data/theme/pdf/pdf.css +3 -0
  62. data/theme/pdf/toc.xsl +81 -0
  63. data/theme/share/default.css +174 -0
  64. data/theme/share/highlight.css +216 -0
  65. data/theme/share/normalize.css +349 -0
  66. metadata +119 -21
  67. data/.gitignore +0 -9
  68. data/.travis.yml +0 -4
  69. data/Gemfile +0 -4
  70. data/README.md +0 -41
  71. data/Rakefile +0 -10
  72. data/asciibook.gemspec +0 -25
@@ -0,0 +1,45 @@
1
+ module Asciibook
2
+ module Builders
3
+ class HtmlBuilder < BaseBuilder
4
+ def initialize(book)
5
+ super
6
+ @dest_dir = File.join(@book.dest_dir, 'html')
7
+ @theme_dir = File.join(@book.theme_dir, 'html')
8
+ end
9
+
10
+ def build
11
+ FileUtils.mkdir_p @dest_dir
12
+ FileUtils.rm_r Dir.glob("#{@dest_dir}/*")
13
+
14
+ generate_pages
15
+ copy_assets
16
+ end
17
+
18
+ def generate_pages
19
+ layout = Liquid::Template.parse(File.read(File.join(@theme_dir, 'layout.html')))
20
+ @book.pages.each do |page|
21
+ File.open(File.join(@dest_dir, page.path), 'w') do |file|
22
+ file.write layout.render({
23
+ 'book' => @book.to_hash,
24
+ 'page' => page.to_hash
25
+ })
26
+ end
27
+ end
28
+ end
29
+
30
+ def copy_assets
31
+ @book.assets.each do |path|
32
+ copy_file(path, @book.base_dir, @dest_dir)
33
+ end
34
+
35
+ Dir.glob('**/*.{jpb,png,gif,svg,css,js}', File::FNM_CASEFOLD, base: @theme_share_dir).each do |path|
36
+ copy_file(path, @theme_share_dir, @dest_dir)
37
+ end
38
+
39
+ Dir.glob('**/*.{jpb,png,gif,svg,css,js}', File::FNM_CASEFOLD, base: @theme_dir).each do |path|
40
+ copy_file(path, @theme_dir, @dest_dir)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ module Asciibook
2
+ module Builders
3
+ class MobiBuilder < EpubBuilder
4
+ def initialize(book)
5
+ super
6
+
7
+ @dest_dir = File.join(@book.dest_dir, 'mobi')
8
+ @theme_dir = File.join(@book.theme_dir, 'mobi')
9
+ end
10
+
11
+ def build
12
+ super
13
+
14
+ epub_file = File.join(@dest_dir, "#{@book.basename}.epub")
15
+ system 'kindlegen', epub_file
16
+
17
+ FileUtils.rm epub_file
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,154 @@
1
+ module Asciibook
2
+ module Builders
3
+ class PdfBuilder < BaseBuilder
4
+ def initialize(book)
5
+ super
6
+ @dest_dir = File.join(@book.dest_dir, 'pdf')
7
+ @tmp_dir = File.join(@dest_dir, 'tmp')
8
+ @theme_dir = File.join(@book.theme_dir, 'pdf')
9
+ @theme_config = YAML.safe_load(File.read(File.join(@theme_dir, 'config.yml')))
10
+ end
11
+
12
+ def build
13
+ prepare_workdir
14
+ generate_pages
15
+ copy_assets
16
+ generate_header_footer
17
+ generate_pdf
18
+ #clean_workdir
19
+ end
20
+
21
+ def prepare_workdir
22
+ FileUtils.mkdir_p @tmp_dir
23
+ FileUtils.rm_r Dir.glob("#{@tmp_dir}/*")
24
+ end
25
+
26
+ def clean_workdir
27
+ FileUtils.rm_r @tmp_dir
28
+ end
29
+
30
+ def generate_pages
31
+ layout = Liquid::Template.parse(File.read(File.join(@theme_dir, 'layout.html')))
32
+ @book.pages.each do |page|
33
+ File.open(File.join(@tmp_dir, page.path), 'w') do |file|
34
+ file.write layout.render({
35
+ 'book' => @book.to_hash,
36
+ 'page' => page.to_hash
37
+ })
38
+ end
39
+ end
40
+ end
41
+
42
+ def copy_assets
43
+ @book.assets.each do |path|
44
+ copy_file(path, @book.base_dir, @tmp_dir)
45
+ end
46
+
47
+ Dir.glob('**/*.{jpb,png,gif,svg,css,js}', File::FNM_CASEFOLD, base: @theme_share_dir).each do |path|
48
+ copy_file(path, @theme_share_dir, @tmp_dir)
49
+ end
50
+
51
+ Dir.glob('**/*.{jpb,png,gif,svg,css,js}', File::FNM_CASEFOLD, base: @theme_dir).each do |path|
52
+ copy_file(path, @theme_dir, @tmp_dir)
53
+ end
54
+ end
55
+
56
+ def generate_header_footer
57
+ layout = Liquid::Template.parse <<~EOF
58
+ <!DOCTYPE html>
59
+ <html>
60
+ <head>
61
+ <script>
62
+ function subst() {
63
+ var vars = {};
64
+ var query_strings_from_url = document.location.search.substring(1).split('&');
65
+ for (var query_string in query_strings_from_url) {
66
+ if (query_strings_from_url.hasOwnProperty(query_string)) {
67
+ var temp_var = query_strings_from_url[query_string].split('=', 2);
68
+ vars[temp_var[0]] = decodeURI(temp_var[1]);
69
+ }
70
+ }
71
+ var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages'];
72
+ for (var css_class in css_selector_classes) {
73
+ if (css_selector_classes.hasOwnProperty(css_class)) {
74
+ var element = document.getElementsByClassName(css_selector_classes[css_class]);
75
+ for (var j = 0; j < element.length; ++j) {
76
+ element[j].textContent = vars[css_selector_classes[css_class]];
77
+ }
78
+ }
79
+ }
80
+ }
81
+ </script>
82
+ <style>
83
+ html, body {
84
+ margin: 0;
85
+ padding: 0;
86
+ }
87
+ </style>
88
+ </head>
89
+ <body onload="subst()">
90
+ {{ content }}
91
+ </body>
92
+ </html>
93
+ EOF
94
+
95
+ File.open(File.join(@tmp_dir, 'header.html'), 'w') do |file|
96
+ file.write layout.render('content' => File.read(File.join(@theme_dir, 'header.html')))
97
+ end
98
+
99
+ File.open(File.join(@tmp_dir, 'footer.html'), 'w') do |file|
100
+ file.write layout.render('content' => File.read(File.join(@theme_dir, 'footer.html')))
101
+ end
102
+ end
103
+
104
+ def generate_pdf
105
+ command = ['wkhtmltopdf']
106
+ command << '--header-html' << File.expand_path('header.html', @tmp_dir)
107
+ command << '--footer-html' << File.expand_path('footer.html', @tmp_dir)
108
+ command << '--margin-top' << @theme_config.fetch('margin_top', 10).to_s
109
+ command << '--margin-left' << @theme_config.fetch('margin_left', 10).to_s
110
+ command << '--margin-right' << @theme_config.fetch('margin_right', 10).to_s
111
+ command << '--margin-bottom' << @theme_config.fetch('margin_bottom', 10).to_s
112
+ command << '--header-spacing' << @theme_config.fetch('header_spacing', 0).to_s
113
+ command << '--footer-spacing' << @theme_config.fetch('footer_spacing', 0).to_s
114
+
115
+ if @book.cover_image_path
116
+ prepare_cover
117
+ command << 'cover' << 'cover.html'
118
+ end
119
+
120
+ @book.pages.each do |page|
121
+ if page.node.is_a?(Asciidoctor::Section) && page.node.sectname == 'toc'
122
+ prepare_toc_xsl(page)
123
+ command << 'toc' << '--xsl-style-sheet' << 'toc.xsl'
124
+ else
125
+ command << page.path
126
+ end
127
+ end
128
+ filename = "#{@book.basename}.pdf"
129
+ command << filename
130
+ command << { chdir: @tmp_dir }
131
+ system(*command)
132
+
133
+ FileUtils.cp File.join(@tmp_dir, filename), @dest_dir
134
+ end
135
+
136
+ def prepare_cover
137
+ File.open(File.join(@tmp_dir, 'cover.html'), 'w') do |file|
138
+ file.write <<~EOF
139
+ <img src="#{@book.cover_image_path}" />
140
+ EOF
141
+ end
142
+ end
143
+
144
+ def prepare_toc_xsl(page)
145
+ File.open(File.join(@tmp_dir, 'toc.xsl'), 'w') do |file|
146
+ file.write Liquid::Template.parse(File.read(File.join(@theme_dir, 'toc.xsl'))).render({
147
+ 'book' => @book.to_hash,
148
+ 'page' => page.to_hash
149
+ })
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,85 @@
1
+ require "asciibook"
2
+ require "mercenary"
3
+
4
+ module Asciibook
5
+ class Command
6
+ def self.execute(argv)
7
+ p = Mercenary::Program.new(:asciibook)
8
+
9
+ p.version Asciibook::VERSION
10
+ p.description 'Asciibook is a ebook generator from Asciidoc to html/pdf/epub/mobi'
11
+ p.syntax 'asciibook <command> [options]'
12
+
13
+ p.command(:new) do |c|
14
+ c.description 'Create a new book scaffold in PATH'
15
+ c.syntax 'new PATH'
16
+ c.action do |args, options|
17
+ path = args[0]
18
+ if path
19
+ FileUtils.mkdir_p path
20
+ template_dir = File.expand_path('../../../book_template', __FILE__)
21
+ files = Dir.glob('*', base: template_dir).map { |file| File.join(template_dir, file) }
22
+ # TODO: confirm if file exists
23
+ FileUtils.cp_r files, path
24
+ else
25
+ abort "Please specify PATH to create book"
26
+ end
27
+ end
28
+ end
29
+
30
+ p.command(:init) do |c|
31
+ c.description 'Init a asciibook config for a Asciidoc file'
32
+ c.syntax 'init FILE'
33
+ c.action do |args, options|
34
+ source = args[0]
35
+ if File.file?(source)
36
+ dir = File.dirname source
37
+ filename = File.basename source
38
+ File.open(File.join(dir, 'asciibook.yml'), 'w') do |file|
39
+ file.write <<~EOF
40
+ source: #{filename}
41
+ #formats: ['html', 'pdf', 'epub', 'mobi']
42
+ #theme_dir:
43
+ #template_dir:
44
+ #page_level: 1
45
+ EOF
46
+ end
47
+ else
48
+ abort "Please specify the Asciidoc document to build"
49
+ end
50
+ end
51
+ end
52
+
53
+ p.command(:build) do |c|
54
+ c.description 'Build book'
55
+ c.syntax 'build [FILE|DIR]'
56
+ c.option :formats, '--format FORMAT1[,FORMAT2[,FORMAT3...]]', Array, 'Formats you want to build, allow: html,pdf,epub,mobi, default is all.'
57
+ c.option :theme_dir, '--theme-dir DIR', 'Theme dir.'
58
+ c.option :template_dir, '--template-dir DIR', 'Template dir.'
59
+ c.option :dest_dir, '--dest-dir DIR', 'Destination dir.'
60
+ c.option :page_level, '--page-level NUM', Integer, 'Page split base on section level, default is 1.'
61
+ c.action do |args, options|
62
+ source = args[0] || '.'
63
+ if File.directory?(source)
64
+ config_options = YAML.safe_load(File.read(File.join(source, 'asciibook.yml'))).reduce({}) do |hash, (key, value)|
65
+ hash[key.to_sym] = %w(source theme_dir template_dir dest_dir).include?(key) ? File.join(source, value) : value
66
+ hash
67
+ end
68
+ options = config_options.merge(options)
69
+ Asciibook::Book.load_file(options.delete(:source), options).build
70
+ elsif File.file?(source)
71
+ Asciibook::Book.load_file(source, options).build
72
+ else
73
+ abort "Build target '#{source}' neither a folder nor a file"
74
+ end
75
+ end
76
+ end
77
+
78
+ p.action do |args, _|
79
+ puts p
80
+ end
81
+
82
+ p.go(argv)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,303 @@
1
+ module Asciibook
2
+ class Converter < Asciidoctor::Converter::Base
3
+ register_for "asciibook"
4
+
5
+ DEFAULT_TEMPLATE_PATH = File.expand_path('../../../templates', __FILE__)
6
+
7
+ def initialize(backend, options = {})
8
+ super
9
+ init_backend_traits outfilesuffix: '.html', basebackend: 'html'
10
+ @template_dirs = (options[:template_dirs] || []).unshift(DEFAULT_TEMPLATE_PATH)
11
+ @templates = {}
12
+ end
13
+
14
+ def convert(node, transform = node.node_name, options = {})
15
+ get_template(transform).render 'node' => node_to_hash(node)
16
+ end
17
+
18
+ private
19
+
20
+ def get_template(name)
21
+ return @templates[name] if @templates[name]
22
+
23
+ @template_dirs.reverse.each do |template_dir|
24
+ path = File.join template_dir, "#{name}.html"
25
+ if File.exist?(path)
26
+ @templates[name] = Liquid::Template.parse(File.read(path))
27
+ break
28
+ end
29
+ end
30
+
31
+ unless @templates[name]
32
+ raise "Template not found #{name}"
33
+ end
34
+
35
+ @templates[name]
36
+ end
37
+
38
+ def node_to_hash(node)
39
+ case node
40
+ when Asciidoctor::Document
41
+ document_to_hash(node)
42
+ when Asciidoctor::Section
43
+ section_to_hash(node)
44
+ when Asciidoctor::Block
45
+ block_to_hash(node)
46
+ when Asciidoctor::List
47
+ list_to_hash(node)
48
+ when Asciidoctor::Table
49
+ table_to_hash(node)
50
+ when Asciidoctor::Inline
51
+ inline_to_hash(node)
52
+ else
53
+ raise "Uncatched type #{node} #{node.attributes}"
54
+ end
55
+ end
56
+
57
+ def abstract_node_to_hash(node)
58
+ {
59
+ 'context' => node.context.to_s,
60
+ 'node_name' => node.node_name,
61
+ 'id' => node.id,
62
+ 'attributes' => node.attributes
63
+ }
64
+ end
65
+
66
+ def abstract_block_to_hash(node)
67
+ abstract_node_to_hash(node).merge!({
68
+ 'level' => node.level,
69
+ 'title' => node.title,
70
+ 'caption' => node.caption,
71
+ 'captioned_title' => node.captioned_title,
72
+ 'style' => node.style,
73
+ 'content' => node_content(node),
74
+ 'xreftext' => node.xreftext
75
+ })
76
+ end
77
+
78
+ def node_content(node)
79
+ case node
80
+ when Asciidoctor::Document, Asciidoctor::Section
81
+ if node.node_name == 'section' && node.sectname == 'toc'
82
+ outline(node.document)
83
+ elsif node.node_name == 'section' && node.sectname == 'index'
84
+ get_template('index').render('indexterms' => node.document.references[:indexterms])
85
+ else
86
+ content = node.blocks.select { |b| b.page.nil? }.map {|b| b.convert }.join("\n")
87
+
88
+ if node.page && node.page.footnotes.any?
89
+ content += get_template('footnotes').render('footnotes' => node.page.footnotes)
90
+ end
91
+
92
+ content
93
+ end
94
+ else
95
+ case node.node_name
96
+ when 'listing'
97
+ if node.style == 'source' && node.document.syntax_highlighter
98
+ node.document.syntax_highlighter.format node, node.attributes['language'], { css_mode: :class }
99
+ else
100
+ "<pre>#{node.content}</pre>"
101
+ end
102
+ else
103
+ node.content
104
+ end
105
+ end
106
+ end
107
+
108
+ def document_to_hash(node)
109
+ title = node.attributes['doctitle'] && node.doctitle(partition: true)
110
+ abstract_block_to_hash(node).merge!({
111
+ 'title' => title&.main,
112
+ 'subtitle' => title&.subtitle,
113
+ 'outline' => outline(node),
114
+ 'authors' => node.authors.map { |author| author.to_h.map { |key, value| [key.to_s, value] }.to_h }
115
+ })
116
+ end
117
+
118
+ def section_to_hash(node)
119
+ abstract_block_to_hash(node).merge!({
120
+ 'index' => node.index,
121
+ 'number' => node.number,
122
+ 'sectname' => node.sectname,
123
+ 'special' => node.special,
124
+ 'numbered' => node.numbered,
125
+ 'sectnum' => node.sectnum
126
+ })
127
+ end
128
+
129
+ def block_to_hash(node)
130
+ abstract_block_to_hash(node).merge!({
131
+ 'blockname' => node.blockname
132
+ })
133
+ end
134
+
135
+ def list_to_hash(node)
136
+ case node.context
137
+ when :dlist
138
+ abstract_block_to_hash(node).merge!({
139
+ 'items' => node.items.map { |terms, item|
140
+ {
141
+ 'terms' => terms.map {|term| listitem_to_hash(term) },
142
+ 'description' => listitem_to_hash(item)
143
+ }
144
+ }
145
+ })
146
+ else
147
+ abstract_block_to_hash(node).merge!({
148
+ 'items' => node.blocks.map { |item| listitem_to_hash(item) }
149
+ })
150
+ end
151
+ end
152
+
153
+ def listitem_to_hash(node)
154
+ abstract_block_to_hash(node).merge!({
155
+ 'text' => (node.text? ? node.text : nil)
156
+ })
157
+ end
158
+
159
+ def table_to_hash(node)
160
+ abstract_block_to_hash(node).merge!({
161
+ 'columns' => node.columns,
162
+ 'rows' => {
163
+ 'head' => node.rows.head.map { |row| row.map {|cell| cell_to_hash(cell) } },
164
+ 'body' => node.rows.body.map { |row| row.map {|cell| cell_to_hash(cell) } },
165
+ 'foot' => node.rows.foot.map { |row| row.map {|cell| cell_to_hash(cell) } }
166
+ }
167
+ })
168
+ end
169
+
170
+ def cell_to_hash(node)
171
+ abstract_node_to_hash(node).merge!({
172
+ 'text' => node.text,
173
+ 'content' => node.content,
174
+ 'style' => node.style,
175
+ 'colspan' => node.colspan,
176
+ 'rowspan' => node.rowspan
177
+ })
178
+ end
179
+
180
+ def inline_to_hash(node)
181
+ data = abstract_node_to_hash(node).merge!({
182
+ 'text' => node.text,
183
+ 'type' => node.type.to_s,
184
+ 'target' => node.target,
185
+ 'xreftext' => node.xreftext
186
+ })
187
+
188
+ case node.node_name
189
+ when 'inline_anchor'
190
+ data['text'] ||= node.document.references[:refs][node.attributes['refid']]&.xreftext || "[#{node.attributes['refid']}]"
191
+
192
+ if (node.type == :xref) && (target_node = node.document.references[:refs][node.attributes['refid']])
193
+ data['target'] = if target_node.page
194
+ target_node.page.path
195
+ else
196
+ "#{find_page_node(target_node).page&.path}#{node.target}"
197
+ end
198
+ end
199
+ when 'inline_footnote'
200
+ if page = find_page_node(node).page
201
+ if index = page.footnotes.find_index { |footnote| footnote['text'] == node.text }
202
+ footnote = page.footnotes[index]
203
+ data['id'] = nil
204
+ data['index'] = footnote['index']
205
+ data['target'] = "#_footnotedef_#{footnote['index']}"
206
+ else
207
+ index = page.footnotes.count + 1
208
+ page.footnotes.push({
209
+ 'index' => index,
210
+ 'text' => node.text,
211
+ 'refid' => "_footnoteref_#{index}",
212
+ 'id' => "_footnotedef_#{index}"
213
+ })
214
+ data['id'] = "_footnoteref_#{index}"
215
+ data['index'] = index
216
+ data['target'] = "#_footnotedef_#{index}"
217
+ end
218
+ else
219
+ index = node.attr 'index'
220
+ data['id'] = (node.type == :xref ? nil : "_footnoteref_#{index}")
221
+ data['index'] = index
222
+ data['target'] = "#_footnotedef_#{index}"
223
+ end
224
+ when 'inline_indexterm'
225
+ node.document.references[:indexterms] ||= []
226
+ node.document.references[:indexcount] ||= 0
227
+ indexterms = node.document.references[:indexterms]
228
+ indexcount = node.document.references[:indexcount] += 1
229
+ id = "_indexterm_#{indexcount}"
230
+ target = "#{find_page_node(node).page&.path}#_indexterm_#{indexcount}"
231
+ if node.type == :visible
232
+ register_term(indexterms, [node.text], target)
233
+ else
234
+ register_term(indexterms, node.attributes['terms'], target)
235
+ end
236
+
237
+ data['id'] = id
238
+ end
239
+
240
+ data
241
+ end
242
+
243
+ def register_term(indexterms, terms, target)
244
+ items = indexterms
245
+ item = nil
246
+ terms.each_with_index do |term, index|
247
+ term_index = items.find_index { |item| item['term'] == term }
248
+ if term_index
249
+ item = items[term_index]
250
+ else
251
+ item = {
252
+ 'term' => term,
253
+ 'targets' => [],
254
+ 'items' => []
255
+ }
256
+ items.push item
257
+ items.sort_by! { |item| item['term'] }
258
+ end
259
+
260
+ if index == terms.size - 1
261
+ # save targe now
262
+ item['targets'].push target
263
+ else
264
+ # go deeper
265
+ items = item['items']
266
+ end
267
+ end
268
+ end
269
+
270
+ def find_page_node(node)
271
+ page_node = node
272
+
273
+ until page_node.page or page_node.parent.nil?
274
+ page_node = page_node.parent
275
+ end
276
+
277
+ page_node
278
+ end
279
+
280
+ def outline(node)
281
+ result = ''
282
+ if node.sections.any? && node.level < (node.document.attributes['toclevels'] || 2).to_i
283
+ result << "<ol>"
284
+ node.sections.each do |section|
285
+ result << "<li>"
286
+ target = if section.page
287
+ "#{section.page.path}"
288
+ else
289
+ "#{find_page_node(section).page&.path}##{section.id}"
290
+ end
291
+ result << %Q(<a href="#{target}">)
292
+ result << "#{section.sectnum} " if section.numbered && section.level < (node.document.attributes['sectnumlevels'] || 3).to_i
293
+ result << section.title
294
+ result << "</a>"
295
+ result << outline(section)
296
+ result << "</li>"
297
+ end
298
+ result << "</ol>"
299
+ end
300
+ result
301
+ end
302
+ end
303
+ end