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
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# =========================================================================
|
|
4
|
+
# Frontmatter Validation Library
|
|
5
|
+
# =========================================================================
|
|
6
|
+
# Config-aware shared functions for frontmatter validation.
|
|
7
|
+
# All schema data is read from YAML config files — nothing hardcoded.
|
|
8
|
+
#
|
|
9
|
+
# Requires: ruby (for YAML parsing), common.sh
|
|
10
|
+
#
|
|
11
|
+
# Usage: source "$(dirname "$0")/lib/frontmatter.sh"
|
|
12
|
+
# =========================================================================
|
|
13
|
+
|
|
14
|
+
# Source common utilities if not already loaded
|
|
15
|
+
FRONTMATTER_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
if ! declare -f debug &>/dev/null; then
|
|
17
|
+
source "$FRONTMATTER_LIB_DIR/common.sh"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# -------------------------------------------------------------------------
|
|
21
|
+
# Configuration paths (overridable via environment)
|
|
22
|
+
# -------------------------------------------------------------------------
|
|
23
|
+
FRONTMATTER_SCHEMA_PATH="${FRONTMATTER_SCHEMA_PATH:-.github/config/frontmatter_schema.yml}"
|
|
24
|
+
CONTENT_RULES_PATH="${CONTENT_RULES_PATH:-.github/config/content_rules.yml}"
|
|
25
|
+
|
|
26
|
+
# -------------------------------------------------------------------------
|
|
27
|
+
# Schema loading
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
# Load and validate the schema file exists
|
|
31
|
+
# Usage: load_schema [schema_path]
|
|
32
|
+
load_schema() {
|
|
33
|
+
local schema_path="${1:-$FRONTMATTER_SCHEMA_PATH}"
|
|
34
|
+
local repo_root
|
|
35
|
+
repo_root="$(get_repo_root)"
|
|
36
|
+
local full_path="$repo_root/$schema_path"
|
|
37
|
+
|
|
38
|
+
if [[ ! -f "$full_path" ]]; then
|
|
39
|
+
echo "ERROR: Schema file not found: $full_path" >&2
|
|
40
|
+
return 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Validate YAML is parseable
|
|
44
|
+
if ! ruby -ryaml -e "YAML.load_file('$full_path')" 2>/dev/null; then
|
|
45
|
+
echo "ERROR: Invalid YAML in schema: $full_path" >&2
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "$full_path"
|
|
50
|
+
return 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Get list of collection names from schema
|
|
54
|
+
# Usage: get_collections [schema_path]
|
|
55
|
+
get_collections() {
|
|
56
|
+
local schema_path="${1:-$FRONTMATTER_SCHEMA_PATH}"
|
|
57
|
+
local repo_root
|
|
58
|
+
repo_root="$(get_repo_root)"
|
|
59
|
+
local full_path="$repo_root/$schema_path"
|
|
60
|
+
|
|
61
|
+
ruby -ryaml -e "
|
|
62
|
+
schema = YAML.load_file('$full_path')
|
|
63
|
+
(schema['collections'] || {}).keys.each { |k| puts k }
|
|
64
|
+
" 2>/dev/null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Get required fields for a collection
|
|
68
|
+
# Usage: get_required_fields <collection> [schema_path]
|
|
69
|
+
get_required_fields() {
|
|
70
|
+
local collection="$1"
|
|
71
|
+
local schema_path="${2:-$FRONTMATTER_SCHEMA_PATH}"
|
|
72
|
+
local repo_root
|
|
73
|
+
repo_root="$(get_repo_root)"
|
|
74
|
+
local full_path="$repo_root/$schema_path"
|
|
75
|
+
|
|
76
|
+
ruby -ryaml -e "
|
|
77
|
+
schema = YAML.load_file('$full_path')
|
|
78
|
+
global_required = schema.dig('global', 'required_fields') || []
|
|
79
|
+
collection_required = schema.dig('collections', '$collection', 'required') || []
|
|
80
|
+
(global_required | collection_required).each { |f| puts f }
|
|
81
|
+
" 2>/dev/null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Get optional fields for a collection
|
|
85
|
+
# Usage: get_optional_fields <collection> [schema_path]
|
|
86
|
+
get_optional_fields() {
|
|
87
|
+
local collection="$1"
|
|
88
|
+
local schema_path="${2:-$FRONTMATTER_SCHEMA_PATH}"
|
|
89
|
+
local repo_root
|
|
90
|
+
repo_root="$(get_repo_root)"
|
|
91
|
+
local full_path="$repo_root/$schema_path"
|
|
92
|
+
|
|
93
|
+
ruby -ryaml -e "
|
|
94
|
+
schema = YAML.load_file('$full_path')
|
|
95
|
+
fields = schema.dig('collections', '$collection', 'optional') || []
|
|
96
|
+
fields.each { |f| puts f }
|
|
97
|
+
" 2>/dev/null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Get allowed layout values for a collection
|
|
101
|
+
# Usage: get_allowed_layouts <collection> [schema_path]
|
|
102
|
+
get_allowed_layouts() {
|
|
103
|
+
local collection="$1"
|
|
104
|
+
local schema_path="${2:-$FRONTMATTER_SCHEMA_PATH}"
|
|
105
|
+
local repo_root
|
|
106
|
+
repo_root="$(get_repo_root)"
|
|
107
|
+
local full_path="$repo_root/$schema_path"
|
|
108
|
+
|
|
109
|
+
ruby -ryaml -e "
|
|
110
|
+
schema = YAML.load_file('$full_path')
|
|
111
|
+
layouts = schema.dig('collections', '$collection', 'layout', 'allowed') || []
|
|
112
|
+
layouts.each { |l| puts l }
|
|
113
|
+
" 2>/dev/null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Get path pattern for a collection
|
|
117
|
+
# Usage: get_path_pattern <collection> [schema_path]
|
|
118
|
+
get_path_pattern() {
|
|
119
|
+
local collection="$1"
|
|
120
|
+
local schema_path="${2:-$FRONTMATTER_SCHEMA_PATH}"
|
|
121
|
+
local repo_root
|
|
122
|
+
repo_root="$(get_repo_root)"
|
|
123
|
+
local full_path="$repo_root/$schema_path"
|
|
124
|
+
|
|
125
|
+
ruby -ryaml -e "
|
|
126
|
+
schema = YAML.load_file('$full_path')
|
|
127
|
+
puts schema.dig('collections', '$collection', 'path_pattern') || ''
|
|
128
|
+
" 2>/dev/null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Get a global setting from schema
|
|
132
|
+
# Usage: get_global_setting <key> [schema_path]
|
|
133
|
+
get_global_setting() {
|
|
134
|
+
local key="$1"
|
|
135
|
+
local schema_path="${2:-$FRONTMATTER_SCHEMA_PATH}"
|
|
136
|
+
local repo_root
|
|
137
|
+
repo_root="$(get_repo_root)"
|
|
138
|
+
local full_path="$repo_root/$schema_path"
|
|
139
|
+
|
|
140
|
+
ruby -ryaml -e "
|
|
141
|
+
schema = YAML.load_file('$full_path')
|
|
142
|
+
val = schema.dig('global', '$key')
|
|
143
|
+
if val.is_a?(Hash)
|
|
144
|
+
val.each { |k, v| puts \"#{k}=#{v}\" }
|
|
145
|
+
elsif val.is_a?(Array)
|
|
146
|
+
val.each { |v| puts v }
|
|
147
|
+
else
|
|
148
|
+
puts val
|
|
149
|
+
end
|
|
150
|
+
" 2>/dev/null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Get field type pattern from schema
|
|
154
|
+
# Usage: get_field_type_pattern <type_name> [schema_path]
|
|
155
|
+
get_field_type_pattern() {
|
|
156
|
+
local type_name="$1"
|
|
157
|
+
local schema_path="${2:-$FRONTMATTER_SCHEMA_PATH}"
|
|
158
|
+
local repo_root
|
|
159
|
+
repo_root="$(get_repo_root)"
|
|
160
|
+
local full_path="$repo_root/$schema_path"
|
|
161
|
+
|
|
162
|
+
ruby -ryaml -e "
|
|
163
|
+
schema = YAML.load_file('$full_path')
|
|
164
|
+
puts schema.dig('field_types', '$type_name', 'pattern') || ''
|
|
165
|
+
" 2>/dev/null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Get canonical field mappings (deprecated → canonical)
|
|
169
|
+
# Usage: get_canonical_fields [schema_path]
|
|
170
|
+
get_canonical_fields() {
|
|
171
|
+
local schema_path="${1:-$FRONTMATTER_SCHEMA_PATH}"
|
|
172
|
+
local repo_root
|
|
173
|
+
repo_root="$(get_repo_root)"
|
|
174
|
+
local full_path="$repo_root/$schema_path"
|
|
175
|
+
|
|
176
|
+
ruby -ryaml -e "
|
|
177
|
+
schema = YAML.load_file('$full_path')
|
|
178
|
+
(schema.dig('global', 'canonical_fields') || {}).each { |k, v| puts \"#{k}=#{v}\" }
|
|
179
|
+
" 2>/dev/null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# -------------------------------------------------------------------------
|
|
183
|
+
# Content rules loading
|
|
184
|
+
# -------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
# Get template mappings from content rules
|
|
187
|
+
# Usage: get_template_mappings [rules_path]
|
|
188
|
+
get_template_mappings() {
|
|
189
|
+
local rules_path="${1:-$CONTENT_RULES_PATH}"
|
|
190
|
+
local repo_root
|
|
191
|
+
repo_root="$(get_repo_root)"
|
|
192
|
+
local full_path="$repo_root/$rules_path"
|
|
193
|
+
|
|
194
|
+
if [[ ! -f "$full_path" ]]; then
|
|
195
|
+
return 1
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
ruby -ryaml -e "
|
|
199
|
+
rules = YAML.load_file('$full_path')
|
|
200
|
+
(rules['template_mappings'] || {}).each { |k, v| puts \"#{k}=#{v}\" }
|
|
201
|
+
" 2>/dev/null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# Get auto-fixable rule names
|
|
205
|
+
# Usage: get_auto_fixable_rules [rules_path]
|
|
206
|
+
get_auto_fixable_rules() {
|
|
207
|
+
local rules_path="${1:-$CONTENT_RULES_PATH}"
|
|
208
|
+
local repo_root
|
|
209
|
+
repo_root="$(get_repo_root)"
|
|
210
|
+
local full_path="$repo_root/$rules_path"
|
|
211
|
+
|
|
212
|
+
if [[ ! -f "$full_path" ]]; then
|
|
213
|
+
return 1
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
ruby -ryaml -e "
|
|
217
|
+
rules = YAML.load_file('$full_path')
|
|
218
|
+
(rules['auto_fixable'] || []).each { |r| puts r }
|
|
219
|
+
" 2>/dev/null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# -------------------------------------------------------------------------
|
|
223
|
+
# Collection detection
|
|
224
|
+
# -------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
# Detect which collection a file belongs to based on path_pattern in schema
|
|
227
|
+
# Usage: detect_collection <filepath> [schema_path]
|
|
228
|
+
detect_collection() {
|
|
229
|
+
local filepath="$1"
|
|
230
|
+
local schema_path="${2:-$FRONTMATTER_SCHEMA_PATH}"
|
|
231
|
+
local repo_root
|
|
232
|
+
repo_root="$(get_repo_root)"
|
|
233
|
+
local full_path="$repo_root/$schema_path"
|
|
234
|
+
|
|
235
|
+
ruby -ryaml -e "
|
|
236
|
+
require 'pathname'
|
|
237
|
+
schema = YAML.load_file('$full_path')
|
|
238
|
+
filepath = '$filepath'
|
|
239
|
+
# Make path relative to repo root if absolute
|
|
240
|
+
filepath = Pathname.new(filepath).relative_path_from(Pathname.new('$repo_root')).to_s rescue filepath
|
|
241
|
+
|
|
242
|
+
matched = nil
|
|
243
|
+
(schema['collections'] || {}).each do |name, config|
|
|
244
|
+
pattern = config['path_pattern'] || ''
|
|
245
|
+
# Convert glob pattern to regex
|
|
246
|
+
regex_str = pattern.gsub('**/', '(.+/)?').gsub('**', '.*').gsub('*', '[^/]*')
|
|
247
|
+
if filepath.match?(Regexp.new('^' + regex_str + '$'))
|
|
248
|
+
matched = name
|
|
249
|
+
break
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
puts matched || 'unknown'
|
|
253
|
+
" 2>/dev/null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# -------------------------------------------------------------------------
|
|
257
|
+
# Frontmatter extraction
|
|
258
|
+
# -------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
# Extract frontmatter from a markdown file as YAML
|
|
261
|
+
# Usage: extract_frontmatter <filepath>
|
|
262
|
+
# Returns: YAML content between --- delimiters, or empty string
|
|
263
|
+
extract_frontmatter() {
|
|
264
|
+
local filepath="$1"
|
|
265
|
+
|
|
266
|
+
if [[ ! -f "$filepath" ]]; then
|
|
267
|
+
echo ""
|
|
268
|
+
return 1
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# Frontmatter must START on the first line with ---
|
|
272
|
+
# Otherwise mid-document horizontal rules would be misread as frontmatter.
|
|
273
|
+
local first_line
|
|
274
|
+
IFS= read -r first_line < "$filepath" || true
|
|
275
|
+
if [[ ! "$first_line" =~ ^---[[:space:]]*$ ]]; then
|
|
276
|
+
echo ""
|
|
277
|
+
return 1
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Extract content between first pair of ---
|
|
281
|
+
awk '
|
|
282
|
+
/^---[[:space:]]*$/ {
|
|
283
|
+
if (count == 0) { count++; next }
|
|
284
|
+
if (count == 1) { exit }
|
|
285
|
+
}
|
|
286
|
+
count == 1 { print }
|
|
287
|
+
' "$filepath"
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Get a specific frontmatter field value
|
|
291
|
+
# Usage: get_frontmatter_field <filepath> <field_name>
|
|
292
|
+
get_frontmatter_field() {
|
|
293
|
+
local filepath="$1"
|
|
294
|
+
local field="$2"
|
|
295
|
+
local fm
|
|
296
|
+
|
|
297
|
+
fm="$(extract_frontmatter "$filepath")"
|
|
298
|
+
if [[ -z "$fm" ]]; then
|
|
299
|
+
echo ""
|
|
300
|
+
return 1
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
echo "$fm" | ruby -ryaml -rdate -e "
|
|
304
|
+
begin
|
|
305
|
+
data = YAML.safe_load(STDIN.read, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
306
|
+
val = data['$field']
|
|
307
|
+
format = lambda do |v|
|
|
308
|
+
case v
|
|
309
|
+
when Time then v.utc.strftime('%Y-%m-%dT%H:%M:%S.') + format('%03d', v.usec / 1000) + 'Z'
|
|
310
|
+
when Date then v.strftime('%Y-%m-%d')
|
|
311
|
+
else v.to_s
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
if val.is_a?(Array)
|
|
315
|
+
val.each { |v| puts format.call(v) }
|
|
316
|
+
elsif val.nil?
|
|
317
|
+
# output nothing
|
|
318
|
+
else
|
|
319
|
+
puts format.call(val)
|
|
320
|
+
end
|
|
321
|
+
rescue => e
|
|
322
|
+
STDERR.puts \"YAML parse error: #{e.message}\"
|
|
323
|
+
exit 1
|
|
324
|
+
end
|
|
325
|
+
" 2>/dev/null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# List all frontmatter field names in a file
|
|
329
|
+
# Usage: list_frontmatter_fields <filepath>
|
|
330
|
+
list_frontmatter_fields() {
|
|
331
|
+
local filepath="$1"
|
|
332
|
+
local fm
|
|
333
|
+
|
|
334
|
+
fm="$(extract_frontmatter "$filepath")"
|
|
335
|
+
if [[ -z "$fm" ]]; then
|
|
336
|
+
return 1
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
echo "$fm" | ruby -ryaml -rdate -e "
|
|
340
|
+
begin
|
|
341
|
+
data = YAML.safe_load(STDIN.read, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
342
|
+
data.keys.each { |k| puts k }
|
|
343
|
+
rescue => e
|
|
344
|
+
STDERR.puts \"YAML parse error: #{e.message}\"
|
|
345
|
+
exit 1
|
|
346
|
+
end
|
|
347
|
+
" 2>/dev/null
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
# -------------------------------------------------------------------------
|
|
351
|
+
# Field validation
|
|
352
|
+
# -------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
# Validate a field value matches a pattern from schema
|
|
355
|
+
# Usage: validate_field_pattern <value> <pattern>
|
|
356
|
+
# Returns: 0 if valid, 1 if invalid
|
|
357
|
+
validate_field_pattern() {
|
|
358
|
+
local value="$1"
|
|
359
|
+
local pattern="$2"
|
|
360
|
+
|
|
361
|
+
if echo "$value" | grep -qE "$pattern"; then
|
|
362
|
+
return 0
|
|
363
|
+
fi
|
|
364
|
+
return 1
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# Check if a value is a valid boolean
|
|
368
|
+
# Usage: validate_boolean <value>
|
|
369
|
+
validate_boolean() {
|
|
370
|
+
local value="$1"
|
|
371
|
+
case "$value" in
|
|
372
|
+
true|false) return 0 ;;
|
|
373
|
+
*) return 1 ;;
|
|
374
|
+
esac
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Check if a category follows the expected casing
|
|
378
|
+
# Usage: validate_category_casing <value> <casing_rule>
|
|
379
|
+
validate_category_casing() {
|
|
380
|
+
local value="$1"
|
|
381
|
+
local casing="${2:-title}"
|
|
382
|
+
|
|
383
|
+
case "$casing" in
|
|
384
|
+
title)
|
|
385
|
+
# Title case: first letter of each whitespace-separated word uppercase, rest lowercase
|
|
386
|
+
local expected="" word first rest
|
|
387
|
+
for word in $value; do
|
|
388
|
+
first="${word:0:1}"
|
|
389
|
+
rest="${word:1}"
|
|
390
|
+
first="$(printf '%s' "$first" | tr '[:lower:]' '[:upper:]')"
|
|
391
|
+
rest="$(printf '%s' "$rest" | tr '[:upper:]' '[:lower:]')"
|
|
392
|
+
expected+="${expected:+ }${first}${rest}"
|
|
393
|
+
done
|
|
394
|
+
[[ "$value" == "$expected" ]]
|
|
395
|
+
;;
|
|
396
|
+
lower)
|
|
397
|
+
[[ "$value" == "$(echo "$value" | tr '[:upper:]' '[:lower:]')" ]]
|
|
398
|
+
;;
|
|
399
|
+
upper)
|
|
400
|
+
[[ "$value" == "$(echo "$value" | tr '[:lower:]' '[:upper:]')" ]]
|
|
401
|
+
;;
|
|
402
|
+
*)
|
|
403
|
+
return 0
|
|
404
|
+
;;
|
|
405
|
+
esac
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# -------------------------------------------------------------------------
|
|
409
|
+
# Bulk frontmatter parsing (performance-optimized)
|
|
410
|
+
# -------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
# Parse a file's frontmatter and emit ALL needed validation data in a
|
|
413
|
+
# single Ruby invocation. Output is line-prefixed for easy bash parsing:
|
|
414
|
+
#
|
|
415
|
+
# FIELD:<name> (one line per top-level key present)
|
|
416
|
+
# LAYOUT:<value>
|
|
417
|
+
# DATE:<iso8601>
|
|
418
|
+
# LASTMOD:<iso8601>
|
|
419
|
+
# DRAFT:<value>
|
|
420
|
+
# CATEGORY:<value> (one line per category)
|
|
421
|
+
#
|
|
422
|
+
# Special outputs:
|
|
423
|
+
# __NO_FRONTMATTER__ file lacks a leading frontmatter block
|
|
424
|
+
# __PARSE_ERROR__:<msg> YAML parse failure
|
|
425
|
+
#
|
|
426
|
+
# Usage: parse_file_frontmatter_all <filepath>
|
|
427
|
+
parse_file_frontmatter_all() {
|
|
428
|
+
local filepath="$1"
|
|
429
|
+
|
|
430
|
+
# Cheap pre-check: must start with --- on line 1
|
|
431
|
+
local first_line
|
|
432
|
+
IFS= read -r first_line < "$filepath" || true
|
|
433
|
+
if [[ ! "$first_line" =~ ^---[[:space:]]*$ ]]; then
|
|
434
|
+
echo "__NO_FRONTMATTER__"
|
|
435
|
+
return 0
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
ruby -ryaml -rdate -e '
|
|
439
|
+
path = ARGV[0]
|
|
440
|
+
# Extract frontmatter block (between first pair of --- on their own lines)
|
|
441
|
+
content = File.read(path)
|
|
442
|
+
unless content =~ /\A---\s*\n(.*?)\n---\s*$/m
|
|
443
|
+
puts "__NO_FRONTMATTER__"
|
|
444
|
+
exit 0
|
|
445
|
+
end
|
|
446
|
+
fm_text = $1
|
|
447
|
+
|
|
448
|
+
begin
|
|
449
|
+
data = YAML.safe_load(fm_text, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
450
|
+
rescue => e
|
|
451
|
+
puts "__PARSE_ERROR__:#{e.message}"
|
|
452
|
+
exit 0
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
unless data.is_a?(Hash)
|
|
456
|
+
puts "__PARSE_ERROR__:frontmatter is not a mapping"
|
|
457
|
+
exit 0
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
fmt = lambda do |v|
|
|
461
|
+
case v
|
|
462
|
+
when Time then v.utc.strftime("%Y-%m-%dT%H:%M:%S.") + format("%03d", v.usec / 1000) + "Z"
|
|
463
|
+
when Date then v.strftime("%Y-%m-%d")
|
|
464
|
+
else v.to_s
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
data.keys.each { |k| puts "FIELD:#{k}" }
|
|
469
|
+
|
|
470
|
+
["layout", "date", "lastmod", "draft"].each do |key|
|
|
471
|
+
v = data[key]
|
|
472
|
+
next if v.nil?
|
|
473
|
+
tag = key.upcase
|
|
474
|
+
if v.is_a?(Array)
|
|
475
|
+
v.each { |item| puts "#{tag}:#{fmt.call(item)}" }
|
|
476
|
+
else
|
|
477
|
+
puts "#{tag}:#{fmt.call(v)}"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
cats = data["categories"]
|
|
482
|
+
if cats.is_a?(Array)
|
|
483
|
+
cats.each { |c| puts "CATEGORY:#{fmt.call(c)}" }
|
|
484
|
+
elsif !cats.nil?
|
|
485
|
+
puts "CATEGORY:#{fmt.call(cats)}"
|
|
486
|
+
end
|
|
487
|
+
' "$filepath" 2>/dev/null || echo "__PARSE_ERROR__:ruby failed"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# -------------------------------------------------------------------------
|
|
491
|
+
# Fix transformations
|
|
492
|
+
# -------------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
# Normalize a date to ISO 8601 format
|
|
495
|
+
# Usage: fix_date_format <date_string>
|
|
496
|
+
fix_date_format() {
|
|
497
|
+
local date_str="$1"
|
|
498
|
+
|
|
499
|
+
# Already ISO 8601 with time
|
|
500
|
+
if echo "$date_str" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T'; then
|
|
501
|
+
echo "$date_str"
|
|
502
|
+
return 0
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
# Simple date: append time
|
|
506
|
+
if echo "$date_str" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then
|
|
507
|
+
echo "${date_str}T00:00:00.000Z"
|
|
508
|
+
return 0
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
# Return as-is if unrecognized
|
|
512
|
+
echo "$date_str"
|
|
513
|
+
return 1
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# Convert string draft values to boolean
|
|
517
|
+
# Usage: fix_draft_to_boolean <value>
|
|
518
|
+
fix_draft_to_boolean() {
|
|
519
|
+
local value="$1"
|
|
520
|
+
|
|
521
|
+
case "$value" in
|
|
522
|
+
true|True|TRUE|draft|Draft|"in progress"|"In Progress")
|
|
523
|
+
echo "true"
|
|
524
|
+
;;
|
|
525
|
+
false|False|FALSE|published|Published)
|
|
526
|
+
echo "false"
|
|
527
|
+
;;
|
|
528
|
+
*)
|
|
529
|
+
echo "$value"
|
|
530
|
+
return 1
|
|
531
|
+
;;
|
|
532
|
+
esac
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# Export functions for use by other scripts
|
|
536
|
+
export -f load_schema get_collections get_required_fields get_optional_fields
|
|
537
|
+
export -f get_allowed_layouts get_path_pattern get_global_setting
|
|
538
|
+
export -f get_field_type_pattern get_canonical_fields
|
|
539
|
+
export -f get_template_mappings get_auto_fixable_rules
|
|
540
|
+
export -f detect_collection extract_frontmatter get_frontmatter_field
|
|
541
|
+
export -f list_frontmatter_fields validate_field_pattern validate_boolean
|
|
542
|
+
export -f validate_category_casing fix_date_format fix_draft_to_boolean
|
|
543
|
+
export -f parse_file_frontmatter_all
|