diff --git a/.gitignore b/.gitignore index c3957fcc..b7b67466 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ codeception.yml *.phar *.bak infection.json.dist -c3.php \ No newline at end of file +c3.php + +# Theme JSON Schema +theme.schema.json \ No newline at end of file diff --git a/Makefile b/Makefile index c94cb7e4..6c0b25fd 100644 --- a/Makefile +++ b/Makefile @@ -65,11 +65,21 @@ composer/update: up ### Update the composer dependencies @echo "Updating the composer dependencies" @$(DOCKER_DIR) ./composer update +.PHONY: composer/update/nodev +composer/update/nodev: up ### Update the composer dependencies + @echo "Updating the composer dependencies" + @$(DOCKER_DIR) ./composer update --no-dev + .PHONY: composer/dump composer/dump: up ### Dump the composer autoload @echo "Dumping the composer autoload" @$(DOCKER_DIR) ./composer dump-autoload +.PHONY: composer/validate +composer/validate: up ### Validate the composer.json file + @echo "Validating the composer.json file" + @$(DOCKER_DIR) ./composer validate + # Codestyle commands .PHONY: cs diff --git a/bin/theme-json.php b/bin/theme-json.php index b6c42f9f..e8e3389f 100644 --- a/bin/theme-json.php +++ b/bin/theme-json.php @@ -8,8 +8,6 @@ namespace ItalyStrap\ThemeJsonGenerator; -use ItalyStrap\ThemeJsonGenerator\Infrastructure\Cli\Composer\Bootstrap; - /** @psalm-suppress UnresolvableInclude */ require $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; diff --git a/composer.json b/composer.json index ca28a766..dea3b3ae 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,6 @@ "require": { "php" : ">=7.4", "ext-json": "*", - "composer/composer": "^2.1", - "composer-plugin-api": "^2.0", "italystrap/config": "^2.4", "italystrap/empress": "^2.0", @@ -30,10 +28,15 @@ "justinrainbow/json-schema": "^5.2", "scssphp/scssphp": "^1.12.1", + "sabberworm/php-css-parser": "^8.5", "brick/varexporter": "^0.3.8", "webimpress/safe-writer": "^2.2", - "symfony/polyfill-php80": "^1.22" + "symfony/console": "^v5.4", + "symfony/process": "^v5.4", + "symfony/polyfill-php80": "^1.22", + "symfony/event-dispatcher": "^5.4", + "webmozart/assert": "^1.11" }, "require-dev": { "lucatume/wp-browser": "~3.2.3", @@ -61,11 +64,7 @@ "rector/rector": "^0.19.0", "symplify/easy-coding-standard": "^12.0", - "italystrap/debug": "dev-master", - "wp-cli/wp-cli": "^2.7", - - "psr/event-dispatcher": "^1.0", - "sabberworm/php-css-parser": "^8.0" + "italystrap/debug": "dev-master" }, "autoload": { "psr-4": { @@ -86,7 +85,6 @@ } }, "suggest": { - "spatie/color": "Allows to convert CSS color" }, "bin": [ "bin/theme-json" diff --git a/docs/02-advanced-usage.md b/docs/02-advanced-usage.md index 71c57b67..c5a7ea97 100644 --- a/docs/02-advanced-usage.md +++ b/docs/02-advanced-usage.md @@ -222,11 +222,15 @@ Once you've added the various Preset instances to the `Presets` collection, the ## Styles -The `Styles` directory offers builder classes implementing the `Fluent Interface` pattern, enabling intuitive and chainable configuration of your theme's styles. Importantly, each class is immutable, ensuring robustness by returning new instances for every method call. This design allows for clear and concise style definitions within your entrypoint file. +The `Styles` directory offers builder classes implementing the `Fluent Interface` pattern, enabling intuitive and chainable configuration of your theme's styles. Importantly, each class is immutable, ensuring robustness by returning new instance on every method call. This design allows for clear and concise style definitions within your entrypoint file. + +In this directory, you'll find classes tailored to different aspects of theme styling, aligned with the `theme.json` structure. These classes, such as `Border`, `Color`, `Css`, `Outline`, `Spacing`, `Typography`, and more, offer specific methods for configuring corresponding style properties, all method declared in each class follows the `theme.json` schema and each class has its own methods following the properties they represent. These classes support three primary methods of utilization, each offering a unique approach to styling: -Directly create an instance of a style class, such as `Color`, and chain methods to define properties. This approach is straightforward and effective for setting styles directly: +We will take the `Color` class as an example, but all the other classes follow the same pattern. + +Directly create an instance of a `Color::class`, and chain methods to define properties. This approach is straightforward and effective for setting styles directly: ```php use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Color; @@ -263,13 +267,13 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Color; [ SectionNames::STYLES => [ 'color' => (new Color($presets)) - ->background(Palette::CATEGORY . '.base') - ->text(Palette::CATEGORY . '.bodyColor'), + ->background(Palette::TYPE . '.base') + ->text(Palette::TYPE . '.bodyColor'), ], ]; ``` -In this case the key passed to the method will be used as a key in the `$presets` collection. +In this case the key passed to the method will be used as a key in the `$presets` collection, the `key` has the format `type.slug` where `type` is the type of the preset and `slug` is the slug of the preset. In JSON format: @@ -309,19 +313,113 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Color; [ SectionNames::STYLES => [ 'color' => $container->get(Color::class) - ->background(Palette::CATEGORY . '.base') - ->text(Palette::CATEGORY . '.bodyColor'), + ->background(Palette::TYPE . '.base') + ->text(Palette::TYPE . '.bodyColor'), ], ]; ``` -As you can see with the `$container` object you can use a `Color` class without the need to pass the `$presets` object to the constructor because all the dependencies of the `Color` are already registered in the container. +As you can see with the `$container` object you can use a `Color` class without the need to pass the `$presets` object to the constructor because all the dependencies of the `Color` (and all other classes in the `Styles` directory) are already registered in the container. + +You can find an implementation example in the [tests/_data/fixtures/advanced-example.json.php](../tests/_data/fixtures/advanced-example.json.php) file. + +### Custom CSS for Global Styles and per Block + +More information about the `css` field can be found in: + +* [WordPress 6.2 release notes](https://wordpress.org/news/2023/03/dolphy/). +* [Custom CSS for Global Styles and per Block](https://make.wordpress.org/core/2023/03/06/custom-css-for-global-styles-and-per-block/). +* [Per Block CSS with theme.json](https://developer.wordpress.org/news/2023/04/21/per-block-css-with-theme-json/). +* [Global Settings and Styles](https://developer.wordpress.org/themes/global-settings-and-styles/). +* [How to use custom CSS in theme.json - fullsiteediting.com](https://fullsiteediting.com/lessons/how-to-use-custom-css-in-theme-json/). + +The introduction of the `css` field in [WordPress 6.2](https://wordpress.org/news/2023/03/dolphy/) enables the addition of [custom CSS](https://make.wordpress.org/core/2023/03/06/custom-css-for-global-styles-and-per-block/) directly within the `theme.json` file, both globally under `styles.css` and per block within `styles.blocks.[block-name].css`. Utilizing the {`\ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css`|`\ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Scss`} classes and theirs `parse(string $css, string $selector = ''): string` method, developers can now seamlessly integrate custom styles without the need to remember the format to use with `&` separator, just write your CSS (or Scss) as you would in a regular CSS|SCSS file and let the `Css`|`Scss` class handle the rest. + +This method accepts two parameters: the CSS to parse and an optional selector to scope the CSS rules accordingly. + +How It Works + +The `Css`|`Scss` class efficiently parses the provided CSS, extracting and formatting rules based on the specified selector. This functionality ensures that the output is fully compatible with the `theme.json` structure, enhancing the flexibility and customization of theme development. + +So, let's see some examples: + +```php +// Result: 'height: 100%;' +echo (new Css($presets))->parse('.test-selector{height: 100%;}', '.test-selector'); +``` + +```php +// Result: 'height: 100%;width: 100%;color: red;&:hover {color: red;}&::placeholder {color: red;}' +echo (new Css($presets))->parse('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'); +``` + +Like the other classes in the `Styles` directory, you can use the `Css` class directly or pass the `$presets` collection to the constructor or use the `$container` object to get the instance you need, the only exception is for the `Scss` class that need an instance of `Css` class and an instance of `ScssPhp\ScssPhp\Compiler` class to work, but if you use the `$container` object you don't need to worry about it because all the dependencies are already registered in the container. + +As the name suggests, the `Scss` class is used to parse SCSS styles, so you are free to write your styles in SCSS format and let the class handle the conversion for you. + +Let's see it in action: + +```php +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; + +[ + SectionNames::STYLES => [ + 'css' => $container->get(Css::class) // Or (new Css($presets)) or (new Scss(new Css($presets), $scssCompiler, $presets)) + ->parse('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'), + ], +]; +``` + +For block style: + +```php +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; + +[ + SectionNames::STYLES => [ + 'blocks' => [ + 'my-namespace/test-block' => [ + 'css' => $container->get(Css::class) // Or (new Css($presets)) or (new Scss(new Css($presets), $scssCompiler, $presets)) + ->parse('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'), + ], + ], + ], +]; +``` + +All methods also support a special syntax to resolve value in the `$presets` collection, `{{type.slug}}`, this syntax will be used internally to find the value in the `$presets` collection registered before. + + +```php +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; + +[ + SectionNames::STYLES => [ + 'css' => $container->get(Css::class) // Or (new Css($presets)) + ->parse('.test-selector{color: {{color.base}};}', '.test-selector'), + ], +]; +``` + +The `{{color.base}}` will be replaced with the value of the `color.base` previously set in the `$presets` collection. + +```json +{ + "styles": { + "css": ".test-selector{color: var(--wp--preset--color--base);}" + } +} +``` -### Available Style Classes +More examples can be found in the [tests/_data/fixtures/advanced-example.json.php](../tests/_data/fixtures/advanced-example.json.php) file. -The `Styles` directory includes various classes tailored to different aspects of theme styling, aligned with the `theme.json` structure. These classes, such as `Border`, `Color`, `Css`, `Outline`, `Spacing`, `Typography`, offer specific methods for configuring corresponding style properties, all method declared in each class follows the `theme.json` schema and each class has its own methods following the properties they represent. +To know more about `css` field: -You can see more examples in the [tests/_data/fixtures/advanced-example.json.php](../tests/_data/fixtures/advanced-example.json.php) file. +* https://make.wordpress.org/core/2023/03/06/custom-css-for-global-styles-and-per-block/ +* https://developer.wordpress.org/news/2023/04/21/per-block-css-with-theme-json/ +* https://developer.wordpress.org/themes/global-settings-and-styles/ +* https://fullsiteediting.com/lessons/how-to-use-custom-css-in-theme-json/ +* https://make.wordpress.org/core/2022/12/22/whats-new-in-gutenberg-14-8-21-december/#Add-custom-CSS-rules-for-your-site ### Advanced Service Injection with Empress and PSR-11 Container diff --git a/functions/autoload.php b/functions/autoload.php index abc25d0a..95933a96 100644 --- a/functions/autoload.php +++ b/functions/autoload.php @@ -3,7 +3,3 @@ declare(strict_types=1); namespace ItalyStrap\ThemeJsonGenerator; - -use ItalyStrap\ThemeJsonGenerator\Infrastructure\Cli\WPCLI\Bootstrap; - -(new Bootstrap())(); diff --git a/psalm.xml b/psalm.xml index ce0ee58c..955e3ecb 100644 --- a/psalm.xml +++ b/psalm.xml @@ -15,7 +15,6 @@ - diff --git a/src/Application/Commands/Composer/DumpCommand.php b/src/Application/Commands/DumpCommand.php similarity index 71% rename from src/Application/Commands/Composer/DumpCommand.php rename to src/Application/Commands/DumpCommand.php index f8f1c819..8f3e376d 100644 --- a/src/Application/Commands/Composer/DumpCommand.php +++ b/src/Application/Commands/DumpCommand.php @@ -2,15 +2,14 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer; +namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; -use Composer\Command\BaseCommand; -use ItalyStrap\Config\ConfigInterface; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\DumpMessage; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Utils\RootFolderTrait; +use ItalyStrap\ThemeJsonGenerator\Application\DumpMessage; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Dump; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\GeneratedFile; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\GeneratingFile; +use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\NoFileFound; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -20,7 +19,7 @@ /** * @psalm-api */ -final class DumpCommand extends BaseCommand +final class DumpCommand extends Command { use RootFolderTrait; @@ -44,20 +43,21 @@ final class DumpCommand extends BaseCommand */ public const PATH_FOR_THEME_SASS = 'path-for-theme-sass'; - private Dump $dump; + /** + * @var string + */ + public const FILE = 'file'; - private ConfigInterface $config; + private Dump $dump; private \Symfony\Component\EventDispatcher\EventDispatcher $subscriber; public function __construct( \Symfony\Component\EventDispatcher\EventDispatcher $subscriber, - Dump $dump, - ConfigInterface $config + Dump $dump ) { $this->subscriber = $subscriber; $this->dump = $dump; - $this->config = $config; parent::__construct(); } @@ -87,6 +87,26 @@ protected function configure(): void ) ); + $this->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + \sprintf( + 'If set, %s will generate the json file in the specified path.', + self::NAME + ) + ); + + $this->addOption( + self::FILE, + null, + InputOption::VALUE_OPTIONAL, + \sprintf( + 'If set, %s will generate only the specified file.', + self::NAME + ) + ); + /** * @todo other options: * --no-pretty-print @@ -99,18 +119,6 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $composer = $this->requireComposer(); - $rootFolder = $this->rootFolder(); - - $package = $composer->getPackage(); - /** @psalm-suppress MixedArgument */ - $this->config->merge($package->getExtra()[self::COMPOSER_EXTRA_THEME_JSON_KEY] ?? []); - - /** - * @todo The callback is temporary until the new files generation are in place. - * @psalm-suppress MixedAssignment - */ - $callback = $this->config->get(self::CALLBACK); $this->subscriber->addListener( GeneratingFile::class, @@ -133,23 +141,23 @@ static function (GeneratedFile $event) use ($output): void { } ); + $this->subscriber->addListener( + NoFileFound::class, + /** @psalm-suppress UnusedClosureParam */ + static function (NoFileFound $event) use ($output): void { + $output->writeln(NoFileFound::M_NO_FILE_FOUND); + } + ); + + $rootFolder = $this->rootFolder((string)$input->getOption('path')); + $message = new DumpMessage( $rootFolder, - (string)$this->config->get(self::PATH_FOR_THEME_SASS, ''), - (bool)$input->getOption('dry-run') + '', + (bool)$input->getOption('dry-run'), + (string)$input->getOption(self::FILE) ); - try { - if (\is_callable($callback)) { - $output->writeln('Generating theme.json file'); - $this->dump->processBlueprint($message, 'theme', $callback); - $output->writeln('Generated theme.json file'); - $output->writeln('========================'); - } - } catch (\Exception $exception) { - $output->writeln($exception->getMessage()); - } - $this->dump->handle($message); if ($input->getOption(ValidateCommand::NAME)) { diff --git a/src/Application/Commands/Composer/InfoCommand.php b/src/Application/Commands/InfoCommand.php similarity index 81% rename from src/Application/Commands/Composer/InfoCommand.php rename to src/Application/Commands/InfoCommand.php index ebd3e3f8..eaa46908 100644 --- a/src/Application/Commands/Composer/InfoCommand.php +++ b/src/Application/Commands/InfoCommand.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer; +namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; -use Composer\Command\BaseCommand; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Utils\RootFolderTrait; +use ItalyStrap\ThemeJsonGenerator\Application\InfoMessage; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -13,7 +13,7 @@ /** * @psalm-api */ -class InfoCommand extends BaseCommand +class InfoCommand extends Command { use RootFolderTrait; @@ -38,7 +38,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $rootFolder = $this->rootFolder(); - $message = new \ItalyStrap\ThemeJsonGenerator\Application\Commands\InfoMessage($rootFolder); + $message = new InfoMessage($rootFolder); try { return (int)$this->bus->handle($message); diff --git a/src/Application/Commands/Composer/InitCommand.php b/src/Application/Commands/InitCommand.php similarity index 93% rename from src/Application/Commands/Composer/InitCommand.php rename to src/Application/Commands/InitCommand.php index 0bb8d4d7..db36fac9 100644 --- a/src/Application/Commands/Composer/InitCommand.php +++ b/src/Application/Commands/InitCommand.php @@ -2,12 +2,11 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer; +namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; -use Composer\Command\BaseCommand; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\InitMessage; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Utils\DataFromJsonTrait; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Utils\RootFolderTrait; +use ItalyStrap\ThemeJsonGenerator\Application\InitMessage; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\EntryPointCanNotBeCreated; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\EntryPointCreated; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\EntryPointDoesNotExist; @@ -19,7 +18,7 @@ /** * @psalm-api */ -class InitCommand extends BaseCommand +class InitCommand extends Command { use RootFolderTrait; use DataFromJsonTrait; diff --git a/src/Application/Commands/Middleware/DeleteSchemaJsonMiddleware.php b/src/Application/Commands/Middleware/DeleteSchemaJsonMiddleware.php index b818c7a2..a0995f47 100644 --- a/src/Application/Commands/Middleware/DeleteSchemaJsonMiddleware.php +++ b/src/Application/Commands/Middleware/DeleteSchemaJsonMiddleware.php @@ -5,8 +5,7 @@ namespace ItalyStrap\ThemeJsonGenerator\Application\Commands\Middleware; use ItalyStrap\Bus\HandlerInterface; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\ValidateMessage; -use ItalyStrap\ThemeJsonGenerator\Domain\Output\Validate; +use ItalyStrap\ThemeJsonGenerator\Application\ValidateMessage; class DeleteSchemaJsonMiddleware implements \ItalyStrap\Bus\MiddlewareInterface { diff --git a/src/Application/Commands/Middleware/SchemaJsonMiddleware.php b/src/Application/Commands/Middleware/SchemaJsonMiddleware.php index 3bb88a06..f60ded49 100644 --- a/src/Application/Commands/Middleware/SchemaJsonMiddleware.php +++ b/src/Application/Commands/Middleware/SchemaJsonMiddleware.php @@ -4,8 +4,7 @@ namespace ItalyStrap\ThemeJsonGenerator\Application\Commands\Middleware; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\ValidateMessage; -use ItalyStrap\ThemeJsonGenerator\Domain\Output\Validate; +use ItalyStrap\ThemeJsonGenerator\Application\ValidateMessage; class SchemaJsonMiddleware implements \ItalyStrap\Bus\MiddlewareInterface { diff --git a/src/Application/Commands/Utils/RootFolderTrait.php b/src/Application/Commands/Utils/RootFolderTrait.php index 90561fd0..45d13bcd 100644 --- a/src/Application/Commands/Utils/RootFolderTrait.php +++ b/src/Application/Commands/Utils/RootFolderTrait.php @@ -6,12 +6,8 @@ trait RootFolderTrait { - private function rootFolder(): string + private function rootFolder(string $path = ''): string { - $composer = $this->requireComposer(); - - /** @var string $vendorPath */ - $vendorPath = $composer->getConfig()->get('vendor-dir'); - return \dirname($vendorPath); + return $path !== '' ? $path : (string)\getcwd(); } } diff --git a/src/Application/Commands/Composer/ValidateCommand.php b/src/Application/Commands/ValidateCommand.php similarity index 94% rename from src/Application/Commands/Composer/ValidateCommand.php rename to src/Application/Commands/ValidateCommand.php index c1fd8734..21f89dfd 100644 --- a/src/Application/Commands/Composer/ValidateCommand.php +++ b/src/Application/Commands/ValidateCommand.php @@ -2,12 +2,11 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer; +namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; -use Composer\Command\BaseCommand; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Utils\DataFromJsonTrait; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Utils\RootFolderTrait; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\ValidateMessage; +use ItalyStrap\ThemeJsonGenerator\Application\ValidateMessage; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\ValidatedFails; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\ValidatingFile; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\ValidFile; @@ -19,7 +18,7 @@ /** * @psalm-api */ -class ValidateCommand extends BaseCommand +class ValidateCommand extends Command { use RootFolderTrait; use DataFromJsonTrait; diff --git a/src/Application/Commands/WPCLI/Dump.php b/src/Application/Commands/WPCLI/Dump.php deleted file mode 100644 index 4aed5de4..00000000 --- a/src/Application/Commands/WPCLI/Dump.php +++ /dev/null @@ -1,95 +0,0 @@ - $args - * @param array $assoc_args - */ - public function __invoke(array $args, array $assoc_args): void - { - throw new Exception('This command is not implemented yet'); - - /** - * --parent - * : Argument to generate theme.json also for parent theme - * @var array $assoc_args_default - */ - $assoc_args_default = [ - 'parent' => false, - ]; - - $assoc_args = array_replace_recursive($assoc_args_default, $assoc_args); - - /** - * @var array $extra_config - */ - $extra_config = WP_CLI::get_runner()->extra_config; - - $callable = \array_key_exists('THEME_JSON_CALLABLE', $extra_config) - ? $extra_config['THEME_JSON_CALLABLE'] - : ''; - - if (!is_callable($callable)) { - WP_CLI::line(\sprintf( - "No valid callable provided, got '%s'", - $callable - )); - return; - } - - $theme_json_path = []; - $theme_json_path[] = get_stylesheet_directory() . '/theme.json'; - - if (is_child_theme() && $assoc_args['parent']) { - $theme_json_path[] = get_template_directory() . '/theme.json'; - } - - foreach ($theme_json_path as $path) { - $this->loopsThemePathAndGenerateFile($path, $callable); - } - } - - /** - * @param string $path - * @param callable $callable - */ - private function loopsThemePathAndGenerateFile(string $path, callable $callback): void - { - try { - $result = (array)$callback(); - $data = new Config($result); - (new JsonFileWriter($path))->write($data); - WP_CLI::success(sprintf( - '%s was generated!', - $path - )); - } catch (Exception $exception) { - WP_CLI::line($exception->getMessage()); - } - } -} diff --git a/src/Application/Config/Blueprint.php b/src/Application/Config/Blueprint.php index 9ceb7818..9e1b3265 100644 --- a/src/Application/Config/Blueprint.php +++ b/src/Application/Config/Blueprint.php @@ -6,6 +6,7 @@ use ItalyStrap\Config\Config; use ItalyStrap\ThemeJsonGenerator\Domain\Input\SectionNames; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Shadow; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\PresetsInterface; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Duotone; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Gradient; @@ -28,6 +29,12 @@ public function setGlobalCss(string $css): bool return $this->set(SectionNames::STYLES . '.css', $css); } + public function appendGlobalCss(string $css): bool + { + $currentCss = (string)$this->get(SectionNames::STYLES . '.css'); + return $this->set(SectionNames::STYLES . '.css', $currentCss . $css); + } + public function setElementStyle(string $elementName, array $config): bool { return $this->set(SectionNames::STYLES . '.elements.' . $elementName, $config); @@ -54,6 +61,7 @@ public function setPresets(PresetsInterface $presets): bool 'settings.color.palette' => Palette::TYPE, 'settings.color.gradients' => Gradient::TYPE, 'settings.color.duotone' => Duotone::TYPE, + 'settings.shadow.presets' => Shadow::TYPE, 'settings.typography.fontSizes' => FontSize::TYPE, 'settings.typography.fontFamilies' => FontFamily::TYPE, 'settings.custom' => Custom::TYPE, diff --git a/src/Application/Commands/DumpMessage.php b/src/Application/DumpMessage.php similarity index 74% rename from src/Application/Commands/DumpMessage.php rename to src/Application/DumpMessage.php index bc53792a..24b3bc1b 100644 --- a/src/Application/Commands/DumpMessage.php +++ b/src/Application/DumpMessage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; +namespace ItalyStrap\ThemeJsonGenerator\Application; /** * @psalm-api @@ -14,15 +14,18 @@ class DumpMessage private bool $dry_run; private string $sassFolder; + private string $file; public function __construct( string $rootFolder, string $sassFolder, - bool $dry_run + bool $dry_run, + string $file ) { $this->rootFolder = $rootFolder; $this->dry_run = $dry_run; $this->sassFolder = $sassFolder; + $this->file = $file; } public function getRootFolder(): string @@ -39,4 +42,9 @@ public function isDryRun(): bool { return $this->dry_run; } + + public function getFile(): string + { + return $this->file; + } } diff --git a/src/Application/Commands/InfoMessage.php b/src/Application/InfoMessage.php similarity index 83% rename from src/Application/Commands/InfoMessage.php rename to src/Application/InfoMessage.php index 65dfb71f..24aca104 100644 --- a/src/Application/Commands/InfoMessage.php +++ b/src/Application/InfoMessage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; +namespace ItalyStrap\ThemeJsonGenerator\Application; /** * @psalm-api diff --git a/src/Application/Commands/InitMessage.php b/src/Application/InitMessage.php similarity index 89% rename from src/Application/Commands/InitMessage.php rename to src/Application/InitMessage.php index 3ce5d305..889cdd61 100644 --- a/src/Application/Commands/InitMessage.php +++ b/src/Application/InitMessage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; +namespace ItalyStrap\ThemeJsonGenerator\Application; /** * @psalm-api diff --git a/src/Application/Commands/ValidateMessage.php b/src/Application/ValidateMessage.php similarity index 92% rename from src/Application/Commands/ValidateMessage.php rename to src/Application/ValidateMessage.php index 87b7dddd..a4f62d0d 100644 --- a/src/Application/Commands/ValidateMessage.php +++ b/src/Application/ValidateMessage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; +namespace ItalyStrap\ThemeJsonGenerator\Application; /** * @psalm-api diff --git a/src/Infrastructure/Cli/Composer/Bootstrap.php b/src/Bootstrap.php similarity index 74% rename from src/Infrastructure/Cli/Composer/Bootstrap.php rename to src/Bootstrap.php index 7f854f9b..85103fad 100644 --- a/src/Infrastructure/Cli/Composer/Bootstrap.php +++ b/src/Bootstrap.php @@ -2,19 +2,20 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Infrastructure\Cli\Composer; +namespace ItalyStrap\ThemeJsonGenerator; -use Composer\Console\Application; +use Symfony\Component\Console\Application; +use ItalyStrap\Bus\Bus; use ItalyStrap\Config\Config; use ItalyStrap\Config\ConfigInterface; use ItalyStrap\Empress\Injector; use ItalyStrap\Finder\Finder; use ItalyStrap\Finder\FinderFactory; use ItalyStrap\Finder\FinderInterface; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer\DumpCommand; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer\InfoCommand; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer\InitCommand; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\Composer\ValidateCommand; +use ItalyStrap\ThemeJsonGenerator\Application\Commands\DumpCommand; +use ItalyStrap\ThemeJsonGenerator\Application\Commands\InfoCommand; +use ItalyStrap\ThemeJsonGenerator\Application\Commands\InitCommand; +use ItalyStrap\ThemeJsonGenerator\Application\Commands\ValidateCommand; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Middleware\DeleteSchemaJsonMiddleware; use ItalyStrap\ThemeJsonGenerator\Application\Commands\Middleware\SchemaJsonMiddleware; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Info; @@ -49,8 +50,8 @@ public function run(): int $application->add($injector->make(DumpCommand::class)); /** @psalm-suppress InvalidArgument */ $application->add($injector->make(ValidateCommand::class, [ - '+bus' => static function (string $named_param, Injector $injector): \ItalyStrap\Bus\Bus { - $bus = new \ItalyStrap\Bus\Bus( + '+bus' => static function (string $named_param, Injector $injector): Bus { + $bus = new Bus( $injector->make(Validate::class) ); $bus->addMiddleware( @@ -61,11 +62,9 @@ public function run(): int }, ])); $application->add($injector->make(InfoCommand::class, [ - '+bus' => static function (string $named_param, Injector $injector): \ItalyStrap\Bus\Bus { - return new \ItalyStrap\Bus\Bus( - $injector->make(Info::class) - ); - }, + '+bus' => static fn(string $named_param, Injector $injector): Bus => new Bus( + $injector->make(Info::class) + ), ])); return $application->run(); } diff --git a/src/Domain/Input/Settings/Color/Shadow.php b/src/Domain/Input/Settings/Color/Shadow.php new file mode 100644 index 00000000..65183d85 --- /dev/null +++ b/src/Domain/Input/Settings/Color/Shadow.php @@ -0,0 +1,49 @@ +slug = $slug; + $this->name = $name; + $this->shadow = $shadow; + } + + /** + * @return array{slug: string, name: string, shadow: string} + */ + public function toArray(): array + { + return [ + 'slug' => $this->slug, + 'name' => $this->name, + 'shadow' => \trim(\implode(', ', $this->shadow), ', ') + ]; + } +} diff --git a/src/Domain/Input/Settings/Color/Utilities/BoxShadow.php b/src/Domain/Input/Settings/Color/Utilities/BoxShadow.php new file mode 100644 index 00000000..3633f050 --- /dev/null +++ b/src/Domain/Input/Settings/Color/Utilities/BoxShadow.php @@ -0,0 +1,149 @@ +inset = $inset; + return $this; + } + + public function offsetX(string $x): self + { + $this->assertIsUnique($this->x, 'offset-x'); + $this->assertValidCssDimension($x); + $this->x = $x; + return $this; + } + + public function offsetY(string $y): self + { + $this->assertIsUnique($this->y, 'offset-y'); + $this->assertValidCssDimension($y); + $this->y = $y; + return $this; + } + + public function blur(string $blur): self + { + $this->assertIsUnique($this->blur, 'blur'); + $this->assertValidCssDimension($blur); + $this->blur = $blur; + return $this; + } + + public function spread(string $spread): self + { + $this->assertIsUnique($this->spread, 'spread'); + $this->assertValidCssDimension($spread); + $this->spread = $spread; + return $this; + } + + /** + * @param Palette|ColorInterface|string $color + * @throws \Exception + */ + public function color($color): self + { + $this->assertIsUnique($this->color, 'color'); + + if ($color instanceof Palette) { + $this->color = $color->var((string)$color->color()); + return $this; + } + + if ($color instanceof ColorInterface) { + $this->color = (string)$color; + return $this; + } + + $this->color = (string)(new ColorFactory())->fromColorString($color); + return $this; + } + + public function __toString(): string + { + if ($this->x === '' && $this->y === '') { + throw new \RuntimeException('You must add at least 2 value, offset-x and offset-y'); + } + + $shadow = [ + $this->inset ? 'inset' : '', + $this->x, + $this->y, + $this->blur, + $this->spread, + $this->color, + ]; + + $this->reset(); + return \trim(\implode(' ', \array_filter($shadow, static fn($value) => $value !== ''))); + } + + public function __clone() + { + $this->reset(); + } + + private function assertValidCssDimension(string $value): void + { + $unit = \implode('|', [ + 'px', + 'em', + 'rem', + '%', + 'in', + 'cm', + 'mm', + 'pt', + 'pc', + 'ch', + 'ex', + 'vw', + 'vh', + 'vmin', + 'vmax', + ]); + + if (!\preg_match('/^(-?\d*\.?\d+)(' . $unit . ')$/', $value) && $value !== '0') { + throw new \RuntimeException('Invalid CSS dimension: given ' . $value); + } + } + + private function assertIsUnique(string $param, string $name): void + { + if ($param !== '') { + throw new \RuntimeException('You can add only one value for ' . $name); + } + } + + /** + * @return void + */ + private function reset(): void + { + $this->inset = false; + $this->x = ''; + $this->y = ''; + $this->blur = ''; + $this->spread = ''; + $this->color = ''; + } +} diff --git a/src/Domain/Input/Settings/Color/Utilities/Color.php b/src/Domain/Input/Settings/Color/Utilities/Color.php index a0149a48..eb990a9b 100644 --- a/src/Domain/Input/Settings/Color/Utilities/Color.php +++ b/src/Domain/Input/Settings/Color/Utilities/Color.php @@ -15,8 +15,15 @@ final class Color implements ColorInterface { private SpatieColor $spatieColor; + private string $type; + private Hsla $hsla; + /** + * @var string|float + */ + private $alpha = 1.0; + /** * Luminance of #808080 or rgb(128,128,128) or hsl(0,0%,50%) * @var float @@ -26,7 +33,21 @@ final class Color implements ColorInterface public function __construct(string $color) { $this->spatieColor = ColorFactory::fromString($color); - $this->hsla = $this->spatieColor->toHsla(); + + $reflected = new \ReflectionObject($this->spatieColor); + $this->type = $reflected->getShortName(); + if ($reflected->hasProperty('alpha')) { + $reflectionProperty = $reflected->getProperty('alpha'); + $reflectionProperty->setAccessible(true); + /** + * @psalm-suppress MixedAssignment + */ + $this->alpha = $reflectionProperty->getValue($this->spatieColor); + $reflectionProperty->setAccessible(false); + } + + $alpha = $this->fromHexToFloat($this->alpha); + $this->hsla = $this->spatieColor->toHsla($alpha); } public function isDark(): bool @@ -129,14 +150,14 @@ public function lightness(): int return (int)\round($this->hsla->lightness()); } - public function alpha(): float + public function alpha() { - return $this->hsla->alpha(); + return $this->alpha; } public function type(): string { - return (new \ReflectionClass($this->spatieColor))->getShortName(); + return $this->type; } public function toHex(): self @@ -149,8 +170,9 @@ public function toHsl(): self return new self((string) $this->spatieColor->toHsl()); } - public function toHsla(float $alpha = 1): self + public function toHsla(float $alpha = null): self { + $alpha = $alpha ?? $this->fromHexToFloat($this->alpha); return new self((string) $this->spatieColor->toHsla($alpha)); } @@ -159,13 +181,22 @@ public function toRgb(): self return new self((string) $this->spatieColor->toRgb()); } - public function toRgba(float $alpha = 1): self + public function toRgba(float $alpha = null): self { + $alpha = $alpha ?? $this->fromHexToFloat($this->alpha); return new self((string) $this->spatieColor->toRgba($alpha)); } public function __toString(): string { - return (string) $this->spatieColor; + return (string)$this->spatieColor; + } + + /** + * @param mixed $alpha + */ + private function fromHexToFloat($alpha): float + { + return \is_string($alpha) ? \hexdec($alpha) / 255 : (float)$alpha; } } diff --git a/src/Domain/Input/Settings/Color/Utilities/ColorFactory.php b/src/Domain/Input/Settings/Color/Utilities/ColorFactory.php index 73259f99..e078caee 100644 --- a/src/Domain/Input/Settings/Color/Utilities/ColorFactory.php +++ b/src/Domain/Input/Settings/Color/Utilities/ColorFactory.php @@ -24,4 +24,20 @@ public function fromColorString(string $color): ColorInterface { return new Color($color); } + + /** + * @throws \Exception + */ + public function hsla(int $hue, float $saturation, float $lightness, float $alpha = 1): ColorInterface + { + return new Color("hsla($hue, $saturation%, $lightness%, $alpha)"); + } + + /** + * @throws \Exception + */ + public function rgba(int $red, int $green, int $blue, float $alpha = 1): ColorInterface + { + return new Color("rgba($red, $green, $blue, $alpha)"); + } } diff --git a/src/Domain/Input/Settings/Color/Utilities/ColorInterface.php b/src/Domain/Input/Settings/Color/Utilities/ColorInterface.php index db3343b9..4fd15496 100644 --- a/src/Domain/Input/Settings/Color/Utilities/ColorInterface.php +++ b/src/Domain/Input/Settings/Color/Utilities/ColorInterface.php @@ -54,7 +54,10 @@ public function saturation(): int; public function lightness(): int; - public function alpha(): float; + /** + * @return string|float + */ + public function alpha(); public function type(): string; } diff --git a/src/Domain/Input/Settings/Color/Utilities/ColorModifier.php b/src/Domain/Input/Settings/Color/Utilities/ColorModifier.php index 15867013..fc3c54a7 100644 --- a/src/Domain/Input/Settings/Color/Utilities/ColorModifier.php +++ b/src/Domain/Input/Settings/Color/Utilities/ColorModifier.php @@ -148,11 +148,11 @@ private function createNewColorFrom( string $alpha ): ColorInterface { $newColor = $this->color_factory->fromColorString(\sprintf( - 'hsla(%s, %s%%, %s%%, %s)', + 'hsla(%s, %s%%, %s%%, %d)', $hue, $saturation, $lightness, - $alpha + \ctype_digit($alpha) ? $alpha : \hexdec($alpha) / 255 )); return $this->callMethodOnColorObject($newColor); diff --git a/src/Domain/Input/Settings/Color/Utilities/LinearGradient.php b/src/Domain/Input/Settings/Color/Utilities/LinearGradient.php index c25cda38..4873bc3b 100644 --- a/src/Domain/Input/Settings/Color/Utilities/LinearGradient.php +++ b/src/Domain/Input/Settings/Color/Utilities/LinearGradient.php @@ -11,22 +11,64 @@ */ class LinearGradient implements GradientInterface { - private string $direction; + private string $direction = ''; - private array $colors; + /** + * @var string[] + */ + private array $colors = []; - public function __construct(string $direction, Palette ...$colors) + public function direction(string $direction): self { $this->direction = $direction; - $this->colors = $colors; + return $this; + } + + /** + * @param Palette|ColorInterface|string $color + * @throws \Exception + */ + public function colorStop($color = null, string $stop = ''): self + { + $colorVar = ''; + if ($color instanceof Palette) { + $colorVar = $color->var((string)$color->color()); + } + + if ($color instanceof ColorInterface) { + $colorVar = (string)$color; + } + + if (\is_string($color)) { + $colorVar = (string)(new ColorFactory())->fromColorString($color); + } + + $result = \trim($colorVar . ' ' . $stop); + + if ($result === '') { + return $this; + } + + $this->colors[] = $result; + return $this; } public function __toString(): string { + if (\count($this->colors) < 2) { + throw new \RuntimeException('You must add at least 2 colors'); + } + return \sprintf( 'linear-gradient(%s, %s)', $this->direction === '' ? 'to bottom' : $this->direction, - \implode(', ', \array_map(static fn (Palette $color): string => $color->var(), $this->colors)) + \implode(', ', $this->colors) ); } + + public function __clone() + { + $this->colors = []; + $this->direction = ''; + } } diff --git a/src/Domain/Input/Settings/Utilities/DimensionExperimental.php b/src/Domain/Input/Settings/Utilities/DimensionExperimental.php index 0868d75d..0d8f3962 100644 --- a/src/Domain/Input/Settings/Utilities/DimensionExperimental.php +++ b/src/Domain/Input/Settings/Utilities/DimensionExperimental.php @@ -5,6 +5,10 @@ namespace ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Utilities; /** + * https://github.com/marfurt/measurements + * https://github.com/pimlie/php-unit-conversion + * https://github.com/PhpUnitsOfMeasure/php-units-of-measure + * https://wiki.php.net/rfc/clamp * @psalm-api */ final class DimensionExperimental diff --git a/src/Domain/Input/Styles/Css.php b/src/Domain/Input/Styles/Css.php index aa05000c..b8fbfa50 100644 --- a/src/Domain/Input/Styles/Css.php +++ b/src/Domain/Input/Styles/Css.php @@ -7,24 +7,24 @@ use ItalyStrap\Tests\Unit\Domain\Input\Styles\CssTest; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\PresetsInterface; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\NullPresets; +use Sabberworm\CSS\Parser; +use Sabberworm\CSS\Parsing\SourceException; +use Sabberworm\CSS\Property\Selector; /** - * @link https://make.wordpress.org/core/2023/03/06/custom-css-for-global-styles-and-per-block/ - * @link https://fullsiteediting.com/lessons/how-to-use-custom-css-in-theme-json/ - * @link https://developer.wordpress.org/news/2023/04/21/per-block-css-with-theme-json/ * @link https://github.com/WordPress/wordpress-develop/blob/trunk/tests/phpunit/tests/theme/wpThemeJson.php - * @link https://developer.wordpress.org/themes/global-settings-and-styles/ * * @link https://www.google.it/search?q=php+inline+css+content&sca_esv=596560865&ei=mAicZaTCGp3Axc8Pq7yT8AQ&ved=0ahUKEwik7p-Rgs6DAxUdYPEDHSveBE4Q4dUDCBA&uact=5&oq=php+inline+css+content&gs_lp=Egxnd3Mtd2l6LXNlcnAiFnBocCBpbmxpbmUgY3NzIGNvbnRlbnQyBRAhGKABMgUQIRigATIIECEYFhgeGB0yCBAhGBYYHhgdMggQIRgWGB4YHUjvogFQmgdYwJcBcAF4AZABAJgBsQGgAZkSqgEEMC4xOLgBA8gBAPgBAcICChAAGEcY1gQYsAPCAgoQABiABBiKBRhDwgIFEAAYgATCAgYQABgWGB7CAgcQABiABBgTwgIIEAAYFhgeGBPiAwQYACBBiAYBkAYI&sclient=gws-wiz-serp#ip=1 * @link https://github.com/topics/inline-css?l=php - * @link https://github.com/sabberworm/PHP-CSS-Parser * * @psalm-api * @see CssTest */ -class Css +class Css implements CssInterface { private PresetsInterface $presets; + private bool $isCompressed = true; + private bool $shouldResolveVariables = true; public function __construct( PresetsInterface $presets = null @@ -32,9 +32,96 @@ public function __construct( $this->presets = $presets ?? new NullPresets(); } + public function expanded(): self + { + $this->isCompressed = false; + return $this; + } + + public function stopResolveVariables(): self + { + $this->shouldResolveVariables = false; + return $this; + } + + /** + * @throws SourceException + */ + public function parse(string $css, string $selector = ''): string + { + if (\str_starts_with(\trim($css), '&')) { + throw new \RuntimeException(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + } + + if ($this->shouldResolveVariables) { + $css = $this->presets->parse($css); + } + + $selector = \trim($selector); + + if ($selector === '') { + return $css; + } + + $parser = new Parser($css); + $doc = $parser->parse(); + + $rootRules = ''; + $additionalSelectors = []; + + $newLine = $this->isCompressed ? '' : PHP_EOL; + $newLineAfterBlock = $this->isCompressed ? '' : PHP_EOL . PHP_EOL; + $space = $this->isCompressed ? '' : \implode('', \array_fill(0, 4, ' ')); + $spaceAfterSelector = $this->isCompressed ? '' : ' '; + + foreach ($doc->getAllDeclarationBlocks() as $declarationBlock) { + foreach ($declarationBlock->getSelectors() as $cssSelector) { + if (\is_string($cssSelector)) { + $cssSelector = new Selector($cssSelector); + } + + if ($cssSelector->getSelector() === $selector) { + foreach ($declarationBlock->getRules() as $rule) { + $important = $rule->getIsImportant() ? ' !important' : ''; + // phpcs:disable + $ruleText = $space . $rule->getRule() . ': ' . (string)$rule->getValue() . $important . ';' . $newLine; + // phpcs:enable + $rootRules .= $ruleText; + } + + continue; + } + + $actualSelector = $cssSelector->getSelector(); + $newSelector = \substr($actualSelector, \strlen($selector)); + + $cssBlock = $newSelector . $spaceAfterSelector . '{' . $newLine; + foreach ($declarationBlock->getRules() as $rule) { + $important = $rule->getIsImportant() ? ' !important' : ''; + // phpcs:disable + $cssBlock .= $space . $rule->getRule() . ': ' . (string)$rule->getValue() . $important . ';' . $newLine; + // phpcs:enable + } + $cssBlock .= '}' . $newLineAfterBlock; + $additionalSelectors[] = $cssBlock; + } + } + + \array_unshift($additionalSelectors, $rootRules . $newLine); + return \trim(\implode('&', $additionalSelectors), "\t\n\r\0\x0B&"); + } + + /** + * @deprecated Use parse() instead + */ public function parseString(string $css, string $selector = ''): string { + if (\str_starts_with(\trim($css), '&')) { + throw new \RuntimeException(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + } + $css = $this->presets->parse($css); + $css = $this->duplicateRulesForSelectorList($css); if ($selector === '') { return $css; @@ -61,4 +148,44 @@ public function parseString(string $css, string $selector = ''): string return \ltrim(\implode('', $rootRule) . \implode('&', $explodedNew), "\t\n\r\0\x0B&"); } + + /** + * Right now the algorithm used by WordPress to apply custom CSS does not convert selector list + * correctly, so I need to duplicate the rules for each selector in the list. + */ + private function duplicateRulesForSelectorList(string $css): string + { + $pattern = '/\{(.*)}/s'; + \preg_match($pattern, $css, $matches); + + if (!isset($matches[1])) { + return $css; + } + + + $pos = \strpos($css, '{'); + if ($pos === false) { + return $css; + } + + $selectors = \substr($css, 0, $pos); + $selectorArray = \explode(',', $selectors); + + if (\count($selectorArray) === 1) { + return $css; + } + + $lastSelector = \array_pop($selectorArray); + + $cssFinal = ""; + $rules = $matches[0]; + + foreach ($selectorArray as $selector) { + $cssFinal .= \rtrim($selector) . " $rules\n"; + } + + $cssFinal .= \rtrim($lastSelector) . " $rules\n"; + + return $cssFinal; + } } diff --git a/src/Domain/Input/Styles/CssInterface.php b/src/Domain/Input/Styles/CssInterface.php new file mode 100644 index 00000000..ec55bbf5 --- /dev/null +++ b/src/Domain/Input/Styles/CssInterface.php @@ -0,0 +1,17 @@ +css = $css; + $this->compiler = $compiler; + $this->presets = $presets ?? new NullPresets(); + } + + public function expanded(): self + { + $this->css->expanded(); + $this->outputStyle = OutputStyle::EXPANDED; + return $this; + } + + public function parse(string $css, string $selector = ''): string + { + if (\str_starts_with(\trim($css), '&')) { + throw new \RuntimeException(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + } + + $this->css->stopResolveVariables(); + $css = $this->presets->parse($css); + + $selector = \trim($selector); + + $this->compiler->setOutputStyle($this->outputStyle); + $cssCompiled = $this->compiler->compileString($css); + + return $this->css->parse($cssCompiled->getCss(), $selector); + } +} diff --git a/src/Domain/Output/Dump.php b/src/Domain/Output/Dump.php index 7c950045..4c99a4f8 100644 --- a/src/Domain/Output/Dump.php +++ b/src/Domain/Output/Dump.php @@ -5,13 +5,14 @@ namespace ItalyStrap\ThemeJsonGenerator\Domain\Output; use ItalyStrap\Config\ConfigInterface; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\DumpMessage; +use ItalyStrap\ThemeJsonGenerator\Application\DumpMessage; use ItalyStrap\ThemeJsonGenerator\Application\Config\Blueprint; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Presets; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\PresetsInterface; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\DryRunMode; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\GeneratedFile; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\GeneratingFile; +use ItalyStrap\ThemeJsonGenerator\Domain\Output\Events\NoFileFound; use ItalyStrap\ThemeJsonGenerator\Infrastructure\Filesystem\FilesFinder; use ItalyStrap\ThemeJsonGenerator\Infrastructure\Filesystem\JsonFileWriter; use ItalyStrap\ThemeJsonGenerator\Infrastructure\Filesystem\ScssFileWriter; @@ -43,6 +44,7 @@ public function __construct( public function handle(DumpMessage $message): void { + $count = 0; /** * Let's test the new workflow * @example $name => $file @@ -55,6 +57,7 @@ public function handle(DumpMessage $message): void $presets = $injector->make(PresetsInterface::class); $blueprint = $injector->make(Blueprint::class); $blueprint->setPresets($presets); + $count++; /** * @todo Add subscription configuration. @@ -70,50 +73,37 @@ public function handle(DumpMessage $message): void $this->generateJsonFile($message, $fileName, $file, $blueprint); $this->generateScssFile($message, $fileName, $blueprint); } + + if ($count === 0) { + $this->dispatcher->dispatch(new NoFileFound()); + } } private function generateJsonFile( - DumpMessage $command, + DumpMessage $message, string $fileName, \SplFileInfo $file, Blueprint $blueprint ): void { $this->dispatcher->dispatch(new GeneratingFile($fileName . self::JSON_FILE_SUFFIX)); - (new JsonFileWriter($file->getPath() . DIRECTORY_SEPARATOR . $fileName . self::JSON_FILE_SUFFIX)) + (new JsonFileWriter($this->filesFinder->resolveJsonFile($file))) ->write($blueprint); $this->dispatcher->dispatch(new GeneratedFile($fileName . self::JSON_FILE_SUFFIX)); } - private function generateScssFile(DumpMessage $command, string $fileName, Blueprint $blueprint): void + private function generateScssFile(DumpMessage $message, string $fileName, Blueprint $blueprint): void { - $this->dispatcher->dispatch(new GeneratingFile($fileName . '.scss')); - - $path_for_theme_sass = $command->getRootFolder() . DIRECTORY_SEPARATOR . $command->getSassFolder(); - if ($command->getSassFolder() !== '' && \is_writable($path_for_theme_sass)) { + $path_for_theme_sass = $message->getRootFolder() . DIRECTORY_SEPARATOR . $message->getSassFolder(); + if ($message->getSassFolder() !== '' && \is_writable($path_for_theme_sass)) { + $this->dispatcher->dispatch(new GeneratingFile($fileName . '.scss')); (new ScssFileWriter($path_for_theme_sass . DIRECTORY_SEPARATOR . $fileName . '.scss')) ->write($blueprint); $this->dispatcher->dispatch(new GeneratedFile($fileName . '.scss')); } } - public function processBlueprint( - DumpMessage $message, - string $fileName, - callable $entryPoint - ): void { - $injector = $this->configureContainer(); - $injector->execute($entryPoint); - - $presets = $injector->make(PresetsInterface::class); - $blueprint = $injector->make(Blueprint::class); - $blueprint->setPresets($presets); - - (new JsonFileWriter($message->getRootFolder() . DIRECTORY_SEPARATOR . $fileName . '.json')) - ->write($blueprint); - } - private function configureContainer(): \ItalyStrap\Empress\Injector { $injector = new \ItalyStrap\Empress\Injector(); diff --git a/src/Domain/Output/Events/NoFileFound.php b/src/Domain/Output/Events/NoFileFound.php new file mode 100644 index 00000000..16b3196e --- /dev/null +++ b/src/Domain/Output/Events/NoFileFound.php @@ -0,0 +1,20 @@ +jsonFile = $jsonFile; - } - - /** - * @param array $input_data - * @param int $options - * @throws \Exception - */ - public function write(array $input_data, int $options = 448): void - { - $this->jsonFile->write($input_data, $options); - } -} diff --git a/src/Application/Commands/FilesExtension.php b/src/Infrastructure/Filesystem/FilesExtension.php similarity index 69% rename from src/Application/Commands/FilesExtension.php rename to src/Infrastructure/Filesystem/FilesExtension.php index be61b7d2..965a75b8 100644 --- a/src/Application/Commands/FilesExtension.php +++ b/src/Infrastructure/Filesystem/FilesExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ItalyStrap\ThemeJsonGenerator\Application\Commands; +namespace ItalyStrap\ThemeJsonGenerator\Infrastructure\Filesystem; /** * @psalm-api diff --git a/src/Infrastructure/Filesystem/FilesFinder.php b/src/Infrastructure/Filesystem/FilesFinder.php index 2ef19895..1a028e80 100644 --- a/src/Infrastructure/Filesystem/FilesFinder.php +++ b/src/Infrastructure/Filesystem/FilesFinder.php @@ -13,6 +13,10 @@ class FilesFinder { public const ROOT_FILE_NAME = 'theme'; + public const STYLES_FOLDER = 'styles'; + + public const JSON_FILE_SUFFIX = '.json'; + private FinderInterface $finder; public function __construct( @@ -44,7 +48,7 @@ public function find( yield $this->extractFileName($rootFileInfo) => $rootFileInfo; } - $stylesFolder = $rootFolder . '/styles'; + $stylesFolder = $rootFolder . DIRECTORY_SEPARATOR . self::STYLES_FOLDER; if (!\is_dir($stylesFolder)) { return; @@ -64,6 +68,36 @@ public function find( } } + public function resolveJsonFile(\SplFileInfo $file): string + { + $fileName = $this->extractFileName($file); + $themeRoot = \getcwd(); + $stylesFolder = ''; + if ($fileName !== self::ROOT_FILE_NAME) { + $stylesFolder = self::STYLES_FOLDER; + } + + $styleCss = \implode(DIRECTORY_SEPARATOR, [ + $themeRoot, + 'style.css', + ]); + + if (!\file_exists($styleCss)) { + throw new \RuntimeException('The style.css file was not found in the root folder'); + } + + $styleCssContent = \file_get_contents($styleCss); + if (\strpos($styleCssContent, 'Theme Name:') === false) { + throw new \RuntimeException('The style.css file is not a valid WordPress theme'); + } + + return \implode(DIRECTORY_SEPARATOR, \array_filter([ + $themeRoot, + $stylesFolder, + $fileName . self::JSON_FILE_SUFFIX, + ])); + } + private function extractFileName(\SplFileInfo $file): string { return \explode('.', $file->getBasename())[0]; diff --git a/src/Infrastructure/Filesystem/JsonFileWriter.php b/src/Infrastructure/Filesystem/JsonFileWriter.php index 29d439cb..c7ac1146 100644 --- a/src/Infrastructure/Filesystem/JsonFileWriter.php +++ b/src/Infrastructure/Filesystem/JsonFileWriter.php @@ -4,7 +4,6 @@ namespace ItalyStrap\ThemeJsonGenerator\Infrastructure\Filesystem; -use Composer\Json\JsonFile; use ItalyStrap\Config\ConfigInterface; class JsonFileWriter implements FileWriter @@ -12,7 +11,6 @@ class JsonFileWriter implements FileWriter private string $path; /** - * ThemeJsonGenerator constructor. * @param string $path */ public function __construct(string $path) @@ -29,7 +27,23 @@ public function write(ConfigInterface $data): void throw new \RuntimeException('No data to write'); } - $json_file = new ComposerJsonFileAdapter(new JsonFile($this->path)); - $json_file->write($data->toArray()); + // This part is borrowed from \Composer\Json\JsonFile + $json_data = \json_encode( + $data->toArray(), + \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE + ); + if (false === $json_data) { + throw new \RuntimeException('Error encoding JSON data'); + } + + $dir = \dirname($this->path); + if (!\is_dir($dir)) { + \mkdir($dir, 0777, true); + } + + $result = \file_put_contents($this->path, $json_data . \PHP_EOL); + if (false === $result) { + throw new \RuntimeException('Error writing JSON data to file'); + } } } diff --git a/tests/_data/fixtures/advanced-example.json.php b/tests/_data/fixtures/advanced-example.json.php index 8daa7aac..d461d9e5 100644 --- a/tests/_data/fixtures/advanced-example.json.php +++ b/tests/_data/fixtures/advanced-example.json.php @@ -9,6 +9,8 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Duotone; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Gradient; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Palette; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Shadow; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Utilities\BoxShadow; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Utilities\Color; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Utilities\ColorModifier; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Utilities\LinearGradient; @@ -38,11 +40,10 @@ ->add(new Gradient( 'base-to-white', 'Base to white', - new LinearGradient( - '135deg', - $presets->get('color.base'), - $presets->get('color.bodyColor') - ) + (new LinearGradient()) + ->direction('135deg') + ->colorStop($presets->get('color.base')) + ->colorStop($presets->get('color.bodyColor')) )); // Duotone @@ -54,6 +55,25 @@ $presets->get('color.bodyColor') )); + $presets + ->add(new Shadow( + 'base', + 'Base shadow', + (new BoxShadow()) + ->offsetX('0') + ->offsetY('4px') + ->blur('8px') + ->spread('0') + ->color($presets->get('color.bodyColor')), + (new BoxShadow()) + ->inset() + ->offsetX('0') + ->offsetY('4px') + ->blur('8px') + ->spread('0') + ->color('#fff') + )); + // Font size $presets ->add(new FontSize('base', 'Base font size 16px', 'clamp(1rem, 2vw, 1.5rem)')) diff --git a/tests/_data/fixtures/theme.json.php b/tests/_data/fixtures/theme.json.php new file mode 100644 index 00000000..bfacfc50 --- /dev/null +++ b/tests/_data/fixtures/theme.json.php @@ -0,0 +1,6 @@ +runShellCommand(\sprintf( + 'bin/theme-json dump --path="%s" --dry-run', + $data + )); +// $i->runShellCommand('bin/theme-json dump --file="theme.json"'); +// $i->runShellCommand('bin/theme-json dump --path="tests"'); +// $i->runShellCommand('bin/theme-json dump --path="tests/_data/fixtures/themes/theme-flat/"'); + $i->dontSeeInShellOutput(NoFileFound::M_NO_FILE_FOUND); + $i->seeResultCodeIs(0); + } + + public function testInit(FunctionalTester $i): void + { + $i->runShellCommand('bin/theme-json init'); + $i->seeResultCodeIs(0); + } + + public function testValidate(FunctionalTester $i): void + { + $i->runShellCommand('bin/theme-json validate'); + $i->seeResultCodeIs(0); + } + + public function testInfo(FunctionalTester $i): void + { + $i->runShellCommand('bin/theme-json info'); + $i->seeResultCodeIs(0); + } +} diff --git a/tests/functional/DumpCest.php b/tests/functional/DumpCest.php deleted file mode 100644 index 4bab6ae7..00000000 --- a/tests/functional/DumpCest.php +++ /dev/null @@ -1,19 +0,0 @@ -runShellCommand('bin/theme-json dump'); -// $i->seeInShellOutput('Generating theme.json file'); -// $i->seeInShellOutput('Generated theme.json file'); - $i->seeResultCodeIs(0); - } -} diff --git a/tests/integration/Domain/Input/Styles/CssTest.php b/tests/integration/Domain/Input/Styles/CssTest.php index c2d932ff..1ad3591e 100644 --- a/tests/integration/Domain/Input/Styles/CssTest.php +++ b/tests/integration/Domain/Input/Styles/CssTest.php @@ -12,7 +12,8 @@ class CssTest extends IntegrationTestCase { use ProcessBlocksCustomCssTrait; use CssStyleStringProviderTrait { - styleProvider as styleProviderTrait; + CssStyleStringProviderTrait::styleProvider as styleProviderTrait; + CssStyleStringProviderTrait::newStyleProvider as newStyleProviderTrait; } private function makeInstance(): Css @@ -36,6 +37,38 @@ public function testItProcessInRealScenario(string $selector, string $actual, st $this->assertSame($expected, $parseString, 'The parsed string is not the same as expected'); + $result = $this->process_blocks_custom_css( + $parseString, + $selector + ); + + $this->assertSame($actual, $result, 'The result string is not the same as original'); + } + + public static function newStyleProvider(): iterable + { +// foreach (self::newStyleProviderTrait() as $key => $value) { +// yield $key => $value; +// } + + yield 'root custom properties mixed with css' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'actual' => '.test-selector{--foo: 100%;--bar: 100%;}.test-selector #firstParagraph{background-color: var(--first-color);color: var(--second-color);}.test-selector .foo{--bar: 50%;color: red;width: var(--foo);height: var(--bar);}', + 'expected' => '--foo: 100%;--bar: 100%;& #firstParagraph{background-color: var(--first-color);color: var(--second-color);}& .foo{--bar: 50%;color: red;width: var(--foo);height: var(--bar);}', + // phpcs:enable + ]; + } + + /** + * @dataProvider newStyleProvider + */ + public function testNewItProcessInRealScenario(string $selector, string $actual, string $expected): void + { + $parseString = $this->makeInstance()->parseString($actual, $selector); + $this->assertSame($expected, $parseString, 'The parsed string is not the same as expected'); + + $result = $this->process_blocks_custom_css( $parseString, $selector diff --git a/tests/src/CssStyleStringProviderTrait.php b/tests/src/CssStyleStringProviderTrait.php index 4f97ef02..5504114d 100644 --- a/tests/src/CssStyleStringProviderTrait.php +++ b/tests/src/CssStyleStringProviderTrait.php @@ -140,4 +140,153 @@ public static function styleProvider(): iterable // phpcs:enable ]; } + + public static function newStyleProvider(): iterable + { + yield 'root element' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{height: 100%;}', + 'expected' => 'height: 100%;', + ]; + + yield 'root element with multiple rules' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{height: 100%;width: 100%;color: red;}', + 'expected' => 'height: 100%;width: 100%;color: red;', + ]; + + yield 'root element with multiple rules and pseudo class' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}', + 'expected' => 'height: 100%;width: 100%;color: red;&:hover{color: red;}', + ]; + + yield 'root element with multiple rules and pseudo class and pseudo element' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'actual' => '.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', + 'expected' => 'height: 100%;width: 100%;color: red;&:hover{color: red;}&::placeholder{color: red;}', + // phpcs:enable + ]; + + yield 'pseudo single class' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector:hover {color: red;}', + 'expected' => ':hover{color: red;}', + ]; + + yield 'pseudo single class with multiple rules' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector:hover {color: red;height: 100%;}', + 'expected' => ':hover{color: red;height: 100%;}', + ]; + + yield 'pseudo single class with multiple rules and pseudo element' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector:hover {color: red;height: 100%;}.test-selector::placeholder {color: red;}', + 'expected' => ':hover{color: red;height: 100%;}&::placeholder{color: red;}', + ]; + + yield 'simple pseudo element ::placeholder ' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector::placeholder {color: red;}', + 'expected' => '::placeholder{color: red;}', + ]; + + yield 'simple pseudo element with multiple rules ::placeholder ' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector::placeholder {color: red;height: 100%;}', + 'expected' => '::placeholder{color: red;height: 100%;}', + ]; + + yield 'root element with pseudo element' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{height: 100%;}.test-selector::placeholder {color: red;}', + 'expected' => 'height: 100%;&::placeholder{color: red;}', + ]; + + yield 'mixed css example' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{height: 100%;}.test-selector .foo{color: red;}', + 'expected' => 'height: 100%;& .foo{color: red;}', + ]; + + yield 'mixed css example with multiple rules' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{height: 100%;width: 100%;}.test-selector .foo{color: red;height: 100%;}', + 'expected' => 'height: 100%;width: 100%;& .foo{color: red;height: 100%;}', + ]; + + yield 'simple css example' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;}', + 'expected' => ' .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;}', + ]; + + yield 'simple css example with multiple rules' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'actual' => '.test-selector .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}.test-selector .foo .bar{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}', + 'expected' => ' .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}& .foo .bar{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}', + // phpcs:enable + ]; + + yield 'simple css example with multiple rules and pseudo class' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'actual' => '.test-selector .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}.test-selector .foo .bar{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}.test-selector table{border-collapse: collapse;border-spacing: 0;}.test-selector:hover {height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}', + 'expected' => ' .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}& .foo .bar{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}& table{border-collapse: collapse;border-spacing: 0;}&:hover{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}', + // phpcs:enable + ]; + + yield 'simple css example with multiple rules and pseudo class and pseudo element' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'actual' => '.test-selector .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}.test-selector .foo .bar{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}.test-selector table{border-collapse: collapse;border-spacing: 0;}.test-selector:hover{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}.test-selector::placeholder{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}', + 'expected' => ' .foo{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}& .foo .bar{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}& table{border-collapse: collapse;border-spacing: 0;}&:hover{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}&::placeholder{height: 100%;left: 0;position: absolute;top: 0;width: 100%;color: red;}', + // phpcs:enable + ]; + + yield 'root custom properties' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{--foo: 100%;}', + 'expected' => '--foo: 100%;', + ]; + + yield 'root custom properties with multiple rules' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{--foo: 100%;--bar: 100%;}', + 'expected' => '--foo: 100%;--bar: 100%;', + ]; + + yield 'root with pseudo elements' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'original' => '.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', + 'expected' => 'height: 100%;width: 100%;color: red;&:hover{color: red;}&::placeholder{color: red;}', + // phpcs:enable + ]; + + yield 'with nested selector' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'original' => '.test-selector{color: red; margin: auto;}.test-selector.one{color: blue;}.test-selector .two{color: green;}', + 'expected' => 'color: red;margin: auto;&.one{color: blue;}& .two{color: green;}', + // phpcs:enable + ]; + + yield 'with !important' => [ + 'selector' => '.test-selector', + 'actual' => '.test-selector{color: red !important;}', + 'expected' => 'color: red !important;', + ]; + + yield 'with nested selector and !important to some rule' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'original' => '.test-selector{color: red; margin: auto;}.test-selector.one{color: blue;}.test-selector .two{color: green !important;}', + 'expected' => 'color: red;margin: auto;&.one{color: blue;}& .two{color: green !important;}', + // phpcs:enable + ]; + } } diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 8a929801..2c9f3b4f 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -5,19 +5,13 @@ namespace ItalyStrap\Tests; use Codeception\Test\Unit; -use Composer\Composer; -use Composer\Config; -use Composer\IO\IOInterface; -use Composer\Json\JsonFile; -use Composer\Package\Link; -use Composer\Package\PackageInterface; -use Composer\Package\RootPackageInterface; -use Composer\Repository\RepositoryManager; use ItalyStrap\Config\ConfigInterface; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Palette; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Utilities\BoxShadow; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Utilities\ColorInterface; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\Color\Utilities\GradientInterface; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\PresetInterface; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\PresetsInterface; use ItalyStrap\ThemeJsonGenerator\Infrastructure\Filesystem\FilesFinder; use JsonSchema\Validator; use Prophecy\PhpUnit\ProphecyTrait; @@ -57,62 +51,6 @@ public function makeConfig(): ConfigInterface return $this->config->reveal(); } - protected ObjectProphecy $jsonFile; - - protected function makeJsonFile(): JsonFile - { - return $this->jsonFile->reveal(); - } - - protected ObjectProphecy $composer; - - protected function makeComposer(): Composer - { - return $this->composer->reveal(); - } - - protected ObjectProphecy $composerConfig; - - protected function makeComposerConfig(): Config - { - return $this->composerConfig->reveal(); - } - - protected ObjectProphecy $io; - - protected function makeIo(): IOInterface - { - return $this->io->reveal(); - } - - protected ObjectProphecy $rootPackage; - - protected function makeRootPackage(): RootPackageInterface - { - return $this->rootPackage->reveal(); - } - - protected ObjectProphecy $link; - - protected function makeLink(): Link - { - return $this->link->reveal(); - } - - protected ObjectProphecy $repositoryManager; - - protected function makeRepositoryManager(): RepositoryManager - { - return $this->repositoryManager->reveal(); - } - - protected ObjectProphecy $package; - - protected function makePackage(): PackageInterface - { - return $this->package->reveal(); - } - protected ObjectProphecy $colorInfo; protected function makeColorInfo(): ColorInterface @@ -127,6 +65,13 @@ protected function makeGradient(): GradientInterface return $this->gradient->reveal(); } + protected ObjectProphecy $boxShadow; + + protected function makeBoxShadow(): BoxShadow + { + return $this->boxShadow->reveal(); + } + protected ObjectProphecy $palette; protected function makePalette(): Palette @@ -162,30 +107,28 @@ protected function makeCompiler(): Compiler return $this->compiler->reveal(); } + protected ObjectProphecy $presets; + + protected function makePresets(): PresetsInterface + { + return $this->presets->reveal(); + } + // phpcs:ignore -- Method from Codeception protected function _before() { $this->item = $this->prophesize(PresetInterface::class); $this->colorInfo = $this->prophesize(ColorInterface::class); $this->gradient = $this->prophesize(GradientInterface::class); + $this->boxShadow = $this->prophesize(BoxShadow::class); $this->palette = $this->prophesize(Palette::class); $this->config = $this->prophesize(ConfigInterface::class); - $this->jsonFile = $this->prophesize(JsonFile::class); - $this->composer = $this->prophesize(Composer::class); - $this->composerConfig = $this->prophesize(Config::class); - $this->io = $this->prophesize(IOInterface::class); - $this->rootPackage = $this->prophesize(RootPackageInterface::class); - $this->link = $this->prophesize(Link::class); - $this->repositoryManager = $this->prophesize(RepositoryManager::class); - $this->package = $this->prophesize(PackageInterface::class); $this->dispatcher = $this->prophesize(EventDispatcherInterface::class); $this->filesFinder = $this->prophesize(FilesFinder::class); $this->validator = $this->prophesize(Validator::class); $this->compiler = $this->prophesize(Compiler::class); - - $this->composer->getConfig()->willReturn($this->makeComposerConfig()); - $this->composer->getPackage()->willReturn($this->makeRootPackage()); + $this->presets = $this->prophesize(PresetsInterface::class); $this->input_data = require \codecept_data_dir('fixtures/input-data.php'); $this->color = '#000000'; diff --git a/tests/unit/Application/Commands/Composer/ThemeJson.php b/tests/unit/Application/Commands/Composer/ThemeJson.php deleted file mode 100644 index 3eb9035e..00000000 --- a/tests/unit/Application/Commands/Composer/ThemeJson.php +++ /dev/null @@ -1,164 +0,0 @@ -makeConfig()); - } - - public function testItShouldBeInstantiatable(): void - { - $sut = $this->makeInstance(); - } - - public function testItShouldRun(): void - { - $sut = $this->makeInstance(); - -// $application = new Application(); -// $application->setAutoExit(false); -// $application->add($sut); -// $application->run(); - -// $tester = new CommandTester($sut); -// $tester->setInputs([]); -// $tester->execute([]); - -// $tester->assertCommandIsSuccessful(); - } - - /** - * @return never - */ - public function testItShouldNotCreateThemeJsonFileFromRootPackage(): void - { -// $this->markTestSkipped('This test needs to be fixed'); - $theme_json_file_path = \codecept_output_dir(random_int(0, mt_getrandmax()) . '/vendor'); - $this->assertDirectoryExists($theme_json_file_path, ''); - - -// $this->config -// ->get(Argument::type('string')) -// ->willReturn($theme_json_file_path); -// -// $this->rootPackage->getType()->willReturn('wordpress-theme'); -// $this->rootPackage->getExtra()->willReturn([ -// 'theme-json' => [ -// 'callable' => false, -// ], -// ]); -// -// $this->expectException(\RuntimeException::class); -// -// $sut = $this->makeInstance(); - //// $sut->process($this->makeComposer(), $this->makeIo()); -// -// $theme_json_file_path = dirname($theme_json_file_path); -// $this->assertFileNotExists($theme_json_file_path . '/theme.json', ''); - } - - /** - * @return never - */ - public function testItShouldCreateThemeJsonFileFromRootPackage(): void - { - $this->markTestSkipped('This test needs to be fixed'); - $rand = (string) random_int(0, mt_getrandmax()); - $temp_dir_path = codecept_output_dir($rand . '/vendor'); - - $this->composerConfig - ->get(Argument::type('string')) - ->willReturn($temp_dir_path); - - $this->rootPackage->getType()->willReturn('wordpress-theme'); - $this->rootPackage->getExtra()->willReturn([ - 'theme-json' => [ - 'callable' => static fn (): array => ['key' => 'value'], - 'path-for-theme-sass' => 'assets/', - ], - ]); - - $sut = $this->makeInstance(); -// $sut($this->makeComposer(), $this->makeIo()); - - $theme_json_file_path = dirname($temp_dir_path) . '/theme.json'; - $this->assertFileExists($theme_json_file_path, ''); - $this->assertFileIsReadable($theme_json_file_path, ''); - $this->assertFileIsWritable($theme_json_file_path, ''); - - /** - * @todo QUi il test fa merda, da rifare - */ -// $theme_scss_file_path = dirname( $temp_dir_path ) . '/theme.scss'; -// $this->assertFileExists( $theme_scss_file_path, ''); -// $this->assertFileIsReadable( $theme_scss_file_path, ''); -// $this->assertFileIsWritable( $theme_scss_file_path, ''); - - \unlink($theme_json_file_path); -// \unlink($theme_scss_file_path); - } - - /** - * @return never - */ - public function testItShouldCreateThemeJsonFileFromRequiredPackage(): void - { - $this->markTestSkipped('This test needs to be fixed'); - $theme_json_file_path = codecept_output_dir(random_int(0, mt_getrandmax()) . '/vendor'); - - $this->composerConfig - ->get(Argument::type('string')) - ->willReturn($theme_json_file_path); - - $this->composer->getRepositoryManager()->willReturn($this->makeRepositoryManager()); - $this->link->getConstraint()->willReturn('dev-master'); - $this->rootPackage->getRequires()->willReturn([ $this->makeLink() ]); - $this->rootPackage->getType()->willReturn('wordpress-theme'); - $this->rootPackage->getExtra()->willReturn([ - 'theme-json' => [ - 'callable' => static fn (): array => ['key' => 'value'], - ], - ]); - - $this->link->getTarget()->willReturn('italystrap/themejsongenerator'); - $this->repositoryManager - ->findPackage('italystrap/themejsongenerator', 'dev-master') - ->willReturn($this->makePackage()); -// $this->package->getType()->willReturn('wordpress-theme'); -// -// $this->package->getExtra()->willReturn([ -// 'theme-json' => [ -// 'callable' => fn(): array => ['key' => 'value'], -// ], -// ]); - - $sut = $this->makeInstance(); -// $sut($this->makeComposer(), $this->makeIo()); - - $theme_json_file_path .= '/italystrap/themejsongenerator/theme.json'; - $this->assertFileExists($theme_json_file_path, ''); - $this->assertFileIsReadable($theme_json_file_path, ''); - $this->assertFileIsWritable($theme_json_file_path, ''); - - \unlink($theme_json_file_path); - - $this->link->getConstraint()->shouldHaveBeenCalled(); - $this->link->getTarget()->shouldHaveBeenCalled(); - $this->repositoryManager - ->findPackage(Argument::type('string'), Argument::type('string')) - ->shouldHaveBeenCalled(); - $this->package->getType()->shouldHaveBeenCalled(); - $this->package->getExtra()->shouldHaveBeenCalled(); - } -} diff --git a/tests/unit/Application/Commands/WPCLI/ThemeJsonTest.php b/tests/unit/Application/Commands/WPCLI/ThemeJsonTest.php deleted file mode 100644 index 53260667..00000000 --- a/tests/unit/Application/Commands/WPCLI/ThemeJsonTest.php +++ /dev/null @@ -1,22 +0,0 @@ -makeInstance(); - } -} diff --git a/tests/unit/Application/Config/BlueprintTest.php b/tests/unit/Application/Config/BlueprintTest.php index fe6fe76a..65013991 100644 --- a/tests/unit/Application/Config/BlueprintTest.php +++ b/tests/unit/Application/Config/BlueprintTest.php @@ -34,4 +34,40 @@ public function testItShouldBeJsonSerializable(): void 'Json encode should be equals' ); } + + public function testSetGlobalCss(): void + { + $sut = $this->makeInstance(); + $this->assertTrue($sut->setGlobalCss('foo'), 'Should return true'); + $this->assertSame('foo', $sut->get('styles.css'), 'Should be equals'); + } + + public function testAppendGlobalCss(): void + { + $sut = $this->makeInstance(); + $this->assertTrue($sut->setGlobalCss('foo'), 'Should return true'); + $this->assertTrue($sut->appendGlobalCss('bar'), 'Should return true'); + $this->assertSame('foobar', $sut->get('styles.css'), 'Should be equals'); + } + + public function testSetElementStyle(): void + { + $sut = $this->makeInstance(); + $this->assertTrue($sut->setElementStyle('foo', ['bar' => 'baz']), 'Should return true'); + $this->assertSame(['bar' => 'baz'], $sut->get('styles.elements.foo'), 'Should be equals'); + } + + public function testSetBlockSettings(): void + { + $sut = $this->makeInstance(); + $this->assertTrue($sut->setBlockSettings('foo', ['bar' => 'baz']), 'Should return true'); + $this->assertSame(['bar' => 'baz'], $sut->get('settings.blocks.foo'), 'Should be equals'); + } + + public function testSetBlockStyle(): void + { + $sut = $this->makeInstance(); + $this->assertTrue($sut->setBlockStyle('foo', ['bar' => 'baz']), 'Should return true'); + $this->assertSame(['bar' => 'baz'], $sut->get('styles.blocks.foo'), 'Should be equals'); + } } diff --git a/tests/unit/Domain/Input/Settings/Color/ShadowTest.php b/tests/unit/Domain/Input/Settings/Color/ShadowTest.php new file mode 100644 index 00000000..0c771133 --- /dev/null +++ b/tests/unit/Domain/Input/Settings/Color/ShadowTest.php @@ -0,0 +1,27 @@ +slug, + $this->name, + $this->makeBoxShadow() + ); + } +} diff --git a/tests/unit/Domain/Input/Settings/Color/Utilities/BoxShadowTest.php b/tests/unit/Domain/Input/Settings/Color/Utilities/BoxShadowTest.php new file mode 100644 index 00000000..35f6bc57 --- /dev/null +++ b/tests/unit/Domain/Input/Settings/Color/Utilities/BoxShadowTest.php @@ -0,0 +1,83 @@ +makeInstance(); + $this->assertInstanceOf(BoxShadow::class, $sut); + } + + public function testItShouldThrowExceptionWhenNoOffsetAreProvided(): void + { + $sut = $this->makeInstance(); + $this->expectException(\RuntimeException::class); + $var = (string)$sut; + } + + public function testItShouldReturnValiShadow(): void + { + $color = $this->colorInfo + ->__toString() + ->willReturn('#fff'); + + $this->palette + ->color() + ->willReturn($this->makeColorInfo()); + + $this->palette + ->var('#fff') + ->willReturn('var(--color-foo, #fff)'); + + $sut = $this->makeInstance(); + $sut->offsetX('0'); + $sut->offsetY('10px'); + $sut->color($this->makePalette()); + $this->assertSame( + '0 10px var(--color-foo, #fff)', + (string)$sut + ); + } + + public function testWithStringColor(): void + { + $sut = $this->makeInstance(); + $sut->offsetX('0') + ->offsetY('10px') + ->blur('0') + ->spread('0') + ->color('#fff'); + $this->assertSame( + '0 10px 0 0 #ffffff', + (string)$sut + ); + } + + public function testWithColorObject(): void + { + $color = $this->colorInfo + ->__toString() + ->willReturn('#fff'); + + $sut = $this->makeInstance(); + $sut->offsetX('0'); + $sut->offsetY('10px'); + $sut->color($this->makeColorInfo()); + $this->assertSame( + '0 10px #fff', + (string)$sut + ); + } +} diff --git a/tests/unit/Domain/Input/Settings/Color/Utilities/ColorFactoryTest.php b/tests/unit/Domain/Input/Settings/Color/Utilities/ColorFactoryTest.php new file mode 100644 index 00000000..889d46a0 --- /dev/null +++ b/tests/unit/Domain/Input/Settings/Color/Utilities/ColorFactoryTest.php @@ -0,0 +1,55 @@ +makeInstance(); + $this->assertInstanceOf(ColorFactory::class, $sut, 'Should be an instance of ColorFactory'); + } + + public function testItShouldReturnColorInstanceFromColorInfo(): void + { + $sut = $this->makeInstance(); + $color = $sut->fromColorInfo(new Color('#ffffff')); + $this->assertInstanceOf(Color::class, $color, 'Should be an instance of Color'); + $this->assertSame('#ffffff', (string) $color, 'Should be equals'); + } + + public function testItShouldReturnColorInstanceFromColorString(): void + { + $sut = $this->makeInstance(); + $color = $sut->fromColorString('rgba(255,255,255,1.00)'); + $this->assertInstanceOf(Color::class, $color, 'Should be an instance of Color'); + $this->assertSame('rgba(255,255,255,1.00)', (string) $color, 'Should be equals'); + } + + public function testItShouldReturnColorInstanceFromHsla(): void + { + $sut = $this->makeInstance(); + $color = $sut->hsla(0, 0, 100); + $this->assertInstanceOf(Color::class, $color, 'Should be an instance of Color'); + $this->assertSame('hsla(0,0%,100%,1)', (string) $color, 'Should be equals'); + } + + public function testItShouldReturnColorInstanceFromRgba(): void + { + $sut = $this->makeInstance(); + $color = $sut->rgba(255, 255, 255); + $this->assertInstanceOf(Color::class, $color, 'Should be an instance of Color'); + $this->assertSame('rgba(255,255,255,1.00)', (string) $color, 'Should be equals'); + } +} diff --git a/tests/unit/Domain/Input/Settings/Color/Utilities/ColorTest.php b/tests/unit/Domain/Input/Settings/Color/Utilities/ColorTest.php index 756028bc..51803146 100644 --- a/tests/unit/Domain/Input/Settings/Color/Utilities/ColorTest.php +++ b/tests/unit/Domain/Input/Settings/Color/Utilities/ColorTest.php @@ -258,7 +258,7 @@ public function testHueSaturationLightnessAlphaMethods(): void $this->assertSame(212, $sut->hue(), ''); $this->assertSame(73, $sut->saturation(), ''); $this->assertSame(75, $sut->lightness(), ''); - $this->assertSame(1.0, $sut->alpha(), ''); + $this->assertSame('ff', $sut->alpha(), ''); } public function testItShouldReturnLuminanceValue(): void @@ -278,4 +278,22 @@ public function testItShouldReturnRelativeLuminanceValue(): void $color = $this->makeInstance('#bada55'); $this->assertTrue($sut->relativeLuminance($color) >= 4.5, ''); } + + public function testConversionToDifferentFormat(): void + { + $sut = $this->makeInstance('rgba(0,0,0,0.25)'); + $this->assertSame(0.25, $sut->alpha(), ''); + + $sut = $this->makeInstance('#000000'); + $this->assertSame('ff', $sut->alpha(), ''); + + $sut = $this->makeInstance('hsla(0,0,0,0.25)'); + $this->assertSame(0.25, $sut->alpha(), ''); + + $this->assertStringMatchesFormat('#000000', (string)$sut->toHex(), ''); + $this->assertStringMatchesFormat('rgb(0,0,0)', (string)$sut->toRgb(), ''); + $this->assertStringMatchesFormat('rgba(0,0,0,0.25)', (string)$sut->toRgba(), ''); + $this->assertStringMatchesFormat('hsl(0,0%,0%)', (string)$sut->toHsl(), ''); + $this->assertStringMatchesFormat('hsla(0,0%,0%,0.25)', (string)$sut->toHsla(), ''); + } } diff --git a/tests/unit/Domain/Input/Settings/Color/Utilities/LinearGradientTest.php b/tests/unit/Domain/Input/Settings/Color/Utilities/LinearGradientTest.php new file mode 100644 index 00000000..f182aece --- /dev/null +++ b/tests/unit/Domain/Input/Settings/Color/Utilities/LinearGradientTest.php @@ -0,0 +1,61 @@ +makeInstance(); + $this->assertInstanceOf(LinearGradient::class, $sut); + } + + public function testItShouldThrowExceptionWhenToStringWithLessThanTwoColors(): void + { + $sut = $this->makeInstance(); + $this->expectException(\RuntimeException::class); + $var = (string)$sut; + } + + public function testItShouldReturnEmptyStringWhenToStringWithDirection(): void + { + $sut = $this->makeInstance(); + $sut->direction('to bottom'); + $this->expectException(\RuntimeException::class); + $var = (string)$sut; + } + + public function testItShouldReturnEmptyStringWhenToStringWithDirectionAndColorAndStop(): void + { + $this->colorInfo + ->__toString() + ->willReturn('#fff'); + + $this->palette + ->color() + ->willReturn($this->makeColorInfo()); + + $this->palette + ->var('#fff') + ->willReturn('var(--color-foo, #fff)'); + + $sut = $this->makeInstance(); + $sut->direction('to bottom'); + $sut->colorStop($this->makePalette()); + $sut->colorStop($this->makePalette()); + $this->assertSame( + 'linear-gradient(to bottom, var(--color-foo, #fff), var(--color-foo, #fff))', + (string)$sut + ); + } +} diff --git a/tests/unit/Domain/Input/Styles/CssTest.php b/tests/unit/Domain/Input/Styles/CssTest.php index 6715072e..40ccab00 100644 --- a/tests/unit/Domain/Input/Styles/CssTest.php +++ b/tests/unit/Domain/Input/Styles/CssTest.php @@ -7,12 +7,15 @@ use ItalyStrap\Tests\CssStyleStringProviderTrait; use ItalyStrap\Tests\UnitTestCase; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\CssInterface; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Scss; use ScssPhp\ScssPhp\Compiler; class CssTest extends UnitTestCase { use CssStyleStringProviderTrait { - styleProvider as styleProviderTrait; + CssStyleStringProviderTrait::styleProvider as styleProviderTrait; + CssStyleStringProviderTrait::newStyleProvider as newStyleProviderTrait; } private function makeInstance(): Css @@ -69,6 +72,30 @@ public static function styleProvider(): iterable } CUSTOM_CSS, ]; + + yield 'with list selectors' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'original' => '.test-selector .one ,.test-selector .two,.test-selector .three{color: red;}', + 'expected' => " .one {color: red;}\n& .two {color: red;}\n& .three {color: red;}\n", + // phpcs:enable + ]; + + yield 'with list selectors and new line' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'original' => ".test-selector .one ,.test-selector .two,.test-selector .three{\ncolor: red;\n}", + 'expected' => " .one {\ncolor: red;\n}\n& .two {\ncolor: red;\n}\n& .three {\ncolor: red;\n}\n", + // phpcs:enable + ]; + + yield 'with list selectors and new line without original selector' => [ + // phpcs:disable + 'selector' => '', + 'original' => " .one,& .two,& .three{\ncolor: red;\n}", + 'expected' => " .one {\ncolor: red;\n}\n& .two {\ncolor: red;\n}\n& .three {\ncolor: red;\n}\n", + // phpcs:enable + ]; } /** @@ -80,30 +107,42 @@ public function testItShouldParse(string $selector, string $actual, string $expe $this->assertSame($expected, $parseString, 'The parsed string is not the same as expected'); } - /** - * @dataProvider styleProvider - */ - public function testItShouldCompileExpanded(string $selector, string $css, string $expected): void + public function testItShouldThrowErrorIfCssStartWithAmpersand(): void { - $this->expandedCompiler($css, 'expanded'); - } + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); - /** - * @dataProvider styleProvider - */ - public function testItShouldCompileCompressed(string $selector, string $css, string $expected): void - { - $this->expandedCompiler($css, 'compressed'); + $this->makeInstance()->parse('& .foo{color: red;}'); } - private function expandedCompiler(string $css, string $style): void + public static function newStyleProvider(): iterable { - $compiler = new Compiler(); - $compiler->setOutputStyle($style); + foreach (self::newStyleProviderTrait() as $key => $value) { + yield $key => $value; + } + + yield 'selector list' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'actual' => '.test-selector .one ,.test-selector .two,.test-selector.three,.test-selector #four{color: red;}', + 'expected' => ' .one{color: red;}& .two{color: red;}&.three{color: red;}& #four{color: red;}', + // phpcs:enable + ]; - $result = $compiler->compileString($css); + yield 'selector used also as prefix for nested selectors' => [ + // phpcs:disable + 'selector' => '.test-selector', + 'actual' => '.test-selector .test-selector-one{color: blue;}.test-selector .test-selector-two{color: red;}', + 'expected' => ' .test-selector-one{color: blue;}& .test-selector-two{color: red;}', + ]; + } - $actual = $this->makeInstance()->parseString($result->getCss(), '.test-selector'); - $this->assertTrue(true, 'Let this test pass, is a check for the compiler'); + /** + * @dataProvider newStyleProvider + */ + public function testItShouldParseWithNewMethod(string $selector, string $actual, string $expected): void + { + $parseString = $this->makeInstance()->parse($actual, $selector); + $this->assertSame($expected, $parseString, 'The parsed string is not the same as expected'); } } diff --git a/tests/unit/Domain/Input/Styles/OnlyCtorPresetsParamTest.php b/tests/unit/Domain/Input/Styles/OnlyCtorPresetsParamTest.php new file mode 100644 index 00000000..aa93f3d2 --- /dev/null +++ b/tests/unit/Domain/Input/Styles/OnlyCtorPresetsParamTest.php @@ -0,0 +1,57 @@ + [Border::class]; + yield Color::class => [Color::class]; + yield Css::class => [Css::class]; + yield Outline::class => [Outline::class]; + yield Scss::class => [Scss::class]; + yield Spacing::class => [Spacing::class]; + yield Typography::class => [Typography::class]; + } + + /** + * @dataProvider classNameDataProvider + */ + public function testClassesThatNeedPresetsAsParameter(string $class): void + { + $reflection = new \ReflectionClass($class); + $constructor = $reflection->getConstructor(); + $parameters = $constructor->getParameters(); + + $this->assertNotEmpty($parameters, 'The constructor of ' . $class . ' is empty'); + + $found = false; + foreach ($parameters as $parameter) { + if ($parameter->getName() === 'presets') { + $found = true; + break; + } + } + + $this->assertTrue( + $found, + \sprintf( + "The constructor of %s does not have a parameter named \$preset, found: %s", + $class, + \implode(', ', \array_map(fn(\ReflectionParameter $p) => '$' . $p->getName(), $parameters)) + ) + ); + } +} diff --git a/tests/unit/Domain/Input/Styles/ScssTest.php b/tests/unit/Domain/Input/Styles/ScssTest.php new file mode 100644 index 00000000..bc90fd9d --- /dev/null +++ b/tests/unit/Domain/Input/Styles/ScssTest.php @@ -0,0 +1,92 @@ +makePresets(); + return new Scss(new Css($presets), new Compiler(), $presets); + } + + public function testItShouldBeInstantiable(): void + { + $instance = $this->makeInstance(); + $this->assertInstanceOf(Scss::class, $instance); + } + + public static function newStyleProvider(): iterable + { + foreach (self::newStyleProviderTrait() as $key => $value) { + yield $key => $value; + } + + yield 'selector used also as prefix for nested selectors' => [ + 'selector' => '.test-selector', + 'actual' => << 'gap: 0;&.test-selector-one{color: blue;}& .test-selector-two{color: blue;}', + ]; + + yield 'selector used also as prefix for nested selectors with nested selectors' => [ + 'selector' => '.test-selector', + 'actual' => << '__button-inside .test-selector__button{margin-left: -1px;transition: margin-left .3s;}', + ]; + + yield 'without selector' => [ + 'selector' => '', + 'actual' => << '.test-selector{gap:0}.test-selector.test-selector-one,.test-selector .test-selector-two{color:blue}', + // phpcs:enable + ]; + } + + /** + * @dataProvider newStyleProvider + */ + public function testItShouldParseWithNewMethod(string $selector, string $actual, string $expected): void + { + $this->presets->parse($actual)->willReturn($actual)->shouldBeCalledTimes(1); + $parseString = $this->makeInstance()->parse($actual, $selector); + $this->assertSame($expected, $parseString, 'The parsed string is not the same as expected'); + } +} diff --git a/tests/unit/Domain/Output/DumpTest.php b/tests/unit/Domain/Output/DumpTest.php index 9288d64d..905d6fb7 100644 --- a/tests/unit/Domain/Output/DumpTest.php +++ b/tests/unit/Domain/Output/DumpTest.php @@ -6,7 +6,7 @@ use ItalyStrap\Config\Config; use ItalyStrap\Tests\UnitTestCase; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\DumpMessage; +use ItalyStrap\ThemeJsonGenerator\Application\DumpMessage; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Dump; use Prophecy\Argument; @@ -29,7 +29,7 @@ public function testItShouldHandleButDoNothing(): void ->willReturn([]) ->shouldBeCalledOnce(); - $this->makeInstance()->handle(new DumpMessage('', '', false)); + $this->makeInstance()->handle(new DumpMessage('', '', false, '')); } public function testItShouldBasicExample(): void @@ -41,7 +41,11 @@ public function testItShouldBasicExample(): void $basicExample->getBasename('.json.php') => $basicExample, ]); - $this->makeInstance()->handle(new DumpMessage('', '', false)); + $this->filesFinder + ->resolveJsonFile($basicExample) + ->willReturn(\codecept_data_dir('fixtures/basic-example.json')); + + $this->makeInstance()->handle(new DumpMessage('', '', false, '')); $generatedFile = new \SplFileInfo(\codecept_data_dir('fixtures/basic-example.json')); $this->assertFileExists($generatedFile->getPathname(), 'The file was not generated'); @@ -58,7 +62,11 @@ public function testItShouldAdvancedExample(): void $advancedExample->getBasename('.json.php') => $advancedExample, ]); - $this->makeInstance()->handle(new DumpMessage('', '', false)); + $this->filesFinder + ->resolveJsonFile($advancedExample) + ->willReturn(\codecept_data_dir('fixtures/advanced-example.json')); + + $this->makeInstance()->handle(new DumpMessage('', '', false, '')); $generatedFile = new \SplFileInfo(\codecept_data_dir('fixtures/advanced-example.json')); $this->assertFileExists($generatedFile->getPathname(), 'The file was not generated'); diff --git a/tests/unit/Domain/Output/InitTest.php b/tests/unit/Domain/Output/InitTest.php index b214f402..5b9dd8a9 100644 --- a/tests/unit/Domain/Output/InitTest.php +++ b/tests/unit/Domain/Output/InitTest.php @@ -5,7 +5,7 @@ namespace ItalyStrap\Tests\Unit\Domain\Output; use ItalyStrap\Tests\UnitTestCase; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\InitMessage; +use ItalyStrap\ThemeJsonGenerator\Application\InitMessage; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Init; use Prophecy\Argument; diff --git a/tests/unit/Domain/Output/ValidateTest.php b/tests/unit/Domain/Output/ValidateTest.php index d01206c5..f9412cd1 100644 --- a/tests/unit/Domain/Output/ValidateTest.php +++ b/tests/unit/Domain/Output/ValidateTest.php @@ -5,7 +5,7 @@ namespace ItalyStrap\Tests\Unit\Domain\Output; use ItalyStrap\Tests\UnitTestCase; -use ItalyStrap\ThemeJsonGenerator\Application\Commands\ValidateMessage; +use ItalyStrap\ThemeJsonGenerator\Application\ValidateMessage; use ItalyStrap\ThemeJsonGenerator\Domain\Output\Validate; use Prophecy\Argument; diff --git a/tests/unit/Infrastructure/Filesystem/ComposerJsonFileAdapterTest.php b/tests/unit/Infrastructure/Filesystem/ComposerJsonFileAdapterTest.php deleted file mode 100644 index b972038e..00000000 --- a/tests/unit/Infrastructure/Filesystem/ComposerJsonFileAdapterTest.php +++ /dev/null @@ -1,32 +0,0 @@ -makeJsonFile()); - } - - public function testItShouldBeInstantiatable(): void - { - $sut = $this->makeInstance(); - } - - /** - * @throws \Exception - */ - public function testItShouldWrite(): void - { - $sut = $this->makeInstance(); - $sut->write($this->inputData()); - $this->jsonFile->write(Argument::type('array'), Argument::any())->shouldHaveBeenCalled(); - } -} diff --git a/wp-cli.local.yml b/wp-cli.local.yml deleted file mode 100644 index ae151bea..00000000 --- a/wp-cli.local.yml +++ /dev/null @@ -1,2 +0,0 @@ -# This is an example file -THEME_JSON_CALLABLE: '\ItalyStrap\ThemeJsonGenerator\test_callable' \ No newline at end of file