Add from-disk rebuild for font manifest
This commit is contained in:
@@ -12,7 +12,7 @@ use Illuminate\Support\Str;
|
||||
|
||||
class SyncGoogleFonts extends Command
|
||||
{
|
||||
protected $signature = 'fonts:sync-google {--count=50 : Number of popular fonts to fetch} {--weights=400,700 : Comma separated numeric font weights to download} {--italic : Also download italic variants where available} {--force : Re-download files even if they exist} {--path= : Optional custom output directory (defaults to public/fonts/google)} {--family= : Download specific family name(s), comma separated (case-insensitive)} {--category= : Filter by category, comma separated (e.g. sans-serif,serif)} {--prune : Remove local font families not included in this sync} {--dry-run : Show what would be downloaded without writing files}';
|
||||
protected $signature = 'fonts:sync-google {--count=50 : Number of popular fonts to fetch} {--weights=400,700 : Comma separated numeric font weights to download} {--italic : Also download italic variants where available} {--force : Re-download files even if they exist} {--path= : Optional custom output directory (defaults to public/fonts/google)} {--family= : Download specific family name(s), comma separated (case-insensitive)} {--category= : Filter by category, comma separated (e.g. sans-serif,serif)} {--prune : Remove local font families not included in this sync} {--dry-run : Show what would be downloaded without writing files} {--from-disk : Rebuild manifest + CSS from existing font files without downloading}';
|
||||
|
||||
protected $description = 'Download the most popular Google Fonts to the local public/fonts/google directory and generate a manifest + CSS file.';
|
||||
|
||||
@@ -20,6 +20,17 @@ class SyncGoogleFonts extends Command
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$fromDisk = (bool) $this->option('from-disk');
|
||||
$pathOption = $this->option('path');
|
||||
$basePath = $pathOption
|
||||
? (Str::startsWith($pathOption, DIRECTORY_SEPARATOR) ? $pathOption : base_path($pathOption))
|
||||
: public_path('fonts/google');
|
||||
|
||||
if ($fromDisk) {
|
||||
return $this->syncFromDisk($basePath, $dryRun);
|
||||
}
|
||||
|
||||
$apiKey = config('services.google_fonts.key');
|
||||
|
||||
if (! $apiKey) {
|
||||
@@ -32,16 +43,10 @@ class SyncGoogleFonts extends Command
|
||||
$weights = $this->prepareWeights($this->option('weights'));
|
||||
$includeItalic = (bool) $this->option('italic');
|
||||
$force = (bool) $this->option('force');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$families = $this->normalizeFamilyOption($this->option('family'));
|
||||
$categories = $this->prepareCategories($this->option('category'));
|
||||
$prune = (bool) $this->option('prune');
|
||||
|
||||
$pathOption = $this->option('path');
|
||||
$basePath = $pathOption
|
||||
? (Str::startsWith($pathOption, DIRECTORY_SEPARATOR) ? $pathOption : base_path($pathOption))
|
||||
: public_path('fonts/google');
|
||||
|
||||
if (count($families)) {
|
||||
$label = count($families) > 1 ? 'families' : 'family';
|
||||
$this->info(sprintf('Fetching Google Font %s "%s" (weights: %s, italic: %s)...', $label, implode(', ', $families), implode(', ', $weights), $includeItalic ? 'yes' : 'no'));
|
||||
@@ -206,6 +211,204 @@ class SyncGoogleFonts extends Command
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function syncFromDisk(string $basePath, bool $dryRun): int
|
||||
{
|
||||
if (! File::isDirectory($basePath)) {
|
||||
$this->error(sprintf('Font directory not found: %s', $basePath));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($this->option('prune')) {
|
||||
$this->warn('Ignoring --prune when rebuilding from disk.');
|
||||
}
|
||||
|
||||
$fonts = $this->buildManifestFromDisk($basePath);
|
||||
|
||||
if (! count($fonts)) {
|
||||
$this->warn('No fonts found on disk.');
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info(sprintf('Dry run complete: %d font families would be written to %s', count($fonts), $basePath));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->writeManifest($basePath, $fonts);
|
||||
$this->writeCss($basePath, $fonts);
|
||||
Cache::forget('fonts:manifest');
|
||||
|
||||
$this->info(sprintf('Rebuilt manifest for %d font families from %s', count($fonts), $basePath));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function buildManifestFromDisk(string $basePath): array
|
||||
{
|
||||
$directories = File::directories($basePath);
|
||||
$fonts = [];
|
||||
|
||||
foreach ($directories as $dir) {
|
||||
$slug = basename($dir);
|
||||
$files = collect(File::files($dir))
|
||||
->filter(function (\SplFileInfo $file) {
|
||||
$extension = strtolower($file->getExtension());
|
||||
|
||||
return in_array($extension, ['woff2', 'woff', 'otf', 'ttf'], true);
|
||||
})
|
||||
->values();
|
||||
|
||||
if (! $files->count()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variantsByKey = [];
|
||||
foreach ($files as $file) {
|
||||
$filename = $file->getFilename();
|
||||
$extension = strtolower($file->getExtension());
|
||||
$style = $this->extractStyleFromFilename($filename);
|
||||
$weight = $this->extractWeightFromFilename($filename);
|
||||
$variantKey = $this->buildVariantKey($weight, $style);
|
||||
$priority = $this->extensionPriority($extension);
|
||||
$relativePath = sprintf('/fonts/google/%s/%s', $slug, $filename);
|
||||
|
||||
$existing = $variantsByKey[$variantKey] ?? null;
|
||||
if ($existing && ($existing['priority'] ?? 0) >= $priority) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variantsByKey[$variantKey] = [
|
||||
'variant' => $variantKey,
|
||||
'weight' => $weight,
|
||||
'style' => $style,
|
||||
'url' => $relativePath,
|
||||
'priority' => $priority,
|
||||
];
|
||||
}
|
||||
|
||||
if (! count($variantsByKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variants = array_values(array_map(function (array $variant) {
|
||||
unset($variant['priority']);
|
||||
|
||||
return $variant;
|
||||
}, $variantsByKey));
|
||||
|
||||
usort($variants, function (array $left, array $right) {
|
||||
$weightCompare = ($left['weight'] ?? 400) <=> ($right['weight'] ?? 400);
|
||||
if ($weightCompare !== 0) {
|
||||
return $weightCompare;
|
||||
}
|
||||
|
||||
return strcmp((string) ($left['style'] ?? 'normal'), (string) ($right['style'] ?? 'normal'));
|
||||
});
|
||||
|
||||
$fonts[] = [
|
||||
'family' => $this->familyFromSlug($slug),
|
||||
'slug' => $slug,
|
||||
'category' => null,
|
||||
'variants' => $variants,
|
||||
];
|
||||
}
|
||||
|
||||
usort($fonts, fn (array $left, array $right) => strcmp((string) $left['family'], (string) $right['family']));
|
||||
|
||||
return $fonts;
|
||||
}
|
||||
|
||||
private function familyFromSlug(string $slug): string
|
||||
{
|
||||
$parts = array_filter(explode('-', $slug), fn ($part) => $part !== '');
|
||||
|
||||
$words = array_map(function (string $part) {
|
||||
if (is_numeric($part)) {
|
||||
return $part;
|
||||
}
|
||||
|
||||
if (strlen($part) <= 3) {
|
||||
return strtoupper($part);
|
||||
}
|
||||
|
||||
return ucfirst(strtolower($part));
|
||||
}, $parts);
|
||||
|
||||
return trim(implode(' ', $words));
|
||||
}
|
||||
|
||||
private function extractStyleFromFilename(string $filename): string
|
||||
{
|
||||
$lower = strtolower($filename);
|
||||
|
||||
return str_contains($lower, 'italic') || str_contains($lower, 'oblique') ? 'italic' : 'normal';
|
||||
}
|
||||
|
||||
private function extractWeightFromFilename(string $filename): int
|
||||
{
|
||||
if (preg_match('/(?:^|[^0-9])(100|200|300|400|500|600|700|800|900)(?:[^0-9]|$)/', $filename, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
$lower = strtolower($filename);
|
||||
$weightMap = [
|
||||
'thin' => 100,
|
||||
'extralight' => 200,
|
||||
'ultralight' => 200,
|
||||
'light' => 300,
|
||||
'regular' => 400,
|
||||
'book' => 400,
|
||||
'medium' => 500,
|
||||
'semibold' => 600,
|
||||
'demibold' => 600,
|
||||
'bold' => 700,
|
||||
'extrabold' => 800,
|
||||
'ultrabold' => 800,
|
||||
'black' => 900,
|
||||
'heavy' => 900,
|
||||
];
|
||||
|
||||
foreach ($weightMap as $label => $weight) {
|
||||
if (str_contains($lower, $label)) {
|
||||
return $weight;
|
||||
}
|
||||
}
|
||||
|
||||
return 400;
|
||||
}
|
||||
|
||||
private function buildVariantKey(int $weight, string $style): string
|
||||
{
|
||||
if ($weight === 400 && $style === 'normal') {
|
||||
return 'regular';
|
||||
}
|
||||
|
||||
if ($weight === 400 && $style === 'italic') {
|
||||
return 'italic';
|
||||
}
|
||||
|
||||
if ($style === 'italic') {
|
||||
return $weight.'italic';
|
||||
}
|
||||
|
||||
return (string) $weight;
|
||||
}
|
||||
|
||||
private function extensionPriority(string $extension): int
|
||||
{
|
||||
return match ($extension) {
|
||||
'woff2' => 4,
|
||||
'woff' => 3,
|
||||
'otf' => 2,
|
||||
'ttf' => 1,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -305,4 +305,50 @@ class SyncGoogleFontsTest extends TestCase
|
||||
|
||||
File::deleteDirectory($targetPath);
|
||||
}
|
||||
|
||||
public function test_it_rebuilds_manifest_from_disk_without_downloading(): void
|
||||
{
|
||||
$targetPath = storage_path('app/test-fonts');
|
||||
File::deleteDirectory($targetPath);
|
||||
|
||||
File::ensureDirectoryExists($targetPath.'/alpha-sans');
|
||||
File::put($targetPath.'/alpha-sans/AlphaSans-400-normal.ttf', 'font-binary');
|
||||
File::put($targetPath.'/alpha-sans/AlphaSans-700-italic.woff2', 'font-binary');
|
||||
|
||||
File::ensureDirectoryExists($targetPath.'/beta-serif');
|
||||
File::put($targetPath.'/beta-serif/BetaSerif-Regular.ttf', 'font-binary');
|
||||
|
||||
File::ensureDirectoryExists($targetPath.'/ibm-plex-sans');
|
||||
File::put($targetPath.'/ibm-plex-sans/IBMPlexSans-Bold.ttf', 'font-binary');
|
||||
|
||||
Http::fake();
|
||||
|
||||
Artisan::call('fonts:sync-google', [
|
||||
'--from-disk' => true,
|
||||
'--path' => 'storage/app/test-fonts',
|
||||
]);
|
||||
|
||||
Http::assertNothingSent();
|
||||
|
||||
$manifestPath = $targetPath.'/manifest.json';
|
||||
$cssPath = $targetPath.'/fonts.css';
|
||||
|
||||
$this->assertFileExists($manifestPath);
|
||||
$this->assertFileExists($cssPath);
|
||||
|
||||
$manifest = json_decode(File::get($manifestPath), true);
|
||||
$this->assertSame(3, $manifest['count']);
|
||||
$this->assertTrue(collect($manifest['fonts'])->pluck('family')->contains('Alpha Sans'));
|
||||
$this->assertTrue(collect($manifest['fonts'])->pluck('family')->contains('Beta Serif'));
|
||||
$this->assertTrue(collect($manifest['fonts'])->pluck('family')->contains('IBM Plex Sans'));
|
||||
|
||||
$this->assertTrue(str_contains(File::get($cssPath), "font-family: 'IBM Plex Sans';"));
|
||||
|
||||
$alpha = collect($manifest['fonts'])->firstWhere('family', 'Alpha Sans');
|
||||
$alphaVariants = collect($alpha['variants'] ?? [])->pluck('variant')->all();
|
||||
$this->assertContains('regular', $alphaVariants);
|
||||
$this->assertContains('700italic', $alphaVariants);
|
||||
|
||||
File::deleteDirectory($targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user