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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +236 -0
  3. data/README.md +66 -19
  4. data/_data/navigation/admin.yml +53 -0
  5. data/_data/theme_backgrounds.yml +121 -0
  6. data/_includes/components/admin-tabs.html +59 -0
  7. data/_includes/components/analytics-dashboard.html +232 -0
  8. data/_includes/components/background-customizer.html +159 -0
  9. data/_includes/components/background-settings.html +137 -0
  10. data/_includes/components/collection-manager.html +151 -0
  11. data/_includes/components/component-showcase.html +452 -0
  12. data/_includes/components/config-editor.html +207 -0
  13. data/_includes/components/config-viewer.html +479 -0
  14. data/_includes/components/env-dashboard.html +154 -0
  15. data/_includes/components/feature-card.html +94 -0
  16. data/_includes/components/info-section.html +172 -149
  17. data/_includes/components/js-cdn.html +4 -1
  18. data/_includes/components/nav-editor.html +99 -0
  19. data/_includes/components/setup-banner.html +28 -0
  20. data/_includes/components/setup-check.html +53 -0
  21. data/_includes/components/svg-background.html +42 -0
  22. data/_includes/components/theme-customizer.html +46 -0
  23. data/_includes/content/seo.html +68 -135
  24. data/_includes/core/footer.html +1 -1
  25. data/_includes/core/head.html +3 -2
  26. data/_includes/core/header.html +14 -7
  27. data/_includes/landing/landing-install-cards.html +18 -7
  28. data/_includes/navigation/admin-nav.html +95 -0
  29. data/_includes/navigation/navbar.html +43 -5
  30. data/_includes/navigation/sidebar-left.html +1 -1
  31. data/_includes/setup/wizard.html +330 -0
  32. data/_layouts/admin.html +166 -0
  33. data/_layouts/landing.html +23 -9
  34. data/_layouts/root.html +12 -6
  35. data/_layouts/setup.html +73 -0
  36. data/_plugins/preview_image_generator.rb +26 -12
  37. data/_sass/core/_navbar.scss +2 -2
  38. data/_sass/custom.scss +28 -6
  39. data/_sass/theme/_background-mixins.scss +95 -0
  40. data/_sass/theme/_backgrounds.scss +156 -0
  41. data/_sass/theme/_color-modes.scss +2 -1
  42. data/assets/backgrounds/gradients/air.svg +15 -0
  43. data/assets/backgrounds/gradients/aqua.svg +15 -0
  44. data/assets/backgrounds/gradients/contrast.svg +15 -0
  45. data/assets/backgrounds/gradients/dark.svg +15 -0
  46. data/assets/backgrounds/gradients/dirt.svg +15 -0
  47. data/assets/backgrounds/gradients/mint.svg +15 -0
  48. data/assets/backgrounds/gradients/neon.svg +15 -0
  49. data/assets/backgrounds/gradients/plum.svg +15 -0
  50. data/assets/backgrounds/gradients/sunrise.svg +15 -0
  51. data/assets/backgrounds/noise/air.svg +8 -0
  52. data/assets/backgrounds/noise/aqua.svg +8 -0
  53. data/assets/backgrounds/noise/contrast.svg +8 -0
  54. data/assets/backgrounds/noise/dark.svg +8 -0
  55. data/assets/backgrounds/noise/dirt.svg +8 -0
  56. data/assets/backgrounds/noise/mint.svg +8 -0
  57. data/assets/backgrounds/noise/neon.svg +8 -0
  58. data/assets/backgrounds/noise/plum.svg +8 -0
  59. data/assets/backgrounds/noise/sunrise.svg +8 -0
  60. data/assets/backgrounds/patterns/air.svg +7 -0
  61. data/assets/backgrounds/patterns/aqua.svg +7 -0
  62. data/assets/backgrounds/patterns/contrast.svg +4 -0
  63. data/assets/backgrounds/patterns/dark.svg +5 -0
  64. data/assets/backgrounds/patterns/dirt.svg +5 -0
  65. data/assets/backgrounds/patterns/mint.svg +6 -0
  66. data/assets/backgrounds/patterns/neon.svg +6 -0
  67. data/assets/backgrounds/patterns/plum.svg +6 -0
  68. data/assets/backgrounds/patterns/sunrise.svg +5 -0
  69. data/assets/js/background-customizer.js +73 -0
  70. data/assets/js/code-copy.js +18 -47
  71. data/assets/js/config-utility.js +307 -0
  72. data/assets/js/nav-editor.js +39 -0
  73. data/assets/js/palette-generator.js +415 -0
  74. data/assets/js/search-modal.js +31 -11
  75. data/assets/js/setup-wizard.js +306 -0
  76. data/assets/js/skin-editor.js +645 -0
  77. data/assets/js/theme-customizer.js +102 -0
  78. data/assets/js/ui-enhancements.js +15 -24
  79. data/assets/vendor/bootstrap/css/bootstrap.min.css +1 -0
  80. data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +1 -0
  81. data/scripts/README.md +45 -0
  82. data/scripts/features/generate-preview-images +297 -7
  83. data/scripts/features/install-preview-generator +51 -33
  84. data/scripts/fork-cleanup.sh +92 -19
  85. data/scripts/github-setup.sh +284 -0
  86. data/scripts/init_setup.sh +0 -1
  87. data/scripts/lib/frontmatter.sh +543 -0
  88. data/scripts/lib/migrate.sh +265 -0
  89. data/scripts/lib/preview_generator.py +607 -32
  90. data/scripts/lint-pages +505 -0
  91. data/scripts/migrate.sh +201 -0
  92. data/scripts/platform/setup-linux.sh +244 -0
  93. data/scripts/platform/setup-macos.sh +187 -0
  94. data/scripts/platform/setup-wsl.sh +196 -0
  95. metadata +71 -6
@@ -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 "$@"