jekyll-theme-zer0 0.22.0 → 0.22.19
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 +236 -0
- data/README.md +66 -19
- data/_data/navigation/admin.yml +53 -0
- data/_data/theme_backgrounds.yml +121 -0
- data/_includes/components/admin-tabs.html +59 -0
- data/_includes/components/analytics-dashboard.html +232 -0
- data/_includes/components/background-customizer.html +159 -0
- data/_includes/components/background-settings.html +137 -0
- data/_includes/components/collection-manager.html +151 -0
- data/_includes/components/component-showcase.html +452 -0
- data/_includes/components/config-editor.html +207 -0
- data/_includes/components/config-viewer.html +479 -0
- data/_includes/components/env-dashboard.html +154 -0
- data/_includes/components/feature-card.html +94 -0
- data/_includes/components/info-section.html +172 -149
- data/_includes/components/js-cdn.html +4 -1
- data/_includes/components/nav-editor.html +99 -0
- data/_includes/components/setup-banner.html +28 -0
- data/_includes/components/setup-check.html +53 -0
- data/_includes/components/svg-background.html +42 -0
- data/_includes/components/theme-customizer.html +46 -0
- data/_includes/content/seo.html +68 -135
- data/_includes/core/footer.html +1 -1
- data/_includes/core/head.html +3 -2
- data/_includes/core/header.html +14 -7
- data/_includes/landing/landing-install-cards.html +18 -7
- data/_includes/navigation/admin-nav.html +95 -0
- data/_includes/navigation/navbar.html +43 -5
- data/_includes/navigation/sidebar-left.html +1 -1
- data/_includes/setup/wizard.html +330 -0
- data/_layouts/admin.html +166 -0
- data/_layouts/landing.html +23 -9
- data/_layouts/root.html +12 -6
- data/_layouts/setup.html +73 -0
- data/_plugins/preview_image_generator.rb +26 -12
- data/_sass/core/_navbar.scss +2 -2
- data/_sass/custom.scss +28 -6
- data/_sass/theme/_background-mixins.scss +95 -0
- data/_sass/theme/_backgrounds.scss +156 -0
- data/_sass/theme/_color-modes.scss +2 -1
- data/assets/backgrounds/gradients/air.svg +15 -0
- data/assets/backgrounds/gradients/aqua.svg +15 -0
- data/assets/backgrounds/gradients/contrast.svg +15 -0
- data/assets/backgrounds/gradients/dark.svg +15 -0
- data/assets/backgrounds/gradients/dirt.svg +15 -0
- data/assets/backgrounds/gradients/mint.svg +15 -0
- data/assets/backgrounds/gradients/neon.svg +15 -0
- data/assets/backgrounds/gradients/plum.svg +15 -0
- data/assets/backgrounds/gradients/sunrise.svg +15 -0
- data/assets/backgrounds/noise/air.svg +8 -0
- data/assets/backgrounds/noise/aqua.svg +8 -0
- data/assets/backgrounds/noise/contrast.svg +8 -0
- data/assets/backgrounds/noise/dark.svg +8 -0
- data/assets/backgrounds/noise/dirt.svg +8 -0
- data/assets/backgrounds/noise/mint.svg +8 -0
- data/assets/backgrounds/noise/neon.svg +8 -0
- data/assets/backgrounds/noise/plum.svg +8 -0
- data/assets/backgrounds/noise/sunrise.svg +8 -0
- data/assets/backgrounds/patterns/air.svg +7 -0
- data/assets/backgrounds/patterns/aqua.svg +7 -0
- data/assets/backgrounds/patterns/contrast.svg +4 -0
- data/assets/backgrounds/patterns/dark.svg +5 -0
- data/assets/backgrounds/patterns/dirt.svg +5 -0
- data/assets/backgrounds/patterns/mint.svg +6 -0
- data/assets/backgrounds/patterns/neon.svg +6 -0
- data/assets/backgrounds/patterns/plum.svg +6 -0
- data/assets/backgrounds/patterns/sunrise.svg +5 -0
- data/assets/js/background-customizer.js +73 -0
- data/assets/js/code-copy.js +18 -47
- data/assets/js/config-utility.js +307 -0
- data/assets/js/nav-editor.js +39 -0
- data/assets/js/palette-generator.js +415 -0
- data/assets/js/search-modal.js +31 -11
- data/assets/js/setup-wizard.js +306 -0
- data/assets/js/skin-editor.js +645 -0
- data/assets/js/theme-customizer.js +102 -0
- data/assets/js/ui-enhancements.js +15 -24
- data/assets/vendor/bootstrap/css/bootstrap.min.css +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +1 -0
- data/scripts/README.md +45 -0
- data/scripts/features/generate-preview-images +297 -7
- data/scripts/features/install-preview-generator +51 -33
- data/scripts/fork-cleanup.sh +92 -19
- data/scripts/github-setup.sh +284 -0
- data/scripts/init_setup.sh +0 -1
- data/scripts/lib/frontmatter.sh +543 -0
- data/scripts/lib/migrate.sh +265 -0
- data/scripts/lib/preview_generator.py +607 -32
- data/scripts/lint-pages +505 -0
- data/scripts/migrate.sh +201 -0
- data/scripts/platform/setup-linux.sh +244 -0
- data/scripts/platform/setup-macos.sh +187 -0
- data/scripts/platform/setup-wsl.sh +196 -0
- metadata +71 -6
data/scripts/lint-pages
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# =========================================================================
|
|
4
|
+
# lint-pages — Config-driven frontmatter validator
|
|
5
|
+
# =========================================================================
|
|
6
|
+
# Validates frontmatter across all Jekyll collections using schemas
|
|
7
|
+
# defined in .github/config/frontmatter_schema.yml
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# scripts/lint-pages # Validate all pages (warn mode)
|
|
11
|
+
# scripts/lint-pages --strict # Fail on any violation
|
|
12
|
+
# scripts/lint-pages --fix # Auto-fix safe violations
|
|
13
|
+
# scripts/lint-pages --fix --dry-run # Preview fixes without modifying
|
|
14
|
+
# scripts/lint-pages --report # Output structured report
|
|
15
|
+
# scripts/lint-pages --collection posts # Validate only posts
|
|
16
|
+
# scripts/lint-pages --verbose # Detailed output
|
|
17
|
+
# scripts/lint-pages --schema PATH # Use alternate schema
|
|
18
|
+
#
|
|
19
|
+
# All validation rules come from config files — nothing is hardcoded.
|
|
20
|
+
# =========================================================================
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
source "$SCRIPT_DIR/lib/common.sh"
|
|
26
|
+
source "$SCRIPT_DIR/lib/frontmatter.sh"
|
|
27
|
+
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
# Defaults (overridable via environment or flags)
|
|
30
|
+
# -------------------------------------------------------------------------
|
|
31
|
+
SCHEMA_PATH="${FRONTMATTER_SCHEMA_PATH:-.github/config/frontmatter_schema.yml}"
|
|
32
|
+
RULES_PATH="${CONTENT_RULES_PATH:-.github/config/content_rules.yml}"
|
|
33
|
+
MODE="warn" # warn | strict
|
|
34
|
+
FIX_MODE=false
|
|
35
|
+
REPORT_MODE=false
|
|
36
|
+
TARGET_COLLECTION=""
|
|
37
|
+
REPO_ROOT="$(get_repo_root)"
|
|
38
|
+
|
|
39
|
+
# Counters
|
|
40
|
+
TOTAL_FILES=0
|
|
41
|
+
TOTAL_ERRORS=0
|
|
42
|
+
TOTAL_WARNINGS=0
|
|
43
|
+
TOTAL_FIXED=0
|
|
44
|
+
TOTAL_SKIPPED=0
|
|
45
|
+
|
|
46
|
+
# Schema cache (populated once per run by load_schema_cache)
|
|
47
|
+
SCHEMA_DATETIME_PATTERN=""
|
|
48
|
+
SCHEMA_DRAFT_TYPE=""
|
|
49
|
+
SCHEMA_CATEGORY_CASING=""
|
|
50
|
+
SCHEMA_CANONICAL_FIELDS=""
|
|
51
|
+
|
|
52
|
+
# Per-collection cache (populated by ensure_collection_cache)
|
|
53
|
+
CACHED_COLLECTION=""
|
|
54
|
+
CACHED_REQUIRED_FIELDS=""
|
|
55
|
+
CACHED_ALLOWED_LAYOUTS=""
|
|
56
|
+
|
|
57
|
+
# -------------------------------------------------------------------------
|
|
58
|
+
# CLI argument parsing
|
|
59
|
+
# -------------------------------------------------------------------------
|
|
60
|
+
parse_args() {
|
|
61
|
+
while [[ $# -gt 0 ]]; do
|
|
62
|
+
case "$1" in
|
|
63
|
+
--strict) MODE="strict"; shift ;;
|
|
64
|
+
--warn) MODE="warn"; shift ;;
|
|
65
|
+
--fix) FIX_MODE=true; shift ;;
|
|
66
|
+
--report) REPORT_MODE=true; shift ;;
|
|
67
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
68
|
+
--verbose|-v) VERBOSE=true; shift ;;
|
|
69
|
+
--collection) TARGET_COLLECTION="$2"; shift 2 ;;
|
|
70
|
+
--schema) SCHEMA_PATH="$2"; shift 2 ;;
|
|
71
|
+
--rules) RULES_PATH="$2"; shift 2 ;;
|
|
72
|
+
--help|-h) show_help; exit 0 ;;
|
|
73
|
+
*) error "Unknown option: $1" ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
show_help() {
|
|
79
|
+
cat << 'EOF'
|
|
80
|
+
lint-pages — Config-driven frontmatter validator
|
|
81
|
+
|
|
82
|
+
USAGE:
|
|
83
|
+
scripts/lint-pages [OPTIONS]
|
|
84
|
+
|
|
85
|
+
OPTIONS:
|
|
86
|
+
--strict Exit non-zero on any violation
|
|
87
|
+
--warn Print warnings only (default)
|
|
88
|
+
--fix Auto-fix safe violations (dates, drafts, field renames)
|
|
89
|
+
--report Output structured report summary
|
|
90
|
+
--dry-run Preview fixes without modifying files
|
|
91
|
+
--verbose, -v Detailed output
|
|
92
|
+
--collection NAME Validate only the named collection
|
|
93
|
+
--schema PATH Path to schema YAML (default: .github/config/frontmatter_schema.yml)
|
|
94
|
+
--rules PATH Path to content rules YAML (default: .github/config/content_rules.yml)
|
|
95
|
+
--help, -h Show this help
|
|
96
|
+
|
|
97
|
+
ENVIRONMENT VARIABLES:
|
|
98
|
+
FRONTMATTER_SCHEMA_PATH Alternate schema path
|
|
99
|
+
CONTENT_RULES_PATH Alternate content rules path
|
|
100
|
+
FRONTMATTER_STRICT Set to "true" for strict mode
|
|
101
|
+
|
|
102
|
+
EXAMPLES:
|
|
103
|
+
scripts/lint-pages --strict # CI: fail on violations
|
|
104
|
+
scripts/lint-pages --fix --dry-run # Preview auto-fixes
|
|
105
|
+
scripts/lint-pages --collection posts --verbose # Debug post frontmatter
|
|
106
|
+
FRONTMATTER_SCHEMA_PATH=test.yml scripts/lint-pages # Custom schema
|
|
107
|
+
EOF
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# -------------------------------------------------------------------------
|
|
111
|
+
# Violation reporting
|
|
112
|
+
# -------------------------------------------------------------------------
|
|
113
|
+
report_violation() {
|
|
114
|
+
local filepath="$1"
|
|
115
|
+
local field="$2"
|
|
116
|
+
local message="$3"
|
|
117
|
+
local severity="${4:-warning}"
|
|
118
|
+
|
|
119
|
+
local rel_path
|
|
120
|
+
rel_path="${filepath#$REPO_ROOT/}"
|
|
121
|
+
|
|
122
|
+
if [[ "$severity" == "error" ]]; then
|
|
123
|
+
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
|
|
124
|
+
echo -e "${RED} ✗ ERROR${NC} [$rel_path] $field: $message"
|
|
125
|
+
else
|
|
126
|
+
TOTAL_WARNINGS=$((TOTAL_WARNINGS + 1))
|
|
127
|
+
echo -e "${YELLOW} ⚠ WARN${NC} [$rel_path] $field: $message"
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
report_fix() {
|
|
132
|
+
local filepath="$1"
|
|
133
|
+
local field="$2"
|
|
134
|
+
local message="$3"
|
|
135
|
+
|
|
136
|
+
local rel_path
|
|
137
|
+
rel_path="${filepath#$REPO_ROOT/}"
|
|
138
|
+
|
|
139
|
+
TOTAL_FIXED=$((TOTAL_FIXED + 1))
|
|
140
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
141
|
+
echo -e "${CYAN} ⬡ DRY${NC} [$rel_path] $field: would fix — $message"
|
|
142
|
+
else
|
|
143
|
+
echo -e "${GREEN} ✓ FIX${NC} [$rel_path] $field: $message"
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# -------------------------------------------------------------------------
|
|
148
|
+
# Schema caching (one Ruby invocation per run instead of per-file)
|
|
149
|
+
# -------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
# Load global schema settings once into shell variables.
|
|
152
|
+
load_schema_cache() {
|
|
153
|
+
local schema_full_path="$1"
|
|
154
|
+
local cache
|
|
155
|
+
cache="$(ruby -ryaml -e "
|
|
156
|
+
s = YAML.load_file('$schema_full_path')
|
|
157
|
+
puts 'DATETIME_PATTERN=' + (s.dig('field_types','datetime','pattern') || '').to_s
|
|
158
|
+
puts 'DRAFT_TYPE=' + (s.dig('global','draft_type') || '').to_s
|
|
159
|
+
puts 'CATEGORY_CASING=' + (s.dig('global','category_casing') || '').to_s
|
|
160
|
+
puts '---CANONICAL---'
|
|
161
|
+
(s.dig('global','canonical_fields') || {}).each { |k,v| puts \"#{k}=#{v}\" }
|
|
162
|
+
" 2>/dev/null)"
|
|
163
|
+
|
|
164
|
+
SCHEMA_DATETIME_PATTERN="$(echo "$cache" | sed -n 's/^DATETIME_PATTERN=//p')"
|
|
165
|
+
SCHEMA_DRAFT_TYPE="$(echo "$cache" | sed -n 's/^DRAFT_TYPE=//p')"
|
|
166
|
+
SCHEMA_CATEGORY_CASING="$(echo "$cache" | sed -n 's/^CATEGORY_CASING=//p')"
|
|
167
|
+
SCHEMA_CANONICAL_FIELDS="$(echo "$cache" | awk '/^---CANONICAL---$/{flag=1;next} flag')"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Load per-collection schema once on first use (memoized).
|
|
171
|
+
ensure_collection_cache() {
|
|
172
|
+
local collection="$1"
|
|
173
|
+
[[ "$CACHED_COLLECTION" == "$collection" ]] && return 0
|
|
174
|
+
|
|
175
|
+
local schema_full_path="$REPO_ROOT/$SCHEMA_PATH"
|
|
176
|
+
local cache
|
|
177
|
+
cache="$(ruby -ryaml -e "
|
|
178
|
+
s = YAML.load_file('$schema_full_path')
|
|
179
|
+
global_req = s.dig('global','required_fields') || []
|
|
180
|
+
col_req = s.dig('collections','$collection','required') || []
|
|
181
|
+
layouts = s.dig('collections','$collection','layout','allowed') || []
|
|
182
|
+
puts '---REQUIRED---'
|
|
183
|
+
(global_req | col_req).each { |f| puts f }
|
|
184
|
+
puts '---LAYOUTS---'
|
|
185
|
+
layouts.each { |l| puts l }
|
|
186
|
+
" 2>/dev/null)"
|
|
187
|
+
|
|
188
|
+
CACHED_REQUIRED_FIELDS="$(echo "$cache" | awk '/^---REQUIRED---$/{f=1;next} /^---LAYOUTS---$/{f=0} f')"
|
|
189
|
+
CACHED_ALLOWED_LAYOUTS="$(echo "$cache" | awk '/^---LAYOUTS---$/{f=1;next} f')"
|
|
190
|
+
CACHED_COLLECTION="$collection"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# -------------------------------------------------------------------------
|
|
194
|
+
# Per-file validation
|
|
195
|
+
# -------------------------------------------------------------------------
|
|
196
|
+
validate_file() {
|
|
197
|
+
local filepath="$1"
|
|
198
|
+
local collection="$2"
|
|
199
|
+
local severity="warning"
|
|
200
|
+
|
|
201
|
+
[[ "$MODE" == "strict" ]] && severity="error"
|
|
202
|
+
|
|
203
|
+
TOTAL_FILES=$((TOTAL_FILES + 1))
|
|
204
|
+
|
|
205
|
+
debug "Validating: ${filepath#$REPO_ROOT/} (collection: $collection)"
|
|
206
|
+
|
|
207
|
+
# Ensure per-collection schema cache is loaded
|
|
208
|
+
ensure_collection_cache "$collection"
|
|
209
|
+
|
|
210
|
+
# Single-pass parse: emits FIELD/LAYOUT/DATE/LASTMOD/DRAFT/CATEGORY lines
|
|
211
|
+
local parsed
|
|
212
|
+
parsed="$(parse_file_frontmatter_all "$filepath")"
|
|
213
|
+
|
|
214
|
+
if echo "$parsed" | head -1 | grep -q "^__NO_FRONTMATTER__$"; then
|
|
215
|
+
report_violation "$filepath" "frontmatter" "No frontmatter block found" "$severity"
|
|
216
|
+
return
|
|
217
|
+
fi
|
|
218
|
+
if echo "$parsed" | head -1 | grep -q "^__PARSE_ERROR__"; then
|
|
219
|
+
report_violation "$filepath" "frontmatter" "YAML parse error" "$severity"
|
|
220
|
+
return
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
local fields_present
|
|
224
|
+
fields_present="$(echo "$parsed" | sed -n 's/^FIELD://p')"
|
|
225
|
+
|
|
226
|
+
# --- Deprecated field renames (run FIRST so renames satisfy required-field checks) ---
|
|
227
|
+
local renamed_any=false
|
|
228
|
+
while IFS= read -r mapping; do
|
|
229
|
+
[[ -z "$mapping" ]] && continue
|
|
230
|
+
local deprecated_name="${mapping%%=*}"
|
|
231
|
+
local canonical_name="${mapping#*=}"
|
|
232
|
+
if echo "$fields_present" | grep -qx "$deprecated_name"; then
|
|
233
|
+
if [[ "$FIX_MODE" == "true" ]]; then
|
|
234
|
+
if echo "$fields_present" | grep -qx "$canonical_name"; then
|
|
235
|
+
report_violation "$filepath" "$deprecated_name" "Deprecated field — '$canonical_name' already present, manual cleanup needed" "$severity"
|
|
236
|
+
else
|
|
237
|
+
fix_rename_field "$filepath" "$deprecated_name" "$canonical_name"
|
|
238
|
+
renamed_any=true
|
|
239
|
+
fi
|
|
240
|
+
else
|
|
241
|
+
report_violation "$filepath" "$deprecated_name" "Deprecated field — use '$canonical_name' instead" "$severity"
|
|
242
|
+
fi
|
|
243
|
+
fi
|
|
244
|
+
done <<< "$SCHEMA_CANONICAL_FIELDS"
|
|
245
|
+
|
|
246
|
+
# Re-parse if any rename happened so subsequent checks see post-rename state
|
|
247
|
+
if [[ "$renamed_any" == "true" ]] && [[ "$DRY_RUN" != "true" ]]; then
|
|
248
|
+
parsed="$(parse_file_frontmatter_all "$filepath")"
|
|
249
|
+
fields_present="$(echo "$parsed" | sed -n 's/^FIELD://p')"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# --- Required field checks ---
|
|
253
|
+
while IFS= read -r req_field; do
|
|
254
|
+
[[ -z "$req_field" ]] && continue
|
|
255
|
+
if ! echo "$fields_present" | grep -qx "$req_field"; then
|
|
256
|
+
if [[ "$req_field" == "lastmod" ]] && [[ "$FIX_MODE" == "true" ]]; then
|
|
257
|
+
fix_add_lastmod "$filepath"
|
|
258
|
+
else
|
|
259
|
+
report_violation "$filepath" "$req_field" "Required field missing" "$severity"
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
done <<< "$CACHED_REQUIRED_FIELDS"
|
|
263
|
+
|
|
264
|
+
# --- Layout validation ---
|
|
265
|
+
local layout_value
|
|
266
|
+
layout_value="$(echo "$parsed" | sed -n 's/^LAYOUT://p' | head -1)"
|
|
267
|
+
if [[ -n "$layout_value" ]] && [[ -n "$CACHED_ALLOWED_LAYOUTS" ]]; then
|
|
268
|
+
if ! echo "$CACHED_ALLOWED_LAYOUTS" | grep -qx "$layout_value"; then
|
|
269
|
+
report_violation "$filepath" "layout" "Value '$layout_value' not in allowed: [$(echo "$CACHED_ALLOWED_LAYOUTS" | tr '\n' ',' | sed 's/,$//' )]" "$severity"
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
# --- Date format validation ---
|
|
274
|
+
if [[ -n "$SCHEMA_DATETIME_PATTERN" ]]; then
|
|
275
|
+
local date_field date_value date_tag
|
|
276
|
+
for date_field in date lastmod; do
|
|
277
|
+
date_tag="$(printf '%s' "$date_field" | tr '[:lower:]' '[:upper:]')"
|
|
278
|
+
date_value="$(echo "$parsed" | sed -n "s/^${date_tag}://p" | head -1)"
|
|
279
|
+
if [[ -n "$date_value" ]]; then
|
|
280
|
+
if ! validate_field_pattern "$date_value" "$SCHEMA_DATETIME_PATTERN"; then
|
|
281
|
+
if [[ "$FIX_MODE" == "true" ]]; then
|
|
282
|
+
fix_date_in_file "$filepath" "$date_field" "$date_value"
|
|
283
|
+
else
|
|
284
|
+
report_violation "$filepath" "$date_field" "Date format '$date_value' does not match pattern" "$severity"
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
fi
|
|
288
|
+
done
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# --- Draft field type validation ---
|
|
292
|
+
local draft_value
|
|
293
|
+
draft_value="$(echo "$parsed" | sed -n 's/^DRAFT://p' | head -1)"
|
|
294
|
+
if [[ -n "$draft_value" ]] && [[ "$SCHEMA_DRAFT_TYPE" == "boolean" ]]; then
|
|
295
|
+
if ! validate_boolean "$draft_value"; then
|
|
296
|
+
if [[ "$FIX_MODE" == "true" ]]; then
|
|
297
|
+
fix_draft_in_file "$filepath" "$draft_value"
|
|
298
|
+
else
|
|
299
|
+
report_violation "$filepath" "draft" "Expected boolean, got '$draft_value'" "$severity"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# --- Category casing validation ---
|
|
305
|
+
if [[ -n "$SCHEMA_CATEGORY_CASING" ]]; then
|
|
306
|
+
local cat_value
|
|
307
|
+
while IFS= read -r cat_value; do
|
|
308
|
+
[[ -z "$cat_value" ]] && continue
|
|
309
|
+
if ! validate_category_casing "$cat_value" "$SCHEMA_CATEGORY_CASING"; then
|
|
310
|
+
report_violation "$filepath" "categories" "Category '$cat_value' does not follow $SCHEMA_CATEGORY_CASING casing" "$severity"
|
|
311
|
+
fi
|
|
312
|
+
done < <(echo "$parsed" | sed -n 's/^CATEGORY://p')
|
|
313
|
+
fi
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# -------------------------------------------------------------------------
|
|
317
|
+
# Fix helpers
|
|
318
|
+
# -------------------------------------------------------------------------
|
|
319
|
+
fix_add_lastmod() {
|
|
320
|
+
local filepath="$1"
|
|
321
|
+
local today
|
|
322
|
+
today="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
|
|
323
|
+
|
|
324
|
+
report_fix "$filepath" "lastmod" "Adding lastmod: $today"
|
|
325
|
+
if [[ "$DRY_RUN" != "true" ]]; then
|
|
326
|
+
# Insert lastmod after the opening --- (portable across macOS & Linux)
|
|
327
|
+
perl -pi -e 'if (/^---\s*$/ && !$done) { $done=1; $_ .= "lastmod: '"$today"'\n" }' "$filepath"
|
|
328
|
+
fi
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fix_rename_field() {
|
|
332
|
+
local filepath="$1"
|
|
333
|
+
local old_name="$2"
|
|
334
|
+
local new_name="$3"
|
|
335
|
+
|
|
336
|
+
report_fix "$filepath" "$old_name" "Renaming to '$new_name'"
|
|
337
|
+
if [[ "$DRY_RUN" != "true" ]]; then
|
|
338
|
+
perl -pi -e "s/^\Q${old_name}\E:/${new_name}:/" "$filepath"
|
|
339
|
+
fi
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fix_date_in_file() {
|
|
343
|
+
local filepath="$1"
|
|
344
|
+
local field="$2"
|
|
345
|
+
local old_value="$3"
|
|
346
|
+
local new_value
|
|
347
|
+
new_value="$(fix_date_format "$old_value")"
|
|
348
|
+
|
|
349
|
+
if [[ "$new_value" != "$old_value" ]]; then
|
|
350
|
+
report_fix "$filepath" "$field" "Normalizing '$old_value' → '$new_value'"
|
|
351
|
+
if [[ "$DRY_RUN" != "true" ]]; then
|
|
352
|
+
perl -pi -e "s/^\Q${field}\E:.*/${field}: ${new_value}/" "$filepath"
|
|
353
|
+
fi
|
|
354
|
+
fi
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fix_draft_in_file() {
|
|
358
|
+
local filepath="$1"
|
|
359
|
+
local old_value="$2"
|
|
360
|
+
local new_value
|
|
361
|
+
new_value="$(fix_draft_to_boolean "$old_value")"
|
|
362
|
+
|
|
363
|
+
if [[ "$new_value" != "$old_value" ]]; then
|
|
364
|
+
report_fix "$filepath" "draft" "Converting '$old_value' → $new_value"
|
|
365
|
+
if [[ "$DRY_RUN" != "true" ]]; then
|
|
366
|
+
perl -pi -e "s/^draft:.*$/draft: ${new_value}/" "$filepath"
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# -------------------------------------------------------------------------
|
|
372
|
+
# Collection scanning
|
|
373
|
+
# -------------------------------------------------------------------------
|
|
374
|
+
scan_collection() {
|
|
375
|
+
local collection="$1"
|
|
376
|
+
local pattern
|
|
377
|
+
pattern="$(get_path_pattern "$collection" "$SCHEMA_PATH")"
|
|
378
|
+
|
|
379
|
+
if [[ -z "$pattern" ]]; then
|
|
380
|
+
warn "No path_pattern for collection: $collection"
|
|
381
|
+
return
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
info "Scanning collection: $collection ($pattern)"
|
|
385
|
+
|
|
386
|
+
# Use find to match the glob pattern
|
|
387
|
+
local search_dir="$REPO_ROOT"
|
|
388
|
+
local files_found=0
|
|
389
|
+
|
|
390
|
+
while IFS= read -r -d '' filepath; do
|
|
391
|
+
validate_file "$filepath" "$collection"
|
|
392
|
+
files_found=$((files_found + 1))
|
|
393
|
+
done < <(find "$REPO_ROOT" -path "$REPO_ROOT/$pattern" -name "*.md" -print0 2>/dev/null || true)
|
|
394
|
+
|
|
395
|
+
# Fallback: use a simpler find if the glob didn't work
|
|
396
|
+
if [[ $files_found -eq 0 ]]; then
|
|
397
|
+
local dir_part="${pattern%%/**}"
|
|
398
|
+
if [[ -d "$REPO_ROOT/$dir_part" ]]; then
|
|
399
|
+
while IFS= read -r -d '' filepath; do
|
|
400
|
+
validate_file "$filepath" "$collection"
|
|
401
|
+
files_found=$((files_found + 1))
|
|
402
|
+
done < <(find "$REPO_ROOT/$dir_part" -name "*.md" -print0 2>/dev/null)
|
|
403
|
+
fi
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
debug " Found $files_found files in $collection"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# -------------------------------------------------------------------------
|
|
410
|
+
# Report generation
|
|
411
|
+
# -------------------------------------------------------------------------
|
|
412
|
+
print_report() {
|
|
413
|
+
echo ""
|
|
414
|
+
echo -e "${BLUE}════════════════════════════════════════════${NC}"
|
|
415
|
+
echo -e "${BLUE} Frontmatter Lint Report${NC}"
|
|
416
|
+
echo -e "${BLUE}════════════════════════════════════════════${NC}"
|
|
417
|
+
echo ""
|
|
418
|
+
echo " Files scanned: $TOTAL_FILES"
|
|
419
|
+
echo -e " Errors: ${RED}$TOTAL_ERRORS${NC}"
|
|
420
|
+
echo -e " Warnings: ${YELLOW}$TOTAL_WARNINGS${NC}"
|
|
421
|
+
if [[ "$FIX_MODE" == "true" ]]; then
|
|
422
|
+
echo -e " Fixed: ${GREEN}$TOTAL_FIXED${NC}"
|
|
423
|
+
fi
|
|
424
|
+
echo ""
|
|
425
|
+
|
|
426
|
+
if [[ $TOTAL_ERRORS -gt 0 ]] && [[ "$MODE" == "strict" ]]; then
|
|
427
|
+
echo -e "${RED} ✗ FAILED — $TOTAL_ERRORS error(s) found in strict mode${NC}"
|
|
428
|
+
elif [[ $TOTAL_WARNINGS -gt 0 ]] || [[ $TOTAL_ERRORS -gt 0 ]]; then
|
|
429
|
+
echo -e "${YELLOW} ⚠ Issues found — run with --fix to auto-correct safe violations${NC}"
|
|
430
|
+
else
|
|
431
|
+
echo -e "${GREEN} ✓ All pages pass frontmatter validation${NC}"
|
|
432
|
+
fi
|
|
433
|
+
echo ""
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# -------------------------------------------------------------------------
|
|
437
|
+
# Main
|
|
438
|
+
# -------------------------------------------------------------------------
|
|
439
|
+
main() {
|
|
440
|
+
parse_args "$@"
|
|
441
|
+
|
|
442
|
+
print_header "lint-pages — Frontmatter Validator"
|
|
443
|
+
|
|
444
|
+
# Validate prerequisites
|
|
445
|
+
if ! command_exists ruby; then
|
|
446
|
+
error "Ruby is required for YAML parsing. Install Ruby first."
|
|
447
|
+
fi
|
|
448
|
+
|
|
449
|
+
# Load and validate schema
|
|
450
|
+
local schema_full_path
|
|
451
|
+
schema_full_path="$(load_schema "$SCHEMA_PATH")" || exit 1
|
|
452
|
+
debug "Schema: $schema_full_path"
|
|
453
|
+
|
|
454
|
+
# Cache global schema settings (one Ruby call instead of per-file)
|
|
455
|
+
load_schema_cache "$schema_full_path"
|
|
456
|
+
|
|
457
|
+
# Load content rules (if available)
|
|
458
|
+
local rules_full_path="$REPO_ROOT/$RULES_PATH"
|
|
459
|
+
if [[ -f "$rules_full_path" ]]; then
|
|
460
|
+
debug "Content rules: $rules_full_path"
|
|
461
|
+
|
|
462
|
+
# Apply strictness from content rules when no explicit CLI mode was given
|
|
463
|
+
if [[ "$MODE" == "warn" ]] && [[ "${FRONTMATTER_STRICT:-}" == "true" ]]; then
|
|
464
|
+
local env_strictness
|
|
465
|
+
env_strictness="$(ruby -ryaml -e "
|
|
466
|
+
rules = YAML.load_file('$rules_full_path')
|
|
467
|
+
puts rules.dig('strictness', 'ci') || ''
|
|
468
|
+
" 2>/dev/null)"
|
|
469
|
+
if [[ "$env_strictness" == "strict" ]]; then
|
|
470
|
+
MODE="strict"
|
|
471
|
+
debug "Strictness set to 'strict' via content rules"
|
|
472
|
+
fi
|
|
473
|
+
fi
|
|
474
|
+
else
|
|
475
|
+
debug "Content rules file not found: $rules_full_path (skipping)"
|
|
476
|
+
fi
|
|
477
|
+
|
|
478
|
+
# Get collections to scan
|
|
479
|
+
local collections
|
|
480
|
+
if [[ -n "$TARGET_COLLECTION" ]]; then
|
|
481
|
+
collections="$TARGET_COLLECTION"
|
|
482
|
+
else
|
|
483
|
+
collections="$(get_collections "$SCHEMA_PATH")"
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
# Scan each collection
|
|
487
|
+
while IFS= read -r collection; do
|
|
488
|
+
[[ -z "$collection" ]] && continue
|
|
489
|
+
scan_collection "$collection"
|
|
490
|
+
done <<< "$collections"
|
|
491
|
+
|
|
492
|
+
# Print report
|
|
493
|
+
if [[ "$REPORT_MODE" == "true" ]] || [[ "$VERBOSE" == "true" ]] || [[ $TOTAL_ERRORS -gt 0 ]] || [[ $TOTAL_WARNINGS -gt 0 ]]; then
|
|
494
|
+
print_report
|
|
495
|
+
else
|
|
496
|
+
success "Validated $TOTAL_FILES pages — no issues found"
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
# Exit code
|
|
500
|
+
if [[ "$MODE" == "strict" ]] && [[ $TOTAL_ERRORS -gt 0 ]]; then
|
|
501
|
+
exit 1
|
|
502
|
+
fi
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
main "$@"
|