jekyll-theme-zer0 1.26.0 → 1.28.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +558 -5
  3. data/README.md +10 -27
  4. data/_data/authors.yml +4 -3
  5. data/_data/backlog.yml +28 -0
  6. data/_data/consumers.yml +147 -0
  7. data/_data/features.yml +142 -23
  8. data/_data/i18n/fr.yml +98 -0
  9. data/_data/i18n/languages.yml +36 -0
  10. data/_data/i18n/manifest.yml +1502 -0
  11. data/_data/navigation/docs.yml +2 -0
  12. data/_data/navigation/main.yml +6 -0
  13. data/_data/series.yml +19 -0
  14. data/_data/theme-manifest.yml +348 -268
  15. data/_data/ui-text.yml +36 -246
  16. data/_includes/README.md +25 -2
  17. data/_includes/analytics/google-tag-manager-body.html +11 -2
  18. data/_includes/analytics/google-tag-manager-head.html +16 -6
  19. data/_includes/analytics/posthog.html +33 -4
  20. data/_includes/components/abc-letter.html +43 -0
  21. data/_includes/components/author-avatar-url.html +4 -2
  22. data/_includes/components/book-card.html +42 -0
  23. data/_includes/components/book-nav.html +80 -0
  24. data/_includes/components/book-plate.html +31 -0
  25. data/_includes/components/book-toc.html +47 -0
  26. data/_includes/components/bookshelf.html +68 -0
  27. data/_includes/components/card-grid.html +60 -0
  28. data/_includes/components/data-card.html +95 -0
  29. data/_includes/components/env-switcher.html +3 -1
  30. data/_includes/components/halfmoon.html +5 -1
  31. data/_includes/components/language-toggle.html +81 -0
  32. data/_includes/components/page-feedback.html +65 -2
  33. data/_includes/components/search-modal.html +2 -2
  34. data/_includes/components/shortcuts-modal.html +1 -1
  35. data/_includes/components/theme-controls-bar.html +10 -2
  36. data/_includes/components/theme-customizer.html +8 -2
  37. data/_includes/components/translation-notice.html +27 -0
  38. data/_includes/content/intro.html +16 -15
  39. data/_includes/content/seo.html +9 -3
  40. data/_includes/core/color-mode-init.html +13 -4
  41. data/_includes/core/favicon.html +46 -0
  42. data/_includes/core/footer.html +9 -4
  43. data/_includes/core/head.html +18 -0
  44. data/_includes/core/header.html +9 -4
  45. data/_includes/core/hreflang.html +33 -0
  46. data/_includes/core/i18n.html +36 -0
  47. data/_includes/custom/body-end.html +18 -0
  48. data/_includes/custom/body-start.html +17 -0
  49. data/_includes/custom/footer.html +18 -0
  50. data/_includes/custom/head.html +18 -0
  51. data/_includes/navigation/breadcrumbs.html +1 -1
  52. data/_includes/navigation/local-graph.html +28 -2
  53. data/_includes/navigation/nav-tree.html +3 -3
  54. data/_includes/navigation/navbar.html +5 -4
  55. data/_includes/navigation/sidebar-config.html +21 -0
  56. data/_includes/navigation/sidebar-nav.html +4 -0
  57. data/_includes/navigation/sidebar-pagetree.html +150 -0
  58. data/_includes/navigation/sidebar-right.html +3 -2
  59. data/_includes/navigation/unified-drawer.html +9 -3
  60. data/_includes/obsidian/full-graph.html +165 -136
  61. data/_layouts/404.html +260 -0
  62. data/_layouts/README.md +2 -0
  63. data/_layouts/article.html +7 -1
  64. data/_layouts/book-abc.html +106 -0
  65. data/_layouts/book-story.html +91 -0
  66. data/_layouts/book.html +113 -0
  67. data/_layouts/collection.html +17 -5
  68. data/_layouts/default.html +5 -3
  69. data/_layouts/home.html +5 -2
  70. data/_layouts/landing.html +9 -0
  71. data/_layouts/news.html +159 -33
  72. data/_layouts/root.html +29 -9
  73. data/_layouts/section.html +61 -17
  74. data/_sass/components/_book.scss +423 -0
  75. data/_sass/core/_navbar.scss +13 -0
  76. data/_sass/core/_obsidian.scss +295 -7
  77. data/_sass/layouts/_navbar-extras.scss +6 -1
  78. data/_sass/theme/_backgrounds.scss +21 -8
  79. data/assets/css/main.scss +1 -0
  80. data/assets/js/auto-hide-nav.js +5 -1
  81. data/assets/js/halfmoon.js +26 -0
  82. data/assets/js/obsidian-graph.js +707 -265
  83. data/assets/js/obsidian-local-graph.js +161 -54
  84. data/assets/js/search-modal.js +4 -1
  85. data/scripts/README.md +20 -26
  86. data/scripts/bin/audit-consumer +1 -1
  87. data/scripts/bin/manifest +31 -5
  88. data/scripts/bin/sync-plugins +0 -1
  89. data/scripts/bin/validate +5 -1
  90. data/scripts/dev/rasterize-svg.js +65 -0
  91. data/scripts/features/generate-preview-images +49 -1390
  92. data/scripts/features/install-preview-generator +55 -33
  93. data/scripts/install/README.md +55 -25
  94. data/scripts/install/ai/client.sh +302 -93
  95. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  96. data/scripts/install/ai/prompts/wizard.system.md +8 -17
  97. data/scripts/install/ai/wizard.sh +10 -5
  98. data/scripts/install/apply.sh +7 -3
  99. data/scripts/install/cli.sh +54 -4
  100. data/scripts/install/config.sh +167 -0
  101. data/scripts/install/doctor.sh +38 -0
  102. data/scripts/install/plan.sh +10 -0
  103. data/scripts/install/spec.sh +15 -7
  104. data/scripts/install/template.sh +4 -0
  105. data/scripts/lib/README.md +1 -5
  106. data/scripts/lib/install/deploy/README.md +3 -9
  107. data/scripts/lib/preview_generator.py +2261 -1341
  108. data/scripts/propagate.rb +277 -0
  109. data/scripts/translate.rb +1204 -0
  110. metadata +34 -3
  111. data/_plugins/preview_image_generator.rb +0 -351
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # =============================================================================
5
+ # scripts/propagate.rb — theme release fan-out
6
+ # =============================================================================
7
+ #
8
+ # Reads _data/consumers.yml (the downstream registry) and either REPORTS which
9
+ # consumers have drifted behind the current theme version, or DISPATCHES a
10
+ # `theme-release` event to each one so it opens its own bump PR.
11
+ #
12
+ # The problem this solves: a release used to end at RubyGems. Nothing told the
13
+ # five sites built on this theme that a new version existed, so their pins sat
14
+ # where someone last edited them by hand — all of them a full minor behind when
15
+ # 1.27.0 shipped.
16
+ #
17
+ # Dispatch, rather than this repo opening PRs directly, because every consumer
18
+ # gates differently: it-journey runs `make build-ci` + `make content-audit`,
19
+ # bashconsultants builds in Docker and runs its editorial lint, the org hubs
20
+ # re-roll member configs with provision-org-sites.rb. Each repo owns its own
21
+ # validation; this script only announces the release.
22
+ #
23
+ # Usage:
24
+ # ruby scripts/propagate.rb # report drift (default)
25
+ # ruby scripts/propagate.rb --format json # machine-readable report
26
+ # ruby scripts/propagate.rb --strict # exit 1 if any consumer is behind
27
+ # ruby scripts/propagate.rb --dispatch # fire theme-release events
28
+ # ruby scripts/propagate.rb --dispatch --dry-run
29
+ #
30
+ # Auth: uses `gh` when available, else GH_TOKEN / GITHUB_TOKEN / LIFECYCLE_PAT
31
+ # against the REST API. Reporting works unauthenticated for public repos;
32
+ # --dispatch always needs a token with `repo` scope on the target.
33
+ # =============================================================================
34
+
35
+ require 'yaml'
36
+ require 'json'
37
+ require 'net/http'
38
+ require 'uri'
39
+ require 'optparse'
40
+ require 'base64'
41
+
42
+ REPO_ROOT = File.expand_path('..', __dir__)
43
+ REGISTRY = File.join(REPO_ROOT, '_data', 'consumers.yml')
44
+ VERSION_RB = File.join(REPO_ROOT, 'lib', 'jekyll-theme-zer0', 'version.rb')
45
+
46
+ # Version-bearing patterns per pin kind. `theme_name` and `path_gem` carry no
47
+ # version by design — they are tracked so a bump does not miss the file that
48
+ # activates the theme, but they never report drift.
49
+ PIN_PATTERNS = {
50
+ 'remote_theme' => /remote_theme\s*:\s*["']?bamr87\/zer0-mistakes(?:@v?([0-9][^"'\s]*))?/,
51
+ 'hub_registry' => /theme_repo\s*:\s*["']?bamr87\/zer0-mistakes(?:@v?([0-9][^"'\s]*))?/,
52
+ 'gem_constraint' => /gem\s+["']jekyll-theme-zer0["']\s*,\s*["'][^0-9]*([0-9][^"']*)["']/,
53
+ 'theme_name' => nil,
54
+ 'path_gem' => nil
55
+ }.freeze
56
+
57
+ options = {
58
+ format: 'text', strict: false, dispatch: false, dry_run: false, version: nil
59
+ }
60
+
61
+ OptionParser.new do |o|
62
+ o.banner = 'Usage: ruby scripts/propagate.rb [options]'
63
+ o.on('--dispatch', 'Send theme-release events instead of reporting') { options[:dispatch] = true }
64
+ o.on('--dry-run', 'Show what --dispatch would send; send nothing') { options[:dry_run] = true }
65
+ o.on('--strict', 'Exit 1 when a consumer is behind the current version') { options[:strict] = true }
66
+ o.on('--format FMT', %w[text json github], 'Output format (text|json|github)') { |v| options[:format] = v }
67
+ o.on('--version VER', 'Theme version to compare against (default: version.rb)') { |v| options[:version] = v }
68
+ o.on('-h', '--help', 'Show this help') { puts o; exit 0 }
69
+ end.parse!
70
+
71
+ # -----------------------------------------------------------------------------
72
+ def theme_version
73
+ content = File.read(VERSION_RB)
74
+ content[/VERSION\s*=\s*"([^"]+)"/, 1] or abort('could not parse version.rb')
75
+ end
76
+
77
+ def gh_available?
78
+ return @gh_available unless @gh_available.nil?
79
+
80
+ @gh_available = system('command -v gh > /dev/null 2>&1')
81
+ end
82
+
83
+ def token
84
+ ENV['GH_TOKEN'] || ENV['GITHUB_TOKEN'] || ENV['LIFECYCLE_PAT']
85
+ end
86
+
87
+ def http_get(uri, headers = {})
88
+ req = Net::HTTP::Get.new(uri)
89
+ headers.each { |k, v| req[k] = v }
90
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 15) do |http|
91
+ http.request(req)
92
+ end
93
+ res.is_a?(Net::HTTPSuccess) ? res.body : nil
94
+ rescue StandardError
95
+ nil
96
+ end
97
+
98
+ # Fetch one file from a consumer repo. Returns nil (never raises) when the repo,
99
+ # branch, or path is unreachable — an unreachable consumer must degrade to
100
+ # "unknown" in the report, not take the whole fan-out down.
101
+ #
102
+ # Two transports, in order of capability:
103
+ # 1. api.github.com/contents with a token — the only path that reads PRIVATE
104
+ # consumer repos, and the one CI uses.
105
+ # 2. raw.githubusercontent.com unauthenticated — public repos only.
106
+ # Never send the token to raw.githubusercontent.com: a token that is valid for
107
+ # the API but not for raw turns a public 200 into a 404, which silently reports
108
+ # every consumer as unreachable.
109
+ def fetch_file(repo, branch, path)
110
+ if gh_available?
111
+ out = `gh api "repos/#{repo}/contents/#{path}?ref=#{branch}" --jq .content 2>/dev/null`
112
+ return Base64.decode64(out) if $?.success? && !out.strip.empty?
113
+ end
114
+
115
+ if token
116
+ body = http_get(
117
+ URI("https://api.github.com/repos/#{repo}/contents/#{path}?ref=#{branch}"),
118
+ 'Authorization' => "Bearer #{token}",
119
+ 'Accept' => 'application/vnd.github.raw',
120
+ 'X-GitHub-Api-Version' => '2022-11-28'
121
+ )
122
+ return body if body
123
+ end
124
+
125
+ http_get(URI("https://raw.githubusercontent.com/#{repo}/#{branch}/#{path}"))
126
+ end
127
+
128
+ def extract_pin(kind, content)
129
+ pattern = PIN_PATTERNS[kind]
130
+ return { versioned: false, version: nil } if pattern.nil?
131
+ return { versioned: true, version: nil, missing: true } if content.nil?
132
+
133
+ match = content.match(pattern)
134
+ return { versioned: true, version: nil, missing: true } if match.nil?
135
+
136
+ # A matched pin with no captured version is a deliberate floating ref
137
+ # (`remote_theme: bamr87/zer0-mistakes`), not a parse failure.
138
+ { versioned: true, version: match[1] }
139
+ end
140
+
141
+ def compare(pinned, current)
142
+ return :floating if pinned.nil?
143
+
144
+ a = Gem::Version.new(pinned.sub(/\A[v=~> ]+/, ''))
145
+ b = Gem::Version.new(current)
146
+ return :current if a == b
147
+ return :behind if a < b
148
+
149
+ :ahead
150
+ rescue ArgumentError
151
+ :unknown
152
+ end
153
+
154
+ def dispatch(repo, event, version, dry_run)
155
+ payload = {
156
+ event_type: event,
157
+ client_payload: { version: version, theme: 'bamr87/zer0-mistakes', tag: "v#{version}" }
158
+ }
159
+
160
+ if dry_run
161
+ puts " [DRY RUN] POST repos/#{repo}/dispatches #{payload[:client_payload].to_json}"
162
+ return :dry_run
163
+ end
164
+
165
+ unless token
166
+ warn " ✗ #{repo}: no token (GH_TOKEN / GITHUB_TOKEN / LIFECYCLE_PAT) — cannot dispatch"
167
+ return :no_token
168
+ end
169
+
170
+ uri = URI("https://api.github.com/repos/#{repo}/dispatches")
171
+ req = Net::HTTP::Post.new(uri)
172
+ req['Authorization'] = "Bearer #{token}"
173
+ req['Accept'] = 'application/vnd.github+json'
174
+ req['Content-Type'] = 'application/json'
175
+ req.body = payload.to_json
176
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 15) do |http|
177
+ http.request(req)
178
+ end
179
+
180
+ if res.is_a?(Net::HTTPSuccess)
181
+ :dispatched
182
+ else
183
+ warn " ✗ #{repo}: dispatch failed (HTTP #{res.code})"
184
+ :failed
185
+ end
186
+ rescue StandardError => e
187
+ warn " ✗ #{repo}: dispatch error (#{e.class})"
188
+ :failed
189
+ end
190
+
191
+ # -----------------------------------------------------------------------------
192
+ abort("registry not found: #{REGISTRY}") unless File.exist?(REGISTRY)
193
+
194
+ registry = YAML.load_file(REGISTRY)
195
+ defaults = registry['defaults'] || {}
196
+ current = options[:version] || theme_version
197
+ consumers = registry['consumers'] || []
198
+
199
+ results = consumers.map do |consumer|
200
+ repo = consumer['repo']
201
+ branch = consumer['branch'] || defaults['branch'] || 'main'
202
+
203
+ pins = (consumer['pins'] || []).map do |pin|
204
+ kind = pin['kind']
205
+ content = PIN_PATTERNS[kind].nil? ? nil : fetch_file(repo, branch, pin['file'])
206
+ info = extract_pin(kind, content)
207
+ status = if !info[:versioned] then :not_versioned
208
+ elsif info[:missing] then :unreachable
209
+ else compare(info[:version], current)
210
+ end
211
+ { 'file' => pin['file'], 'kind' => kind, 'pinned' => info[:version], 'status' => status.to_s }
212
+ end
213
+
214
+ versioned = pins.reject { |p| %w[not_versioned unreachable].include?(p['status']) }
215
+ overall = if versioned.any? { |p| p['status'] == 'behind' } then :behind
216
+ elsif versioned.any? { |p| p['status'] == 'floating' } then :floating
217
+ elsif versioned.empty? then :unknown
218
+ elsif versioned.all? { |p| p['status'] == 'current' } then :current
219
+ else :mixed
220
+ end
221
+
222
+ {
223
+ 'repo' => repo, 'mode' => consumer['mode'], 'dispatch' => consumer['dispatch'] == true,
224
+ 'status' => overall.to_s, 'pins' => pins
225
+ }
226
+ end
227
+
228
+ behind = results.select { |r| r['status'] == 'behind' || r['status'] == 'mixed' }
229
+
230
+ # -----------------------------------------------------------------------------
231
+ if options[:dispatch]
232
+ event = defaults['dispatch_event'] || 'theme-release'
233
+ puts "Dispatching #{event} v#{current} to #{results.count { |r| r['dispatch'] }} consumer(s)"
234
+ results.each do |r|
235
+ unless r['dispatch']
236
+ puts " – #{r['repo']}: skipped (dispatch: false — #{r['mode']})"
237
+ next
238
+ end
239
+ outcome = dispatch(r['repo'], event, current, options[:dry_run])
240
+ puts " ✓ #{r['repo']}: #{outcome}" if %i[dispatched dry_run].include?(outcome)
241
+ end
242
+ exit 0
243
+ end
244
+
245
+ case options[:format]
246
+ when 'json'
247
+ puts JSON.pretty_generate({ 'theme_version' => current, 'consumers' => results })
248
+ when 'github'
249
+ results.each do |r|
250
+ next unless %w[behind mixed].include?(r['status'])
251
+
252
+ stale = r['pins'].select { |p| p['status'] == 'behind' }
253
+ .map { |p| "#{p['file']} (#{p['pinned']})" }.join(', ')
254
+ puts "::warning::#{r['repo']} is behind v#{current} — #{stale}"
255
+ end
256
+ puts "::notice::#{behind.size}/#{results.size} consumers behind v#{current}"
257
+ else
258
+ puts "Theme version: #{current}"
259
+ puts
260
+ results.each do |r|
261
+ marker = { 'current' => '✓', 'behind' => '✗', 'floating' => '~', 'mixed' => '✗' }[r['status']] || '?'
262
+ puts format('%s %-38s %-22s %s', marker, r['repo'], r['mode'], r['status'])
263
+ r['pins'].each do |p|
264
+ detail = case p['status']
265
+ when 'not_versioned' then 'no version (tracked only)'
266
+ when 'unreachable' then 'unreachable — check repo/branch/path'
267
+ when 'floating' then 'floating on main'
268
+ else p['pinned']
269
+ end
270
+ puts format(' %-24s %-16s %s', p['file'], p['kind'], detail)
271
+ end
272
+ puts
273
+ end
274
+ puts "#{behind.size}/#{results.size} consumer(s) behind v#{current}"
275
+ end
276
+
277
+ exit 1 if options[:strict] && !behind.empty?