diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc62d1..2ff983f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,11 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan ### Changed +- **`icon-sets.json` records each pack's `visibility`** (`public` or `private`), synced by `bin/sync-icon-sets.php` from the repos API and kept when a response does not state it (F-02). Documented under *The pack catalogue* in `docs/tools/artisan-commands.md`. `tests/Unit/SyncIconSetsVisibilityTest.php`. +- **The generic Blade snippet uses a prefix this host registers**: the short one while `components.short_prefix` names one, `ichava-core` when it is off (`ComponentPrefixes::snippetPrefix()`). +- **`IconDriverInterface` states `SvgDriver`'s real API** (`render(IconData …)`, `load(string $path …)`), `SvgDriver` implements it, the provider binds it, and `IconRegistry`, `IconSvgReader` and `DirectoryIconDriver` depend on it (C-17). The interface's old directory methods (`all`, `has`, `setConfig`, `getConfig`, `setIconPath`, `getIconPath`) are gone from it; `DirectoryIconDriver` keeps them as its own API. `IconRegistry::driver()` returns the interface. `tests/Feature/IconDriverContractTest.php`. +- **`IconSetBuilder::cache(false)` turns the lookup cache off.** It set a flag nothing read. Caching stays on by default. `tests/Unit/IconSetBuilderCacheTest.php`. +- **`components.test_icons` has a reader**: `ComponentPrefixes::testIconsEnabled()`, false whenever `components.enabled` is. `tests/Feature/TestIconsComponentKeyTest.php`. - **A pack's `config.defaults` applies wherever the pack renders** (P-06). The registry applies `config.defaults.class`, `.attributes`, `.stroke_width` (as `stroke-width`) and `.fallback` to the set it builds, so ``, the pack's own tag and `IconRegistry::render()` draw the same attributes. Before, a pack's defaults lived in its `IconComponent` subclass and reached its own tag only. `tests/Feature/PackConfigDefaultsTest.php`. - **The pack base providers live in `Providers\`** (C-07): `Support\IconPackServiceProvider` is `Providers\IconPackServiceProvider`, and `Support\ServiceProvider` is `Providers\PackServiceProvider`. No alias is left at the old names; a pack changes one `use` line. `tests/Unit/ProvidersNamespaceTest.php`. - **`custom-icons.sets` keys take effect** (C-09): `enabled`, `prefix`, `default_class`, `default_attributes`, `fallback`, `variants`, `default_variant`, `supports_categories`, `blade_namespace`, `blade_component`, plus `package` for a directory without a `config.json` and `bulk`/`only` for several sets. Before, only `path` was read. `tests/Feature/CustomIconSetsTest.php`. @@ -70,6 +75,7 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan ### Removed +- **`IchavaServiceProvider::isIchavaQueueCommand()`**, unused since C-01 moved the runtime limits into `RuntimeLimits`. **`IconSetBuilder::$discoveredIcons` and `$initialized`**, which nothing wrote or read. - **Forward-only renames, with no alias left behind** (owner, 2026-11-05): `Support\IconPackServiceProvider` and `Support\ServiceProvider` (now `Providers\IconPackServiceProvider`, `Providers\PackServiceProvider`), `Actions\CountSvgFiles` (now `Support\SvgFileCounter`), `ClearDiscoveryCaches::generation()` (now `DiscoveryCacheGeneration::current()`), and `__invoke()` on four actions (now `handle()`). Every caller in the estate moves in the same sweep; each is recorded in the deprecations audit. - **The bare `icon` morph alias** (now `ichava-icon`; the migration moves stored rows). The estate is forward-only (owner, 2026-11-05: "never support legacy only forward"). Nothing has been released, so these compatibility paths go rather than stay beside their replacements. diff --git a/bin/sync-icon-sets.php b/bin/sync-icon-sets.php index e547ec3..ac05418 100755 --- a/bin/sync-icon-sets.php +++ b/bin/sync-icon-sets.php @@ -8,7 +8,8 @@ * * For every set pinned in icon-sets.json (key/package/repository) this * script resolves the latest release tag, reads the pack's config.json at - * that tag, counts the committed SVGs (in all, and per kind as icon_counts) + * that tag, counts the committed SVGs (in all, and per kind as icon_counts), + * records the repository's GitHub visibility (public or private) * via the git-trees API, and writes * the snapshot back — so `ichava:install` stays fully local with zero * runtime network calls. @@ -144,6 +145,7 @@ function snapshot(string $repository, string $configPath, array $previous): ?arr } $variants = $config['metadata']['data']['variants'] ?? null; + $visibility = visibilityOf(getJson("https://api.github.com/repos/{$repository}"), $previous['visibility'] ?? null); $tree = $tag !== null ? svgPaths($repository, $tag, $configPath) : null; $count = $tree !== null ? count($tree) : null; $counts = $tree !== null ? countsByKind($tree, $config) : null; @@ -156,9 +158,29 @@ function snapshot(string $repository, string $configPath, array $previous): ?arr 'icon_counts' => $counts ?? ($previous['icon_counts'] ?? null), 'variants' => is_array($variants) ? array_keys($variants) : ($previous['variants'] ?? null), 'latest_version' => $tag !== null ? ltrim($tag, 'vV') : ($previous['latest_version'] ?? null), + 'visibility' => $visibility, ]; } +/** + * The repository's GitHub visibility, `public` or `private`, from the repos + * API response. A response that does not say (a failed request, or a private + * repository the token cannot see, which answers 404) keeps the previous + * value, so one bad night never flips a pack's recorded visibility. + * + * @param array|null $repository + */ +function visibilityOf(?array $repository, mixed $previous): ?string +{ + $visibility = is_array($repository) ? ($repository['visibility'] ?? null) : null; + + if (in_array($visibility, ['public', 'private'], true)) { + return $visibility; + } + + return in_array($previous, ['public', 'private'], true) ? $previous : null; +} + function latestTag(string $repository): ?string { $tags = getJson("https://api.github.com/repos/{$repository}/tags?per_page=100"); diff --git a/config/ichava-core.php b/config/ichava-core.php index 4100826..6fd56a7 100644 --- a/config/ichava-core.php +++ b/config/ichava-core.php @@ -299,6 +299,8 @@ // beside the canonical ``. 'short_prefix' => env('ICHAVA_COMPONENT_SHORT_PREFIX', 'ichava'), + // The bundled component, registered by + // ichava/icon-browser when this is on (ComponentPrefixes::testIconsEnabled()). 'test_icons' => env('ICHAVA_TEST_COMPONENT_ENABLED', env('APP_ENV') === 'local'), ], diff --git a/docs/tools/artisan-commands.md b/docs/tools/artisan-commands.md index 40917c8..3008319 100644 --- a/docs/tools/artisan-commands.md +++ b/docs/tools/artisan-commands.md @@ -57,6 +57,10 @@ php artisan ichava::ichava-core.watch --package=vendor/pack # Rescan one pack php artisan ichava::ichava-core.watch --refresh # Record every pack's fingerprint after the scan ``` +### The pack catalogue, `icon-sets.json` + +`ichava::ichava-core.install` offers the packs listed in `icon-sets.json` at the root of core, and makes no network call. `bin/sync-icon-sets.php` refreshes it daily (`.github/workflows/sync-icon-sets.yml`). Each entry pins `key`, `package` and `repository`; the script syncs `title`, `description`, `license`, `icon_count`, `icon_counts` (per kind), `variants` and `latest_version` from the pack's `config.json` at its latest tag, and `visibility`, the repository's GitHub visibility (`public` or `private`), from the repos API. A response that does not state the visibility keeps the recorded value. A consumer that needs to know which packs are public reads `visibility` here rather than probing GitHub. + ### Scaffolding a new pack moved out of core `ichava::ichava-core.make:icon-package` was removed in core `0.3.0`. It lives in diff --git a/docs/tools/blade-components.md b/docs/tools/blade-components.md index 259ec46..416d452 100644 --- a/docs/tools/blade-components.md +++ b/docs/tools/blade-components.md @@ -80,7 +80,7 @@ Both extend `IconComponent` and accept the same `name`, `class`, `title`, `aria` ``` -Both are registered by `ichava/icon-browser`, and its `test_component_enabled` key turns the test component off. Core's own `components.test_icons` key is read by nothing today; it is kept, and listed for a decision. +Both are registered by `ichava/icon-browser`. Whether the test component is registered is core's `components.test_icons` key (`ICHAVA_TEST_COMPONENT_ENABLED`, on by default only when `APP_ENV` is `local`), read through `ComponentPrefixes::testIconsEnabled()`; `components.enabled` false turns it off with every other core tag. ### Per-package components diff --git a/icon-sets.json b/icon-sets.json index 95453c7..1284fe6 100644 --- a/icon-sets.json +++ b/icon-sets.json @@ -1,6 +1,6 @@ { "schema_version": "3.0", - "_note": "Static snapshot refreshed daily by .github/workflows/sync-icon-sets.yml (bin/sync-icon-sets.php). Each set pins key/package/repository; title, description, license, icon_count, icon_counts (per kind: icon, illustration), variants and latest_version are synced from the pack’s config.json at its latest GitHub tag. To add a set, append an entry carrying at least key, package, repository, title, icon_count and variants — IconSetCatalogService rejects any set missing one of those six, treating an empty string or empty array as missing, so a set pinned with only the first three makes ichava::ichava-core.install throw until the next nightly sync fills the rest. Take the values from the pack’s own config.json and let the workflow refresh them. Write with JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, as this file is generated and any other encoding reflows it. ichava::ichava-core.install reads this file only — zero runtime network calls.", + "_note": "Static snapshot refreshed daily by .github/workflows/sync-icon-sets.yml (bin/sync-icon-sets.php). Each set pins key/package/repository; title, description, license, icon_count, icon_counts (per kind: icon, illustration), variants, latest_version and visibility (the repository's GitHub visibility, public or private) are synced from the pack’s config.json at its latest GitHub tag. To add a set, append an entry carrying at least key, package, repository, title, icon_count and variants — IconSetCatalogService rejects any set missing one of those six, treating an empty string or empty array as missing, so a set pinned with only the first three makes ichava::ichava-core.install throw until the next nightly sync fills the rest. Take the values from the pack’s own config.json and let the workflow refresh them. Write with JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, as this file is generated and any other encoding reflows it. ichava::ichava-core.install reads this file only — zero runtime network calls.", "config_path": "resources/assets/svg/config.json", "sets": [ { @@ -19,7 +19,8 @@ "icon_counts": { "icon": 6238, "illustration": 0 - } + }, + "visibility": "public" }, { "key": "flags", @@ -37,7 +38,8 @@ "icon_counts": { "icon": 542, "illustration": 0 - } + }, + "visibility": "public" }, { "key": "emoji", @@ -54,7 +56,8 @@ "icon_counts": { "icon": 3435, "illustration": 0 - } + }, + "visibility": "public" }, { "key": "openmoji", @@ -71,7 +74,8 @@ "icon_counts": { "icon": 8568, "illustration": 0 - } + }, + "visibility": "public" } ], "synced_at": "2026-11-03T10:18:20Z" diff --git a/src/Contracts/IconDriverInterface.php b/src/Contracts/IconDriverInterface.php index 9f5759b..826cf3c 100644 --- a/src/Contracts/IconDriverInterface.php +++ b/src/Contracts/IconDriverInterface.php @@ -4,55 +4,39 @@ namespace Simtabi\Laranail\Ichava\Contracts; +use Simtabi\Laranail\Ichava\Data\IconData; +use Simtabi\Laranail\Ichava\Drivers\SvgDriver; +use Simtabi\Laranail\Ichava\Exceptions\IchavaException; + +/** + * Reads SVG files safely and renders icons from them. + * + * Bound to {@see SvgDriver}. The registry, the served-SVG reader and the + * directory driver depend on this, so a host substitutes the driver (a + * remote store, an encrypted one) without subclassing. The signatures are + * SvgDriver's: until 2026-11-05 this interface described a directory API no + * driver in use implemented (C-17). + */ interface IconDriverInterface { /** - * Get all available icons + * Render $icon with $attributes applied to its root element. * - * @return array - */ - public function all(): array; - - /** - * Render an icon as SVG string - * - * @param string $name Icon name - * @param array $attributes HTML attributes - */ - public function render(string $name, array $attributes = []): string; - - /** - * Check if an icon exists - * - * @param string $name Icon name - */ - public function has(string $name): bool; - - /** - * Set configuration for the driver - * - * @param array $config + * @param array $attributes + * @param string|null $baseDir the pack directory the file must stay inside + * @param int|null $maxBytes the size ceiling for the icon's kind; null keeps the global one * - * @return $this + * @throws IchavaException when the file is refused or cannot be read */ - public function setConfig(array $config): static; + public function render(IconData $icon, array $attributes = [], ?string $baseDir = null, ?int $maxBytes = null): string; /** - * Get configuration + * The raw, unsanitised content of the SVG at $path, after the safety + * checks: no symlink, contained in $baseDir, within the size bounds. * - * @return array - */ - public function getConfig(): array; - - /** - * Set the icon path + * @param array $options reserved for future loaders * - * @return $this - */ - public function setIconPath(string $path): static; - - /** - * Get the icon path + * @throws IchavaException when the file is refused or cannot be read */ - public function getIconPath(): string; + public function load(string $path, array $options = [], ?string $baseDir = null, ?int $maxBytes = null): string; } diff --git a/src/Drivers/DirectoryIconDriver.php b/src/Drivers/DirectoryIconDriver.php index 4f4c071..ebe03d2 100644 --- a/src/Drivers/DirectoryIconDriver.php +++ b/src/Drivers/DirectoryIconDriver.php @@ -10,18 +10,12 @@ use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface; /** - * `IconDriverInterface` over one directory of SVG files. - * - * The extension point for a driver author: list, check and render icons by - * their path under the directory (`ui/check` → `ui/check.svg`), with no - * registry, pack or database involved. Rendering goes through SvgDriver, so - * the same containment checks, size limit and sanitiser apply. - * - * SvgDriver itself does not implement the interface: its `render()` takes a - * resolved IconData, which is what IconRegistry hands it, and changing that - * signature would break the registry and every subclass. + * Lists, checks and renders the icons in one directory of SVG files, by their + * path under it (`ui/check` → `ui/check.svg`), with no registry, pack or + * database involved. Rendering goes through the bound IconDriverInterface + * (SvgDriver), so the same containment checks, size limit and sanitiser apply. */ -final class DirectoryIconDriver implements IconDriverInterface +final class DirectoryIconDriver { /** @var array */ private array $config = []; @@ -29,7 +23,7 @@ final class DirectoryIconDriver implements IconDriverInterface private string $iconPath = ''; public function __construct( - private readonly SvgDriver $driver, + private readonly IconDriverInterface $driver, ) {} /** diff --git a/src/Drivers/SvgDriver.php b/src/Drivers/SvgDriver.php index 129e3fb..3c1e4c5 100644 --- a/src/Drivers/SvgDriver.php +++ b/src/Drivers/SvgDriver.php @@ -13,6 +13,7 @@ use Simtabi\Laranail\Ichava\Constants\IchavaConstants; use Simtabi\Laranail\Ichava\Services\IconCacheService; use Simtabi\Laranail\Ichava\Exceptions\IchavaException; +use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface; use Simtabi\Laranail\Ichava\Services\SvgProcessingService; /** @@ -37,7 +38,7 @@ * @see IconCacheService * @see IchavaConstants */ -final class SvgDriver +final class SvgDriver implements IconDriverInterface { use ContainsFilePaths; diff --git a/src/Providers/IchavaServiceProvider.php b/src/Providers/IchavaServiceProvider.php index 9bca0d8..1cdb201 100644 --- a/src/Providers/IchavaServiceProvider.php +++ b/src/Providers/IchavaServiceProvider.php @@ -5,7 +5,6 @@ namespace Simtabi\Laranail\Ichava\Providers; use Throwable; -use Illuminate\Support\Str; use Laravel\Horizon\Horizon; use Illuminate\Support\Facades\DB; use Simtabi\Laranail\Ichava\Ichava; @@ -53,6 +52,7 @@ use Simtabi\Laranail\Ichava\Contracts\DatabaseOperations; use Simtabi\Laranail\Ichava\Support\IchavaSessionManager; use Illuminate\Contracts\Auth\Access\Gate as GateContract; +use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface; use Simtabi\Laranail\Ichava\Services\ConfigurationService; use Simtabi\Laranail\Ichava\Services\IconDiscoveryService; use Simtabi\Laranail\Ichava\Services\SvgProcessingService; @@ -195,6 +195,7 @@ public function registeringPackage(): void $this->app->singleton(PathResolver::class); $this->app->singleton(SvgDriver::class); + $this->app->bind(IconDriverInterface::class, fn (Container $app): IconDriverInterface => $app->make(SvgDriver::class)); // 3. Cache service (depends on IchavaLogger only) $this->app->singleton(IconCacheService::class); @@ -645,21 +646,4 @@ protected function configureRuntimeSettings(): RuntimeConfigurator { return $this->app->make(RuntimeLimits::class)->configurator(); } - - /** - * Check if we're running an Ichava queue command. - */ - protected function isIchavaQueueCommand(): bool - { - if (! $this->app->runningInConsole()) { - return false; - } - - $argv = $_SERVER['argv'] ?? []; - $argv = is_array($argv) ? array_map(Coerce::string(...), $argv) : []; - $isQueueCommand = collect($argv)->contains(fn (string $arg) => Str::contains($arg, 'queue:')); - $isIchavaQueue = collect($argv)->contains(fn (string $arg) => Str::contains($arg, 'ichava')); - - return $isQueueCommand && $isIchavaQueue; - } } diff --git a/src/Services/IconBrowserService.php b/src/Services/IconBrowserService.php index e3019a6..22ae6cb 100644 --- a/src/Services/IconBrowserService.php +++ b/src/Services/IconBrowserService.php @@ -19,6 +19,7 @@ use Simtabi\Laranail\Ichava\Models\IconTerm; use Simtabi\Laranail\Ichava\Support\Helpers; use Simtabi\Laranail\Ichava\Data\VariantData; +use Simtabi\Laranail\Ichava\View\ComponentPrefixes; use Illuminate\Database\Query\Builder as QueryBuilder; use Simtabi\Laranail\Ichava\Exceptions\IchavaException; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -37,6 +38,7 @@ public function __construct( private IconCacheService $cacheManager, private IchavaLogger $logger, private DatabaseOperations $database, + private ComponentPrefixes $prefixes, ) {} /** @@ -149,7 +151,9 @@ public function transformIcon(Icon $icon): array * ``, with the * folders in `category` because the pack component reads a name containing * a slash as a full path. Falls back to the generic form otherwise. - * - Generic: ``. + * - Generic: ``, + * under the short prefix while `components.short_prefix` names one and + * `ichava-core` when it is off, so the copied tag renders on this host. * It used to be ``, a * tag nothing registers holding a reference the registry refuses. */ @@ -159,7 +163,7 @@ public function generateBladeComponent(Icon $icon, bool $useCleanSyntax = true, $tag = $useCleanSyntax ? $this->packComponentTag($icon) : null; if ($tag === null) { - return ""; + return "prefixes->snippetPrefix()}::icon name=\"{$path}\" class=\"{$class}\" />"; } $relative = substr($path, strpos($path, '::') + 2); diff --git a/src/Services/IconRegistry.php b/src/Services/IconRegistry.php index 480fafa..6e23f1b 100644 --- a/src/Services/IconRegistry.php +++ b/src/Services/IconRegistry.php @@ -28,6 +28,7 @@ use Simtabi\Laranail\Ichava\Contracts\IconSetInterface; use Simtabi\Laranail\Ichava\Exceptions\IchavaException; use Simtabi\Laranail\Ichava\Events\IconRegistrationEvent; +use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface; /** * IconRegistry - Icon Package Registration & Management @@ -110,7 +111,7 @@ final class IconRegistry public function __construct( private Application $app, - private SvgDriver $driver, + private IconDriverInterface $driver, private IchavaLogger $logger, private PathResolver $pathResolver, private IconAliasMap $aliases, @@ -921,7 +922,7 @@ public function countIconsInDirectory(string $path): int /** * Direct access to the SvgDriver instance (load, process, render SVG files). */ - public function driver(): SvgDriver + public function driver(): IconDriverInterface { return $this->driver; } diff --git a/src/Services/IconSetBuilder.php b/src/Services/IconSetBuilder.php index 207d643..183fcff 100644 --- a/src/Services/IconSetBuilder.php +++ b/src/Services/IconSetBuilder.php @@ -36,10 +36,6 @@ final class IconSetBuilder implements IconSetInterface private IconSetConfig $config; - /** @var array */ - // @phpstan-ignore property.onlyWritten (written and never read since before 2026-11-05; listed for an owner decision, not removed) - private array $discoveredIcons = []; - // From IconSetBuilder (fluent configuration) private string $name; @@ -62,8 +58,8 @@ final class IconSetBuilder implements IconSetInterface private ?string $fallback = null; - // @phpstan-ignore property.onlyWritten (written and never read since before 2026-11-05; listed for an owner decision, not removed) - private bool $enableCache = false; + /** Whether lookups are cached; cache(false) rediscovers on every call. */ + private bool $enableCache = true; /** @var IconSetInterface[] Merged icon sets */ private array $mergedSets = []; @@ -71,10 +67,6 @@ final class IconSetBuilder implements IconSetInterface /** @var bool Whether this is a merged set */ private bool $isMerged = false; - /** @var bool Whether the parent has been initialized */ - // @phpstan-ignore property.onlyWritten (written and never read since before 2026-11-05; listed for an owner decision, not removed) - private bool $initialized = false; - /** * Create new icon set */ @@ -323,7 +315,8 @@ public function fallback(?string $icon): static } /** - * Enable/disable caching + * Enable/disable caching of icon lookups (on by default). Off, get() + * and all() read the directory on every call. * * @param bool $enabled Cache enabled * @@ -403,6 +396,10 @@ public function get(string $name, ?string $variant = null, ?string $category = n return null; } + if (! $this->enableCache) { + return $this->discoverIcon($name, $variant, $category); + } + $cacheKey = $this->getCacheKey($name, $variant, $category); $cached = $this->cache->remember($cacheKey, function () use ($name, $variant, $category) { @@ -449,6 +446,10 @@ public function all(?string $variant = null, ?string $category = null): array return $allIcons; } + if (! $this->enableCache) { + return $this->discoverAllIcons($variant, $category); + } + $cacheKey = $this->getAllCacheKey($variant, $category); $cached = $this->cache->remember($cacheKey, function () use ($variant, $category) { diff --git a/src/Services/IconSvgReader.php b/src/Services/IconSvgReader.php index b14c74c..976e422 100644 --- a/src/Services/IconSvgReader.php +++ b/src/Services/IconSvgReader.php @@ -10,6 +10,7 @@ use Simtabi\Laranail\Ichava\Drivers\SvgDriver; use Simtabi\Laranail\Ichava\Support\AuditLogger; use Simtabi\Laranail\Ichava\Exceptions\IchavaException; +use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface; /** * Reads one SVG file of an icon and returns its served form: read safely, @@ -24,7 +25,7 @@ { public function __construct( private IconCacheService $cache, - private SvgDriver $driver, + private IconDriverInterface $driver, private SvgProcessingService $processor, private AuditLogger $audit, ) {} diff --git a/src/View/ComponentPrefixes.php b/src/View/ComponentPrefixes.php index 4e24764..22e64a5 100644 --- a/src/View/ComponentPrefixes.php +++ b/src/View/ComponentPrefixes.php @@ -43,6 +43,17 @@ public function enabled(): bool return $this->config->get('ichava.ichava-core.components.enabled', true) !== false; } + /** + * Whether the bundled test-icons component should be registered: + * `components.test_icons` (default: on when `APP_ENV` is `local`), and + * never while `components.enabled` is false. Core ships no such + * component; ichava/icon-browser registers it and reads this. + */ + public function testIconsEnabled(): bool + { + return $this->enabled() && (bool) $this->config->get('ichava.ichava-core.components.test_icons', false); + } + /** * The short prefix, or null when it is off. * @@ -67,6 +78,16 @@ public function shortPrefix(): ?string return $prefix; } + /** + * The prefix a generated snippet uses for the icon component: the short + * one while it is on, so existing habits keep working, else the canonical + * one, so a copied tag always renders on this host. + */ + public function snippetPrefix(): string + { + return $this->shortPrefix() ?? self::CANONICAL; + } + /** * The prefixes core registers, canonical first. * diff --git a/tests/Feature/BrowserDataWiringTest.php b/tests/Feature/BrowserDataWiringTest.php index 47c6eeb..babd655 100644 --- a/tests/Feature/BrowserDataWiringTest.php +++ b/tests/Feature/BrowserDataWiringTest.php @@ -215,7 +215,7 @@ public function configurePackage(Package $package): void {} ->and($provider->getPackageMetadata()['kind'])->toBe('icon'); }); -it('implements IconDriverInterface over a directory', function (): void { +it('lists, checks and renders a directory through the bound driver', function (): void { $driver = app(DirectoryIconDriver::class)->setIconPath(fixturePack('KindPack/files')); expect($driver->all())->toContain('glyphs/star', 'scenes/park') diff --git a/tests/Feature/IconDriverContractTest.php b/tests/Feature/IconDriverContractTest.php new file mode 100644 index 0000000..0022b33 --- /dev/null +++ b/tests/Feature/IconDriverContractTest.php @@ -0,0 +1,42 @@ +toBe(app(SvgDriver::class)) + ->and(app(IconRegistry::class)->driver())->toBeInstanceOf(IconDriverInterface::class); +}); + +it('renders through whatever driver the interface is bound to', function (): void { + app()->forgetInstance(IconRegistry::class); + app()->instance(IconDriverInterface::class, new class implements IconDriverInterface + { + public function render(IconData $icon, array $attributes = [], ?string $baseDir = null, ?int $maxBytes = null): string + { + return ''; + } + + public function load(string $path, array $options = [], ?string $baseDir = null, ?int $maxBytes = null): string + { + return ''; + } + }); + + $registry = app(IconRegistry::class); + $registry->fromDirectory(dirname(__DIR__, 2) . '/resources/assets/svg/test-icons'); + + expect($registry->render('ichava/test-icons::test-icons/check'))->toBe(''); +}); diff --git a/tests/Feature/TestIconsComponentKeyTest.php b/tests/Feature/TestIconsComponentKeyTest.php new file mode 100644 index 0000000..a86045a --- /dev/null +++ b/tests/Feature/TestIconsComponentKeyTest.php @@ -0,0 +1,25 @@ +set('ichava.ichava-core.components.test_icons', $value); + + expect(app(ComponentPrefixes::class)->testIconsEnabled())->toBe($value); +})->with([true, false]); + +it('turns the test-icons component off with the rest of the Blade surface', function (): void { + config()->set('ichava.ichava-core.components.test_icons', true); + config()->set('ichava.ichava-core.components.enabled', false); + + expect(app(ComponentPrefixes::class)->testIconsEnabled())->toBeFalse(); +}); diff --git a/tests/Unit/IconSetBuilderCacheTest.php b/tests/Unit/IconSetBuilderCacheTest.php index 57e7619..9601afc 100644 --- a/tests/Unit/IconSetBuilderCacheTest.php +++ b/tests/Unit/IconSetBuilderCacheTest.php @@ -136,3 +136,23 @@ function useHardenedFileCache(): void } }); }); + +describe('cache() switches the set cache', function () { + it('serves a looked-up icon from the cache by default, and rediscovers with cache(false)', function () { + $dir = makeCacheTestDir(); + $cached = makeCacheTestSet($dir); + $uncached = IconSetBuilder::make('test-cache-set-off')->setBasePath($dir)->prefix('tco')->cache(false); + + expect($cached->get('check', 'outline'))->not->toBeNull() + ->and($uncached->get('check', 'outline'))->not->toBeNull(); + + File::delete($dir . '/outline/check.svg'); + + // cache(false) used to set a flag nothing read, so both still answered. + expect($cached->get('check', 'outline'))->not->toBeNull() + ->and($uncached->get('check', 'outline'))->toBeNull() + ->and($uncached->all('outline'))->toBe([]); + + File::deleteDirectory($dir); + }); +}); diff --git a/tests/Unit/IconSnippetFormatTest.php b/tests/Unit/IconSnippetFormatTest.php index 93f1a3a..829dbdc 100644 --- a/tests/Unit/IconSnippetFormatTest.php +++ b/tests/Unit/IconSnippetFormatTest.php @@ -100,3 +100,17 @@ public function variant(string $path, array $map): ?string expect((new Icon)->getMorphClass())->toBe('ichava-icon'); }); }); + +describe('Generic snippet tag follows the registered prefixes', function () { + it('uses the short tag while components.short_prefix names one, and ichava-core when it is off', function (?string $short, string $tag) { + config()->set('ichava.ichava-core.components.short_prefix', $short); + $icon = snippetIcon('ichava/icon-sets-tabler', 'home', 'files/home.svg'); + + expect(app(IconBrowserService::class)->generateBladeComponent($icon, useCleanSyntax: false)) + ->toBe(""); + })->with([ + 'default' => ['ichava', 'ichava'], + 'renamed' => ['icon', 'icon'], + 'off' => [null, 'ichava-core'], + ]); +}); diff --git a/tests/Unit/SyncIconSetsVisibilityTest.php b/tests/Unit/SyncIconSetsVisibilityTest.php new file mode 100644 index 0000000..81202b5 --- /dev/null +++ b/tests/Unit/SyncIconSetsVisibilityTest.php @@ -0,0 +1,36 @@ + $visibility], null))->toBe($visibility); +})->with(['public', 'private']); + +it('keeps the recorded visibility when the API does not say', function (?array $response): void { + expect(visibilityOf($response, 'private'))->toBe('private'); +})->with([ + 'a failed request' => [null], + 'a 404 for a hidden repo' => [['message' => 'Not Found']], + 'an unknown value' => [['visibility' => 'internal']], +]); + +it('records no visibility it has never seen', function (): void { + expect(visibilityOf(null, null))->toBeNull(); +}); + +it('records a visibility for every catalogued pack', function (): void { + $catalog = json_decode((string) file_get_contents(dirname(__DIR__, 2) . '/icon-sets.json'), true, flags: JSON_THROW_ON_ERROR); + + expect($catalog['sets'])->not->toBeEmpty(); + + foreach ($catalog['sets'] as $set) { + expect($set['visibility'] ?? null)->toBeIn(['public', 'private'], "{$set['key']} has no visibility"); + } +});