jekyll-jtd-toc-nav 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 127c3ffb6cd3c92d01d52561f1c63542b3bd4b34a90c5d8c917c1eb267f0fbe4
4
+ data.tar.gz: d5ec6bff6a17bd9e09670665210377b938395fcae2869d807e9fdbf88284d780
5
+ SHA512:
6
+ metadata.gz: 4a182031953e79e0174e8ccb203d99e6b112084deda67e18c37475302ff7c789f44b8ff0c6db9b6a2369d72212afbb6490a52e95c185bcfd01386be72ba24784
7
+ data.tar.gz: 2cae035deb850bccf172eae0af34225983ac545494e994da7b0db98e4fe4c9f9a9caeafcd3a8199a5a1e5674e3f314082f216881c1e4f2268cdf91220ffeac6e
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module JtdTocNav
5
+ class Injector
6
+ DEFAULT_LEVELS = (2..4).to_a
7
+
8
+ def initialize(site:)
9
+ @site = site
10
+ end
11
+
12
+ def levels
13
+ raw = @site.config["sidebar_toc_levels"]
14
+ return DEFAULT_LEVELS if raw.nil?
15
+
16
+ if raw.is_a?(String) && raw.include?("..")
17
+ a, b = raw.split("..", 2).map { |x| Integer(x) rescue nil }
18
+ return DEFAULT_LEVELS if a.nil? || b.nil?
19
+ return (a..b).to_a
20
+ end
21
+
22
+ if raw.is_a?(Array)
23
+ ints = raw.map { |x| Integer(x) rescue nil }.compact
24
+ return DEFAULT_LEVELS if ints.empty?
25
+ return ints
26
+ end
27
+
28
+ DEFAULT_LEVELS
29
+ end
30
+
31
+ def expand_all?
32
+ @site.config.fetch("sidebar_toc_expand", true) != false
33
+ end
34
+
35
+ def process!(page_like)
36
+ return if page_like.output.nil? || page_like.output.empty?
37
+
38
+ html = page_like.output
39
+ doc = Nokogiri::HTML(html)
40
+
41
+ headings = extract_headings(doc)
42
+ return if headings.empty?
43
+
44
+ nav = doc.at_css("#site-nav")
45
+ return unless nav
46
+
47
+ page_url = page_like.url
48
+ link = find_nav_link(nav, page_url)
49
+ return unless link
50
+
51
+ nav_item = link.ancestors("li.nav-list-item").first
52
+ return unless nav_item
53
+
54
+ # Remove any prior injection
55
+ nav_item.css("> ul.nav-list[data-jtd-toc-nav='true']").remove
56
+
57
+ outline_ul = build_outline_ul(doc, headings)
58
+ outline_ul["data-jtd-toc-nav"] = "true"
59
+
60
+ # Ensure current page can expand/collapse like other nav items.
61
+ ensure_expander!(doc, nav_item, label: "Toggle page sections")
62
+
63
+ nav_item.add_class("active")
64
+ nav_item.add_child(outline_ul)
65
+
66
+ page_like.output = doc.to_html
67
+ rescue StandardError => e
68
+ Jekyll.logger.warn("jtd-toc-nav:", "failed to inject sidebar toc for #{page_like.url}: #{e.class}: #{e.message}")
69
+ end
70
+
71
+ private
72
+
73
+ def extract_headings(doc)
74
+ selector = levels.map { |lvl| "main h#{lvl}" }.join(", ")
75
+ doc.css(selector).filter_map do |h|
76
+ next if h["id"].to_s.empty?
77
+ classes = h["class"].to_s.split(/\s+/)
78
+ next if classes.include?("no_toc")
79
+ # Dont pull headings from the TOC itself if it exists in content.
80
+ next if h.ancestors.any? { |a| a["id"] == "markdown-toc" || a["class"].to_s.split(/\s+/).include?("js-page-toc") }
81
+
82
+ {
83
+ id: h["id"],
84
+ text: h.text.strip,
85
+ level: h.name.delete_prefix("h").to_i
86
+ }
87
+ end
88
+ end
89
+
90
+ def find_nav_link(nav, page_url)
91
+ baseurl = @site.config["baseurl"].to_s
92
+ baseurl = "" if baseurl == "/"
93
+
94
+ # Match just-the-docs trailing slash / .html.
95
+ candidates = []
96
+ candidates << page_url
97
+ candidates << "#{page_url}/" unless page_url.end_with?("/")
98
+ candidates << "#{page_url}.html" unless page_url.end_with?(".html")
99
+ candidates = candidates.uniq
100
+
101
+ candidates = candidates.flat_map do |u|
102
+ if baseurl.empty?
103
+ [u]
104
+ else
105
+ [u, File.join(baseurl, u)]
106
+ end
107
+ end.uniq
108
+
109
+ candidates.each do |href|
110
+ link = nav.at_css(%(a.nav-list-link[href="#{css_escape(href)}"]))
111
+ return link if link
112
+ end
113
+
114
+ nil
115
+ end
116
+
117
+ def css_escape(s)
118
+ # Minimal escape for quotes/backslashes in attribute selectors.
119
+ s.to_s.gsub("\\", "\\\\").gsub('"', '\"')
120
+ end
121
+
122
+ def build_outline_ul(doc, headings)
123
+ min_level = headings.map { |h| h[:level] }.min
124
+ stack = []
125
+
126
+ root_ul = Nokogiri::XML::Node.new("ul", doc)
127
+ root_ul["class"] = "nav-list"
128
+ stack << { level: min_level - 1, ul: root_ul }
129
+
130
+ headings.each_with_index do |h, idx|
131
+ while stack.size > 1 && h[:level] <= stack.last[:level]
132
+ stack.pop
133
+ end
134
+
135
+ while h[:level] > stack.last[:level] + 1
136
+ # Skip missing intermediate levels by treating as direct child.
137
+ stack.last[:level] += 1
138
+ end
139
+
140
+ li = Nokogiri::XML::Node.new("li", doc)
141
+ li["class"] = "nav-list-item"
142
+
143
+ a = Nokogiri::XML::Node.new("a", doc)
144
+ a["class"] = "nav-list-link"
145
+ a["href"] = "##{h[:id]}"
146
+ a.content = h[:text]
147
+ li.add_child(a)
148
+
149
+ parent_ul = stack.last[:ul]
150
+ parent_ul.add_child(li)
151
+
152
+ next_h = headings[idx + 1]
153
+ has_children = next_h && next_h[:level] > h[:level]
154
+ next unless has_children
155
+
156
+ # This heading has subheadings. Nest them
157
+ child_ul = Nokogiri::XML::Node.new("ul", doc)
158
+ child_ul["class"] = "nav-list"
159
+ li.add_child(child_ul)
160
+
161
+ # jtd hides nested `.nav-list` by default.
162
+ ensure_expander!(doc, li, label: "Toggle section")
163
+ li.add_class("active") if expand_all?
164
+
165
+ stack << { level: h[:level], ul: child_ul }
166
+ end
167
+
168
+ root_ul
169
+ end
170
+
171
+ def ensure_expander!(doc, li, label:)
172
+ existing = li.at_css("> button.nav-list-expander")
173
+ return existing if existing
174
+
175
+ button = Nokogiri::XML::Node.new("button", doc)
176
+ button["class"] = "nav-list-expander btn-reset"
177
+ button["aria-label"] = label
178
+ button["aria-expanded"] = li["class"].to_s.split(/\s+/).include?("active") ? "true" : "false"
179
+ button.inner_html = '<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>'
180
+
181
+ li.children.first.add_previous_sibling(button)
182
+ button
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ Jekyll::Hooks.register([:pages, :documents], :post_render) do |page, payload|
189
+ # Jekyll 3.x hook signature may pass `payload` as nil.
190
+ site = page.respond_to?(:site) ? page.site : nil
191
+ site ||= payload.is_a?(Hash) ? payload["site"] : nil
192
+ next unless site
193
+
194
+ injector = site.config["__jtd_toc_nav_injector"] ||= Jekyll::JtdTocNav::Injector.new(site: site)
195
+ injector.process!(page)
196
+ end
197
+
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jekyll"
4
+ require "nokogiri"
5
+
6
+ require_relative "jekyll/jtd_toc_nav/injector"
7
+
8
+ module Jekyll
9
+ module JtdTocNav
10
+ end
11
+ end
12
+
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-jtd-toc-nav
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - directsun
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jekyll
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.8'
26
+ - !ruby/object:Gem::Dependency
27
+ name: nokogiri
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.13'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.13'
40
+ email: []
41
+ executables: []
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - lib/jekyll-jtd-toc-nav.rb
46
+ - lib/jekyll/jtd_toc_nav/injector.rb
47
+ homepage: https://github.com/sunflowermans/toc-navigation
48
+ licenses:
49
+ - MIT
50
+ metadata: {}
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '3.8'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.6.9
66
+ specification_version: 4
67
+ summary: Inject page heading outline into Just the Docs sidebar nav
68
+ test_files: []