jekyll-vitepress-theme 1.7.0 → 1.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4843e410a1321baff08b82bd739e58ad72cdb1657445c37f3b2bc30aa59c57ef
4
- data.tar.gz: c974fe75def3a63663a0e2c320798882af8b6878a2fc3261549187a384ce17a8
3
+ metadata.gz: 9e8e0506d69bd7172baf0392133cc0782815c07784a747437be2b7a59503ca37
4
+ data.tar.gz: 7037f54855dc45578def24ab7144a563f9211f7a441b110bfa56172cebdff91d
5
5
  SHA512:
6
- metadata.gz: 146ac84e6b757422be14c5e206b4f0525e271f4bd45afdd70f47dad1388799765a6e6563d4ad45e88b7fb0dee3ebe7a4ea97d85220f62b760740d915383d2fcd
7
- data.tar.gz: 2e99432a58ccbfaf1838acd5b7ddaf0d8e62a3b73e44b90fddcf8f369555bab65fa352c251c747eae46cd69dd14987bdda2366ab5ffebdb8faf44c7b1756c2dc
6
+ metadata.gz: '08cad3bf3e61558a9ca8a9832f1ad95943289691c6832961405551f62d43399294a3f709f1e120a5a265a664c39ee46b3591a2e39dd20e3dc3ad92b3c603ea46'
7
+ data.tar.gz: 2cedeabbc9f8be7ded0ed39164d7808bc63178e84a7ef06aa2da6bddcfd6262047490eff29eb36da1dcd80393c0aba1bc33cc4340a5358af02bc4373c02f568e
data/README.md CHANGED
@@ -78,6 +78,14 @@ bundle install
78
78
  bundle exec jekyll serve --livereload
79
79
  ```
80
80
 
81
+ ### Metadata and jekyll-seo-tag
82
+
83
+ The theme writes the page description, canonical link, Open Graph, and Twitter
84
+ card tags itself. Calling `{% seo %}` as well emits a second copy of each, so
85
+ drop it when you switch to this theme, or drop the theme's tags if you prefer
86
+ jekyll-seo-tag's. Its JSON-LD has no equivalent here, so keep that plugin if
87
+ you want structured data.
88
+
81
89
  ## Screenshots
82
90
 
83
91
  | Home | Docs |
@@ -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 %}
data/_includes/nav.html CHANGED
@@ -103,8 +103,20 @@
103
103
  {% if versions and versions.items and versions.items.size > 0 %}
104
104
  <div class="VPVersionSelector" id="vp-version-selector">
105
105
  <button type="button" class="button" id="vp-version-button" aria-haspopup="true" aria-expanded="false" aria-controls="vp-version-menu">
106
+ {%- comment -%}
107
+ `current` names an item, by id or title, the way
108
+ version_link.html matches it. Show that item's title so a
109
+ manifest holding ids does not label the button "next".
110
+ {%- endcomment -%}
111
+ {%- assign version_current_label = versions.current -%}
112
+ {%- for version_item in versions.items -%}
113
+ {%- assign version_item_key = version_item.id | default: version_item.title -%}
114
+ {%- if version_item_key == versions.current or version_item.title == versions.current -%}
115
+ {%- assign version_current_label = version_item.title -%}
116
+ {%- endif -%}
117
+ {%- endfor -%}
106
118
  <span class="text">
107
- <span>{{ versions.current | default: 'Version' }}</span>
119
+ <span>{{ version_current_label | default: 'Version' }}</span>
108
120
  <span class="vpi-chevron-down text-icon"></span>
109
121
  </span>
110
122
  </button>
@@ -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 %}
@@ -329,6 +329,11 @@ body.vp-nav-screen-open .VPNav {
329
329
  width: 100%;
330
330
  height: 100%;
331
331
  fill: currentcolor;
332
+
333
+ /* These icons draw a 16-unit viewBox into a 13-14px box. Their paths touch
334
+ the viewBox edge, so at a fractional scale the SVG viewport clips the
335
+ outermost column and the shape looks flat on one side. */
336
+ overflow: visible;
332
337
  }
333
338
 
334
339
  .VPNavBarStarText,
@@ -1120,6 +1125,11 @@ body.vp-search-open {
1120
1125
  width: 100%;
1121
1126
  height: 100%;
1122
1127
  fill: currentcolor;
1128
+
1129
+ /* These icons draw a 16-unit viewBox into a 13-14px box. Their paths touch
1130
+ the viewBox edge, so at a fractional scale the SVG viewport clips the
1131
+ outermost column and the shape looks flat on one side. */
1132
+ overflow: visible;
1123
1133
  }
1124
1134
 
1125
1135
  .VPMetricButtonCount {
@@ -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,248 @@ 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
+ # A site with collection documents and no _data/sidebar.yml builds a page
21
+ # with an empty sidebar and no other complaint, which is a slow thing to
22
+ # notice. Overriding `data_dir` without carrying the file over is the
23
+ # usual cause.
24
+ def warn_missing_sidebar_data(site)
25
+ return if site.collections.values.all? { |collection| collection.docs.empty? }
26
+
27
+ Jekyll.logger.warn(
28
+ 'jekyll-vitepress-theme',
29
+ 'No _data/sidebar.yml found, so the sidebar will be empty. ' \
30
+ 'If you set a custom data_dir, copy the theme data files into it.'
31
+ )
32
+ end
33
+
34
+ def generate(site)
35
+ sidebar_groups = site.data['sidebar']
36
+ unless sidebar_groups.respond_to?(:each)
37
+ warn_missing_sidebar_data(site)
38
+ return nil
39
+ end
40
+
41
+ groups = sidebar_groups.filter_map { |group| generated_group(site, group) }
42
+
43
+ {
44
+ 'groups' => groups,
45
+ 'collections' => groups.to_h { |group| [group['collection'], collection_data(group)] }
46
+ }
47
+ end
48
+
49
+ def generated_group(site, group)
50
+ collection_name = group_value(group, 'collection')
51
+ docs = collection_docs(site, collection_name)
52
+ return nil if docs.empty?
53
+
54
+ collection_data = build_collection(collection_name, docs)
55
+ return nil if collection_data['docs'].empty?
56
+
57
+ group_hash(group).merge(collection_data)
58
+ end
59
+
60
+ def collection_data(group)
61
+ group.slice('collection', 'items', 'docs', 'active_urls')
62
+ end
63
+
64
+ def build_collection(collection_name, docs)
65
+ ordered_docs = sort_docs(docs)
66
+ nodes = ordered_docs.to_h { |doc| [doc, node_for(doc)] }
67
+ roots = attach_nodes(collection_name, ordered_docs, nodes)
68
+
69
+ finalize_nodes(roots, 1)
70
+ flat_docs = flatten_docs(roots)
71
+ {
72
+ 'collection' => collection_name.to_s,
73
+ 'items' => roots,
74
+ 'docs' => flat_docs,
75
+ 'active_urls' => flat_docs.filter_map { |doc| doc_url(doc) }
76
+ }
77
+ end
78
+
79
+ def attach_nodes(collection_name, docs, nodes)
80
+ docs.each_with_object([]) do |doc, roots|
81
+ parent_doc = parent_doc_for(doc, docs)
82
+ if valid_parent?(doc, parent_doc, docs)
83
+ nodes[parent_doc]['children'] << nodes[doc]
84
+ else
85
+ warn_missing_parent(collection_name, doc) if parent_title(doc)
86
+ roots << nodes[doc]
87
+ end
88
+ end
89
+ end
90
+
91
+ def valid_parent?(doc, parent_doc, docs)
92
+ parent_doc && !attaching_creates_cycle?(doc, parent_doc, docs)
93
+ end
94
+
95
+ def collection_docs(site, collection_name)
96
+ return [] unless collection_name
97
+
98
+ collection = site.collections[collection_name.to_s]
99
+ return [] unless collection
100
+
101
+ collection.docs
102
+ end
103
+
104
+ def group_hash(group)
105
+ return group if group.is_a?(Hash)
106
+
107
+ group.respond_to?(:to_h) ? group.to_h : {}
108
+ end
109
+
110
+ def group_value(group, key)
111
+ group_hash(group)[key] || group_hash(group)[key.to_sym]
112
+ end
113
+
114
+ def sort_docs(docs)
115
+ docs.sort_by { |doc| sort_key(doc) }
116
+ end
117
+
118
+ def sort_key(doc)
119
+ nav_order = data_value(doc, 'nav_order')
120
+ order_bucket = nav_order.nil? ? 1 : 0
121
+ numeric_order = numeric?(nav_order)
122
+ order_type = numeric_order ? 0 : 1
123
+ order_value = numeric_order ? nav_order.to_f : nav_order.to_s
124
+
125
+ [order_bucket, order_type, order_value, title(doc).downcase, doc_url(doc).to_s]
126
+ end
127
+
128
+ def numeric?(value)
129
+ value.is_a?(Numeric) || value.to_s.match?(/\A-?\d+(?:\.\d+)?\z/)
130
+ end
131
+
132
+ def node_for(doc)
133
+ {
134
+ 'doc' => doc,
135
+ 'title' => title(doc),
136
+ 'url' => doc_url(doc),
137
+ 'collapsed' => truthy?(data_value(doc, 'collapsed')),
138
+ 'children' => [],
139
+ 'active_urls' => []
140
+ }
141
+ end
142
+
143
+ def parent_doc_for(doc, docs)
144
+ direct_parent = parent_title(doc)
145
+ return nil unless direct_parent
146
+
147
+ candidates = docs.select { |candidate| candidate != doc && title(candidate) == direct_parent }
148
+ grand_parent = normalized_string(data_value(doc, 'grand_parent'))
149
+ return candidates.first unless grand_parent
150
+
151
+ candidates.find { |candidate| ancestor_titles(candidate, docs).include?(grand_parent) }
152
+ end
153
+
154
+ def attaching_creates_cycle?(doc, parent_doc, docs)
155
+ current = parent_doc
156
+ seen = []
157
+
158
+ while current
159
+ return true if current == doc || seen.include?(current)
160
+
161
+ seen << current
162
+ current = parent_doc_for(current, docs)
163
+ end
164
+
165
+ false
166
+ end
167
+
168
+ def ancestor_titles(doc, docs)
169
+ titles = []
170
+ current = parent_doc_for(doc, docs)
171
+ seen = []
172
+
173
+ while current && !seen.include?(current)
174
+ seen << current
175
+ titles << title(current)
176
+ current = parent_doc_for(current, docs)
177
+ end
178
+
179
+ titles
180
+ end
181
+
182
+ def finalize_nodes(nodes, level)
183
+ nodes.sort_by! { |node| sort_key(node['doc']) }
184
+ nodes.each do |node|
185
+ finalize_children(node, level)
186
+
187
+ node['active_urls'] = [node['url'], *node['children'].flat_map { |child| child['active_urls'] }].compact
188
+ end
189
+ end
190
+
191
+ def finalize_children(node, level)
192
+ if level >= MAX_ITEM_LEVEL
193
+ warn_depth_limit(node) unless node['children'].empty?
194
+ node['children'] = []
195
+ else
196
+ finalize_nodes(node['children'], level + 1)
197
+ end
198
+ end
199
+
200
+ def flatten_docs(nodes)
201
+ nodes.flat_map do |node|
202
+ [node['doc'], *flatten_docs(node['children'])]
203
+ end
204
+ end
205
+
206
+ def data_value(doc, key)
207
+ data = doc.respond_to?(:data) ? doc.data : {}
208
+ data[key] || data[key.to_sym]
209
+ end
210
+
211
+ def title(doc)
212
+ normalized_string(data_value(doc, 'title')) || ''
213
+ end
214
+
215
+ def parent_title(doc)
216
+ normalized_string(data_value(doc, 'parent'))
217
+ end
218
+
219
+ def doc_url(doc)
220
+ doc.url if doc.respond_to?(:url)
221
+ end
222
+
223
+ def normalized_string(value)
224
+ string = value.to_s.strip
225
+ string.empty? ? nil : string
226
+ end
227
+
228
+ def truthy?(value)
229
+ value == true || value.to_s.casecmp('true').zero?
230
+ end
231
+
232
+ def warn_missing_parent(collection_name, doc)
233
+ Jekyll.logger.warn(
234
+ 'jekyll-vitepress-theme',
235
+ "Missing sidebar parent '#{parent_title(doc)}' for '#{title(doc)}' in #{collection_name}; rendering it at the collection root."
236
+ )
237
+ end
238
+
239
+ def warn_depth_limit(node)
240
+ Jekyll.logger.warn(
241
+ 'jekyll-vitepress-theme',
242
+ "Sidebar item '#{node['title']}' is deeper than #{MAX_ITEM_LEVEL} item levels; nested children were not rendered."
243
+ )
244
+ end
245
+ end
246
+ # rubocop:enable Metrics/ModuleLength
247
+
6
248
  module SearchIndex
7
249
  module_function
8
250
 
@@ -19,9 +261,14 @@ module Jekyll
19
261
  }
20
262
  {% assign first = false %}
21
263
  {% endif %}
22
- {% assign sidebar_groups = site.data.sidebar %}
264
+ {% assign generated_sidebar = site.data.jekyll_vitepress_sidebar %}
265
+ {% assign sidebar_groups = generated_sidebar.groups | default: site.data.sidebar %}
23
266
  {% for group in sidebar_groups %}
24
- {% assign docs = site[group.collection] | sort: 'nav_order' %}
267
+ {% if group.docs %}
268
+ {% assign docs = group.docs %}
269
+ {% else %}
270
+ {% assign docs = site[group.collection] | sort: 'nav_order' %}
271
+ {% endif %}
25
272
  {% for doc in docs %}
26
273
  {% if doc.title and doc.url %}
27
274
  {% unless first %},{% endunless %}
@@ -154,7 +401,18 @@ module Jekyll
154
401
  module_function
155
402
 
156
403
  AUTO_VALUE = 'auto'.freeze
157
-
404
+ VERSION_PATTERN = /VERSION\s*=\s*['"]([^'"]+)['"]/
405
+
406
+ # Resolves `current: auto` to the version of the project being documented.
407
+ # Configure where to read it from:
408
+ #
409
+ # jekyll_vitepress:
410
+ # version:
411
+ # value: "2.0.0" # literal, wins over file
412
+ # file: lib/my_gem/version.rb # or a file holding VERSION = '...'
413
+ #
414
+ # With neither, it falls back to this theme's own version, which is only
415
+ # ever right for the theme's own documentation.
158
416
  def apply(site)
159
417
  versions = site.data['versions']
160
418
  return unless versions.is_a?(Hash)
@@ -162,11 +420,35 @@ module Jekyll
162
420
  current_value = versions['current'] || versions[:current]
163
421
  return unless auto_value?(current_value)
164
422
 
165
- versions['current'] = "v#{Jekyll::VitePressTheme::VERSION}"
423
+ versions['current'] = "v#{site_version(site) || Jekyll::VitePressTheme::VERSION}"
166
424
  rescue StandardError => e
167
425
  Jekyll.logger.warn('jekyll-vitepress-theme', "Version label resolution failed: #{e.message}")
168
426
  end
169
427
 
428
+ def site_version(site)
429
+ config = site.config.dig('jekyll_vitepress', 'version')
430
+ return nil unless config.is_a?(Hash)
431
+
432
+ literal = config['value'].to_s.strip
433
+ return normalize(literal) unless literal.empty?
434
+
435
+ version_from_file(site, config['file'])
436
+ end
437
+
438
+ def version_from_file(site, path)
439
+ return nil if path.to_s.strip.empty?
440
+
441
+ full_path = File.expand_path(path.to_s, site.source)
442
+ return nil unless File.file?(full_path)
443
+
444
+ match = File.read(full_path)[VERSION_PATTERN]
445
+ match && normalize(Regexp.last_match(1))
446
+ end
447
+
448
+ def normalize(value)
449
+ value.to_s.strip.sub(/\Av/, '')
450
+ end
451
+
170
452
  def auto_value?(value)
171
453
  value.to_s.strip.casecmp(AUTO_VALUE).zero?
172
454
  end
@@ -236,6 +518,7 @@ end
236
518
  Jekyll::Hooks.register :site, :post_read do |site|
237
519
  Jekyll::VitePressTheme::VersionLabel.apply(site)
238
520
  Jekyll::VitePressTheme::RougeStyles.apply(site)
521
+ Jekyll::VitePressTheme::Sidebar.apply(site)
239
522
  Jekyll::VitePressTheme::SearchIndex.apply(site)
240
523
  end
241
524
 
@@ -1,5 +1,5 @@
1
1
  module Jekyll
2
2
  module VitePressTheme
3
- VERSION = "1.7.0".freeze
3
+ VERSION = "1.8.1".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.1
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-08-13 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