Module: Jekyll::VitePressTheme::Sidebar

Defined in:
lib/jekyll/vitepress_theme/hooks.rb

Overview

rubocop:disable Metrics/ModuleLength

Constant Summary collapse

DATA_KEY =
'jekyll_vitepress_sidebar'.freeze
MAX_ITEM_LEVEL =
5

Class Method Summary collapse

Class Method Details

.ancestor_titles(doc, docs) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 151

def ancestor_titles(doc, docs)
  titles = []
  current = parent_doc_for(doc, docs)
  seen = []

  while current && !seen.include?(current)
    seen << current
    titles << title(current)
    current = parent_doc_for(current, docs)
  end

  titles
end

.apply(site) ⇒ Object



13
14
15
16
17
18
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 13

def apply(site)
  generated_sidebar = generate(site)
  site.data[DATA_KEY] = generated_sidebar if generated_sidebar
rescue StandardError => e
  Jekyll.logger.warn('jekyll-vitepress-theme', "Sidebar hierarchy generation failed: #{e.message}")
end

.attach_nodes(collection_name, docs, nodes) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 62

def attach_nodes(collection_name, docs, nodes)
  docs.each_with_object([]) do |doc, roots|
    parent_doc = parent_doc_for(doc, docs)
    if valid_parent?(doc, parent_doc, docs)
      nodes[parent_doc]['children'] << nodes[doc]
    else
      warn_missing_parent(collection_name, doc) if parent_title(doc)
      roots << nodes[doc]
    end
  end
end

.attaching_creates_cycle?(doc, parent_doc, docs) ⇒ Boolean

Returns:

  • (Boolean)


137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 137

def attaching_creates_cycle?(doc, parent_doc, docs)
  current = parent_doc
  seen = []

  while current
    return true if current == doc || seen.include?(current)

    seen << current
    current = parent_doc_for(current, docs)
  end

  false
end

.build_collection(collection_name, docs) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 47

def build_collection(collection_name, docs)
  ordered_docs = sort_docs(docs)
  nodes = ordered_docs.to_h { |doc| [doc, node_for(doc)] }
  roots = attach_nodes(collection_name, ordered_docs, nodes)

  finalize_nodes(roots, 1)
  flat_docs = flatten_docs(roots)
  {
    'collection' => collection_name.to_s,
    'items' => roots,
    'docs' => flat_docs,
    'active_urls' => flat_docs.filter_map { |doc| doc_url(doc) }
  }
end

.collection_data(group) ⇒ Object



43
44
45
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 43

def collection_data(group)
  group.slice('collection', 'items', 'docs', 'active_urls')
end

.collection_docs(site, collection_name) ⇒ Object



78
79
80
81
82
83
84
85
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 78

def collection_docs(site, collection_name)
  return [] unless collection_name

  collection = site.collections[collection_name.to_s]
  return [] unless collection

  collection.docs
end

.data_value(doc, key) ⇒ Object



189
190
191
192
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 189

def data_value(doc, key)
  data = doc.respond_to?(:data) ? doc.data : {}
  data[key] || data[key.to_sym]
end

.doc_url(doc) ⇒ Object



202
203
204
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 202

def doc_url(doc)
  doc.url if doc.respond_to?(:url)
end

.finalize_children(node, level) ⇒ Object



174
175
176
177
178
179
180
181
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 174

def finalize_children(node, level)
  if level >= MAX_ITEM_LEVEL
    warn_depth_limit(node) unless node['children'].empty?
    node['children'] = []
  else
    finalize_nodes(node['children'], level + 1)
  end
end

.finalize_nodes(nodes, level) ⇒ Object



165
166
167
168
169
170
171
172
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 165

def finalize_nodes(nodes, level)
  nodes.sort_by! { |node| sort_key(node['doc']) }
  nodes.each do |node|
    finalize_children(node, level)

    node['active_urls'] = [node['url'], *node['children'].flat_map { |child| child['active_urls'] }].compact
  end
end

.flatten_docs(nodes) ⇒ Object



183
184
185
186
187
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 183

def flatten_docs(nodes)
  nodes.flat_map do |node|
    [node['doc'], *flatten_docs(node['children'])]
  end
end

.generate(site) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 20

def generate(site)
  sidebar_groups = site.data['sidebar']
  return nil unless sidebar_groups.respond_to?(:each)

  groups = sidebar_groups.filter_map { |group| generated_group(site, group) }

  {
    'groups' => groups,
    'collections' => groups.to_h { |group| [group['collection'], collection_data(group)] }
  }
end

.generated_group(site, group) ⇒ Object



32
33
34
35
36
37
38
39
40
41
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 32

def generated_group(site, group)
  collection_name = group_value(group, 'collection')
  docs = collection_docs(site, collection_name)
  return nil if docs.empty?

  collection_data = build_collection(collection_name, docs)
  return nil if collection_data['docs'].empty?

  group_hash(group).merge(collection_data)
end

.group_hash(group) ⇒ Object



87
88
89
90
91
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 87

def group_hash(group)
  return group if group.is_a?(Hash)

  group.respond_to?(:to_h) ? group.to_h : {}
end

.group_value(group, key) ⇒ Object



93
94
95
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 93

def group_value(group, key)
  group_hash(group)[key] || group_hash(group)[key.to_sym]
end

.node_for(doc) ⇒ Object



115
116
117
118
119
120
121
122
123
124
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 115

def node_for(doc)
  {
    'doc' => doc,
    'title' => title(doc),
    'url' => doc_url(doc),
    'collapsed' => truthy?(data_value(doc, 'collapsed')),
    'children' => [],
    'active_urls' => []
  }
end

.normalized_string(value) ⇒ Object



206
207
208
209
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 206

def normalized_string(value)
  string = value.to_s.strip
  string.empty? ? nil : string
end

.numeric?(value) ⇒ Boolean

Returns:

  • (Boolean)


111
112
113
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 111

def numeric?(value)
  value.is_a?(Numeric) || value.to_s.match?(/\A-?\d+(?:\.\d+)?\z/)
end

.parent_doc_for(doc, docs) ⇒ Object



126
127
128
129
130
131
132
133
134
135
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 126

def parent_doc_for(doc, docs)
  direct_parent = parent_title(doc)
  return nil unless direct_parent

  candidates = docs.select { |candidate| candidate != doc && title(candidate) == direct_parent }
  grand_parent = normalized_string(data_value(doc, 'grand_parent'))
  return candidates.first unless grand_parent

  candidates.find { |candidate| ancestor_titles(candidate, docs).include?(grand_parent) }
end

.parent_title(doc) ⇒ Object



198
199
200
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 198

def parent_title(doc)
  normalized_string(data_value(doc, 'parent'))
end

.sort_docs(docs) ⇒ Object



97
98
99
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 97

def sort_docs(docs)
  docs.sort_by { |doc| sort_key(doc) }
end

.sort_key(doc) ⇒ Object



101
102
103
104
105
106
107
108
109
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 101

def sort_key(doc)
  nav_order = data_value(doc, 'nav_order')
  order_bucket = nav_order.nil? ? 1 : 0
  numeric_order = numeric?(nav_order)
  order_type = numeric_order ? 0 : 1
  order_value = numeric_order ? nav_order.to_f : nav_order.to_s

  [order_bucket, order_type, order_value, title(doc).downcase, doc_url(doc).to_s]
end

.title(doc) ⇒ Object



194
195
196
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 194

def title(doc)
  normalized_string(data_value(doc, 'title')) || ''
end

.truthy?(value) ⇒ Boolean

Returns:

  • (Boolean)


211
212
213
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 211

def truthy?(value)
  value == true || value.to_s.casecmp('true').zero?
end

.valid_parent?(doc, parent_doc, docs) ⇒ Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 74

def valid_parent?(doc, parent_doc, docs)
  parent_doc && !attaching_creates_cycle?(doc, parent_doc, docs)
end

.warn_depth_limit(node) ⇒ Object



222
223
224
225
226
227
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 222

def warn_depth_limit(node)
  Jekyll.logger.warn(
    'jekyll-vitepress-theme',
    "Sidebar item '#{node['title']}' is deeper than #{MAX_ITEM_LEVEL} item levels; nested children were not rendered."
  )
end

.warn_missing_parent(collection_name, doc) ⇒ Object



215
216
217
218
219
220
# File 'lib/jekyll/vitepress_theme/hooks.rb', line 215

def warn_missing_parent(collection_name, doc)
  Jekyll.logger.warn(
    'jekyll-vitepress-theme',
    "Missing sidebar parent '#{parent_title(doc)}' for '#{title(doc)}' in #{collection_name}; rendering it at the collection root."
  )
end