jekyll-theme-zer0 0.2.1 → 0.5.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 +4 -4
- data/CHANGELOG.md +56 -0
- data/README.md +18 -4
- data/_data/content_statistics.yml +401 -0
- data/_data/generate_statistics.rb +275 -0
- data/_data/navigation/about.yml +2 -0
- data/_data/navigation/home.yml +15 -0
- data/_data/navigation/main.yml +2 -3
- data/_includes/components/mermaid.html +101 -0
- data/_includes/content/sitemap.html +935 -108
- data/_includes/core/head.html +9 -4
- data/_includes/navigation/navbar.html +6 -0
- data/_includes/stats/README.md +273 -0
- data/_includes/stats/stats-categories.html +146 -0
- data/_includes/stats/stats-header.html +123 -0
- data/_includes/stats/stats-metrics.html +243 -0
- data/_includes/stats/stats-no-data.html +180 -0
- data/_includes/stats/stats-overview.html +119 -0
- data/_includes/stats/stats-tags.html +142 -0
- data/_layouts/README.md +8 -0
- data/_layouts/sitemap-collection.html +500 -0
- data/_layouts/stats.html +178 -0
- data/assets/css/stats.css +392 -0
- metadata +17 -3
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'date'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
# Statistics Generator for Jekyll Site
|
|
9
|
+
# Analyzes site content and generates comprehensive statistics
|
|
10
|
+
|
|
11
|
+
class SiteStatisticsGenerator
|
|
12
|
+
def initialize(site_root = '.')
|
|
13
|
+
@site_root = File.expand_path(site_root)
|
|
14
|
+
@posts_dir = File.join(@site_root, '_posts')
|
|
15
|
+
@pages_dir = File.join(@site_root, 'pages')
|
|
16
|
+
@collections_dirs = [
|
|
17
|
+
File.join(@site_root, '_quests'),
|
|
18
|
+
File.join(@site_root, '_docs'),
|
|
19
|
+
File.join(@site_root, '_projects')
|
|
20
|
+
]
|
|
21
|
+
@output_file = File.join(@site_root, '_data', 'content_statistics.yml')
|
|
22
|
+
|
|
23
|
+
@stats = {
|
|
24
|
+
'generated_at' => Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
|
25
|
+
'overview' => {},
|
|
26
|
+
'categories' => {},
|
|
27
|
+
'tags' => {},
|
|
28
|
+
'content_breakdown' => {},
|
|
29
|
+
'monthly_distribution' => {},
|
|
30
|
+
'word_statistics' => {}
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate!
|
|
35
|
+
puts "🔍 Analyzing site content..."
|
|
36
|
+
|
|
37
|
+
analyze_posts
|
|
38
|
+
analyze_pages
|
|
39
|
+
analyze_collections
|
|
40
|
+
calculate_overview_metrics
|
|
41
|
+
sort_and_finalize_data
|
|
42
|
+
write_statistics_file
|
|
43
|
+
|
|
44
|
+
puts "✅ Statistics generated successfully!"
|
|
45
|
+
puts "📊 Results saved to: #{@output_file}"
|
|
46
|
+
print_summary
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def analyze_posts
|
|
52
|
+
posts_pattern = File.join(@posts_dir, '**', '*.{md,markdown,html}')
|
|
53
|
+
posts_pattern2 = File.join(@pages_dir, '_posts', '**', '*.{md,markdown,html}')
|
|
54
|
+
|
|
55
|
+
post_files = Dir[posts_pattern] + Dir[posts_pattern2]
|
|
56
|
+
|
|
57
|
+
puts "📝 Found #{post_files.length} post files"
|
|
58
|
+
|
|
59
|
+
post_files.each do |file|
|
|
60
|
+
process_content_file(file, 'post')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def analyze_pages
|
|
65
|
+
# Look for standalone pages (not in _posts)
|
|
66
|
+
pages_pattern = File.join(@pages_dir, '**', '*.{md,markdown,html}')
|
|
67
|
+
page_files = Dir[pages_pattern].reject { |f| f.include?('_posts') }
|
|
68
|
+
|
|
69
|
+
# Also check root level pages
|
|
70
|
+
root_pages = Dir[File.join(@site_root, '*.{md,markdown,html}')]
|
|
71
|
+
page_files += root_pages
|
|
72
|
+
|
|
73
|
+
puts "📄 Found #{page_files.length} page files"
|
|
74
|
+
|
|
75
|
+
page_files.each do |file|
|
|
76
|
+
process_content_file(file, 'page')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def analyze_collections
|
|
81
|
+
@collections_dirs.each do |dir|
|
|
82
|
+
next unless Dir.exist?(dir)
|
|
83
|
+
|
|
84
|
+
collection_name = File.basename(dir).gsub(/^_/, '')
|
|
85
|
+
collection_files = Dir[File.join(dir, '**', '*.{md,markdown,html}')]
|
|
86
|
+
|
|
87
|
+
puts "📚 Found #{collection_files.length} #{collection_name} files"
|
|
88
|
+
|
|
89
|
+
collection_files.each do |file|
|
|
90
|
+
process_content_file(file, collection_name)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def process_content_file(file_path, content_type)
|
|
96
|
+
return unless File.file?(file_path)
|
|
97
|
+
|
|
98
|
+
content = File.read(file_path)
|
|
99
|
+
|
|
100
|
+
# Extract front matter
|
|
101
|
+
if content =~ /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
|
|
102
|
+
front_matter_str = $1
|
|
103
|
+
body_content = $2
|
|
104
|
+
|
|
105
|
+
begin
|
|
106
|
+
front_matter = YAML.safe_load(front_matter_str, permitted_classes: [Date, Time]) || {}
|
|
107
|
+
rescue YAML::SyntaxError => e
|
|
108
|
+
puts "⚠️ YAML error in #{file_path}: #{e.message}"
|
|
109
|
+
return
|
|
110
|
+
rescue Psych::DisallowedClass => e
|
|
111
|
+
# Try again with basic safe_load
|
|
112
|
+
begin
|
|
113
|
+
front_matter = YAML.safe_load(front_matter_str) || {}
|
|
114
|
+
rescue => e2
|
|
115
|
+
puts "⚠️ YAML parsing error in #{file_path}: #{e2.message}"
|
|
116
|
+
return
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
# No front matter found
|
|
121
|
+
front_matter = {}
|
|
122
|
+
body_content = content
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Skip drafts
|
|
126
|
+
return if front_matter['draft'] == true
|
|
127
|
+
|
|
128
|
+
# Count this content
|
|
129
|
+
@stats['content_breakdown'][content_type] ||= 0
|
|
130
|
+
@stats['content_breakdown'][content_type] += 1
|
|
131
|
+
|
|
132
|
+
# Process categories
|
|
133
|
+
categories = front_matter['categories'] || front_matter['category']
|
|
134
|
+
if categories
|
|
135
|
+
categories = [categories] unless categories.is_a?(Array)
|
|
136
|
+
categories.each do |category|
|
|
137
|
+
next if category.nil? || category.to_s.strip.empty?
|
|
138
|
+
category_str = category.to_s.strip
|
|
139
|
+
@stats['categories'][category_str] ||= 0
|
|
140
|
+
@stats['categories'][category_str] += 1
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Process tags
|
|
145
|
+
tags = front_matter['tags'] || front_matter['tag']
|
|
146
|
+
if tags
|
|
147
|
+
tags = [tags] unless tags.is_a?(Array)
|
|
148
|
+
tags.each do |tag|
|
|
149
|
+
next if tag.nil? || tag.to_s.strip.empty?
|
|
150
|
+
tag_str = tag.to_s.strip.downcase
|
|
151
|
+
@stats['tags'][tag_str] ||= 0
|
|
152
|
+
@stats['tags'][tag_str] += 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Process dates for monthly distribution
|
|
157
|
+
if front_matter['date']
|
|
158
|
+
begin
|
|
159
|
+
# Handle different date formats
|
|
160
|
+
date_value = front_matter['date']
|
|
161
|
+
if date_value.is_a?(String)
|
|
162
|
+
date = Date.parse(date_value)
|
|
163
|
+
elsif date_value.respond_to?(:to_date)
|
|
164
|
+
date = date_value.to_date
|
|
165
|
+
else
|
|
166
|
+
date = Date.parse(date_value.to_s)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
month_key = date.strftime('%Y-%m')
|
|
170
|
+
@stats['monthly_distribution'][month_key] ||= 0
|
|
171
|
+
@stats['monthly_distribution'][month_key] += 1
|
|
172
|
+
rescue => e
|
|
173
|
+
puts "⚠️ Date parsing error in #{file_path}: #{e.message} (date: #{front_matter['date']})"
|
|
174
|
+
# Skip this date
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Count words in content
|
|
179
|
+
word_count = count_words(body_content)
|
|
180
|
+
@stats['word_statistics'][File.basename(file_path)] = {
|
|
181
|
+
'words' => word_count,
|
|
182
|
+
'type' => content_type,
|
|
183
|
+
'title' => front_matter['title'] || File.basename(file_path, '.*')
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def count_words(content)
|
|
188
|
+
# Remove markdown syntax and count words
|
|
189
|
+
text = content.gsub(/[#*_`\[\](){}]/, ' ') # Remove markdown syntax
|
|
190
|
+
.gsub(/!\[.*?\]\(.*?\)/, ' ') # Remove images
|
|
191
|
+
.gsub(/\[.*?\]\(.*?\)/, ' ') # Remove links
|
|
192
|
+
.gsub(/```.*?```/m, ' ') # Remove code blocks
|
|
193
|
+
.gsub(/`.*?`/, ' ') # Remove inline code
|
|
194
|
+
.gsub(/<!--.*?-->/m, ' ') # Remove comments
|
|
195
|
+
.gsub(/\s+/, ' ') # Normalize whitespace
|
|
196
|
+
.strip
|
|
197
|
+
|
|
198
|
+
text.split.length
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def calculate_overview_metrics
|
|
202
|
+
total_content = @stats['content_breakdown'].values.sum
|
|
203
|
+
total_words = @stats['word_statistics'].values.sum { |stat| stat['words'] }
|
|
204
|
+
|
|
205
|
+
@stats['overview'] = {
|
|
206
|
+
'total_posts' => @stats['content_breakdown']['post'] || 0,
|
|
207
|
+
'total_pages' => @stats['content_breakdown']['page'] || 0,
|
|
208
|
+
'total_content' => total_content,
|
|
209
|
+
'total_categories' => @stats['categories'].keys.length,
|
|
210
|
+
'total_tags' => @stats['tags'].keys.length,
|
|
211
|
+
'total_words' => total_words,
|
|
212
|
+
'average_words_per_post' => total_content > 0 ? (total_words.to_f / total_content).round(1) : 0
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def sort_and_finalize_data
|
|
217
|
+
# Sort categories and tags by count (descending)
|
|
218
|
+
@stats['categories'] = @stats['categories'].sort_by { |_, count| -count }.to_h
|
|
219
|
+
@stats['tags'] = @stats['tags'].sort_by { |_, count| -count }.to_h
|
|
220
|
+
|
|
221
|
+
# Sort monthly distribution by date
|
|
222
|
+
@stats['monthly_distribution'] = @stats['monthly_distribution'].sort.to_h
|
|
223
|
+
|
|
224
|
+
# Convert to arrays for Jekyll (categories and tags)
|
|
225
|
+
@stats['categories'] = @stats['categories'].map { |name, count| [name, count] }
|
|
226
|
+
@stats['tags'] = @stats['tags'].map { |name, count| [name, count] }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def write_statistics_file
|
|
230
|
+
# Ensure _data directory exists
|
|
231
|
+
FileUtils.mkdir_p(File.dirname(@output_file))
|
|
232
|
+
|
|
233
|
+
# Write YAML file
|
|
234
|
+
File.open(@output_file, 'w') do |file|
|
|
235
|
+
file.write(@stats.to_yaml)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def print_summary
|
|
240
|
+
puts "\n📊 STATISTICS SUMMARY"
|
|
241
|
+
puts "=" * 50
|
|
242
|
+
puts "📝 Total Posts: #{@stats['overview']['total_posts']}"
|
|
243
|
+
puts "📄 Total Pages: #{@stats['overview']['total_pages']}"
|
|
244
|
+
puts "📚 Total Content: #{@stats['overview']['total_content']}"
|
|
245
|
+
puts "📂 Categories: #{@stats['overview']['total_categories']}"
|
|
246
|
+
puts "🏷️ Tags: #{@stats['overview']['total_tags']}"
|
|
247
|
+
puts "📝 Total Words: #{@stats['overview']['total_words'].to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
|
|
248
|
+
puts "📊 Average Words/Post: #{@stats['overview']['average_words_per_post']}"
|
|
249
|
+
|
|
250
|
+
if @stats['categories'].any?
|
|
251
|
+
puts "\n🏆 TOP CATEGORIES:"
|
|
252
|
+
@stats['categories'].first(5).each_with_index do |(name, count), index|
|
|
253
|
+
puts " #{index + 1}. #{name}: #{count} posts"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if @stats['tags'].any?
|
|
258
|
+
puts "\n🏷️ TOP TAGS:"
|
|
259
|
+
@stats['tags'].first(10).each_with_index do |(name, count), index|
|
|
260
|
+
puts " #{index + 1}. #{name}: #{count} uses"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
puts "\n✅ Statistics generation complete!"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Run the generator if this script is executed directly
|
|
269
|
+
if __FILE__ == $0
|
|
270
|
+
puts "🚀 Starting Jekyll Site Statistics Generation..."
|
|
271
|
+
puts "📁 Working directory: #{Dir.pwd}"
|
|
272
|
+
|
|
273
|
+
generator = SiteStatisticsGenerator.new
|
|
274
|
+
generator.generate!
|
|
275
|
+
end
|
data/_data/navigation/about.yml
CHANGED
data/_data/navigation/main.yml
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
sublinks:
|
|
20
20
|
- title: Jekyll
|
|
21
21
|
url: /docs/jekyll/
|
|
22
|
-
|
|
23
22
|
- title: About
|
|
24
23
|
url: /about/
|
|
25
24
|
sublinks:
|
|
@@ -27,5 +26,5 @@
|
|
|
27
26
|
url: /about/config/
|
|
28
27
|
- title: Theme
|
|
29
28
|
url: /about/theme/
|
|
30
|
-
- title:
|
|
31
|
-
url: /
|
|
29
|
+
- title: Site Map
|
|
30
|
+
url: /sitemap/
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
===================================================================
|
|
3
|
+
MERMAID DIAGRAM INTEGRATION
|
|
4
|
+
===================================================================
|
|
5
|
+
|
|
6
|
+
File: mermaid.html
|
|
7
|
+
Path: _includes/components/mermaid.html
|
|
8
|
+
Purpose: Load and initialize Mermaid.js for diagram rendering
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
- Set 'mermaid: true' in page front matter
|
|
12
|
+
- Use <div class="mermaid">...</div> syntax in content
|
|
13
|
+
- Supports all Mermaid diagram types (flowcharts, sequence, gantt, etc.)
|
|
14
|
+
|
|
15
|
+
Configuration:
|
|
16
|
+
- Latest stable version via CDN
|
|
17
|
+
- Forest theme for dark mode compatibility
|
|
18
|
+
- Initialized on document ready
|
|
19
|
+
|
|
20
|
+
Documentation:
|
|
21
|
+
- https://mermaid.js.org/
|
|
22
|
+
- https://github.com/mermaid-js/mermaid
|
|
23
|
+
===================================================================
|
|
24
|
+
-->
|
|
25
|
+
|
|
26
|
+
<!-- Load Mermaid.js from CDN (latest stable version) -->
|
|
27
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
28
|
+
|
|
29
|
+
<!-- Initialize Mermaid with custom configuration -->
|
|
30
|
+
<script>
|
|
31
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
32
|
+
mermaid.initialize({
|
|
33
|
+
startOnLoad: true,
|
|
34
|
+
theme: 'forest', // Options: default, forest, dark, neutral, base
|
|
35
|
+
themeVariables: {
|
|
36
|
+
primaryColor: '#007bff',
|
|
37
|
+
primaryTextColor: '#fff',
|
|
38
|
+
primaryBorderColor: '#0056b3',
|
|
39
|
+
lineColor: '#6c757d',
|
|
40
|
+
secondaryColor: '#6c757d',
|
|
41
|
+
tertiaryColor: '#f8f9fa'
|
|
42
|
+
},
|
|
43
|
+
flowchart: {
|
|
44
|
+
useMaxWidth: true,
|
|
45
|
+
htmlLabels: true,
|
|
46
|
+
curve: 'basis'
|
|
47
|
+
},
|
|
48
|
+
sequence: {
|
|
49
|
+
diagramMarginX: 50,
|
|
50
|
+
diagramMarginY: 10,
|
|
51
|
+
actorMargin: 50,
|
|
52
|
+
width: 150,
|
|
53
|
+
height: 65,
|
|
54
|
+
boxMargin: 10,
|
|
55
|
+
boxTextMargin: 5,
|
|
56
|
+
noteMargin: 10,
|
|
57
|
+
messageMargin: 35,
|
|
58
|
+
mirrorActors: true,
|
|
59
|
+
bottomMarginAdj: 1,
|
|
60
|
+
useMaxWidth: true
|
|
61
|
+
},
|
|
62
|
+
gantt: {
|
|
63
|
+
titleTopMargin: 25,
|
|
64
|
+
barHeight: 20,
|
|
65
|
+
barGap: 4,
|
|
66
|
+
topPadding: 50,
|
|
67
|
+
leftPadding: 75,
|
|
68
|
+
gridLineStartPadding: 35,
|
|
69
|
+
fontSize: 11,
|
|
70
|
+
numberSectionStyles: 4,
|
|
71
|
+
axisFormat: '%Y-%m-%d'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
console.log('Mermaid.js initialized successfully');
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<!-- FontAwesome for Mermaid icon support (optional) -->
|
|
79
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous">
|
|
80
|
+
|
|
81
|
+
<!-- Custom CSS for Mermaid diagrams -->
|
|
82
|
+
<style>
|
|
83
|
+
.mermaid {
|
|
84
|
+
text-align: center;
|
|
85
|
+
margin: 2rem auto;
|
|
86
|
+
background-color: transparent;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Ensure diagrams are responsive */
|
|
90
|
+
.mermaid svg {
|
|
91
|
+
max-width: 100%;
|
|
92
|
+
height: auto;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Dark mode compatibility */
|
|
96
|
+
@media (prefers-color-scheme: dark) {
|
|
97
|
+
.mermaid {
|
|
98
|
+
filter: brightness(0.9);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
</style>
|