jekyll-theme-zer0 0.10.4 → 0.15.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 +459 -0
- data/README.md +24 -8
- data/_data/navigation/about.yml +39 -11
- data/_data/navigation/docs.yml +53 -23
- data/_data/navigation/home.yml +27 -9
- data/_data/navigation/main.yml +27 -8
- data/_data/navigation/posts.yml +22 -6
- data/_data/navigation/quickstart.yml +8 -3
- data/_includes/README.md +2 -0
- data/_includes/components/js-cdn.html +4 -1
- data/_includes/components/post-card.html +2 -11
- data/_includes/components/preview-image.html +32 -0
- data/_includes/content/intro.html +5 -6
- data/_includes/core/footer.html +5 -3
- data/_includes/core/header.html +14 -0
- data/_includes/navigation/sidebar-categories.html +20 -9
- data/_includes/navigation/sidebar-folders.html +8 -7
- data/_includes/navigation/sidebar-right.html +16 -10
- data/_layouts/blog.html +15 -45
- data/_layouts/category.html +4 -24
- data/_layouts/collection.html +2 -12
- data/_layouts/default.html +1 -1
- data/_layouts/journals.html +2 -12
- data/_layouts/notebook.html +296 -0
- data/_sass/core/_docs.scss +1 -1
- data/_sass/custom.scss +55 -18
- data/_sass/notebooks.scss +458 -0
- data/assets/images/notebooks/test-notebook_files/test-notebook_4_0.png +0 -0
- data/assets/js/sidebar.js +511 -0
- data/scripts/README.md +128 -105
- data/scripts/analyze-commits.sh +9 -311
- data/scripts/bin/build +22 -22
- data/scripts/build +7 -111
- data/scripts/convert-notebooks.sh +415 -0
- data/scripts/features/validate_preview_urls.py +500 -0
- data/scripts/fix-markdown-format.sh +8 -262
- data/scripts/generate-preview-images.sh +7 -787
- data/scripts/install-preview-generator.sh +8 -528
- data/scripts/lib/README.md +5 -5
- data/scripts/lib/gem.sh +19 -7
- data/scripts/release +7 -236
- data/scripts/setup.sh +9 -153
- data/scripts/test-auto-version.sh +7 -256
- data/scripts/test-mermaid.sh +7 -287
- data/scripts/test.sh +9 -154
- metadata +9 -10
- data/scripts/features/preview_generator.py +0 -646
- data/scripts/lib/test/run_tests.sh +0 -140
- data/scripts/lib/test/test_changelog.sh +0 -87
- data/scripts/lib/test/test_gem.sh +0 -68
- data/scripts/lib/test/test_git.sh +0 -82
- data/scripts/lib/test/test_validation.sh +0 -72
- data/scripts/lib/test/test_version.sh +0 -96
- data/scripts/version.sh +0 -178
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Preview URL Validator - Validates all preview image URLs in Jekyll content frontmatter.
|
|
4
|
+
|
|
5
|
+
This script checks:
|
|
6
|
+
1. URL format validity (must start with /, be absolute path)
|
|
7
|
+
2. File extension validity (.png, .jpg, .jpeg, .gif, .webp, .svg)
|
|
8
|
+
3. File existence (actual image file exists on disk)
|
|
9
|
+
4. Detects empty, null, or malformed preview values
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 validate_preview_urls.py
|
|
13
|
+
python3 validate_preview_urls.py --verbose
|
|
14
|
+
python3 validate_preview_urls.py --fix-suggestions
|
|
15
|
+
|
|
16
|
+
Exit codes:
|
|
17
|
+
0 - All previews valid
|
|
18
|
+
1 - Validation errors found
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
# Terminal colors
|
|
31
|
+
class Colors:
|
|
32
|
+
RED = '\033[0;31m'
|
|
33
|
+
GREEN = '\033[0;32m'
|
|
34
|
+
YELLOW = '\033[1;33m'
|
|
35
|
+
BLUE = '\033[0;34m'
|
|
36
|
+
CYAN = '\033[0;36m'
|
|
37
|
+
PURPLE = '\033[0;35m'
|
|
38
|
+
BOLD = '\033[1m'
|
|
39
|
+
NC = '\033[0m' # No Color
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ValidationError:
|
|
44
|
+
"""Represents a validation error for a preview URL."""
|
|
45
|
+
file_path: str
|
|
46
|
+
preview_value: str
|
|
47
|
+
error_type: str
|
|
48
|
+
message: str
|
|
49
|
+
suggestion: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ValidationResult:
|
|
54
|
+
"""Result of validating a single file."""
|
|
55
|
+
file_path: str
|
|
56
|
+
title: str
|
|
57
|
+
preview_value: Optional[str]
|
|
58
|
+
is_valid: bool
|
|
59
|
+
errors: List[ValidationError] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ValidationSummary:
|
|
64
|
+
"""Summary of all validation results."""
|
|
65
|
+
total_files: int = 0
|
|
66
|
+
files_with_preview: int = 0
|
|
67
|
+
files_without_preview: int = 0
|
|
68
|
+
files_with_null_preview: int = 0
|
|
69
|
+
valid_previews: int = 0
|
|
70
|
+
invalid_previews: int = 0
|
|
71
|
+
missing_files: int = 0
|
|
72
|
+
format_errors: int = 0
|
|
73
|
+
results: List[ValidationResult] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PreviewURLValidator:
|
|
77
|
+
"""Validates preview image URLs in Jekyll content frontmatter."""
|
|
78
|
+
|
|
79
|
+
# Valid image extensions
|
|
80
|
+
VALID_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
|
|
81
|
+
|
|
82
|
+
# Content directories to scan
|
|
83
|
+
CONTENT_DIRS = [
|
|
84
|
+
'pages/_posts',
|
|
85
|
+
'pages/_docs',
|
|
86
|
+
'pages/_quickstart',
|
|
87
|
+
'pages/_about',
|
|
88
|
+
'pages/_quests',
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
def __init__(self, project_root: Path, verbose: bool = False):
|
|
92
|
+
self.project_root = project_root
|
|
93
|
+
self.verbose = verbose
|
|
94
|
+
self.summary = ValidationSummary()
|
|
95
|
+
|
|
96
|
+
def log(self, msg: str, level: str = "info"):
|
|
97
|
+
"""Print formatted log message."""
|
|
98
|
+
colors = {
|
|
99
|
+
"info": Colors.BLUE,
|
|
100
|
+
"success": Colors.GREEN,
|
|
101
|
+
"warning": Colors.YELLOW,
|
|
102
|
+
"error": Colors.RED,
|
|
103
|
+
"debug": Colors.PURPLE,
|
|
104
|
+
}
|
|
105
|
+
color = colors.get(level, Colors.NC)
|
|
106
|
+
prefix = f"[{level.upper()}]"
|
|
107
|
+
print(f"{color}{prefix}{Colors.NC} {msg}")
|
|
108
|
+
|
|
109
|
+
def debug(self, msg: str):
|
|
110
|
+
"""Print debug message if verbose mode enabled."""
|
|
111
|
+
if self.verbose:
|
|
112
|
+
self.log(msg, "debug")
|
|
113
|
+
|
|
114
|
+
def parse_front_matter(self, file_path: Path) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
115
|
+
"""Parse front matter from a markdown file.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Tuple of (front_matter_dict, raw_preview_value)
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
content = file_path.read_text(encoding='utf-8')
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.log(f"Failed to read {file_path}: {e}", "error")
|
|
124
|
+
return None, None
|
|
125
|
+
|
|
126
|
+
# Extract front matter
|
|
127
|
+
fm_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
|
|
128
|
+
if not fm_match:
|
|
129
|
+
return None, None
|
|
130
|
+
|
|
131
|
+
fm_raw = fm_match.group(1)
|
|
132
|
+
|
|
133
|
+
# Extract raw preview value for better error messages
|
|
134
|
+
preview_match = re.search(r'^preview:\s*(.*)$', fm_raw, re.MULTILINE)
|
|
135
|
+
raw_preview = preview_match.group(1).strip() if preview_match else None
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
front_matter = yaml.safe_load(fm_raw)
|
|
139
|
+
return front_matter, raw_preview
|
|
140
|
+
except yaml.YAMLError as e:
|
|
141
|
+
self.log(f"YAML parse error in {file_path}: {e}", "error")
|
|
142
|
+
return None, raw_preview
|
|
143
|
+
|
|
144
|
+
def validate_url_format(self, preview: str, file_path: str) -> List[ValidationError]:
|
|
145
|
+
"""Validate the format of a preview URL."""
|
|
146
|
+
errors = []
|
|
147
|
+
|
|
148
|
+
# Check if empty or whitespace only
|
|
149
|
+
if not preview or not preview.strip():
|
|
150
|
+
errors.append(ValidationError(
|
|
151
|
+
file_path=file_path,
|
|
152
|
+
preview_value=repr(preview),
|
|
153
|
+
error_type="EMPTY_VALUE",
|
|
154
|
+
message="Preview value is empty or whitespace only",
|
|
155
|
+
suggestion="Set a valid image path or remove the preview field"
|
|
156
|
+
))
|
|
157
|
+
return errors
|
|
158
|
+
|
|
159
|
+
preview = preview.strip()
|
|
160
|
+
|
|
161
|
+
# Check for 'null' string (should be YAML null, not string)
|
|
162
|
+
if preview.lower() == 'null':
|
|
163
|
+
errors.append(ValidationError(
|
|
164
|
+
file_path=file_path,
|
|
165
|
+
preview_value=preview,
|
|
166
|
+
error_type="STRING_NULL",
|
|
167
|
+
message="Preview is string 'null' instead of YAML null",
|
|
168
|
+
suggestion="Use 'preview: null' (without quotes) or 'preview: ~'"
|
|
169
|
+
))
|
|
170
|
+
return errors
|
|
171
|
+
|
|
172
|
+
# Check if starts with /
|
|
173
|
+
if not preview.startswith('/'):
|
|
174
|
+
errors.append(ValidationError(
|
|
175
|
+
file_path=file_path,
|
|
176
|
+
preview_value=preview,
|
|
177
|
+
error_type="RELATIVE_PATH",
|
|
178
|
+
message="Preview URL should start with /",
|
|
179
|
+
suggestion=f"Use '/{preview}' for absolute path"
|
|
180
|
+
))
|
|
181
|
+
|
|
182
|
+
# Check for valid extension
|
|
183
|
+
ext = Path(preview).suffix.lower()
|
|
184
|
+
if ext not in self.VALID_EXTENSIONS:
|
|
185
|
+
if ext:
|
|
186
|
+
errors.append(ValidationError(
|
|
187
|
+
file_path=file_path,
|
|
188
|
+
preview_value=preview,
|
|
189
|
+
error_type="INVALID_EXTENSION",
|
|
190
|
+
message=f"Invalid image extension: {ext}",
|
|
191
|
+
suggestion=f"Valid extensions: {', '.join(sorted(self.VALID_EXTENSIONS))}"
|
|
192
|
+
))
|
|
193
|
+
else:
|
|
194
|
+
errors.append(ValidationError(
|
|
195
|
+
file_path=file_path,
|
|
196
|
+
preview_value=preview,
|
|
197
|
+
error_type="NO_EXTENSION",
|
|
198
|
+
message="Preview URL has no file extension",
|
|
199
|
+
suggestion="Add an image extension like .png, .jpg, .webp"
|
|
200
|
+
))
|
|
201
|
+
|
|
202
|
+
# Check for suspicious patterns
|
|
203
|
+
if ' ' in preview:
|
|
204
|
+
errors.append(ValidationError(
|
|
205
|
+
file_path=file_path,
|
|
206
|
+
preview_value=preview,
|
|
207
|
+
error_type="DOUBLE_SPACE",
|
|
208
|
+
message="Preview URL contains double spaces",
|
|
209
|
+
suggestion="Remove extra spaces from path"
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
if '\n' in preview or '\t' in preview:
|
|
213
|
+
errors.append(ValidationError(
|
|
214
|
+
file_path=file_path,
|
|
215
|
+
preview_value=repr(preview),
|
|
216
|
+
error_type="WHITESPACE_CHARS",
|
|
217
|
+
message="Preview URL contains newline or tab characters",
|
|
218
|
+
suggestion="Use a clean single-line path"
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
# Check for URL encoding issues
|
|
222
|
+
if '%' in preview and not re.match(r'%[0-9A-Fa-f]{2}', preview):
|
|
223
|
+
errors.append(ValidationError(
|
|
224
|
+
file_path=file_path,
|
|
225
|
+
preview_value=preview,
|
|
226
|
+
error_type="ENCODING_ISSUE",
|
|
227
|
+
message="Preview URL may have encoding issues",
|
|
228
|
+
suggestion="Use properly URL-encoded characters or plain ASCII"
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
return errors
|
|
232
|
+
|
|
233
|
+
def validate_file_exists(self, preview: str, file_path: str) -> List[ValidationError]:
|
|
234
|
+
"""Check if the preview image file exists on disk."""
|
|
235
|
+
errors = []
|
|
236
|
+
|
|
237
|
+
if not preview or not preview.strip():
|
|
238
|
+
return errors
|
|
239
|
+
|
|
240
|
+
preview = preview.strip()
|
|
241
|
+
|
|
242
|
+
# Skip validation for null/empty
|
|
243
|
+
if preview.lower() == 'null':
|
|
244
|
+
return errors
|
|
245
|
+
|
|
246
|
+
# Remove leading slash for path checking
|
|
247
|
+
clean_path = preview.lstrip('/')
|
|
248
|
+
|
|
249
|
+
# Check direct path from project root
|
|
250
|
+
full_path = self.project_root / clean_path
|
|
251
|
+
|
|
252
|
+
if not full_path.exists():
|
|
253
|
+
# Try common variations
|
|
254
|
+
variations = [
|
|
255
|
+
self.project_root / clean_path,
|
|
256
|
+
self.project_root / 'assets' / clean_path,
|
|
257
|
+
self.project_root / clean_path.replace('/assets/', ''),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
found = False
|
|
261
|
+
for var_path in variations:
|
|
262
|
+
if var_path.exists():
|
|
263
|
+
found = True
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
if not found:
|
|
267
|
+
# Look for similar files to suggest
|
|
268
|
+
parent_dir = full_path.parent
|
|
269
|
+
similar_files = []
|
|
270
|
+
if parent_dir.exists():
|
|
271
|
+
target_name = full_path.stem.lower()
|
|
272
|
+
for f in parent_dir.iterdir():
|
|
273
|
+
if f.is_file() and f.suffix.lower() in self.VALID_EXTENSIONS:
|
|
274
|
+
if target_name[:10] in f.stem.lower() or f.stem.lower()[:10] in target_name:
|
|
275
|
+
similar_files.append(f'/{f.relative_to(self.project_root)}')
|
|
276
|
+
|
|
277
|
+
suggestion = "Verify the file path and ensure the image exists"
|
|
278
|
+
if similar_files:
|
|
279
|
+
suggestion = f"Did you mean: {similar_files[0]}"
|
|
280
|
+
|
|
281
|
+
errors.append(ValidationError(
|
|
282
|
+
file_path=file_path,
|
|
283
|
+
preview_value=preview,
|
|
284
|
+
error_type="FILE_NOT_FOUND",
|
|
285
|
+
message=f"Preview image file not found: {clean_path}",
|
|
286
|
+
suggestion=suggestion
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
return errors
|
|
290
|
+
|
|
291
|
+
def validate_file(self, file_path: Path) -> ValidationResult:
|
|
292
|
+
"""Validate a single content file."""
|
|
293
|
+
self.summary.total_files += 1
|
|
294
|
+
|
|
295
|
+
rel_path = str(file_path.relative_to(self.project_root))
|
|
296
|
+
self.debug(f"Checking: {rel_path}")
|
|
297
|
+
|
|
298
|
+
front_matter, raw_preview = self.parse_front_matter(file_path)
|
|
299
|
+
|
|
300
|
+
if front_matter is None:
|
|
301
|
+
return ValidationResult(
|
|
302
|
+
file_path=rel_path,
|
|
303
|
+
title="(parse error)",
|
|
304
|
+
preview_value=raw_preview,
|
|
305
|
+
is_valid=False,
|
|
306
|
+
errors=[ValidationError(
|
|
307
|
+
file_path=rel_path,
|
|
308
|
+
preview_value=raw_preview or "(none)",
|
|
309
|
+
error_type="PARSE_ERROR",
|
|
310
|
+
message="Could not parse front matter"
|
|
311
|
+
)]
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
title = front_matter.get('title', '(no title)')
|
|
315
|
+
preview = front_matter.get('preview')
|
|
316
|
+
|
|
317
|
+
# Handle various null representations
|
|
318
|
+
if preview is None:
|
|
319
|
+
self.summary.files_without_preview += 1
|
|
320
|
+
self.summary.files_with_null_preview += 1
|
|
321
|
+
return ValidationResult(
|
|
322
|
+
file_path=rel_path,
|
|
323
|
+
title=title,
|
|
324
|
+
preview_value=None,
|
|
325
|
+
is_valid=True, # null is valid
|
|
326
|
+
errors=[]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
self.summary.files_with_preview += 1
|
|
330
|
+
|
|
331
|
+
# Convert to string for validation
|
|
332
|
+
preview_str = str(preview).strip()
|
|
333
|
+
|
|
334
|
+
errors = []
|
|
335
|
+
|
|
336
|
+
# Validate format
|
|
337
|
+
format_errors = self.validate_url_format(preview_str, rel_path)
|
|
338
|
+
errors.extend(format_errors)
|
|
339
|
+
|
|
340
|
+
# Validate file exists (only if format is valid enough)
|
|
341
|
+
if not any(e.error_type in ('EMPTY_VALUE', 'STRING_NULL') for e in format_errors):
|
|
342
|
+
existence_errors = self.validate_file_exists(preview_str, rel_path)
|
|
343
|
+
errors.extend(existence_errors)
|
|
344
|
+
|
|
345
|
+
is_valid = len(errors) == 0
|
|
346
|
+
|
|
347
|
+
if is_valid:
|
|
348
|
+
self.summary.valid_previews += 1
|
|
349
|
+
else:
|
|
350
|
+
self.summary.invalid_previews += 1
|
|
351
|
+
# Count error types
|
|
352
|
+
for e in errors:
|
|
353
|
+
if e.error_type == 'FILE_NOT_FOUND':
|
|
354
|
+
self.summary.missing_files += 1
|
|
355
|
+
elif e.error_type in ('RELATIVE_PATH', 'INVALID_EXTENSION', 'NO_EXTENSION', 'EMPTY_VALUE'):
|
|
356
|
+
self.summary.format_errors += 1
|
|
357
|
+
|
|
358
|
+
return ValidationResult(
|
|
359
|
+
file_path=rel_path,
|
|
360
|
+
title=title,
|
|
361
|
+
preview_value=preview_str,
|
|
362
|
+
is_valid=is_valid,
|
|
363
|
+
errors=errors
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def scan_directory(self, directory: Path) -> List[ValidationResult]:
|
|
367
|
+
"""Scan a directory for markdown files and validate them."""
|
|
368
|
+
results = []
|
|
369
|
+
|
|
370
|
+
if not directory.exists():
|
|
371
|
+
self.debug(f"Directory not found: {directory}")
|
|
372
|
+
return results
|
|
373
|
+
|
|
374
|
+
for md_file in directory.rglob("*.md"):
|
|
375
|
+
result = self.validate_file(md_file)
|
|
376
|
+
results.append(result)
|
|
377
|
+
self.summary.results.append(result)
|
|
378
|
+
|
|
379
|
+
return results
|
|
380
|
+
|
|
381
|
+
def run(self) -> ValidationSummary:
|
|
382
|
+
"""Run validation on all content directories."""
|
|
383
|
+
print(f"\n{Colors.CYAN}{'=' * 60}{Colors.NC}")
|
|
384
|
+
print(f"{Colors.CYAN}🔍 Preview URL Validator{Colors.NC}")
|
|
385
|
+
print(f"{Colors.CYAN}{'=' * 60}{Colors.NC}\n")
|
|
386
|
+
|
|
387
|
+
for content_dir in self.CONTENT_DIRS:
|
|
388
|
+
dir_path = self.project_root / content_dir
|
|
389
|
+
if dir_path.exists():
|
|
390
|
+
self.log(f"Scanning: {content_dir}", "info")
|
|
391
|
+
self.scan_directory(dir_path)
|
|
392
|
+
|
|
393
|
+
return self.summary
|
|
394
|
+
|
|
395
|
+
def print_results(self, show_suggestions: bool = False):
|
|
396
|
+
"""Print validation results."""
|
|
397
|
+
# Print errors
|
|
398
|
+
error_results = [r for r in self.summary.results if not r.is_valid]
|
|
399
|
+
|
|
400
|
+
if error_results:
|
|
401
|
+
print(f"\n{Colors.RED}{'=' * 60}{Colors.NC}")
|
|
402
|
+
print(f"{Colors.RED}❌ Validation Errors Found{Colors.NC}")
|
|
403
|
+
print(f"{Colors.RED}{'=' * 60}{Colors.NC}\n")
|
|
404
|
+
|
|
405
|
+
for result in error_results:
|
|
406
|
+
print(f"{Colors.BOLD}{result.file_path}{Colors.NC}")
|
|
407
|
+
print(f" Title: {result.title}")
|
|
408
|
+
print(f" Preview: {result.preview_value}")
|
|
409
|
+
for error in result.errors:
|
|
410
|
+
print(f" {Colors.RED}✗ [{error.error_type}]{Colors.NC} {error.message}")
|
|
411
|
+
if show_suggestions and error.suggestion:
|
|
412
|
+
print(f" {Colors.YELLOW}→ {error.suggestion}{Colors.NC}")
|
|
413
|
+
print()
|
|
414
|
+
|
|
415
|
+
# Print summary
|
|
416
|
+
print(f"\n{Colors.CYAN}{'=' * 60}{Colors.NC}")
|
|
417
|
+
print(f"{Colors.CYAN}📊 Validation Summary{Colors.NC}")
|
|
418
|
+
print(f"{Colors.CYAN}{'=' * 60}{Colors.NC}\n")
|
|
419
|
+
|
|
420
|
+
print(f" Total files scanned: {self.summary.total_files}")
|
|
421
|
+
print(f" Files with preview: {self.summary.files_with_preview}")
|
|
422
|
+
print(f" Files without preview: {self.summary.files_without_preview}")
|
|
423
|
+
print(f" Files with null preview: {self.summary.files_with_null_preview}")
|
|
424
|
+
print()
|
|
425
|
+
print(f" {Colors.GREEN}✓ Valid previews: {self.summary.valid_previews}{Colors.NC}")
|
|
426
|
+
print(f" {Colors.RED}✗ Invalid previews: {self.summary.invalid_previews}{Colors.NC}")
|
|
427
|
+
print(f" - Missing files: {self.summary.missing_files}")
|
|
428
|
+
print(f" - Format errors: {self.summary.format_errors}")
|
|
429
|
+
print()
|
|
430
|
+
|
|
431
|
+
if self.summary.invalid_previews == 0:
|
|
432
|
+
print(f"{Colors.GREEN}✅ All preview URLs are valid!{Colors.NC}\n")
|
|
433
|
+
return 0
|
|
434
|
+
else:
|
|
435
|
+
print(f"{Colors.RED}❌ Found {self.summary.invalid_previews} invalid preview URL(s){Colors.NC}\n")
|
|
436
|
+
return 1
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def main():
|
|
440
|
+
"""Main entry point."""
|
|
441
|
+
parser = argparse.ArgumentParser(
|
|
442
|
+
description="Validate preview image URLs in Jekyll content frontmatter"
|
|
443
|
+
)
|
|
444
|
+
parser.add_argument(
|
|
445
|
+
'-v', '--verbose',
|
|
446
|
+
action='store_true',
|
|
447
|
+
help="Enable verbose output"
|
|
448
|
+
)
|
|
449
|
+
parser.add_argument(
|
|
450
|
+
'-s', '--suggestions', '--fix-suggestions',
|
|
451
|
+
action='store_true',
|
|
452
|
+
help="Show fix suggestions for errors"
|
|
453
|
+
)
|
|
454
|
+
parser.add_argument(
|
|
455
|
+
'--json',
|
|
456
|
+
action='store_true',
|
|
457
|
+
help="Output results as JSON"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
args = parser.parse_args()
|
|
461
|
+
|
|
462
|
+
# Determine project root
|
|
463
|
+
script_dir = Path(__file__).parent
|
|
464
|
+
project_root = script_dir.parent.parent
|
|
465
|
+
|
|
466
|
+
validator = PreviewURLValidator(project_root, verbose=args.verbose)
|
|
467
|
+
summary = validator.run()
|
|
468
|
+
|
|
469
|
+
if args.json:
|
|
470
|
+
import json
|
|
471
|
+
output = {
|
|
472
|
+
'total_files': summary.total_files,
|
|
473
|
+
'files_with_preview': summary.files_with_preview,
|
|
474
|
+
'valid_previews': summary.valid_previews,
|
|
475
|
+
'invalid_previews': summary.invalid_previews,
|
|
476
|
+
'errors': [
|
|
477
|
+
{
|
|
478
|
+
'file': r.file_path,
|
|
479
|
+
'title': r.title,
|
|
480
|
+
'preview': r.preview_value,
|
|
481
|
+
'errors': [
|
|
482
|
+
{
|
|
483
|
+
'type': e.error_type,
|
|
484
|
+
'message': e.message,
|
|
485
|
+
'suggestion': e.suggestion
|
|
486
|
+
}
|
|
487
|
+
for e in r.errors
|
|
488
|
+
]
|
|
489
|
+
}
|
|
490
|
+
for r in summary.results if not r.is_valid
|
|
491
|
+
]
|
|
492
|
+
}
|
|
493
|
+
print(json.dumps(output, indent=2))
|
|
494
|
+
return 1 if summary.invalid_previews > 0 else 0
|
|
495
|
+
else:
|
|
496
|
+
return validator.print_results(show_suggestions=args.suggestions)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
if __name__ == "__main__":
|
|
500
|
+
sys.exit(main())
|