jekyll-theme-zer0 1.26.0 → 1.27.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +172 -1
  3. data/README.md +10 -27
  4. data/_data/authors.yml +4 -3
  5. data/_data/backlog.yml +28 -0
  6. data/_data/features.yml +65 -18
  7. data/_data/i18n/languages.yml +36 -0
  8. data/_data/theme-manifest.yml +0 -2
  9. data/_data/ui-text.yml +36 -246
  10. data/_includes/README.md +4 -0
  11. data/_includes/components/author-avatar-url.html +4 -2
  12. data/_includes/components/env-switcher.html +3 -1
  13. data/_includes/components/language-toggle.html +81 -0
  14. data/_includes/components/search-modal.html +2 -2
  15. data/_includes/components/shortcuts-modal.html +1 -1
  16. data/_includes/components/translation-notice.html +27 -0
  17. data/_includes/content/intro.html +16 -15
  18. data/_includes/core/footer.html +9 -4
  19. data/_includes/core/head.html +9 -0
  20. data/_includes/core/header.html +9 -4
  21. data/_includes/core/hreflang.html +33 -0
  22. data/_includes/core/i18n.html +36 -0
  23. data/_includes/navigation/breadcrumbs.html +1 -1
  24. data/_includes/navigation/navbar.html +5 -4
  25. data/_includes/navigation/sidebar-right.html +3 -2
  26. data/_includes/navigation/unified-drawer.html +1 -1
  27. data/_layouts/article.html +7 -1
  28. data/_layouts/default.html +5 -3
  29. data/_layouts/news.html +4 -2
  30. data/_layouts/root.html +14 -8
  31. data/_layouts/section.html +4 -2
  32. data/_sass/core/_obsidian.scss +9 -1
  33. data/_sass/layouts/_navbar-extras.scss +6 -1
  34. data/assets/js/obsidian-graph.js +5 -1
  35. data/scripts/README.md +20 -26
  36. data/scripts/bin/audit-consumer +1 -1
  37. data/scripts/bin/manifest +0 -1
  38. data/scripts/bin/sync-plugins +0 -1
  39. data/scripts/dev/rasterize-svg.js +65 -0
  40. data/scripts/features/generate-preview-images +49 -1390
  41. data/scripts/features/install-preview-generator +55 -33
  42. data/scripts/install/README.md +9 -20
  43. data/scripts/install/ai/prompts/wizard.system.md +8 -17
  44. data/scripts/lib/README.md +1 -5
  45. data/scripts/lib/install/deploy/README.md +3 -9
  46. data/scripts/lib/preview_generator.py +2261 -1341
  47. data/scripts/translate.rb +1114 -0
  48. metadata +9 -3
  49. data/_plugins/preview_image_generator.rb +0 -351
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-theme-zer0
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.26.0
4
+ version: 1.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amr Abdel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-07 00:00:00.000000000 Z
11
+ date: 2026-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -88,6 +88,7 @@ files:
88
88
  - _data/generate_statistics.sh
89
89
  - _data/github-actions-example.yml
90
90
  - _data/glossary.yml
91
+ - _data/i18n/languages.yml
91
92
  - _data/landing.yml
92
93
  - _data/navigation/README.md
93
94
  - _data/navigation/about.yml
@@ -139,6 +140,7 @@ files:
139
140
  - _includes/components/halfmoon.html
140
141
  - _includes/components/info-section.html
141
142
  - _includes/components/js-cdn.html
143
+ - _includes/components/language-toggle.html
142
144
  - _includes/components/mermaid.html
143
145
  - _includes/components/nanobar.html
144
146
  - _includes/components/nav-editor.html
@@ -163,6 +165,7 @@ files:
163
165
  - _includes/components/theme-customizer.html
164
166
  - _includes/components/theme-info.html
165
167
  - _includes/components/theme-preview-gallery.html
168
+ - _includes/components/translation-notice.html
166
169
  - _includes/components/zer0-env-var.html
167
170
  - _includes/content/backlinks.html
168
171
  - _includes/content/giscus.html
@@ -180,6 +183,8 @@ files:
180
183
  - _includes/core/footer.html
181
184
  - _includes/core/head.html
182
185
  - _includes/core/header.html
186
+ - _includes/core/hreflang.html
187
+ - _includes/core/i18n.html
183
188
  - _includes/core/tokens-inline.html
184
189
  - _includes/docs/bootstrap-docs.html
185
190
  - _includes/landing/landing-install-cards.html
@@ -235,7 +240,6 @@ files:
235
240
  - _plugins/author_pages_generator.rb
236
241
  - _plugins/content_statistics_generator.rb
237
242
  - _plugins/obsidian_links.rb
238
- - _plugins/preview_image_generator.rb
239
243
  - _plugins/sanitize_config_filter.rb
240
244
  - _plugins/search_and_sitemap_generator.rb
241
245
  - _plugins/theme_version.rb
@@ -397,6 +401,7 @@ files:
397
401
  - scripts/content-review.rb
398
402
  - scripts/convert-notebooks.sh
399
403
  - scripts/dev/css-diff.sh
404
+ - scripts/dev/rasterize-svg.js
400
405
  - scripts/dev/shot.js
401
406
  - scripts/docker-publish
402
407
  - scripts/docs/check-freshness.sh
@@ -530,6 +535,7 @@ files:
530
535
  - scripts/test/lib/test_validation.sh
531
536
  - scripts/test/lib/test_version.sh
532
537
  - scripts/test/theme/validate
538
+ - scripts/translate.rb
533
539
  - scripts/update-preview-paths.sh
534
540
  - scripts/utils/analyze-commits
535
541
  - scripts/utils/fix-markdown
@@ -1,351 +0,0 @@
1
- # Feature: ZER0-004
2
- # frozen_string_literal: true
3
-
4
- #
5
- # File: preview_image_generator.rb
6
- # Path: _plugins/preview_image_generator.rb
7
- # Purpose: Jekyll plugin for AI-powered preview image generation
8
- #
9
- # This plugin provides Jekyll integration for the preview image generator,
10
- # including Liquid tags, filters, and hooks for automatic image generation.
11
- #
12
- # Usage in templates:
13
- # {% preview_image_status %} - Shows missing preview count
14
- # {{ page | preview_image_path }} - Returns the preview image path
15
- # {{ page | has_preview_image }} - Returns true/false
16
- #
17
- # Configuration in _config.yml:
18
- # preview_images:
19
- # enabled: true
20
- # provider: openai
21
- # auto_generate: false # Set to true to generate during build (slow!)
22
- # style: "retro pixel art, 8-bit video game aesthetic"
23
- #
24
-
25
- module Jekyll
26
- module PreviewImageGenerator
27
- # Configuration defaults
28
- DEFAULTS = {
29
- 'enabled' => true,
30
- 'provider' => 'openai',
31
- 'model' => 'dall-e-3',
32
- 'size' => '1792x1024',
33
- 'quality' => 'standard',
34
- 'style' => 'retro pixel art, 8-bit video game aesthetic, vibrant colors, nostalgic, clean pixel graphics',
35
- 'style_modifiers' => 'pixelated, retro gaming style, CRT screen glow effect, limited color palette',
36
- 'output_dir' => 'assets/images/previews',
37
- 'assets_prefix' => '/assets',
38
- 'auto_prefix' => true,
39
- 'auto_generate' => false,
40
- 'collections' => ['posts', 'docs', 'quickstart']
41
- }.freeze
42
-
43
- # Get configuration with defaults
44
- def self.config(site)
45
- site_config = site.config['preview_images'] || {}
46
- DEFAULTS.merge(site_config)
47
- end
48
-
49
- # Check if a document has a preview image defined
50
- def self.has_preview?(doc)
51
- cached = cached_preview_entry(doc)
52
- return cached['has_preview'] unless cached.nil?
53
-
54
- preview = doc.data['preview']
55
- return false if preview.nil? || preview.to_s.strip.empty?
56
-
57
- # Reject non-path values (text descriptions used as preview)
58
- return false unless preview.match?(/\.(png|jpe?g|gif|svg|webp)$/i) || preview.start_with?('http')
59
-
60
- # Check if the preview file actually exists
61
- site = doc.site
62
- config = self.config(site)
63
-
64
- # External URL — assume it exists
65
- return true if preview.start_with?('http')
66
-
67
- # Normalize the preview path using assets_prefix
68
- normalized_preview = normalize_preview_path(preview, config)
69
-
70
- # Build candidate file paths to check
71
- candidates = []
72
- if normalized_preview.start_with?('/')
73
- candidates << File.join(site.source, normalized_preview.sub(/^\//, ''))
74
- candidates << File.join(site.source, 'assets', normalized_preview.sub(/^\//, ''))
75
- else
76
- candidates << File.join(site.source, 'assets', normalized_preview)
77
- candidates << File.join(site.source, normalized_preview)
78
- candidates << File.join(site.source, config['output_dir'], normalized_preview)
79
- end
80
-
81
- candidates.any? { |path| File.exist?(path) }
82
- end
83
-
84
- # Normalize a preview path by adding assets_prefix if needed
85
- def self.normalize_preview_path(preview, config)
86
- return preview if preview.nil? || preview.to_s.strip.empty?
87
- return preview if preview.start_with?('http')
88
-
89
- assets_prefix = config['assets_prefix'] || '/assets'
90
- auto_prefix = config['auto_prefix'] != false # Default to true
91
-
92
- # If auto_prefix is enabled and path doesn't already contain assets_prefix
93
- if auto_prefix && !preview.include?(assets_prefix)
94
- "#{assets_prefix}#{preview}"
95
- else
96
- preview
97
- end
98
- end
99
-
100
- # Get the preview image path for a document
101
- def self.preview_path(doc)
102
- cached = cached_preview_entry(doc)
103
- return cached['path'] unless cached.nil?
104
-
105
- preview = doc.data['preview']
106
- return nil if preview.nil? || preview.to_s.strip.empty?
107
-
108
- # Reject non-path values
109
- return nil unless preview.match?(/\.(png|jpe?g|gif|svg|webp)$/i) || preview.start_with?('http')
110
-
111
- site = doc.site
112
- config = self.config(site)
113
-
114
- # If it's an external URL, return as-is
115
- return preview if preview.start_with?('http')
116
-
117
- # Normalize path with assets_prefix
118
- normalize_preview_path(preview, config)
119
- end
120
-
121
- # Get list of documents missing preview images
122
- def self.missing_previews(site)
123
- build_index(site)['missing']
124
- end
125
-
126
- def self.build_index(site)
127
- cached = site.data['preview_image_index']
128
- return cached if cached
129
-
130
- config = self.config(site)
131
- index = {}
132
- missing = []
133
-
134
- preview_documents(site, config).each do |doc, collection_name|
135
- key = preview_cache_key(doc)
136
- next if index.key?(key)
137
-
138
- preview = doc.data['preview']
139
- normalized_path = nil
140
- has_preview = false
141
-
142
- if preview && !preview.to_s.strip.empty? && (preview.match?(/\.(png|jpe?g|gif|svg|webp)$/i) || preview.start_with?('http'))
143
- normalized_path = preview.start_with?('http') ? preview : normalize_preview_path(preview, config)
144
- has_preview = preview.start_with?('http') || preview_file_exists?(site, normalized_path, config)
145
- end
146
-
147
- index[key] = {
148
- 'has_preview' => has_preview,
149
- 'path' => normalized_path,
150
- 'collection' => collection_name
151
- }
152
-
153
- next if has_preview
154
-
155
- missing << {
156
- 'path' => doc.relative_path,
157
- 'title' => doc.data['title'] || File.basename(doc.relative_path),
158
- 'collection' => collection_name
159
- }
160
- end
161
-
162
- site.data['preview_image_index'] = {
163
- 'documents' => index,
164
- 'missing' => missing
165
- }
166
- end
167
-
168
- def self.cached_preview_entry(doc)
169
- return nil unless doc.respond_to?(:site) && doc.site && doc.site.data['preview_image_index']
170
-
171
- doc.site.data['preview_image_index']['documents'][preview_cache_key(doc)]
172
- end
173
-
174
- def self.preview_documents(site, config)
175
- docs = []
176
- Array(config['collections']).each do |collection_name|
177
- collection = site.collections[collection_name]
178
- next unless collection
179
-
180
- collection.docs.each { |doc| docs << [doc, collection_name] }
181
- end
182
-
183
- site.posts.docs.each { |doc| docs << [doc, 'posts'] } if site.respond_to?(:posts) && site.posts
184
- docs
185
- end
186
-
187
- def self.preview_cache_key(doc)
188
- doc.respond_to?(:relative_path) ? doc.relative_path : doc.path
189
- end
190
-
191
- def self.preview_file_exists?(site, normalized_preview, config)
192
- candidates = []
193
- if normalized_preview.start_with?('/')
194
- candidates << File.join(site.source, normalized_preview.sub(/^\//, ''))
195
- candidates << File.join(site.source, 'assets', normalized_preview.sub(/^\//, ''))
196
- else
197
- candidates << File.join(site.source, 'assets', normalized_preview)
198
- candidates << File.join(site.source, normalized_preview)
199
- candidates << File.join(site.source, config['output_dir'], normalized_preview)
200
- end
201
-
202
- candidates.any? { |path| File.exist?(path) }
203
- end
204
-
205
- # Generate a preview image filename from document
206
- def self.generate_filename(doc)
207
- # Use the document's slug or generate from title
208
- slug = doc.data['slug'] || doc.basename_without_ext
209
-
210
- # Sanitize the slug for use as filename
211
- sanitized = slug.to_s.downcase
212
- .gsub(/[^a-z0-9\-_]/, '-')
213
- .gsub(/-+/, '-')
214
- .gsub(/^-|-$/, '')
215
-
216
- "#{sanitized}-preview.png"
217
- end
218
- end
219
-
220
- # ==========================================================================
221
- # Liquid Filters
222
- # ==========================================================================
223
-
224
- module PreviewImageFilters
225
- # Check if a page/document has a preview image
226
- # Usage: {{ page | has_preview_image }}
227
- def has_preview_image(doc)
228
- return false unless doc.is_a?(Hash) || doc.respond_to?(:data)
229
-
230
- # Handle both Hash (from assign) and Document objects
231
- if doc.is_a?(Hash)
232
- preview = doc['preview']
233
- return false if preview.nil? || preview.to_s.strip.empty?
234
- # Only consider values that look like image paths or URLs
235
- preview.to_s.match?(/\.(png|jpe?g|gif|svg|webp)$/i) || preview.to_s.start_with?('http')
236
- else
237
- PreviewImageGenerator.has_preview?(doc)
238
- end
239
- end
240
-
241
- # Get the preview image path
242
- # Usage: {{ page | preview_image_path }}
243
- def preview_image_path(doc)
244
- return nil unless doc.is_a?(Hash) || doc.respond_to?(:data)
245
-
246
- if doc.is_a?(Hash)
247
- preview = doc['preview']
248
- return nil if preview.nil? || preview.to_s.strip.empty?
249
- return nil unless preview.to_s.match?(/\.(png|jpe?g|gif|svg|webp)$/i) || preview.to_s.start_with?('http')
250
-
251
- return preview if preview.start_with?('/') || preview.start_with?('http')
252
-
253
- # Get config from context if available
254
- site_config = @context.registers[:site].config['preview_images'] || {}
255
- output_dir = site_config['output_dir'] || 'assets/images/previews'
256
- "#{output_dir}/#{preview}"
257
- else
258
- PreviewImageGenerator.preview_path(doc)
259
- end
260
- end
261
-
262
- # Get suggested filename for a preview image
263
- # Usage: {{ page | preview_filename }}
264
- def preview_filename(doc)
265
- return nil unless doc.respond_to?(:data) || doc.respond_to?(:basename_without_ext)
266
- PreviewImageGenerator.generate_filename(doc)
267
- end
268
- end
269
-
270
- # ==========================================================================
271
- # Liquid Tags
272
- # ==========================================================================
273
-
274
- # Tag to display preview image status/count
275
- # Usage: {% preview_image_status %}
276
- class PreviewImageStatusTag < Liquid::Tag
277
- def render(context)
278
- site = context.registers[:site]
279
- missing = PreviewImageGenerator.missing_previews(site)
280
-
281
- if missing.empty?
282
- "<span class=\"badge bg-success\">All preview images present</span>"
283
- else
284
- "<span class=\"badge bg-warning text-dark\">#{missing.length} missing preview images</span>"
285
- end
286
- end
287
- end
288
-
289
- # Tag to list missing preview images
290
- # Usage: {% preview_images_missing %}
291
- class PreviewImagesMissingTag < Liquid::Tag
292
- def render(context)
293
- site = context.registers[:site]
294
- missing = PreviewImageGenerator.missing_previews(site)
295
-
296
- return "<p>All documents have preview images!</p>" if missing.empty?
297
-
298
- html = "<ul class=\"list-group\">\n"
299
- missing.each do |item|
300
- html += " <li class=\"list-group-item d-flex justify-content-between align-items-center\">\n"
301
- html += " <span>#{item['title']}</span>\n"
302
- html += " <span class=\"badge bg-secondary\">#{item['collection']}</span>\n"
303
- html += " </li>\n"
304
- end
305
- html += "</ul>"
306
- html
307
- end
308
- end
309
-
310
- # ==========================================================================
311
- # Generator Hook (optional auto-generation during build)
312
- # ==========================================================================
313
-
314
- class PreviewImageGeneratorHook < Generator
315
- safe true
316
- priority :low
317
-
318
- def generate(site)
319
- config = PreviewImageGenerator.config(site)
320
-
321
- return unless config['enabled']
322
-
323
- # Store missing previews in site data for access in templates
324
- preview_index = PreviewImageGenerator.build_index(site)
325
- site.data['preview_images_missing'] = preview_index['missing']
326
- site.data['preview_images_config'] = config
327
-
328
- # Log status
329
- missing_count = site.data['preview_images_missing'].length
330
- if missing_count > 0
331
- Jekyll.logger.info "Preview Images:", "#{missing_count} documents missing preview images"
332
- Jekyll.logger.info "Preview Images:", "Run 'scripts/generate-preview-images.sh' to generate them"
333
- end
334
-
335
- # Auto-generate is disabled by default (it's slow and requires API calls)
336
- # Users should run the shell script manually or via CI/CD
337
- if config['auto_generate']
338
- Jekyll.logger.warn "Preview Images:", "Auto-generation is enabled but not implemented in plugin"
339
- Jekyll.logger.warn "Preview Images:", "Use 'scripts/generate-preview-images.sh' instead"
340
- end
341
- end
342
- end
343
-
344
- # ==========================================================================
345
- # Register Liquid components
346
- # ==========================================================================
347
-
348
- Liquid::Template.register_filter(PreviewImageFilters)
349
- Liquid::Template.register_tag('preview_image_status', PreviewImageStatusTag)
350
- Liquid::Template.register_tag('preview_images_missing', PreviewImagesMissingTag)
351
- end