asciibook 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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