jekyll-theme-zer0 1.24.0 → 1.25.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 +94 -0
- data/README.md +3 -3
- data/_data/authors.yml +1 -0
- data/_data/features.yml +37 -2
- data/_data/navigation/README.md +15 -5
- data/_data/navigation/main.yml +0 -8
- data/_data/roadmap.yml +1 -0
- data/_includes/README.md +10 -7
- data/_includes/analytics/google-analytics.html +1 -0
- data/_includes/analytics/google-tag-manager-body.html +1 -0
- data/_includes/analytics/google-tag-manager-head.html +1 -0
- data/_includes/analytics/posthog.html +1 -0
- data/_includes/components/README.md +2 -1
- data/_includes/components/admin-links.html +63 -0
- data/_includes/components/admin-tabs.html +1 -0
- data/_includes/components/ai-chat.html +27 -5
- data/_includes/components/author-eeat.html +1 -0
- data/_includes/components/background-customizer.html +1 -0
- data/_includes/components/background-settings.html +36 -37
- data/_includes/components/component-showcase.html +1 -0
- data/_includes/components/cookie-consent.html +1 -0
- data/_includes/components/dev-shortcuts.html +1 -8
- data/_includes/components/env-switcher.html +90 -138
- data/_includes/components/feature-card.html +1 -1
- data/_includes/components/halfmoon.html +22 -41
- data/_includes/components/info-section.html +127 -130
- data/_includes/components/mermaid.html +1 -0
- data/_includes/components/nanobar.html +1 -0
- data/_includes/components/post-type-badge.html +1 -0
- data/_includes/components/preview-image.html +1 -0
- data/_includes/components/search-modal.html +1 -0
- data/_includes/components/searchbar.html +1 -0
- data/_includes/components/shortcuts-modal.html +1 -0
- data/_includes/components/theme-info.html +21 -30
- data/_includes/components/theme-preview-gallery.html +1 -0
- data/_includes/content/backlinks.html +1 -0
- data/_includes/content/giscus.html +1 -0
- data/_includes/content/jsonld-faq.html +1 -0
- data/_includes/content/seo.html +1 -0
- data/_includes/content/sitemap.html +1 -0
- data/_includes/content/toc.html +1 -0
- data/_includes/core/color-mode-init.html +1 -0
- data/_includes/core/footer.html +1 -0
- data/_includes/core/head.html +1 -0
- data/_includes/core/header.html +5 -15
- data/_includes/navigation/admin-nav.html +1 -0
- data/_includes/navigation/breadcrumbs.html +1 -0
- data/_includes/navigation/local-graph-fab.html +3 -9
- data/_includes/navigation/local-graph.html +4 -9
- data/_includes/navigation/navbar.html +1 -0
- data/_includes/navigation/sidebar-categories.html +56 -14
- data/_includes/navigation/sidebar-config.html +115 -0
- data/_includes/navigation/sidebar-folders.html +168 -62
- data/_includes/navigation/sidebar-left.html +29 -52
- data/_includes/navigation/sidebar-nav.html +30 -0
- data/_includes/navigation/sidebar-right.html +1 -0
- data/_includes/navigation/toc-fab.html +3 -9
- data/_includes/navigation/unified-drawer.html +7 -14
- data/_includes/obsidian/full-graph.html +1 -0
- data/_includes/setup/wizard.html +1 -0
- data/_includes/stats/stats-categories.html +1 -0
- data/_includes/stats/stats-header.html +1 -0
- data/_includes/stats/stats-metrics.html +1 -0
- data/_includes/stats/stats-overview.html +1 -0
- data/_includes/stats/stats-tags.html +1 -0
- data/_layouts/admin.html +1 -0
- data/_layouts/article.html +1 -0
- data/_layouts/author.html +1 -0
- data/_layouts/authors.html +1 -0
- data/_layouts/collection.html +1 -0
- data/_layouts/default.html +14 -37
- data/_layouts/home.html +1 -0
- data/_layouts/index.html +1 -0
- data/_layouts/landing.html +1 -0
- data/_layouts/news.html +1 -0
- data/_layouts/note.html +1 -0
- data/_layouts/notebook.html +1 -0
- data/_layouts/root.html +3 -2
- data/_layouts/search.html +1 -0
- data/_layouts/section.html +1 -0
- data/_layouts/setup.html +1 -0
- data/_layouts/sitemap-collection.html +1 -0
- data/_layouts/stats.html +1 -0
- data/_layouts/tag.html +1 -0
- data/_layouts/welcome.html +1 -0
- data/_plugins/author_pages_generator.rb +1 -0
- data/_plugins/obsidian_links.rb +1 -0
- data/_plugins/preview_image_generator.rb +1 -0
- data/_plugins/search_and_sitemap_generator.rb +1 -0
- data/_plugins/theme_version.rb +1 -0
- data/_sass/components/_author.scss +12 -1
- data/_sass/components/_footer.scss +22 -0
- data/_sass/components/_ui-enhancements.scss +9 -0
- data/_sass/core/_docs-layout.scss +1 -0
- data/_sass/core/_navbar.scss +10 -27
- data/_sass/core/_obsidian.scss +2 -2
- data/_sass/core/code-copy.scss +10 -0
- data/_sass/custom.scss +1 -0
- data/_sass/notebooks.scss +1 -0
- data/_sass/theme/_backgrounds.scss +7 -2
- data/_sass/utilities/_layout.scss +11 -0
- data/assets/css/main.scss +2 -1
- data/assets/js/ai-chat.js +1 -0
- data/assets/js/author-profile.js +1 -0
- data/assets/js/auto-hide-nav.js +1 -0
- data/assets/js/back-to-top.js +1 -0
- data/assets/js/background-customizer.js +1 -0
- data/assets/js/code-copy.js +1 -0
- data/assets/js/halfmoon.js +23 -22
- data/assets/js/modules/navigation/gestures.js +1 -0
- data/assets/js/modules/navigation/keyboard.js +1 -0
- data/assets/js/modules/navigation/scroll-spy.js +1 -0
- data/assets/js/modules/navigation/sidebar-state.js +1 -0
- data/assets/js/modules/navigation/sidebar-visibility.js +1 -0
- data/assets/js/modules/navigation/smooth-scroll.js +1 -0
- data/assets/js/modules/theme/appearance.js +52 -16
- data/assets/js/nanobar-init.js +1 -0
- data/assets/js/navigation.js +1 -0
- data/assets/js/obsidian-graph.js +1 -0
- data/assets/js/obsidian-wiki-links.js +1 -0
- data/assets/js/palette-generator.js +1 -0
- data/assets/js/particles-source.js +1 -0
- data/assets/js/search-modal.js +1 -0
- data/assets/js/setup-wizard.js +1 -0
- data/assets/js/share-actions.js +1 -0
- data/assets/js/skin-editor.js +1 -0
- data/assets/js/table-copy.js +1 -0
- data/assets/js/theme-customizer.js +1 -0
- data/scripts/analyze-commits.sh +1 -0
- data/scripts/bin/giscus-discussions +1 -0
- data/scripts/bin/install +1 -0
- data/scripts/bin/validate +1 -0
- data/scripts/build +1 -0
- data/scripts/content-review.rb +1 -0
- data/scripts/convert-notebooks.sh +4 -3
- data/scripts/fix-markdown-format.sh +1 -0
- data/scripts/generate-preview-images.sh +1 -0
- data/scripts/generate-roadmap.sh +1 -0
- data/scripts/install-preview-generator.sh +1 -0
- data/scripts/release +1 -0
- data/scripts/requirements.txt +11 -0
- data/scripts/setup.sh +1 -0
- data/scripts/tag-features +136 -0
- data/scripts/validate-features.rb +7 -0
- data/scripts/vendor-install.sh +1 -0
- metadata +8 -3
- data/_plugins/admin_page_urls.rb +0 -16
data/assets/js/halfmoon.js
CHANGED
|
@@ -30,36 +30,37 @@
|
|
|
30
30
|
setTheme(getPreferredTheme())
|
|
31
31
|
|
|
32
32
|
const showActiveTheme = (theme, focus = false) => {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
if (!themeSwitcher) {
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const themeSwitcherText = document.querySelector('#bd-theme-text')
|
|
40
|
-
const activeThemeIcon = document.querySelector('.theme-icon-active use')
|
|
41
|
-
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
|
|
42
|
-
const themeIconUse = btnToActive?.querySelector('svg use')
|
|
43
|
-
if (!btnToActive || !themeIconUse || !activeThemeIcon) {
|
|
33
|
+
const activeButtons = document.querySelectorAll(`[data-bs-theme-value="${theme}"]`)
|
|
34
|
+
if (!activeButtons.length) {
|
|
44
35
|
return
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
// The control can render more than once per page (settings offcanvas +
|
|
39
|
+
// theme-controls-bar), so sync active state across every instance.
|
|
49
40
|
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
|
|
50
41
|
element.classList.remove('active')
|
|
51
42
|
element.setAttribute('aria-pressed', 'false')
|
|
52
43
|
})
|
|
44
|
+
activeButtons.forEach(element => {
|
|
45
|
+
element.classList.add('active')
|
|
46
|
+
element.setAttribute('aria-pressed', 'true')
|
|
47
|
+
})
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
// Legacy dropdown affordances (#bd-theme toggle with a mirrored icon) —
|
|
50
|
+
// updated only when that markup is present.
|
|
51
|
+
const themeSwitcher = document.querySelector('#bd-theme')
|
|
52
|
+
const activeThemeIcon = document.querySelector('.theme-icon-active use')
|
|
53
|
+
const themeIconUse = activeButtons[0].querySelector('svg use')
|
|
54
|
+
if (activeThemeIcon && themeIconUse) {
|
|
55
|
+
activeThemeIcon.setAttribute('href', themeIconUse.getAttribute('href'))
|
|
56
|
+
}
|
|
57
|
+
if (themeSwitcher) {
|
|
58
|
+
const themeSwitcherText = document.querySelector('#bd-theme-text')
|
|
59
|
+
const baseLabel = themeSwitcherText?.textContent?.trim() || 'Toggle theme'
|
|
60
|
+
themeSwitcher.setAttribute('aria-label', `${baseLabel} (${theme})`)
|
|
61
|
+
if (focus) {
|
|
62
|
+
themeSwitcher.focus()
|
|
63
|
+
}
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Feature: ZER0-031
|
|
1
2
|
/**
|
|
2
3
|
* ===================================================================
|
|
3
4
|
* APPEARANCE — Runtime theme controls (color mode + primary color)
|
|
@@ -11,13 +12,20 @@
|
|
|
11
12
|
*
|
|
12
13
|
* - Color mode buttons (light / dark / auto) — writes data-bs-theme
|
|
13
14
|
* to <html> and persists to localStorage["theme"] (compatible with
|
|
14
|
-
* halfmoon.js so both stay in sync).
|
|
15
|
+
* halfmoon.js so both stay in sync). Skipped in the settings
|
|
16
|
+
* offcanvas (compact mode), where the halfmoon segmented control
|
|
17
|
+
* directly above already owns color mode.
|
|
15
18
|
*
|
|
16
19
|
* - Primary color picker — writes to localStorage["zer0-appearance"]
|
|
17
20
|
* and updates --zer0-color-primary live so every component that uses
|
|
18
21
|
* the token (buttons, callouts, FABs, focus rings) reflects the
|
|
19
22
|
* change without a reload. Cleared by the Reset button.
|
|
20
23
|
*
|
|
24
|
+
* Host resolution: an explicit [data-appearance-panel-host] (e.g. the
|
|
25
|
+
* theme-preview sidebar) gets the full panel; otherwise the panel
|
|
26
|
+
* mounts compactly into #zer0AppearanceSlot inside the Settings
|
|
27
|
+
* offcanvas Appearance tab (legacy fallback: the offcanvas body).
|
|
28
|
+
*
|
|
21
29
|
* Initial paint is already handled by the script emitted in
|
|
22
30
|
* _includes/core/tokens-inline.html so there is no flash of default
|
|
23
31
|
* color when the page loads.
|
|
@@ -33,6 +41,7 @@
|
|
|
33
41
|
const STORAGE_KEY = 'zer0-appearance';
|
|
34
42
|
const THEME_KEY = 'theme';
|
|
35
43
|
const PANEL_HOST_SELECTOR = '[data-appearance-panel-host]';
|
|
44
|
+
const SLOT_HOST_SELECTOR = '#zer0AppearanceSlot';
|
|
36
45
|
const FALLBACK_HOST_SELECTOR = '#info-section .offcanvas-body';
|
|
37
46
|
|
|
38
47
|
function readPrefs() {
|
|
@@ -75,18 +84,15 @@
|
|
|
75
84
|
try { localStorage.setItem(THEME_KEY, mode); } catch (e) { /* ignore */ }
|
|
76
85
|
}
|
|
77
86
|
|
|
78
|
-
function buildPanel(host) {
|
|
87
|
+
function buildPanel(host, compact) {
|
|
79
88
|
const prefs = readPrefs();
|
|
80
89
|
const mode = resolveColorMode();
|
|
81
90
|
const primary = prefs.primary || getInitialPrimary();
|
|
82
91
|
|
|
83
92
|
const wrapper = document.createElement('section');
|
|
84
|
-
wrapper.className = 'zer0-appearance-panel border rounded p-3 mt-3';
|
|
85
93
|
wrapper.setAttribute('aria-labelledby', 'zer0-appearance-heading');
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'<i class="bi bi-palette me-2" aria-hidden="true"></i>Appearance' +
|
|
89
|
-
'</h3>' +
|
|
94
|
+
|
|
95
|
+
const modeGroupHtml =
|
|
90
96
|
'<div class="mb-3" role="group" aria-label="Color mode">' +
|
|
91
97
|
'<label class="form-label small text-body-secondary mb-2">Color mode</label>' +
|
|
92
98
|
'<div class="btn-group w-100" role="group">' +
|
|
@@ -97,18 +103,45 @@
|
|
|
97
103
|
'<button type="button" class="btn btn-outline-secondary btn-sm" data-mode="auto">' +
|
|
98
104
|
'<i class="bi bi-circle-half me-1" aria-hidden="true"></i>Auto</button>' +
|
|
99
105
|
'</div>' +
|
|
100
|
-
'</div>'
|
|
106
|
+
'</div>';
|
|
107
|
+
|
|
108
|
+
// In compact mode the section heading is the visible label, so the
|
|
109
|
+
// <label> stays for assistive tech but is visually hidden.
|
|
110
|
+
const pickerHtml = (labelClass) =>
|
|
101
111
|
'<div class="mb-3">' +
|
|
102
|
-
'<label for="zer0-appearance-primary" class="
|
|
112
|
+
'<label for="zer0-appearance-primary" class="' + labelClass + '">' +
|
|
103
113
|
'Primary color</label>' +
|
|
104
114
|
'<input type="color" id="zer0-appearance-primary" class="form-control form-control-color" ' +
|
|
105
115
|
'aria-describedby="zer0-appearance-primary-help">' +
|
|
106
116
|
'<div id="zer0-appearance-primary-help" class="form-text small">' +
|
|
107
117
|
'Overrides <code>--zer0-color-primary</code> across the theme.</div>' +
|
|
108
|
-
'</div>'
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
'</div>';
|
|
119
|
+
|
|
120
|
+
if (compact) {
|
|
121
|
+
// Settings offcanvas: color mode lives in the halfmoon control just
|
|
122
|
+
// above, so render only the primary-color section, styled to match
|
|
123
|
+
// the panel's other h6 section headings.
|
|
124
|
+
wrapper.className = 'zer0-appearance-panel mb-4';
|
|
125
|
+
wrapper.innerHTML =
|
|
126
|
+
'<h6 id="zer0-appearance-heading" class="text-body-secondary small text-uppercase fw-semibold mb-2">' +
|
|
127
|
+
'<i class="bi bi-eyedropper me-1" aria-hidden="true"></i>Primary Color' +
|
|
128
|
+
'</h6>' +
|
|
129
|
+
pickerHtml('visually-hidden') +
|
|
130
|
+
'<button type="button" class="btn btn-link btn-sm p-0" data-appearance-reset>' +
|
|
131
|
+
'<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset color' +
|
|
132
|
+
'</button>';
|
|
133
|
+
} else {
|
|
134
|
+
wrapper.className = 'zer0-appearance-panel border rounded p-3 mt-3';
|
|
135
|
+
wrapper.innerHTML =
|
|
136
|
+
'<h3 id="zer0-appearance-heading" class="h6 mb-3">' +
|
|
137
|
+
'<i class="bi bi-palette me-2" aria-hidden="true"></i>Appearance' +
|
|
138
|
+
'</h3>' +
|
|
139
|
+
modeGroupHtml +
|
|
140
|
+
pickerHtml('form-label small text-body-secondary') +
|
|
141
|
+
'<button type="button" class="btn btn-link btn-sm p-0" data-appearance-reset>' +
|
|
142
|
+
'<i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset to defaults' +
|
|
143
|
+
'</button>';
|
|
144
|
+
}
|
|
112
145
|
|
|
113
146
|
host.appendChild(wrapper);
|
|
114
147
|
|
|
@@ -116,7 +149,7 @@
|
|
|
116
149
|
const picker = wrapper.querySelector('#zer0-appearance-primary');
|
|
117
150
|
picker.value = hexishOrDefault(primary, '#007bff');
|
|
118
151
|
|
|
119
|
-
// Wire color-mode buttons
|
|
152
|
+
// Wire color-mode buttons (full panel only)
|
|
120
153
|
wrapper.querySelectorAll('[data-mode]').forEach((btn) => {
|
|
121
154
|
const m = btn.dataset.mode;
|
|
122
155
|
btn.setAttribute('aria-pressed', String(m === mode));
|
|
@@ -183,10 +216,13 @@
|
|
|
183
216
|
}
|
|
184
217
|
|
|
185
218
|
function mount() {
|
|
186
|
-
const
|
|
219
|
+
const explicitHost = document.querySelector(PANEL_HOST_SELECTOR);
|
|
220
|
+
const host = explicitHost
|
|
221
|
+
|| document.querySelector(SLOT_HOST_SELECTOR)
|
|
222
|
+
|| document.querySelector(FALLBACK_HOST_SELECTOR);
|
|
187
223
|
if (!host) return; // info-section not present on this page
|
|
188
224
|
if (host.querySelector('.zer0-appearance-panel')) return; // already mounted
|
|
189
|
-
buildPanel(host);
|
|
225
|
+
buildPanel(host, !explicitHost);
|
|
190
226
|
}
|
|
191
227
|
|
|
192
228
|
if (document.readyState === 'loading') {
|
data/assets/js/nanobar-init.js
CHANGED
data/assets/js/navigation.js
CHANGED
data/assets/js/obsidian-graph.js
CHANGED
data/assets/js/search-modal.js
CHANGED
data/assets/js/setup-wizard.js
CHANGED
data/assets/js/share-actions.js
CHANGED
data/assets/js/skin-editor.js
CHANGED
data/assets/js/table-copy.js
CHANGED
data/scripts/analyze-commits.sh
CHANGED
data/scripts/bin/install
CHANGED
data/scripts/bin/validate
CHANGED
data/scripts/build
CHANGED
data/scripts/content-review.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# Features: ZER0-012, ZER0-028
|
|
2
3
|
#
|
|
3
4
|
# Script Name: convert-notebooks.sh
|
|
4
5
|
# Description: Converts Jupyter notebooks (.ipynb) to Jekyll-compatible Markdown
|
|
@@ -339,7 +340,7 @@ clean_converted() {
|
|
|
339
340
|
rm -f "$md_file"
|
|
340
341
|
info "Removed: $md_file"
|
|
341
342
|
fi
|
|
342
|
-
((count
|
|
343
|
+
count=$((count + 1))
|
|
343
344
|
fi
|
|
344
345
|
done < <(find "$OUTPUT_DIR" -name "*.md" -print0 2>/dev/null)
|
|
345
346
|
|
|
@@ -364,7 +365,7 @@ list_notebooks() {
|
|
|
364
365
|
else
|
|
365
366
|
while IFS= read -r -d '' notebook_file; do
|
|
366
367
|
echo "$notebook_file"
|
|
367
|
-
((count
|
|
368
|
+
count=$((count + 1))
|
|
368
369
|
done < <(find "$NOTEBOOKS_DIR" -name "*.ipynb" -print0 2>/dev/null)
|
|
369
370
|
fi
|
|
370
371
|
|
|
@@ -400,7 +401,7 @@ main() {
|
|
|
400
401
|
local count=0
|
|
401
402
|
while IFS= read -r -d '' notebook_file; do
|
|
402
403
|
convert_notebook "$notebook_file"
|
|
403
|
-
((count
|
|
404
|
+
count=$((count + 1))
|
|
404
405
|
done < <(find "$NOTEBOOKS_DIR" -name "*.ipynb" -print0 2>/dev/null)
|
|
405
406
|
|
|
406
407
|
if [[ $count -eq 0 ]]; then
|
data/scripts/generate-roadmap.sh
CHANGED
data/scripts/release
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Python toolchain for repository automation scripts.
|
|
2
|
+
#
|
|
3
|
+
# Currently the notebook-conversion pipeline: scripts/convert-notebooks.sh and
|
|
4
|
+
# .github/workflows/convert-notebooks.yml use nbconvert to render
|
|
5
|
+
# pages/_notebooks/*.ipynb into Jekyll-compatible Markdown.
|
|
6
|
+
#
|
|
7
|
+
# This file also acts as the pip cache key for actions/setup-python in that
|
|
8
|
+
# workflow (see `cache-dependency-path`). Keep it in sync with the toolchain the
|
|
9
|
+
# scripts import; a missing file here makes `cache: pip` hard-fail the run.
|
|
10
|
+
jupyter
|
|
11
|
+
nbconvert
|
data/scripts/setup.sh
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ---------------------------------------------------------------------------
|
|
5
|
+
# tag-features — reverse-traceability enforcement for the feature registry.
|
|
6
|
+
#
|
|
7
|
+
# Every source file a feature lists under `references:` carries a top-of-file
|
|
8
|
+
# `Feature: ZER0-NNN` comment, so the link runs both ways (registry -> code and
|
|
9
|
+
# code -> registry). This tool is the single source of truth for that rule:
|
|
10
|
+
#
|
|
11
|
+
# ruby scripts/tag-features # --check (default): exit 1 + list any
|
|
12
|
+
# # referenced source file missing its tag
|
|
13
|
+
# ruby scripts/tag-features --write # insert the missing tags (idempotent,
|
|
14
|
+
# # line-ending-preserving)
|
|
15
|
+
#
|
|
16
|
+
# The `features` test suite runs `--check`, so a new feature that forgets to tag
|
|
17
|
+
# its files fails CI. Governance: .github/instructions/features.instructions.md.
|
|
18
|
+
#
|
|
19
|
+
# Scope (mirrors what a feature's references implement, and only what can carry a
|
|
20
|
+
# comment): .rb .js .mjs .scss .sh .html .yml, plus Dockerfile/Makefile and the
|
|
21
|
+
# scripts/{bin/*,build,release} wrappers. Excluded by design: markdown docs,
|
|
22
|
+
# JSON (no comment syntax), vendored/minified third-party libs, directory refs,
|
|
23
|
+
# and _config.yml (it has an exact-copy admin mirror enforced elsewhere).
|
|
24
|
+
#
|
|
25
|
+
# Portable across Ruby 2.6 (macOS system) and 3.x (CI/Docker).
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
require 'date'
|
|
29
|
+
require 'yaml'
|
|
30
|
+
|
|
31
|
+
MIRROR = '_data/features.yml'
|
|
32
|
+
MODE = ARGV.include?('--write') ? :write : :check
|
|
33
|
+
|
|
34
|
+
CODE_EXT = %w[.rb .js .mjs .scss .sh .html .yml].freeze
|
|
35
|
+
SLASH_EXT = %w[.js .mjs .scss].freeze
|
|
36
|
+
NOEXT_FILES = %w[docker/Dockerfile Makefile scripts/release scripts/build].freeze
|
|
37
|
+
EXCLUDE_FILES = %w[_config.yml assets/js/particles.js assets/js/halfmoon.js].freeze
|
|
38
|
+
|
|
39
|
+
def load_yaml(path)
|
|
40
|
+
YAML.load_file(path, permitted_classes: [Date, Time], aliases: true)
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
YAML.load_file(path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ref_paths(refs)
|
|
46
|
+
return [] unless refs.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
refs.values.flat_map { |v| v.is_a?(Array) ? v : [v] }.select { |v| v.is_a?(String) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def vendored?(path)
|
|
52
|
+
path.include?('/vendor/') || path.end_with?('.min.js') || EXCLUDE_FILES.include?(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Which referenced paths must carry a tag.
|
|
56
|
+
def taggable?(path)
|
|
57
|
+
return false if path.end_with?('/') || vendored?(path)
|
|
58
|
+
|
|
59
|
+
ext = File.extname(path)
|
|
60
|
+
return false if %w[.md .json].include?(ext)
|
|
61
|
+
|
|
62
|
+
CODE_EXT.include?(ext) || NOEXT_FILES.include?(path) || (path.start_with?('scripts/bin/') && ext.empty?)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def tag_text(ids)
|
|
66
|
+
(ids.length == 1 ? 'Feature: ' : 'Features: ') + ids.join(', ')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def already_tagged?(raw, ids)
|
|
70
|
+
head = raw[0, 500].to_s
|
|
71
|
+
ids.any? { |id| head.include?(id) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Insert the tag with type-aware placement, preserving the file's line endings.
|
|
75
|
+
def insert_tag(path, ids)
|
|
76
|
+
raw = File.binread(path).force_encoding('UTF-8')
|
|
77
|
+
return :skipped if already_tagged?(raw, ids)
|
|
78
|
+
|
|
79
|
+
nl = raw.include?("\r\n") ? "\r\n" : "\n"
|
|
80
|
+
ext = File.extname(path)
|
|
81
|
+
tag = tag_text(ids)
|
|
82
|
+
lines = raw.each_line.to_a
|
|
83
|
+
|
|
84
|
+
if ext == '.html' && raw.lstrip.start_with?('---')
|
|
85
|
+
idx = lines.index { |l| l.strip == '---' } || 0
|
|
86
|
+
lines.insert(idx + 1, "# #{tag}#{nl}") # invisible YAML front-matter comment
|
|
87
|
+
elsif ext == '.html'
|
|
88
|
+
lines.unshift("{% comment %} #{tag} {% endcomment %}#{nl}") # stripped at build
|
|
89
|
+
else
|
|
90
|
+
prefix = SLASH_EXT.include?(ext) ? '//' : '#'
|
|
91
|
+
line = "#{prefix} #{tag}#{nl}"
|
|
92
|
+
first = lines[0].to_s
|
|
93
|
+
if first.start_with?('#!') || first.lstrip.start_with?('# syntax=') || first.lstrip.start_with?('# escape=')
|
|
94
|
+
lines.insert(1, line) # keep shebang / Dockerfile directive on line 1
|
|
95
|
+
else
|
|
96
|
+
lines.unshift(line)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
File.binwrite(path, lines.join)
|
|
101
|
+
:written
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
data = load_yaml(MIRROR)
|
|
105
|
+
feats = data && data['features']
|
|
106
|
+
abort 'tag-features: could not read features registry' unless feats.is_a?(Array)
|
|
107
|
+
|
|
108
|
+
by_file = Hash.new { |h, k| h[k] = [] }
|
|
109
|
+
feats.each do |f|
|
|
110
|
+
next if f['implemented'] == false # removed features keep (absent) refs; nothing to tag
|
|
111
|
+
|
|
112
|
+
ref_paths(f['references']).each do |p|
|
|
113
|
+
by_file[p] << f['id'] if taggable?(p) && File.exist?(p)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if MODE == :write
|
|
118
|
+
written = 0
|
|
119
|
+
by_file.each { |path, ids| written += 1 if insert_tag(path, ids.uniq.sort) == :written }
|
|
120
|
+
puts "tag-features: wrote #{written} tag(s); #{by_file.size - written} already current"
|
|
121
|
+
exit 0
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
missing = by_file.reject do |path, ids|
|
|
125
|
+
already_tagged?(File.binread(path).force_encoding('UTF-8'), ids.uniq.sort)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if missing.empty?
|
|
129
|
+
puts "tag-features: all #{by_file.size} referenced source files carry their Feature tag"
|
|
130
|
+
exit 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
warn "tag-features: #{missing.length} referenced source file(s) missing a `Feature: ZER0-NNN` comment:"
|
|
134
|
+
missing.each { |path, ids| warn " - #{path} (needs #{ids.uniq.sort.join(', ')})" }
|
|
135
|
+
warn 'Fix: run `ruby scripts/tag-features --write` (or add the comment by hand).'
|
|
136
|
+
exit 1
|
|
@@ -81,6 +81,13 @@ feats.each_with_index do |f, i|
|
|
|
81
81
|
die "#{id}: required field `#{key}` is missing/empty" if val.nil? || (val.respond_to?(:empty?) && val.empty?)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Title/description are rendered UNwrapped into HTML on /features/; a raw
|
|
85
|
+
# angle bracket (e.g. "/authors/<key>/") becomes a stray tag that swallows
|
|
86
|
+
# every following card. Keep them plain prose.
|
|
87
|
+
%w[title description].each do |key|
|
|
88
|
+
die "#{id}: `#{key}` must not contain raw `<`/`>` (breaks HTML on /features/ — use :key or <)" if f[key].to_s =~ /[<>]/
|
|
89
|
+
end
|
|
90
|
+
|
|
84
91
|
die "#{id}: id must match ZER0-NNN" unless f['id'] =~ /\AZER0-\d{3}\z/
|
|
85
92
|
die "#{id}: duplicate id" if seen[f['id']]
|
|
86
93
|
seen[f['id']] = true
|