asciidoctor-html 0.1.2 → 0.1.4

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -3
  3. data/README.md +29 -11
  4. data/Rakefile +15 -6
  5. data/assets/css/fonts/bootstrap-icons.woff +0 -0
  6. data/assets/css/fonts/bootstrap-icons.woff2 +0 -0
  7. data/assets/css/styles.css +5 -0
  8. data/assets/css/styles.css.map +1 -0
  9. data/exe/adoctohtml +6 -0
  10. data/lib/asciidoctor/html/bi_inline_macro.rb +25 -0
  11. data/lib/asciidoctor/html/book.rb +228 -0
  12. data/lib/asciidoctor/html/cli.rb +113 -0
  13. data/lib/asciidoctor/html/converter.rb +165 -24
  14. data/lib/asciidoctor/html/cref_inline_macro.rb +37 -0
  15. data/lib/asciidoctor/html/figure.rb +10 -10
  16. data/lib/asciidoctor/html/highlightjs.rb +99 -0
  17. data/lib/asciidoctor/html/list.rb +38 -0
  18. data/lib/asciidoctor/html/popovers.rb +49 -0
  19. data/lib/asciidoctor/html/ref_tree_processor.rb +134 -57
  20. data/lib/asciidoctor/html/template.rb +145 -0
  21. data/lib/asciidoctor/html/tree_walker.rb +3 -1
  22. data/lib/asciidoctor/html/utils.rb +6 -0
  23. data/lib/asciidoctor/html/webmanifest.rb +23 -0
  24. data/lib/asciidoctor/html.rb +13 -1
  25. data/lib/minitest/html_plugin.rb +18 -22
  26. metadata +52 -27
  27. data/docs/_config.yml +0 -5
  28. data/docs/_layouts/default.html +0 -25
  29. data/docs/_sass/_custom.scss +0 -35
  30. data/docs/_sass/_example.scss +0 -30
  31. data/docs/_sass/_figure.scss +0 -17
  32. data/docs/_sass/_olist.scss +0 -101
  33. data/docs/_sass/main.scss +0 -40
  34. data/docs/assets/css/fonts +0 -1
  35. data/docs/assets/css/styles.scss +0 -3
  36. data/docs/assets/img/cat1.jpg +0 -0
  37. data/docs/assets/img/cat2.jpg +0 -0
  38. data/docs/assets/img/cat3.jpg +0 -0
  39. data/docs/package-lock.json +0 -59
  40. data/docs/package.json +0 -6
  41. data/docs/site.webmanifest +0 -1
  42. data/lib/asciidoctor/html/olist.rb +0 -18
  43. data/lib/asciidoctor/html/version.rb +0 -7
  44. /data/{docs → assets/favicon}/android-chrome-192x192.png +0 -0
  45. /data/{docs → assets/favicon}/android-chrome-512x512.png +0 -0
  46. /data/{docs → assets/favicon}/apple-touch-icon.png +0 -0
  47. /data/{docs → assets/favicon}/favicon-16x16.png +0 -0
  48. /data/{docs → assets/favicon}/favicon-32x32.png +0 -0
  49. /data/{docs → assets/favicon}/favicon.ico +0 -0
@@ -1,30 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "olist"
4
-
5
3
  module Asciidoctor
6
4
  module Html
7
5
  # Helper functions for the image/figure conversion.
8
6
  # Mixed into the Converter class.
9
7
  module Figure
10
- def display_image(node, target, attrs)
8
+ def display_image(node, target, title_attr: false)
9
+ attrs = image_attrs(node, title_attr:)
11
10
  %(<img src="#{node.image_uri target}" #{attrs}#{@void_element_slash}>)
12
11
  end
13
12
 
14
- def image_attrs(node)
13
+ def image_attrs(node, title_attr: false)
15
14
  width = node.attr?("width") ? %( width="#{node.attr "width"}") : ""
16
15
  height = node.attr?("height") ? %( height="#{node.attr "height"}") : ""
16
+ title = encode_attribute_value node.attr("title") if node.attr?("title") && title_attr
17
+ title = title ? %( data-bs-toggle="tooltip" data-bs-title="#{title}") : ""
17
18
  alt = encode_attribute_value node.alt
18
- %(alt="#{alt}"#{width}#{height})
19
+ %(alt="#{alt}"#{width}#{height}#{title})
19
20
  end
20
21
 
21
22
  def display_figure(node)
22
23
  target = node.attr "target"
23
24
  title = node.title? ? node.title : ""
24
- attrs = image_attrs node
25
- image = display_image node, target, attrs
26
- caption = %(<figcaption>#{Utils.display_title_prefix node}#{title}</figcaption>)
27
- %(<figure>\n #{image}\n #{caption}\n</figure>)
25
+ image = display_image node, target
26
+ caption = %( <figcaption>#{Utils.display_title_prefix node}#{title}</figcaption>)
27
+ %(<figure>\n #{image}\n#{caption}\n</figure>)
28
28
  end
29
29
 
30
30
  def convert_figlist(node)
@@ -32,7 +32,7 @@ module Asciidoctor
32
32
  %(<li#{Utils.id_class_attr_str item.id}><figure>\n#{item.text}\n</figure></li>)
33
33
  end
34
34
  content = Utils.wrap_id_classes result.join("\n"), nil, "figlist loweralpha", :ol
35
- title = Utils.display_title node
35
+ title = %(<div class="figlist-title">#{Utils.display_title_prefix(node)}#{node.title}</div>)
36
36
  classes = ["figlist-wrapper", node.role].compact.join(" ")
37
37
  Utils.wrap_id_classes %(#{content}#{title}), node.id, classes
38
38
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module Html
5
+ # Constants for the highlightjs syntax highlighting library
6
+ module Highlightjs
7
+ CDN_PATH = "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build"
8
+
9
+ INCLUDED_LANGS = {
10
+ "bash" => true,
11
+ "c" => true,
12
+ "cpp" => true,
13
+ "csharp" => true,
14
+ "css" => true,
15
+ "diff" => true,
16
+ "go" => true,
17
+ "graphql" => true,
18
+ "ini" => true,
19
+ "java" => true,
20
+ "javascript" => true,
21
+ "json" => true,
22
+ "kotlin" => true,
23
+ "less" => true,
24
+ "lua" => true,
25
+ "makefile" => true,
26
+ "markdown" => true,
27
+ "objectivec" => true,
28
+ "perl" => true,
29
+ "php" => true,
30
+ "php-template" => true,
31
+ "plaintext" => true,
32
+ "python" => true,
33
+ "python-repl" => true,
34
+ "r" => true,
35
+ "ruby" => true,
36
+ "rust" => true,
37
+ "scss" => true,
38
+ "shell" => true,
39
+ "sql" => true,
40
+ "swift" => true,
41
+ "typescript" => true,
42
+ "vbnet" => true,
43
+ "wasm" => true,
44
+ "xml" => true,
45
+ "yaml" => true
46
+ }.freeze
47
+
48
+ PLUGIN = <<~JS
49
+ function toggleCopyIcon(copyIcon) {
50
+ copyIcon.classList.toggle("bi-clipboard");
51
+ copyIcon.classList.toggle("bi-clipboard-check");
52
+ }
53
+ hljs.addPlugin({
54
+ "after:highlightElement": function({ el, result, text }) {
55
+ const wrapper = el.parentElement; // pre element
56
+ if(wrapper == null) { return; }
57
+
58
+ const overlay = document.createElement("div");
59
+ overlay.classList.add("copy-button");
60
+ overlay.textContent = result.language.toUpperCase() + ' ';
61
+
62
+ const copyButton = document.createElement("button");
63
+ copyButton.classList.add("btn");
64
+ copyButton.setAttribute("type", "button");
65
+ copyButton.setAttribute("data-bs-toggle", "tooltip");
66
+ copyButton.setAttribute("data-bs-title", "Copy to clipboard");
67
+ if(!touch) {bootstrap.Tooltip.getOrCreateInstance(copyButton);}
68
+
69
+ const copyIcon = document.createElement("i");
70
+ copyIcon.classList.add("bi", "bi-clipboard");
71
+
72
+ copyButton.append(copyIcon);
73
+ overlay.append(copyButton);
74
+
75
+ copyButton.onclick = function() {
76
+ navigator.clipboard.writeText(text);
77
+ if(!copyIcon.classList.contains("bi-clipboard-check")) {
78
+ toggleCopyIcon(copyIcon);
79
+ setTimeout(() => { toggleCopyIcon(copyIcon); }, 1500);
80
+ }
81
+ };
82
+
83
+ // Append the copy button to the wrapper
84
+ wrapper.appendChild(overlay);
85
+
86
+ // Find and replace inline callouts
87
+ const rgx = /[\u2460-\u2468]/gu;
88
+ if(text.match(rgx)) {
89
+ text = text.replaceAll(rgx, "");
90
+ el.innerHTML = el.innerHTML.replaceAll(rgx, (match) => {
91
+ return '<i class="bi bi-' + (match.charCodeAt() - 9311) + '-circle"></i>';
92
+ });
93
+ }
94
+ }
95
+ });
96
+ JS
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module Html
5
+ # Helper functions for the list conversion.
6
+ module List
7
+ def self.convert(node, tag_name = :ol)
8
+ depth = node.attr "list-depth"
9
+ flat = node.attr? "flat-style"
10
+ level = depth + 1
11
+ classes = [
12
+ "list",
13
+ "list-#{node.context}",
14
+ "level-#{level}",
15
+ flat ? "pseudocode" : node.style,
16
+ node.role
17
+ ].compact
18
+ classes << "checklist" if node.option?("checklist")
19
+ result = [%(<#{tag_name}#{Utils.dyn_id_class_attr_str node, classes.join(" ")}>)]
20
+ node.items.each do |item|
21
+ result << display_list_item(item)
22
+ end
23
+ result << %(</#{tag_name}> <!-- .level-#{level} -->\n)
24
+ Utils.wrap_id_classes_with_title result.join("\n"), node, node.id, "list-wrapper"
25
+ end
26
+
27
+ def self.display_list_item(item)
28
+ result = []
29
+ result << %(<li#{Utils.id_class_attr_str item.id,
30
+ item.role}><div class="li-mark">#{item.attr "mark"}</div>)
31
+ result << %(<div class="li-content"><p>#{item.text}</p>)
32
+ result << "\n#{item.content}" if item.blocks?
33
+ result << %(</div></li>#{Utils.id_class_sel_comment item.id, item.role})
34
+ result.join "\n"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module Html
5
+ # Configure the popovers for footnotes and citations.
6
+ module Popovers
7
+ POPOVERS = <<~JS
8
+ function initPopovers() {
9
+ document.querySelectorAll(".btn-po[data-contentid]").forEach(el => {
10
+ const id = el.dataset.contentid;
11
+ let content = document.getElementById(id);
12
+ if(content) {
13
+ if(content.tagName == "A") {
14
+ // This is an anchor of a bibitem
15
+ const listItem = content.parentElement.cloneNode(true)
16
+ listItem.removeChild(listItem.firstChild)
17
+ content = listItem
18
+ }
19
+ new bootstrap.Popover(el, {
20
+ trigger: "focus",
21
+ content: content,
22
+ html: true,
23
+ sanitize: false
24
+ });
25
+ }
26
+ });
27
+ }
28
+ MathJax = {
29
+ startup: {
30
+ pageReady: function() {
31
+ return MathJax.startup.defaultPageReady().then(initPopovers);
32
+ }
33
+ }
34
+ };
35
+ JS
36
+
37
+ TOOLTIPS = <<~JS
38
+ // Only enable tooltips on images if not a touch screen device
39
+ if(!touch) {
40
+ document.querySelectorAll('img[data-bs-toggle="tooltip"]').forEach(el => {
41
+ bootstrap.Tooltip.getOrCreateInstance(el);
42
+ });
43
+ }
44
+ JS
45
+
46
+ INIT = "#{TOOLTIPS}\n#{POPOVERS}".freeze
47
+ end
48
+ end
49
+ end
@@ -1,71 +1,99 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "asciidoctor"
3
4
  require "roman-numerals"
4
5
  require_relative "tree_walker"
6
+ require_relative "highlightjs"
5
7
 
6
8
  module Asciidoctor
7
9
  module Html
8
- # Traverses the document tree and attaches a correct reftext to
9
- # numbered nodes.
10
+ # Traverses the document tree and:
11
+ # - attaches a correct reftext to numbered nodes;
12
+ # - populates the text (= reftext for inline nodes) of anchors at
13
+ # the beginning of a list item for an ordered list;
14
+ # - registers every encountered source code language not included
15
+ # in the default highlightjs build.
10
16
  class RefTreeProcessor < Asciidoctor::Extensions::TreeProcessor
11
17
  NUMBERED_CONTEXTS = {
12
18
  example: "thm-number",
13
19
  table: "tbl-number",
14
- image: "fig-number"
20
+ image: "fig-number",
21
+ stem: "eqn-number",
22
+ listing: "ltg-number"
15
23
  }.freeze
16
24
 
17
- def number_within(document)
18
- return :chapter if document.attr? "chapnum"
19
- return :section if document.attr? "sectnums"
20
-
21
- :document
22
- end
23
-
24
25
  def assign_numeral!(node, document, counter_name)
25
26
  document.counters[counter_name] ||= 0
26
27
  node.numeral = (document.counters[counter_name] += 1)
27
28
  end
28
29
 
29
- def relative_numeral(node, document, sectnum)
30
- if node.numeral
31
- prefix_number = (document.attr("chapnum") || sectnum).to_i
32
- prefix_number.positive? ? "#{prefix_number}.#{node.numeral}" : node.numeral.to_s
33
- else
34
- ""
35
- end
30
+ def relative_numeral(node, document)
31
+ return "" unless node.numeral
32
+
33
+ chapnum = document.attr "chapnum"
34
+ has_prefix = chapnum && !chapnum.empty? && chapnum != "0"
35
+ has_prefix ? "#{chapnum}.#{node.numeral}" : node.numeral.to_s
36
36
  end
37
37
 
38
- def process_numbered_block!(block, document, sectnum)
38
+ def process_numbered_block!(block, document)
39
39
  context = block.context
40
- env = (block.style || context).to_s
41
- env = "figure" if context == :image || env == "figlist"
42
- block.set_attr "showcaption", true
40
+ style = block.style
41
+ context = :image if style == "figlist"
42
+ env = env context, style
43
+ block.set_attr("showcaption", true) unless context == :stem
43
44
  assign_numeral! block, document, NUMBERED_CONTEXTS[context]
44
- title_prefix = "#{env.capitalize} #{relative_numeral block, document, sectnum}"
45
- block.set_attr "reftext", title_prefix
45
+ relative_numeral = relative_numeral block, document
46
+ reftext = if context == :stem
47
+ "(#{relative_numeral})"
48
+ else
49
+ "#{env.capitalize} #{relative_numeral}"
50
+ end
51
+ block.set_attr "reftext", reftext
52
+ end
53
+
54
+ def env(context, style)
55
+ case context
56
+ when :image then "figure"
57
+ when :stem then "equation"
58
+ when :listing then "listing"
59
+ else style || context.to_s
60
+ end
46
61
  end
47
62
 
48
63
  def process_numbered_block?(block)
49
- NUMBERED_CONTEXTS.key?(block.context) || block.style == "figlist"
64
+ context = block.context
65
+ case context
66
+ when :olist
67
+ block.style == "figlist"
68
+ when :stem, :listing
69
+ block.option? "numbered"
70
+ else
71
+ NUMBERED_CONTEXTS.include? context
72
+ end
50
73
  end
51
74
 
52
75
  def li_mark(depth, idx)
53
76
  case depth
54
- when 0
55
- idx + 1
56
- when 1
57
- ("a".."z").to_a[idx]
58
- when 2
59
- RomanNumerals.to_roman(idx + 1).downcase
60
- when 3
61
- ("a".."z").to_a[idx].upcase
77
+ when 1 then ("a".."z").to_a[idx]
78
+ when 2 then RomanNumerals.to_roman(idx + 1).downcase
79
+ when 3 then ("a".."z").to_a[idx].upcase
80
+ else idx + 1
62
81
  end
63
82
  end
64
83
 
65
- def ref_li_mark(mark, depth)
66
- return mark.to_s if depth.even?
84
+ def bullet(depth)
85
+ case depth
86
+ when 1 then "&#8208;"
87
+ when 2 then "&#11089;"
88
+ when 3 then "&#9702;"
89
+ else "&#8226;"
90
+ end
91
+ end
92
+
93
+ def ref_li_mark(mark, depth, style = nil)
94
+ return "[#{mark}]" if style == "bibliography"
67
95
 
68
- "(#{mark})"
96
+ depth.even? ? mark.to_s : "(#{mark})"
69
97
  end
70
98
 
71
99
  def offset(list)
@@ -76,7 +104,7 @@ module Asciidoctor
76
104
  # its reftext to that of item's if necessary.
77
105
  def register_reftext!(item, reftext)
78
106
  item.set_attr "reftext", reftext
79
- /^<a id="(?<anchor_id>.+?)"/ =~ item.text
107
+ /\A<a id="(?<anchor_id>.+?)"/ =~ item.text
80
108
  node = item.document.catalog[:refs][anchor_id]
81
109
  node&.text ||= reftext
82
110
  end
@@ -93,57 +121,106 @@ module Asciidoctor
93
121
  block.set_attr("flat-style", true)
94
122
  else
95
123
  offset = offset block
124
+ style = block.style
96
125
  block.items.each_with_index do |item, idx|
97
- d = block.style == "figlist" ? 1 : depth
126
+ d = style == "figlist" ? 1 : depth
98
127
  mark = li_mark(d, idx + offset)
99
128
  item.set_attr "mark", mark
100
- item_reftext = "#{parent_reftext}#{ref_li_mark mark, d}"
129
+ item_reftext = "#{parent_reftext}#{ref_li_mark mark, d, style}"
101
130
  register_reftext! item, item_reftext
102
131
  end
103
132
  end
104
133
  end
105
134
 
135
+ def process_colist!(block)
136
+ block.set_attr "list-depth", 0
137
+ block.items.each_with_index do |item, idx|
138
+ icon = %(<i class="bi bi-#{idx + 1}-circle"></i>)
139
+ item.set_attr "mark", icon
140
+ register_reftext! item, icon
141
+ end
142
+ end
143
+
144
+ def process_ulist!(block, depth)
145
+ block.set_attr "list-depth", depth
146
+ block.items.each do |item|
147
+ is_checkbox = item.attr? "checkbox"
148
+ icon_class = item.attr?("checked") ? "check-" : ""
149
+ icon = %(<i class="bi bi-#{icon_class}square"></i>)
150
+ mark = is_checkbox ? icon : bullet(depth)
151
+ item.role = "checked" if is_checkbox
152
+ item.set_attr "mark", mark
153
+ end
154
+ end
155
+
106
156
  def process_flat_item!(item, idx)
107
157
  mark = li_mark(0, idx)
108
158
  item.set_attr "mark", mark
109
159
  register_reftext! item, ref_li_mark(mark, 0)
110
160
  end
111
161
 
112
- def reset_counters!(document)
113
- counters = document.counters
114
- NUMBERED_CONTEXTS.each_value do |counter_name|
115
- counters[counter_name] = 0
116
- end
162
+ def process_source_code!(document, lang)
163
+ document.set_attr("source-langs", {}) unless document.attr?("source-langs")
164
+ langs = document.attr "source-langs"
165
+ langs[lang] = true unless Highlightjs::INCLUDED_LANGS.include?(lang)
166
+ end
167
+
168
+ def olist_item?(node)
169
+ node.context == :list_item && node.parent.context == :olist
170
+ end
171
+
172
+ def ulist_item?(node)
173
+ node.context == :list_item && node.parent.context == :ulist
174
+ end
175
+
176
+ def ulist?(node)
177
+ node.context == :ulist
178
+ end
179
+
180
+ def olist?(node)
181
+ node.context == :olist
182
+ end
183
+
184
+ def colist?(node)
185
+ node.context == :colist
186
+ end
187
+
188
+ def source_code?(node)
189
+ node.context == :listing && node.style == "source" && node.attr?("language")
117
190
  end
118
191
 
119
192
  def process(document)
120
- sectnum = 0
121
193
  listdepth = 0
194
+ bulletdepth = 0
122
195
  flat_style = false
123
196
  flat_idx = 0 # flat index for (pseudocode) list
124
- tw = TreeWalker.new(document)
197
+ tw = TreeWalker.new document
125
198
  while (block = tw.next_block)
126
- context = block.context
127
199
  unless block.attr? "refprocessed"
128
- process_numbered_block!(block, document, sectnum) if process_numbered_block?(block)
129
- if context == :section && block.level == 1 && number_within(document) == :section
130
- sectnum += 1
131
- reset_counters! document
132
- elsif context == :olist
200
+ process_numbered_block!(block, document) if process_numbered_block?(block)
201
+ if colist? block
202
+ process_colist! block
203
+ elsif olist? block
133
204
  if listdepth.zero?
134
205
  flat_style = (block.style == "pseudocode")
135
- flat_idx = offset block
206
+ flat_idx = offset block # rubocop:disable Lint/UselessAssignment
136
207
  end
137
- process_olist!(block, listdepth, flat_style:)
138
- elsif context == :list_item && flat_style
139
- process_flat_item!(block, flat_idx)
208
+ process_olist! block, listdepth, flat_style:
209
+ elsif olist_item?(block) && flat_style
210
+ process_flat_item! block, flat_idx
140
211
  flat_idx += 1
212
+ elsif source_code? block
213
+ process_source_code! document, block.attr("language")
214
+ elsif ulist? block
215
+ process_ulist! block, bulletdepth
141
216
  end
142
217
  block.set_attr "refprocessed", true
143
218
  end
144
219
  tw.walk do |move|
145
- listdepth += 1 if context == :olist && move == :explore
146
- listdepth -= 1 if context == :list_item && move == :retreat
220
+ listdepth += 1 if olist?(block) && move == :explore
221
+ listdepth -= 1 if olist_item?(block) && move == :retreat
222
+ bulletdepth += 1 if ulist?(block) && move == :explore
223
+ bulletdepth -= 1 if ulist_item?(block) && move == :retreat
147
224
  end
148
225
  end
149
226
  end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require_relative "highlightjs"
5
+ require_relative "popovers"
6
+
7
+ module Asciidoctor
8
+ module Html
9
+ # The template for the book layout
10
+ module Template
11
+ def self.nav_item(target, text, content = "", active: false)
12
+ active_class = active ? %( class="active") : ""
13
+ link = %(<a href="#{target}">#{text}</a>)
14
+ subnav = content.empty? ? content : "\n#{content}\n"
15
+ %(<li#{active_class}>#{link}#{subnav}</li>\n)
16
+ end
17
+
18
+ def self.nav_text(chapnum, chaptitle)
19
+ return chaptitle if chapnum.empty?
20
+
21
+ %(<span class="title-mark">#{chapnum}</span>#{chaptitle})
22
+ end
23
+
24
+ def self.appendix_title(chapname, numeral, doctitle, num_appendices)
25
+ numeral = num_appendices == 1 ? "" : " #{numeral}"
26
+ %(<span class="title-prefix">#{chapname}#{numeral}</span>#{doctitle})
27
+ end
28
+
29
+ def self.sidebar(nav_items)
30
+ <<~HTML
31
+ <div id="sidebar" class="sidebar collapse collapse-horizontal">
32
+ <nav id="sidenav"><ul>
33
+ #{nav_items.join "\n"}
34
+ </ul></nav>
35
+ </div> <!-- .sidebar -->
36
+ HTML
37
+ end
38
+
39
+ def self.main(content, chapnum, chaptitle, author, year)
40
+ <<~HTML
41
+ <main class="main">
42
+ <div class="content-container">
43
+ <h2>#{nav_text chapnum, chaptitle}</h2>
44
+ #{content}
45
+ #{footer author, year}
46
+ </div>
47
+ </main>
48
+ HTML
49
+ end
50
+
51
+ def self.header(title, short_title, nav: true)
52
+ nav_btn = if nav
53
+ <<~HTML
54
+ <button type="button" class="btn menu"
55
+ data-bs-toggle="collapse" data-bs-target="#sidebar"
56
+ aria-expanded="false" aria-controls="sidebar">
57
+ <i class="bi bi-list"></i>
58
+ </button>
59
+ HTML
60
+ else
61
+ ""
62
+ end
63
+ <<~HTML
64
+ <header class="header">
65
+ <a class="home d-none d-sm-block" href="./">#{title}</a>
66
+ <a class="home d-block d-sm-none" href="./">#{short_title}</a>
67
+ #{nav_btn}
68
+ </header>
69
+ HTML
70
+ end
71
+
72
+ def self.footer(author, year)
73
+ %(<footer class="footer">&#169; #{year} #{author}</footer>\n)
74
+ end
75
+
76
+ def self.highlightjs(langs)
77
+ langs.map do |lang|
78
+ %(<script src="#{Highlightjs::CDN_PATH}/languages/#{lang}.min.js"></script>)
79
+ end.join("\n ")
80
+ end
81
+
82
+ def self.head(title, langs)
83
+ <<~HTML
84
+ <head>
85
+ <meta charset="utf-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1">
87
+ <title>#{title}</title>
88
+ <link rel="apple-touch-icon" sizes="180x180" href="#{FAVICON_PATH}/apple-touch-icon.png">
89
+ <link rel="icon" type="image/png" sizes="32x32" href="#{FAVICON_PATH}/favicon-32x32.png">
90
+ <link rel="icon" type="image/png" sizes="16x16" href="#{FAVICON_PATH}/favicon-16x16.png">
91
+ <link rel="manifest" href="#{FAVICON_PATH}/site.webmanifest">
92
+ <link rel="stylesheet" href="#{CSS_PATH}/styles.css">
93
+ <link rel="stylesheet" href="#{Highlightjs::CDN_PATH}/styles/tomorrow-night-blue.min.css">
94
+ <script src="#{Highlightjs::CDN_PATH}/highlight.min.js"></script>
95
+ #{highlightjs langs}
96
+ <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
97
+ </head>
98
+ HTML
99
+ end
100
+
101
+ # opts:
102
+ # - title: String
103
+ # - short_title: String
104
+ # - author: String
105
+ # - date: Date
106
+ # - nav: Boolean
107
+ # - chapnum: Int
108
+ # - chaptitle: String
109
+ # - langs: Array[String]
110
+ def self.html(content, nav_items, opts = {})
111
+ hash_listener = if opts[:nav]
112
+ <<~JS
113
+ addEventListener('hashchange', function() {
114
+ collapse = bootstrap.Collapse.getInstance('#sidebar');
115
+ if(collapse) collapse.hide();
116
+ });
117
+ JS
118
+ else
119
+ ""
120
+ end
121
+ <<~HTML
122
+ <!DOCTYPE html>
123
+ <html lang="en">
124
+ #{head opts[:title], opts[:langs]}
125
+ <body>
126
+ #{header opts[:title], opts[:short_title], nav: opts[:nav]}
127
+ #{sidebar(nav_items) if opts[:nav]}
128
+ #{main content, opts[:chapnum], opts[:chaptitle], opts[:author], opts[:date].year}
129
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"
130
+ integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO"
131
+ crossorigin="anonymous"></script>
132
+ <script>
133
+ const touch = matchMedia('(hover: none)').matches;
134
+ #{Highlightjs::PLUGIN}
135
+ hljs.highlightAll();
136
+ #{hash_listener}
137
+ #{Popovers::POPOVERS}
138
+ </script>
139
+ </body>
140
+ </html>
141
+ HTML
142
+ end
143
+ end
144
+ end
145
+ end
@@ -23,7 +23,9 @@ module Asciidoctor
23
23
 
24
24
  if block.blocks? && @level < @max_levels && @idx[@level + 1] < block.blocks.size
25
25
  @level += 1
26
- @path.push(block.blocks[@idx[@level]])
26
+ el = block.blocks[@idx[@level]]
27
+ el = el.last if el.is_a?(Array) # Get the <dd> from an array of <dt>'s followed by <dd>
28
+ @path.push(el)
27
29
  callback.call(:explore)
28
30
  else
29
31
  @idx[@level + 1] = 0 if @level < @max_levels
@@ -65,6 +65,12 @@ module Asciidoctor
65
65
  def self.wrap_id_classes_with_title(content, node, id, classes, needs_prefix: false)
66
66
  show_title?(node) ? wrap_id_classes(display_title(node, needs_prefix:) + content, id, classes) : content
67
67
  end
68
+
69
+ def self.popover_button(content, content_id, classes = nil)
70
+ extra_classes = classes ? " #{classes}" : ""
71
+ attrs = %( tabindex="0" role="button" class="btn-po#{extra_classes}" data-contentid="#{content_id}")
72
+ %(<a#{attrs}>#{content}</a>)
73
+ end
68
74
  end
69
75
  end
70
76
  end