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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +558 -5
- data/README.md +10 -27
- data/_data/authors.yml +4 -3
- data/_data/backlog.yml +28 -0
- data/_data/consumers.yml +147 -0
- data/_data/features.yml +142 -23
- data/_data/i18n/fr.yml +98 -0
- data/_data/i18n/languages.yml +36 -0
- data/_data/i18n/manifest.yml +1502 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +6 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +348 -268
- data/_data/ui-text.yml +36 -246
- data/_includes/README.md +25 -2
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/author-avatar-url.html +4 -2
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/env-switcher.html +3 -1
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/language-toggle.html +81 -0
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +1 -1
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/translation-notice.html +27 -0
- data/_includes/content/intro.html +16 -15
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/footer.html +9 -4
- data/_includes/core/head.html +18 -0
- data/_includes/core/header.html +9 -4
- data/_includes/core/hreflang.html +33 -0
- data/_includes/core/i18n.html +36 -0
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/breadcrumbs.html +1 -1
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/navbar.html +5 -4
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +3 -2
- data/_includes/navigation/unified-drawer.html +9 -3
- data/_includes/obsidian/full-graph.html +165 -136
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +2 -0
- data/_layouts/article.html +7 -1
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/default.html +5 -3
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +159 -33
- data/_layouts/root.html +29 -9
- data/_layouts/section.html +61 -17
- data/_sass/components/_book.scss +423 -0
- data/_sass/core/_navbar.scss +13 -0
- data/_sass/core/_obsidian.scss +295 -7
- data/_sass/layouts/_navbar-extras.scss +6 -1
- data/_sass/theme/_backgrounds.scss +21 -8
- data/assets/css/main.scss +1 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/obsidian-graph.js +707 -265
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/search-modal.js +4 -1
- data/scripts/README.md +20 -26
- data/scripts/bin/audit-consumer +1 -1
- data/scripts/bin/manifest +31 -5
- data/scripts/bin/sync-plugins +0 -1
- data/scripts/bin/validate +5 -1
- data/scripts/dev/rasterize-svg.js +65 -0
- data/scripts/features/generate-preview-images +49 -1390
- data/scripts/features/install-preview-generator +55 -33
- data/scripts/install/README.md +55 -25
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/prompts/wizard.system.md +8 -17
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/lib/README.md +1 -5
- data/scripts/lib/install/deploy/README.md +3 -9
- data/scripts/lib/preview_generator.py +2261 -1341
- data/scripts/propagate.rb +277 -0
- data/scripts/translate.rb +1204 -0
- metadata +34 -3
- 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?
|