jekyll-vitepress-theme 1.7.0 → 1.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4843e410a1321baff08b82bd739e58ad72cdb1657445c37f3b2bc30aa59c57ef
4
- data.tar.gz: c974fe75def3a63663a0e2c320798882af8b6878a2fc3261549187a384ce17a8
3
+ metadata.gz: 7ca0f243e32e2dd051ee2c8bd0c073ec5ab1d2469d05e606531df740508a036f
4
+ data.tar.gz: caac258aed1a827394fdf8f6f239d81a6978b297567d5558c8433ead4504ded9
5
5
  SHA512:
6
- metadata.gz: 146ac84e6b757422be14c5e206b4f0525e271f4bd45afdd70f47dad1388799765a6e6563d4ad45e88b7fb0dee3ebe7a4ea97d85220f62b760740d915383d2fcd
7
- data.tar.gz: 2e99432a58ccbfaf1838acd5b7ddaf0d8e62a3b73e44b90fddcf8f369555bab65fa352c251c747eae46cd69dd14987bdda2366ab5ffebdb8faf44c7b1756c2dc
6
+ metadata.gz: 04b1d2d66942c3c0e8277955be4936e268a8f7cbc0d3a818e6e7685ca7cc6b89f35944b252444ce708f6334a48e5d2a8cf62f55a3bcb707669c5fe33616db39d
7
+ data.tar.gz: 3d72ee616cf769d5c242de1bd5a0de440187206ecece4574fab664a5d98fc12efb0597b8dc1e2995c356602ba66676e351ffb3112b1fbe3c04f532ceec431bf6
@@ -56,7 +56,14 @@
56
56
  {% assign prev_doc = nil %}
57
57
  {% assign next_doc = nil %}
58
58
  {% if page.collection %}
59
- {% assign docs = site[page.collection] | sort: 'nav_order' %}
59
+ {% assign generated_sidebar = site.data.jekyll_vitepress_sidebar %}
60
+ {% assign sidebar_collections = generated_sidebar.collections %}
61
+ {% assign sidebar_collection = sidebar_collections[page.collection] %}
62
+ {% if sidebar_collection and sidebar_collection.docs %}
63
+ {% assign docs = sidebar_collection.docs %}
64
+ {% else %}
65
+ {% assign docs = site[page.collection] | sort: 'nav_order' %}
66
+ {% endif %}
60
67
  {% assign found_current = false %}
61
68
  {% for doc in docs %}
62
69
  {% if found_current and next_doc == nil %}
@@ -3,42 +3,60 @@
3
3
  <nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
4
4
  <span class="visually-hidden" id="sidebar-aria-label">Sidebar Navigation</span>
5
5
 
6
- {% assign sidebar_groups = site.data.sidebar %}
6
+ {% assign generated_sidebar = site.data.jekyll_vitepress_sidebar %}
7
+ {% assign sidebar_groups = generated_sidebar.groups | default: site.data.sidebar %}
7
8
  {% for group in sidebar_groups %}
8
- {% assign docs = site[group.collection] | sort: 'nav_order' %}
9
- {% if docs.size > 0 %}
9
+ {% if group.items %}
10
+ {% assign sidebar_items = group.items %}
11
+ {% else %}
12
+ {% assign docs = site[group.collection] | sort: 'nav_order' %}
13
+ {% assign sidebar_items = docs %}
14
+ {% endif %}
15
+ {% if sidebar_items.size > 0 %}
10
16
  {% assign has_active = false %}
11
- {% for doc in docs %}
12
- {% if doc.url == page.url %}
13
- {% assign has_active = true %}
14
- {% endif %}
15
- {% endfor %}
17
+ {% if group.active_urls contains page.url %}
18
+ {% assign has_active = true %}
19
+ {% else %}
20
+ {% for doc in docs %}
21
+ {% if doc.url == page.url %}
22
+ {% assign has_active = true %}
23
+ {% endif %}
24
+ {% endfor %}
25
+ {% endif %}
26
+ {% assign collapsed = false %}
27
+ {% if group.collapsed and has_active == false %}
28
+ {% assign collapsed = true %}
29
+ {% endif %}
16
30
 
17
31
  <div class="group no-transition">
18
- <section class="VPSidebarItem level-0 collapsible{% if has_active %} has-active{% endif %}" data-vp-sidebar-group data-collection="{{ group.collection }}">
19
- <div class="item" role="button" tabindex="0">
32
+ <section class="VPSidebarItem level-0 collapsible{% if has_active %} has-active{% endif %}{% if collapsed %} collapsed{% endif %}" data-vp-sidebar-group data-collection="{{ group.collection }}">
33
+ <div class="item" role="button" tabindex="0" aria-expanded="{% if collapsed %}false{% else %}true{% endif %}">
20
34
  <div class="indicator"></div>
21
35
  <h2 class="text">{{ group.title }}</h2>
22
- <div class="caret" role="button" aria-label="toggle section" tabindex="0">
36
+ <div class="caret" role="button" aria-label="toggle section" tabindex="0" aria-expanded="{% if collapsed %}false{% else %}true{% endif %}">
23
37
  <span class="vpi-chevron-right caret-icon"></span>
24
38
  </div>
25
39
  </div>
26
40
 
27
41
  <div class="items">
28
- {% for doc in docs %}
29
- {% assign active = false %}
30
- {% if doc.url == page.url %}
31
- {% assign active = true %}
32
- {% endif %}
33
- <div class="VPSidebarItem level-1 is-link{% if active %} is-active{% endif %}">
34
- <div class="item">
35
- <div class="indicator"></div>
36
- <a class="VPLink link link" href="{{ doc.url | relative_url }}" data-vp-sidebar-link data-turbo="true" data-turbo-frame="vp-content-frame" data-turbo-action="advance">
37
- <p class="text">{{ doc.title }}</p>
38
- </a>
42
+ {% if group.items %}
43
+ {% include sidebar_items.html nodes=group.items level=1 %}
44
+ {% else %}
45
+ {% for doc in docs %}
46
+ {% assign active = false %}
47
+ {% if doc.url == page.url %}
48
+ {% assign active = true %}
49
+ {% endif %}
50
+ <div class="VPSidebarItem level-1 is-link{% if active %} is-active{% endif %}">
51
+ <div class="item">
52
+ <div class="indicator"></div>
53
+ <a class="VPLink link link" href="{{ doc.url | relative_url }}" data-vp-sidebar-link data-turbo="true" data-turbo-frame="vp-content-frame" data-turbo-action="advance">
54
+ <p class="text">{{ doc.title }}</p>
55
+ </a>
56
+ </div>
39
57
  </div>
40
- </div>
41
- {% endfor %}
58
+ {% endfor %}
59
+ {% endif %}
42
60
  </div>
43
61
  </section>
44
62
  </div>
@@ -0,0 +1,41 @@
1
+ {% for node in include.nodes %}
2
+ {% assign item_level = include.level | default: 1 %}
3
+ {% assign doc = node.doc %}
4
+ {% assign has_children = false %}
5
+ {% if node.children and node.children.size > 0 %}
6
+ {% assign has_children = true %}
7
+ {% endif %}
8
+ {% assign active = false %}
9
+ {% if node.url == page.url %}
10
+ {% assign active = true %}
11
+ {% endif %}
12
+ {% assign has_active = false %}
13
+ {% if node.active_urls contains page.url %}
14
+ {% assign has_active = true %}
15
+ {% endif %}
16
+ {% assign collapsed = false %}
17
+ {% if node.collapsed and has_active == false %}
18
+ {% assign collapsed = true %}
19
+ {% endif %}
20
+
21
+ <div class="VPSidebarItem level-{{ item_level }} is-link{% if has_children %} collapsible{% endif %}{% if active %} is-active{% endif %}{% if has_active %} has-active{% endif %}{% if collapsed %} collapsed{% endif %}">
22
+ <div class="item"{% if has_children %} aria-expanded="{% if collapsed %}false{% else %}true{% endif %}"{% endif %}>
23
+ <div class="indicator"></div>
24
+ <a class="VPLink link link" href="{{ node.url | relative_url }}" data-vp-sidebar-link data-turbo="true" data-turbo-frame="vp-content-frame" data-turbo-action="advance">
25
+ <p class="text">{{ node.title | default: doc.title }}</p>
26
+ </a>
27
+ {% if has_children %}
28
+ <div class="caret" role="button" aria-label="toggle section" tabindex="0" aria-expanded="{% if collapsed %}false{% else %}true{% endif %}">
29
+ <span class="vpi-chevron-right caret-icon"></span>
30
+ </div>
31
+ {% endif %}
32
+ </div>
33
+
34
+ {% if has_children %}
35
+ <div class="items">
36
+ {% assign child_level = item_level | plus: 1 %}
37
+ {% include sidebar_items.html nodes=node.children level=child_level %}
38
+ </div>
39
+ {% endif %}
40
+ </div>
41
+ {% endfor %}
@@ -684,12 +684,28 @@
684
684
  syncNavTop();
685
685
  window.addEventListener('scroll', syncNavTop, { passive: true });
686
686
 
687
- document.querySelectorAll('.VPSidebarItem.level-0.collapsible').forEach(function (section) {
687
+ function setSidebarSectionCollapsed(section, collapsed) {
688
+ section.classList.toggle('collapsed', collapsed);
689
+
690
+ var item = section.querySelector(':scope > .item');
691
+ var caret = section.querySelector(':scope > .item .caret');
692
+ var expanded = String(!collapsed);
693
+
694
+ if (item) {
695
+ item.setAttribute('aria-expanded', expanded);
696
+ }
697
+
698
+ if (caret) {
699
+ caret.setAttribute('aria-expanded', expanded);
700
+ }
701
+ }
702
+
703
+ document.querySelectorAll('.VPSidebarItem.collapsible').forEach(function (section) {
688
704
  var item = section.querySelector(':scope > .item');
689
705
  var caret = section.querySelector(':scope > .item .caret');
690
706
 
691
707
  function toggleSection() {
692
- section.classList.toggle('collapsed');
708
+ setSidebarSectionCollapsed(section, !section.classList.contains('collapsed'));
693
709
  }
694
710
 
695
711
  if (item) {
@@ -701,6 +717,10 @@
701
717
  });
702
718
 
703
719
  item.addEventListener('keydown', function (event) {
720
+ if (event.target.closest('a')) {
721
+ return;
722
+ }
723
+
704
724
  if (event.key === 'Enter' || event.key === ' ') {
705
725
  event.preventDefault();
706
726
  toggleSection();
@@ -2174,7 +2194,7 @@
2174
2194
  return null;
2175
2195
  }
2176
2196
 
2177
- var activeItems = Array.from(sidebar.querySelectorAll('.VPSidebarItem.level-1.is-link.is-active'));
2197
+ var activeItems = Array.from(sidebar.querySelectorAll('.VPSidebarItem.is-link.is-active'));
2178
2198
 
2179
2199
  if (!activeItems.length) {
2180
2200
  return null;
@@ -2212,16 +2232,19 @@
2212
2232
  }
2213
2233
 
2214
2234
  document.querySelectorAll('[data-vp-sidebar-link]').forEach(function (link) {
2215
- var item = link.closest('.VPSidebarItem.level-1.is-link');
2235
+ var item = link.closest('.VPSidebarItem.is-link');
2216
2236
  var active = normalizePathname(link.getAttribute('href')) === currentPath;
2217
2237
  if (item) {
2218
2238
  item.classList.toggle('is-active', active);
2219
2239
  }
2220
2240
  });
2221
2241
 
2222
- document.querySelectorAll('[data-vp-sidebar-group]').forEach(function (group) {
2223
- var hasActiveLink = !!group.querySelector('.VPSidebarItem.level-1.is-link.is-active');
2242
+ document.querySelectorAll('.VPSidebarItem.collapsible').forEach(function (group) {
2243
+ var hasActiveLink = group.classList.contains('is-active') || !!group.querySelector('.VPSidebarItem.is-link.is-active');
2224
2244
  group.classList.toggle('has-active', hasActiveLink);
2245
+ if (hasActiveLink) {
2246
+ setSidebarSectionCollapsed(group, false);
2247
+ }
2225
2248
  });
2226
2249
 
2227
2250
  document.querySelectorAll('[data-vp-nav-link]').forEach(function (link) {
@@ -3,6 +3,231 @@ require 'rouge'
3
3
 
4
4
  module Jekyll
5
5
  module VitePressTheme
6
+ # rubocop:disable Metrics/ModuleLength
7
+ module Sidebar
8
+ module_function
9
+
10
+ DATA_KEY = 'jekyll_vitepress_sidebar'.freeze
11
+ MAX_ITEM_LEVEL = 5
12
+
13
+ def apply(site)
14
+ generated_sidebar = generate(site)
15
+ site.data[DATA_KEY] = generated_sidebar if generated_sidebar
16
+ rescue StandardError => e
17
+ Jekyll.logger.warn('jekyll-vitepress-theme', "Sidebar hierarchy generation failed: #{e.message}")
18
+ end
19
+
20
+ def generate(site)
21
+ sidebar_groups = site.data['sidebar']
22
+ return nil unless sidebar_groups.respond_to?(:each)
23
+
24
+ groups = sidebar_groups.filter_map { |group| generated_group(site, group) }
25
+
26
+ {
27
+ 'groups' => groups,
28
+ 'collections' => groups.to_h { |group| [group['collection'], collection_data(group)] }
29
+ }
30
+ end
31
+
32
+ def generated_group(site, group)
33
+ collection_name = group_value(group, 'collection')
34
+ docs = collection_docs(site, collection_name)
35
+ return nil if docs.empty?
36
+
37
+ collection_data = build_collection(collection_name, docs)
38
+ return nil if collection_data['docs'].empty?
39
+
40
+ group_hash(group).merge(collection_data)
41
+ end
42
+
43
+ def collection_data(group)
44
+ group.slice('collection', 'items', 'docs', 'active_urls')
45
+ end
46
+
47
+ def build_collection(collection_name, docs)
48
+ ordered_docs = sort_docs(docs)
49
+ nodes = ordered_docs.to_h { |doc| [doc, node_for(doc)] }
50
+ roots = attach_nodes(collection_name, ordered_docs, nodes)
51
+
52
+ finalize_nodes(roots, 1)
53
+ flat_docs = flatten_docs(roots)
54
+ {
55
+ 'collection' => collection_name.to_s,
56
+ 'items' => roots,
57
+ 'docs' => flat_docs,
58
+ 'active_urls' => flat_docs.filter_map { |doc| doc_url(doc) }
59
+ }
60
+ end
61
+
62
+ def attach_nodes(collection_name, docs, nodes)
63
+ docs.each_with_object([]) do |doc, roots|
64
+ parent_doc = parent_doc_for(doc, docs)
65
+ if valid_parent?(doc, parent_doc, docs)
66
+ nodes[parent_doc]['children'] << nodes[doc]
67
+ else
68
+ warn_missing_parent(collection_name, doc) if parent_title(doc)
69
+ roots << nodes[doc]
70
+ end
71
+ end
72
+ end
73
+
74
+ def valid_parent?(doc, parent_doc, docs)
75
+ parent_doc && !attaching_creates_cycle?(doc, parent_doc, docs)
76
+ end
77
+
78
+ def collection_docs(site, collection_name)
79
+ return [] unless collection_name
80
+
81
+ collection = site.collections[collection_name.to_s]
82
+ return [] unless collection
83
+
84
+ collection.docs
85
+ end
86
+
87
+ def group_hash(group)
88
+ return group if group.is_a?(Hash)
89
+
90
+ group.respond_to?(:to_h) ? group.to_h : {}
91
+ end
92
+
93
+ def group_value(group, key)
94
+ group_hash(group)[key] || group_hash(group)[key.to_sym]
95
+ end
96
+
97
+ def sort_docs(docs)
98
+ docs.sort_by { |doc| sort_key(doc) }
99
+ end
100
+
101
+ def sort_key(doc)
102
+ nav_order = data_value(doc, 'nav_order')
103
+ order_bucket = nav_order.nil? ? 1 : 0
104
+ numeric_order = numeric?(nav_order)
105
+ order_type = numeric_order ? 0 : 1
106
+ order_value = numeric_order ? nav_order.to_f : nav_order.to_s
107
+
108
+ [order_bucket, order_type, order_value, title(doc).downcase, doc_url(doc).to_s]
109
+ end
110
+
111
+ def numeric?(value)
112
+ value.is_a?(Numeric) || value.to_s.match?(/\A-?\d+(?:\.\d+)?\z/)
113
+ end
114
+
115
+ def node_for(doc)
116
+ {
117
+ 'doc' => doc,
118
+ 'title' => title(doc),
119
+ 'url' => doc_url(doc),
120
+ 'collapsed' => truthy?(data_value(doc, 'collapsed')),
121
+ 'children' => [],
122
+ 'active_urls' => []
123
+ }
124
+ end
125
+
126
+ def parent_doc_for(doc, docs)
127
+ direct_parent = parent_title(doc)
128
+ return nil unless direct_parent
129
+
130
+ candidates = docs.select { |candidate| candidate != doc && title(candidate) == direct_parent }
131
+ grand_parent = normalized_string(data_value(doc, 'grand_parent'))
132
+ return candidates.first unless grand_parent
133
+
134
+ candidates.find { |candidate| ancestor_titles(candidate, docs).include?(grand_parent) }
135
+ end
136
+
137
+ def attaching_creates_cycle?(doc, parent_doc, docs)
138
+ current = parent_doc
139
+ seen = []
140
+
141
+ while current
142
+ return true if current == doc || seen.include?(current)
143
+
144
+ seen << current
145
+ current = parent_doc_for(current, docs)
146
+ end
147
+
148
+ false
149
+ end
150
+
151
+ def ancestor_titles(doc, docs)
152
+ titles = []
153
+ current = parent_doc_for(doc, docs)
154
+ seen = []
155
+
156
+ while current && !seen.include?(current)
157
+ seen << current
158
+ titles << title(current)
159
+ current = parent_doc_for(current, docs)
160
+ end
161
+
162
+ titles
163
+ end
164
+
165
+ def finalize_nodes(nodes, level)
166
+ nodes.sort_by! { |node| sort_key(node['doc']) }
167
+ nodes.each do |node|
168
+ finalize_children(node, level)
169
+
170
+ node['active_urls'] = [node['url'], *node['children'].flat_map { |child| child['active_urls'] }].compact
171
+ end
172
+ end
173
+
174
+ def finalize_children(node, level)
175
+ if level >= MAX_ITEM_LEVEL
176
+ warn_depth_limit(node) unless node['children'].empty?
177
+ node['children'] = []
178
+ else
179
+ finalize_nodes(node['children'], level + 1)
180
+ end
181
+ end
182
+
183
+ def flatten_docs(nodes)
184
+ nodes.flat_map do |node|
185
+ [node['doc'], *flatten_docs(node['children'])]
186
+ end
187
+ end
188
+
189
+ def data_value(doc, key)
190
+ data = doc.respond_to?(:data) ? doc.data : {}
191
+ data[key] || data[key.to_sym]
192
+ end
193
+
194
+ def title(doc)
195
+ normalized_string(data_value(doc, 'title')) || ''
196
+ end
197
+
198
+ def parent_title(doc)
199
+ normalized_string(data_value(doc, 'parent'))
200
+ end
201
+
202
+ def doc_url(doc)
203
+ doc.url if doc.respond_to?(:url)
204
+ end
205
+
206
+ def normalized_string(value)
207
+ string = value.to_s.strip
208
+ string.empty? ? nil : string
209
+ end
210
+
211
+ def truthy?(value)
212
+ value == true || value.to_s.casecmp('true').zero?
213
+ end
214
+
215
+ def warn_missing_parent(collection_name, doc)
216
+ Jekyll.logger.warn(
217
+ 'jekyll-vitepress-theme',
218
+ "Missing sidebar parent '#{parent_title(doc)}' for '#{title(doc)}' in #{collection_name}; rendering it at the collection root."
219
+ )
220
+ end
221
+
222
+ def warn_depth_limit(node)
223
+ Jekyll.logger.warn(
224
+ 'jekyll-vitepress-theme',
225
+ "Sidebar item '#{node['title']}' is deeper than #{MAX_ITEM_LEVEL} item levels; nested children were not rendered."
226
+ )
227
+ end
228
+ end
229
+ # rubocop:enable Metrics/ModuleLength
230
+
6
231
  module SearchIndex
7
232
  module_function
8
233
 
@@ -19,9 +244,14 @@ module Jekyll
19
244
  }
20
245
  {% assign first = false %}
21
246
  {% endif %}
22
- {% assign sidebar_groups = site.data.sidebar %}
247
+ {% assign generated_sidebar = site.data.jekyll_vitepress_sidebar %}
248
+ {% assign sidebar_groups = generated_sidebar.groups | default: site.data.sidebar %}
23
249
  {% for group in sidebar_groups %}
24
- {% assign docs = site[group.collection] | sort: 'nav_order' %}
250
+ {% if group.docs %}
251
+ {% assign docs = group.docs %}
252
+ {% else %}
253
+ {% assign docs = site[group.collection] | sort: 'nav_order' %}
254
+ {% endif %}
25
255
  {% for doc in docs %}
26
256
  {% if doc.title and doc.url %}
27
257
  {% unless first %},{% endunless %}
@@ -236,6 +466,7 @@ end
236
466
  Jekyll::Hooks.register :site, :post_read do |site|
237
467
  Jekyll::VitePressTheme::VersionLabel.apply(site)
238
468
  Jekyll::VitePressTheme::RougeStyles.apply(site)
469
+ Jekyll::VitePressTheme::Sidebar.apply(site)
239
470
  Jekyll::VitePressTheme::SearchIndex.apply(site)
240
471
  end
241
472
 
@@ -1,5 +1,5 @@
1
1
  module Jekyll
2
2
  module VitePressTheme
3
- VERSION = "1.7.0".freeze
3
+ VERSION = "1.8.0".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-vitepress-theme
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-16 00:00:00.000000000 Z
11
+ date: 2026-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -79,6 +79,7 @@ files:
79
79
  - _includes/rubygems_downloads_button.html
80
80
  - _includes/search.html
81
81
  - _includes/sidebar.html
82
+ - _includes/sidebar_items.html
82
83
  - _includes/version_link.html
83
84
  - _includes/vp_image.html
84
85
  - _layouts/default.html