From e6fba13e6d5b1ebd58c6f9978ac7d08a419d67e0 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 21 Apr 2024 20:05:21 +0200 Subject: [PATCH 1/8] introduce a new CSS:parse() method using AST instead of regex --- composer.json | 2 +- src/Domain/Input/Styles/Css.php | 61 +++++++++- tests/src/CssStyleStringProviderTrait.php | 135 +++++++++++++++++++++ tests/unit/Domain/Input/Styles/CssTest.php | 50 ++++++++ 4 files changed, 246 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a2c66892..65530a8c 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "italystrap/debug": "dev-master", "wp-cli/wp-cli": "^2.7", - "sabberworm/php-css-parser": "^8.0" + "sabberworm/php-css-parser": "^8.5" }, "autoload": { "psr-4": { diff --git a/src/Domain/Input/Styles/Css.php b/src/Domain/Input/Styles/Css.php index 53b7a5f9..316d498b 100644 --- a/src/Domain/Input/Styles/Css.php +++ b/src/Domain/Input/Styles/Css.php @@ -24,9 +24,10 @@ */ class Css { - const M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING = 'CSS cannot begin with an ampersand (&)'; + public const M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING = 'CSS cannot begin with an ampersand (&)'; private PresetsInterface $presets; + private bool $isCompressed = false; public function __construct( PresetsInterface $presets = null @@ -69,6 +70,64 @@ public function parseString(string $css, string $selector = ''): string return \ltrim(\implode('', $rootRule) . \implode('&', $explodedNew), "\t\n\r\0\x0B&"); } + public function parse(string $css, string $selector = ''): string + { + if (\str_starts_with(\trim($css), '&')) { + throw new \RuntimeException(self::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + } + + $css = $this->presets->parse($css); + + if ($selector === '') { + return $css; + } + + $parser = new \Sabberworm\CSS\Parser($css); + $doc = $parser->parse(); + + $rootRules = ''; + $additionalSelectors = []; + $this->isCompressed = true; + + /** + * @psalm-suppress RedundantCondition + * @psalm-suppress TypeDoesNotContainType + */ + $newLine = $this->isCompressed ? '' : PHP_EOL; + + /** + * @psalm-suppress RedundantCondition + * @psalm-suppress TypeDoesNotContainType + */ + $space = $this->isCompressed ? '' : \implode('', \array_fill(0, 4, ' ')); + + foreach ($doc->getAllDeclarationBlocks() as $declarationBlock) { + foreach ($declarationBlock->getSelectors() as $cssSelector) { + if ($cssSelector->getSelector() === $selector) { + foreach ($declarationBlock->getRules() as $rule) { + $ruleText = $rule->getRule() . ': ' . $rule->getValue() . ';' . $newLine; + $rootRules .= $ruleText; + } + + continue; + } + + $actualSelector = $cssSelector->getSelector(); + $newSelector = \substr($actualSelector, \strlen($selector)); + + $cssBlock = $newSelector . ' {' . $newLine; + foreach ($declarationBlock->getRules() as $rule) { + $cssBlock .= $space . $rule->getRule() . ': ' . $rule->getValue() . ';' . $newLine; + } + $cssBlock .= '}' . $newLine; + $additionalSelectors[] = $cssBlock; + } + } + + \array_unshift($additionalSelectors, $rootRules); + return \trim(\implode('&', $additionalSelectors), "\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. diff --git a/tests/src/CssStyleStringProviderTrait.php b/tests/src/CssStyleStringProviderTrait.php index 4f97ef02..4017b259 100644 --- a/tests/src/CssStyleStringProviderTrait.php +++ b/tests/src/CssStyleStringProviderTrait.php @@ -140,4 +140,139 @@ 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 + ]; + } } diff --git a/tests/unit/Domain/Input/Styles/CssTest.php b/tests/unit/Domain/Input/Styles/CssTest.php index 1728ff20..aaa45011 100644 --- a/tests/unit/Domain/Input/Styles/CssTest.php +++ b/tests/unit/Domain/Input/Styles/CssTest.php @@ -13,6 +13,7 @@ class CssTest extends UnitTestCase { use CssStyleStringProviderTrait { CssStyleStringProviderTrait::styleProvider as styleProviderTrait; + CssStyleStringProviderTrait::newStyleProvider as newStyleProviderTrait; } private function makeInstance(): Css @@ -138,4 +139,53 @@ private function expandedCompiler(string $css, string $style): void $actual = $this->makeInstance()->parseString($result->getCss(), '.test-selector'); $this->assertTrue(true, 'Let this test pass, is a check for the compiler'); } + + public static function newStyleProvider(): iterable + { + foreach (self::newStyleProviderTrait() as $key => $value) { + yield $key => $value; + } + } + + /** + * @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'); + } + + public function testCssParser(): void + { + $css = <<makeInstance(); +// codecept_debug($sut->parse($css, $selector)); + } } From 593c0d89fb04d7b95b8a3f6b11858d31580c7eb2 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 21 Apr 2024 22:43:59 +0200 Subject: [PATCH 2/8] psalm --- src/Application/Config/Blueprint.php | 2 +- src/Domain/Input/Styles/Css.php | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Application/Config/Blueprint.php b/src/Application/Config/Blueprint.php index 52eaa4ea..16292076 100644 --- a/src/Application/Config/Blueprint.php +++ b/src/Application/Config/Blueprint.php @@ -30,7 +30,7 @@ public function setGlobalCss(string $css): bool public function appendGlobalCss(string $css): bool { - $currentCss = $this->get(SectionNames::STYLES . '.css', ''); + $currentCss = (string)$this->get(SectionNames::STYLES . '.css'); return $this->set(SectionNames::STYLES . '.css', $currentCss . $css); } diff --git a/src/Domain/Input/Styles/Css.php b/src/Domain/Input/Styles/Css.php index 316d498b..156b3ddb 100644 --- a/src/Domain/Input/Styles/Css.php +++ b/src/Domain/Input/Styles/Css.php @@ -27,7 +27,7 @@ class Css public const M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING = 'CSS cannot begin with an ampersand (&)'; private PresetsInterface $presets; - private bool $isCompressed = false; + private bool $isCompressed = true; public function __construct( PresetsInterface $presets = null @@ -87,25 +87,19 @@ public function parse(string $css, string $selector = ''): string $rootRules = ''; $additionalSelectors = []; - $this->isCompressed = true; - /** - * @psalm-suppress RedundantCondition - * @psalm-suppress TypeDoesNotContainType - */ $newLine = $this->isCompressed ? '' : PHP_EOL; - - /** - * @psalm-suppress RedundantCondition - * @psalm-suppress TypeDoesNotContainType - */ $space = $this->isCompressed ? '' : \implode('', \array_fill(0, 4, ' ')); foreach ($doc->getAllDeclarationBlocks() as $declarationBlock) { foreach ($declarationBlock->getSelectors() as $cssSelector) { + if (\is_string($cssSelector)) { + $cssSelector = new \Sabberworm\CSS\Property\Selector($cssSelector); + } + if ($cssSelector->getSelector() === $selector) { foreach ($declarationBlock->getRules() as $rule) { - $ruleText = $rule->getRule() . ': ' . $rule->getValue() . ';' . $newLine; + $ruleText = $rule->getRule() . ': ' . (string)$rule->getValue() . ';' . $newLine; $rootRules .= $ruleText; } @@ -117,7 +111,7 @@ public function parse(string $css, string $selector = ''): string $cssBlock = $newSelector . ' {' . $newLine; foreach ($declarationBlock->getRules() as $rule) { - $cssBlock .= $space . $rule->getRule() . ': ' . $rule->getValue() . ';' . $newLine; + $cssBlock .= $space . $rule->getRule() . ': ' . (string)$rule->getValue() . ';' . $newLine; } $cssBlock .= '}' . $newLine; $additionalSelectors[] = $cssBlock; From bbd9fb4349a4fb33b1d82c97fb453d4a6f313f1a Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 22 Apr 2024 07:29:46 +0200 Subject: [PATCH 3/8] improve css parsing --- composer.json | 5 ++- docs/02-advanced-usage.md | 22 ++++++++---- src/Domain/Input/Styles/Css.php | 7 +++- .../Domain/Input/Styles/CssTest.php | 35 ++++++++++++++++++- tests/src/CssStyleStringProviderTrait.php | 32 ++++++++--------- tests/unit/Domain/Input/Styles/CssTest.php | 15 ++++++++ 6 files changed, 89 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index 65530a8c..33298029 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "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", @@ -64,9 +65,7 @@ "symplify/easy-coding-standard": "^12.0", "italystrap/debug": "dev-master", - "wp-cli/wp-cli": "^2.7", - - "sabberworm/php-css-parser": "^8.5" + "wp-cli/wp-cli": "^2.7" }, "autoload": { "psr-4": { diff --git a/docs/02-advanced-usage.md b/docs/02-advanced-usage.md index 2586c8cb..17b3d31d 100644 --- a/docs/02-advanced-usage.md +++ b/docs/02-advanced-usage.md @@ -325,7 +325,7 @@ You can find an implementation example in the [tests/_data/fixtures/advanced-exa ### Custom CSS for Global Styles and per Block -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` class and its `parseString(string $css, string $selector = ''): string` method, developers can now seamlessly integrate custom styles without the need to remember the format to use like spaces and separators, just write your CSS as you would in a regular CSS file and let the `Css` class handle the rest. +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` class and its `parse(string $css, string $selector = ''): string` method, developers can now seamlessly integrate custom styles without the need to remember the format to use like spaces and separators, just write your CSS as you would in a regular CSS file and let the `Css` 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 @@ -336,12 +336,12 @@ So, let's see some examples: ```php // Result: 'height: 100%;' -echo (new Css($presets))->parseString('.test-selector{height: 100%;}', '.test-selector'); +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))->parseString('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'); +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. @@ -354,7 +354,7 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; [ SectionNames::STYLES => [ 'css' => $container->get(Css::class) // Or (new Css($presets)) - ->parseString('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'), + ->parse('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'), ], ]; ``` @@ -369,7 +369,7 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; 'blocks' => [ 'my-namespace/test-block' => [ 'css' => $container->get(Css::class) // Or (new Css($presets)) - ->parseString('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'), + ->parse('.test-selector{height: 100%;width: 100%;color: red;}.test-selector:hover {color: red;}.test-selector::placeholder {color: red;}', '.test-selector'), ], ], ], @@ -385,11 +385,21 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; [ SectionNames::STYLES => [ 'css' => $container->get(Css::class) // Or (new Css($presets)) - ->parseString('.test-selector{color: {{color.base}};}', '.test-selector'), + ->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);}" + } +} +``` + More examples can be found in the [tests/_data/fixtures/advanced-example.json.php](../tests/_data/fixtures/advanced-example.json.php) file. To know more about `css` field: diff --git a/src/Domain/Input/Styles/Css.php b/src/Domain/Input/Styles/Css.php index 156b3ddb..c9202cb6 100644 --- a/src/Domain/Input/Styles/Css.php +++ b/src/Domain/Input/Styles/Css.php @@ -35,6 +35,9 @@ public function __construct( $this->presets = $presets ?? new NullPresets(); } + /** + * @deprecated Use parse() instead + */ public function parseString(string $css, string $selector = ''): string { if (\str_starts_with(\trim($css), '&')) { @@ -77,6 +80,7 @@ public function parse(string $css, string $selector = ''): string } $css = $this->presets->parse($css); + $selector = \trim($selector); if ($selector === '') { return $css; @@ -90,6 +94,7 @@ public function parse(string $css, string $selector = ''): string $newLine = $this->isCompressed ? '' : PHP_EOL; $space = $this->isCompressed ? '' : \implode('', \array_fill(0, 4, ' ')); + $spaceAfterSelector = $this->isCompressed ? '' : ' '; foreach ($doc->getAllDeclarationBlocks() as $declarationBlock) { foreach ($declarationBlock->getSelectors() as $cssSelector) { @@ -109,7 +114,7 @@ public function parse(string $css, string $selector = ''): string $actualSelector = $cssSelector->getSelector(); $newSelector = \substr($actualSelector, \strlen($selector)); - $cssBlock = $newSelector . ' {' . $newLine; + $cssBlock = $newSelector . $spaceAfterSelector . '{' . $newLine; foreach ($declarationBlock->getRules() as $rule) { $cssBlock .= $space . $rule->getRule() . ': ' . (string)$rule->getValue() . ';' . $newLine; } 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 4017b259..e0ddbfd1 100644 --- a/tests/src/CssStyleStringProviderTrait.php +++ b/tests/src/CssStyleStringProviderTrait.php @@ -158,76 +158,76 @@ public static function newStyleProvider(): iterable 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;}', + '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;}', + '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;}', + '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%;}', + '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;}', + '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;}', + '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%;}', + '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;}', + '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;}', + '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%;}', + '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%;}', + '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;}', + '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 ]; @@ -235,7 +235,7 @@ public static function newStyleProvider(): iterable // 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;}', + '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 ]; @@ -243,7 +243,7 @@ public static function newStyleProvider(): iterable // 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;}', + '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 ]; @@ -263,7 +263,7 @@ public static function newStyleProvider(): iterable // 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;}', + 'expected' => 'height: 100%;width: 100%;color: red;&:hover{color: red;}&::placeholder{color: red;}', // phpcs:enable ]; @@ -271,7 +271,7 @@ public static function newStyleProvider(): iterable // 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;}', + 'expected' => 'color: red;margin: auto;&.one{color: blue;}& .two{color: green;}', // phpcs:enable ]; } diff --git a/tests/unit/Domain/Input/Styles/CssTest.php b/tests/unit/Domain/Input/Styles/CssTest.php index aaa45011..3fa13261 100644 --- a/tests/unit/Domain/Input/Styles/CssTest.php +++ b/tests/unit/Domain/Input/Styles/CssTest.php @@ -145,6 +145,21 @@ public static function newStyleProvider(): iterable 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 + ]; + + 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;}', + ]; } /** From caa3e35e12f795730e69e8efce5273631f324366 Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 22 Apr 2024 08:32:36 +0200 Subject: [PATCH 4/8] introduce Scss parser --- src/Domain/Input/Styles/Css.php | 18 ++++++-- src/Domain/Input/Styles/Scss.php | 46 ++++++++++++++++++++ tests/unit/Domain/Input/Styles/CssTest.php | 50 +++++++++++++++++++++- 3 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 src/Domain/Input/Styles/Scss.php diff --git a/src/Domain/Input/Styles/Css.php b/src/Domain/Input/Styles/Css.php index c9202cb6..7bdb5b6a 100644 --- a/src/Domain/Input/Styles/Css.php +++ b/src/Domain/Input/Styles/Css.php @@ -7,6 +7,8 @@ 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; /** * @link https://make.wordpress.org/core/2023/03/06/custom-css-for-global-styles-and-per-block/ @@ -35,6 +37,12 @@ public function __construct( $this->presets = $presets ?? new NullPresets(); } + public function expanded(): self + { + $this->isCompressed = false; + return $this; + } + /** * @deprecated Use parse() instead */ @@ -73,6 +81,9 @@ public function parseString(string $css, string $selector = ''): string return \ltrim(\implode('', $rootRule) . \implode('&', $explodedNew), "\t\n\r\0\x0B&"); } + /** + * @throws SourceException + */ public function parse(string $css, string $selector = ''): string { if (\str_starts_with(\trim($css), '&')) { @@ -86,13 +97,14 @@ public function parse(string $css, string $selector = ''): string return $css; } - $parser = new \Sabberworm\CSS\Parser($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 ? '' : ' '; @@ -118,12 +130,12 @@ public function parse(string $css, string $selector = ''): string foreach ($declarationBlock->getRules() as $rule) { $cssBlock .= $space . $rule->getRule() . ': ' . (string)$rule->getValue() . ';' . $newLine; } - $cssBlock .= '}' . $newLine; + $cssBlock .= '}' . $newLineAfterBlock; $additionalSelectors[] = $cssBlock; } } - \array_unshift($additionalSelectors, $rootRules); + \array_unshift($additionalSelectors, $rootRules . $newLine); return \trim(\implode('&', $additionalSelectors), "\t\n\r\0\x0B&"); } diff --git a/src/Domain/Input/Styles/Scss.php b/src/Domain/Input/Styles/Scss.php new file mode 100644 index 00000000..39d792e3 --- /dev/null +++ b/src/Domain/Input/Styles/Scss.php @@ -0,0 +1,46 @@ +css = $css; + $this->compiler = $compiler; + } + + public function expanded(): self + { + $this->css->expanded(); + $this->outputStyle = OutputStyle::EXPANDED; + return $this; + } + + public function parse(string $scss, string $selector = ''): string + { + $this->compiler->setOutputStyle($this->outputStyle); + + $css = $this->compiler->compileString($scss); + + return $this->css->parse($css->getCss(), $selector); + } +} diff --git a/tests/unit/Domain/Input/Styles/CssTest.php b/tests/unit/Domain/Input/Styles/CssTest.php index 3fa13261..ab4c66b6 100644 --- a/tests/unit/Domain/Input/Styles/CssTest.php +++ b/tests/unit/Domain/Input/Styles/CssTest.php @@ -7,6 +7,7 @@ use ItalyStrap\Tests\CssStyleStringProviderTrait; use ItalyStrap\Tests\UnitTestCase; use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Scss; use ScssPhp\ScssPhp\Compiler; class CssTest extends UnitTestCase @@ -110,7 +111,7 @@ public function testItShouldThrowErrorIfCssStartWithAmpersand(): void $this->expectException(\RuntimeException::class); $this->expectExceptionMessage(Css::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); - $this->makeInstance()->parseString('& .foo{color: red;}'); + $this->makeInstance()->parse('& .foo{color: red;}'); } /** @@ -136,7 +137,7 @@ private function expandedCompiler(string $css, string $style): void $result = $compiler->compileString($css); - $actual = $this->makeInstance()->parseString($result->getCss(), '.test-selector'); + $actual = $this->makeInstance()->parse($result->getCss(), '.test-selector'); $this->assertTrue(true, 'Let this test pass, is a check for the compiler'); } @@ -203,4 +204,49 @@ public function testCssParser(): void $sut = $this->makeInstance(); // codecept_debug($sut->parse($css, $selector)); } + + public function testScssParser(): void + { + $css = <<makeInstance(); +// codecept_debug($sut->parse($css, $selector)); + +// $compiler = new Compiler(); +// $compiler->setOutputStyle('expanded'); +// +// $result = $compiler->compileString($css); +// +// codecept_debug($result->getCss()); +// +// $actual = $this->makeInstance()->expanded()->parse($result->getCss(), '.wp-block-query-pagination'); +// codecept_debug($actual); + + $scss = new Scss($sut, new Compiler()); + $result = $scss->expanded()->parse($css, $selector); +// codecept_debug($result); + } } From 54154eded9c268fa82d0c604df650ff3fc7ce8f1 Mon Sep 17 00:00:00 2001 From: Enea Date: Tue, 23 Apr 2024 07:42:09 +0200 Subject: [PATCH 5/8] improve ScssTest --- docs/02-advanced-usage.md | 25 +++-- src/Domain/Input/Styles/Css.php | 90 ++++++++------- src/Domain/Input/Styles/Scss.php | 12 +- tests/src/UnitTestCase.php | 9 ++ tests/unit/Domain/Input/Styles/CssTest.php | 105 ------------------ .../Input/Styles/OnlyCtorPresetsParamTest.php | 57 ++++++++++ tests/unit/Domain/Input/Styles/ScssTest.php | 62 +++++++++++ 7 files changed, 205 insertions(+), 155 deletions(-) create mode 100644 tests/unit/Domain/Input/Styles/OnlyCtorPresetsParamTest.php create mode 100644 tests/unit/Domain/Input/Styles/ScssTest.php diff --git a/docs/02-advanced-usage.md b/docs/02-advanced-usage.md index 17b3d31d..c5a7ea97 100644 --- a/docs/02-advanced-usage.md +++ b/docs/02-advanced-usage.md @@ -325,12 +325,21 @@ You can find an implementation example in the [tests/_data/fixtures/advanced-exa ### Custom CSS for Global Styles and per Block -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` class and its `parse(string $css, string $selector = ''): string` method, developers can now seamlessly integrate custom styles without the need to remember the format to use like spaces and separators, just write your CSS as you would in a regular CSS file and let the `Css` class handle the rest. +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` 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. +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: @@ -344,16 +353,18 @@ echo (new Css($presets))->parse('.test-selector{height: 100%;}', '.test-selector 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. +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. -Let's see in action: +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)) + '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'), ], ]; @@ -368,7 +379,7 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; SectionNames::STYLES => [ 'blocks' => [ 'my-namespace/test-block' => [ - 'css' => $container->get(Css::class) // Or (new Css($presets)) + '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'), ], ], @@ -376,7 +387,7 @@ use ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles\Css; ]; ``` -All methods also support a special syntax to find value in the `$presets` collection, `{{type.slug}}`, this syntax will be used internally to find the value in the `$presets` collection. +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 diff --git a/src/Domain/Input/Styles/Css.php b/src/Domain/Input/Styles/Css.php index 7bdb5b6a..16bbfc6e 100644 --- a/src/Domain/Input/Styles/Css.php +++ b/src/Domain/Input/Styles/Css.php @@ -9,17 +9,13 @@ 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 @@ -30,6 +26,7 @@ class Css private PresetsInterface $presets; private bool $isCompressed = true; + private bool $shouldResolveVariables = true; public function __construct( PresetsInterface $presets = null @@ -43,42 +40,10 @@ public function expanded(): self return $this; } - /** - * @deprecated Use parse() instead - */ - public function parseString(string $css, string $selector = ''): string + public function stopResolveVariables(): self { - if (\str_starts_with(\trim($css), '&')) { - throw new \RuntimeException(self::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); - } - - $css = $this->presets->parse($css); - $css = $this->duplicateRulesForSelectorList($css); - - if ($selector === '') { - return $css; - } - - $exploded = \explode($selector, $css); - - $rootRule = []; - $explodedNew = []; - foreach ($exploded as $key => $value) { - // @todo remove after PHP >= 8 - // phpcs:disable - if (\str_starts_with(\trim($value), '{')) { - $value = \str_replace(['{', '}'], '', \rtrim($value)); - $value = \preg_replace('/^ +/m', '', $value); - $rootRule[] = $value; - continue; - } - - // phpcs:enable - - $explodedNew[$key] = $value; - } - - return \ltrim(\implode('', $rootRule) . \implode('&', $explodedNew), "\t\n\r\0\x0B&"); + $this->shouldResolveVariables = false; + return $this; } /** @@ -90,7 +55,10 @@ public function parse(string $css, string $selector = ''): string throw new \RuntimeException(self::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); } - $css = $this->presets->parse($css); + if ($this->shouldResolveVariables) { + $css = $this->presets->parse($css); + } + $selector = \trim($selector); if ($selector === '') { @@ -111,7 +79,7 @@ public function parse(string $css, string $selector = ''): string foreach ($doc->getAllDeclarationBlocks() as $declarationBlock) { foreach ($declarationBlock->getSelectors() as $cssSelector) { if (\is_string($cssSelector)) { - $cssSelector = new \Sabberworm\CSS\Property\Selector($cssSelector); + $cssSelector = new Selector($cssSelector); } if ($cssSelector->getSelector() === $selector) { @@ -139,6 +107,44 @@ public function parse(string $css, string $selector = ''): string 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(self::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + } + + $css = $this->presets->parse($css); + $css = $this->duplicateRulesForSelectorList($css); + + if ($selector === '') { + return $css; + } + + $exploded = \explode($selector, $css); + + $rootRule = []; + $explodedNew = []; + foreach ($exploded as $key => $value) { + // @todo remove after PHP >= 8 + // phpcs:disable + if (\str_starts_with(\trim($value), '{')) { + $value = \str_replace(['{', '}'], '', \rtrim($value)); + $value = \preg_replace('/^ +/m', '', $value); + $rootRule[] = $value; + continue; + } + + // phpcs:enable + + $explodedNew[$key] = $value; + } + + 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. diff --git a/src/Domain/Input/Styles/Scss.php b/src/Domain/Input/Styles/Scss.php index 39d792e3..553f05ff 100644 --- a/src/Domain/Input/Styles/Scss.php +++ b/src/Domain/Input/Styles/Scss.php @@ -4,16 +4,21 @@ namespace ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles; +use ItalyStrap\Tests\Unit\Domain\Input\Styles\ScssTest; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\NullPresets; +use ItalyStrap\ThemeJsonGenerator\Domain\Input\Settings\PresetsInterface; use ScssPhp\ScssPhp\Compiler; use ScssPhp\ScssPhp\OutputStyle; /** * @psalm-api + * @see ScssTest */ class Scss { private Css $css; private Compiler $compiler; + private PresetsInterface $presets; /** * @var 'compressed'|'expanded' @@ -22,10 +27,12 @@ class Scss public function __construct( Css $css, - Compiler $compiler + Compiler $compiler, + PresetsInterface $presets = null ) { $this->css = $css; $this->compiler = $compiler; + $this->presets = $presets ?? new NullPresets(); } public function expanded(): self @@ -39,6 +46,9 @@ public function parse(string $scss, string $selector = ''): string { $this->compiler->setOutputStyle($this->outputStyle); + $this->css->stopResolveVariables(); + $scss = $this->presets->parse($scss); + $css = $this->compiler->compileString($scss); return $this->css->parse($css->getCss(), $selector); diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 8a929801..0c2d922e 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -18,6 +18,7 @@ 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; @@ -162,6 +163,13 @@ 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() { @@ -183,6 +191,7 @@ protected function _before() $this->filesFinder = $this->prophesize(FilesFinder::class); $this->validator = $this->prophesize(Validator::class); $this->compiler = $this->prophesize(Compiler::class); + $this->presets = $this->prophesize(PresetsInterface::class); $this->composer->getConfig()->willReturn($this->makeComposerConfig()); $this->composer->getPackage()->willReturn($this->makeRootPackage()); diff --git a/tests/unit/Domain/Input/Styles/CssTest.php b/tests/unit/Domain/Input/Styles/CssTest.php index ab4c66b6..a770d82f 100644 --- a/tests/unit/Domain/Input/Styles/CssTest.php +++ b/tests/unit/Domain/Input/Styles/CssTest.php @@ -114,33 +114,6 @@ public function testItShouldThrowErrorIfCssStartWithAmpersand(): void $this->makeInstance()->parse('& .foo{color: red;}'); } - /** - * @dataProvider styleProvider - */ - public function testItShouldCompileExpanded(string $selector, string $css, string $expected): void - { - $this->expandedCompiler($css, 'expanded'); - } - - /** - * @dataProvider styleProvider - */ - public function testItShouldCompileCompressed(string $selector, string $css, string $expected): void - { - $this->expandedCompiler($css, 'compressed'); - } - - private function expandedCompiler(string $css, string $style): void - { - $compiler = new Compiler(); - $compiler->setOutputStyle($style); - - $result = $compiler->compileString($css); - - $actual = $this->makeInstance()->parse($result->getCss(), '.test-selector'); - $this->assertTrue(true, 'Let this test pass, is a check for the compiler'); - } - public static function newStyleProvider(): iterable { foreach (self::newStyleProviderTrait() as $key => $value) { @@ -171,82 +144,4 @@ public function testItShouldParseWithNewMethod(string $selector, string $actual, $parseString = $this->makeInstance()->parse($actual, $selector); $this->assertSame($expected, $parseString, 'The parsed string is not the same as expected'); } - - public function testCssParser(): void - { - $css = <<makeInstance(); -// codecept_debug($sut->parse($css, $selector)); - } - - public function testScssParser(): void - { - $css = <<makeInstance(); -// codecept_debug($sut->parse($css, $selector)); - -// $compiler = new Compiler(); -// $compiler->setOutputStyle('expanded'); -// -// $result = $compiler->compileString($css); -// -// codecept_debug($result->getCss()); -// -// $actual = $this->makeInstance()->expanded()->parse($result->getCss(), '.wp-block-query-pagination'); -// codecept_debug($actual); - - $scss = new Scss($sut, new Compiler()); - $result = $scss->expanded()->parse($css, $selector); -// codecept_debug($result); - } } 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..fa83e9d5 --- /dev/null +++ b/tests/unit/Domain/Input/Styles/ScssTest.php @@ -0,0 +1,62 @@ +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;}', + ]; + } + + /** + * @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'); + } +} From 2119d27767276fe2da2acc6b355f3630390da4ef Mon Sep 17 00:00:00 2001 From: Enea Date: Tue, 23 Apr 2024 07:56:59 +0200 Subject: [PATCH 6/8] update symfony/event-dispatcher and webmozart/assert version --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 33298029..74715742 100644 --- a/composer.json +++ b/composer.json @@ -35,8 +35,8 @@ "webimpress/safe-writer": "^2.2", "symfony/polyfill-php80": "^1.22", - "symfony/event-dispatcher": "*", - "webmozart/assert": "*" + "symfony/event-dispatcher": "^5.4", + "webmozart/assert": "^1.11" }, "require-dev": { "lucatume/wp-browser": "^3.1", From 5dee36d012a62e5bd97ed8242b42c35eb206d585 Mon Sep 17 00:00:00 2001 From: Enea Date: Thu, 9 May 2024 07:48:01 +0200 Subject: [PATCH 7/8] makes Scss parser more robust and add new test scenario for ScssTest --- src/Domain/Input/Styles/Css.php | 8 +++----- src/Domain/Input/Styles/CssInterface.php | 14 ++++++++++++++ src/Domain/Input/Styles/Scss.php | 13 +++++++++++-- tests/unit/Domain/Input/Styles/CssTest.php | 3 ++- tests/unit/Domain/Input/Styles/ScssTest.php | 13 +++++++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/Domain/Input/Styles/CssInterface.php diff --git a/src/Domain/Input/Styles/Css.php b/src/Domain/Input/Styles/Css.php index 16bbfc6e..e2207566 100644 --- a/src/Domain/Input/Styles/Css.php +++ b/src/Domain/Input/Styles/Css.php @@ -20,10 +20,8 @@ * @psalm-api * @see CssTest */ -class Css +class Css implements CssInterface { - public const M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING = 'CSS cannot begin with an ampersand (&)'; - private PresetsInterface $presets; private bool $isCompressed = true; private bool $shouldResolveVariables = true; @@ -52,7 +50,7 @@ public function stopResolveVariables(): self public function parse(string $css, string $selector = ''): string { if (\str_starts_with(\trim($css), '&')) { - throw new \RuntimeException(self::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + throw new \RuntimeException(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); } if ($this->shouldResolveVariables) { @@ -113,7 +111,7 @@ public function parse(string $css, string $selector = ''): string public function parseString(string $css, string $selector = ''): string { if (\str_starts_with(\trim($css), '&')) { - throw new \RuntimeException(self::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + throw new \RuntimeException(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); } $css = $this->presets->parse($css); diff --git a/src/Domain/Input/Styles/CssInterface.php b/src/Domain/Input/Styles/CssInterface.php new file mode 100644 index 00000000..ac3405a4 --- /dev/null +++ b/src/Domain/Input/Styles/CssInterface.php @@ -0,0 +1,14 @@ +compiler->setOutputStyle($this->outputStyle); + if (\str_starts_with(\trim($scss), '&')) { + throw new \RuntimeException(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + } $this->css->stopResolveVariables(); $scss = $this->presets->parse($scss); + $selector = \trim($selector); + + if ($selector === '') { + return $scss; + } + + $this->compiler->setOutputStyle($this->outputStyle); $css = $this->compiler->compileString($scss); return $this->css->parse($css->getCss(), $selector); diff --git a/tests/unit/Domain/Input/Styles/CssTest.php b/tests/unit/Domain/Input/Styles/CssTest.php index a770d82f..40ccab00 100644 --- a/tests/unit/Domain/Input/Styles/CssTest.php +++ b/tests/unit/Domain/Input/Styles/CssTest.php @@ -7,6 +7,7 @@ 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; @@ -109,7 +110,7 @@ public function testItShouldParse(string $selector, string $actual, string $expe public function testItShouldThrowErrorIfCssStartWithAmpersand(): void { $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage(Css::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); + $this->expectExceptionMessage(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); $this->makeInstance()->parse('& .foo{color: red;}'); } diff --git a/tests/unit/Domain/Input/Styles/ScssTest.php b/tests/unit/Domain/Input/Styles/ScssTest.php index fa83e9d5..7bb65fca 100644 --- a/tests/unit/Domain/Input/Styles/ScssTest.php +++ b/tests/unit/Domain/Input/Styles/ScssTest.php @@ -48,6 +48,19 @@ public static function newStyleProvider(): iterable CSS, 'expected' => '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;}', + ]; } /** From d488bb910ccd8bae6a3e0947ce9f745f4649a143 Mon Sep 17 00:00:00 2001 From: Enea Date: Thu, 9 May 2024 07:58:14 +0200 Subject: [PATCH 8/8] Psalm fix --- src/Domain/Input/Styles/CssInterface.php | 3 +++ src/Domain/Input/Styles/Scss.php | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Domain/Input/Styles/CssInterface.php b/src/Domain/Input/Styles/CssInterface.php index ac3405a4..ec55bbf5 100644 --- a/src/Domain/Input/Styles/CssInterface.php +++ b/src/Domain/Input/Styles/CssInterface.php @@ -4,6 +4,9 @@ namespace ItalyStrap\ThemeJsonGenerator\Domain\Input\Styles; +/** + * @psalm-api + */ interface CssInterface { public const M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING = 'CSS cannot begin with an ampersand (&)'; diff --git a/src/Domain/Input/Styles/Scss.php b/src/Domain/Input/Styles/Scss.php index 60c88aca..3f022e7b 100644 --- a/src/Domain/Input/Styles/Scss.php +++ b/src/Domain/Input/Styles/Scss.php @@ -42,24 +42,24 @@ public function expanded(): self return $this; } - public function parse(string $scss, string $selector = ''): string + public function parse(string $css, string $selector = ''): string { - if (\str_starts_with(\trim($scss), '&')) { + if (\str_starts_with(\trim($css), '&')) { throw new \RuntimeException(CssInterface::M_AMPERSAND_MUST_NOT_BE_AT_THE_BEGINNING); } $this->css->stopResolveVariables(); - $scss = $this->presets->parse($scss); + $css = $this->presets->parse($css); $selector = \trim($selector); if ($selector === '') { - return $scss; + return $css; } $this->compiler->setOutputStyle($this->outputStyle); - $css = $this->compiler->compileString($scss); + $cssCompiled = $this->compiler->compileString($css); - return $this->css->parse($css->getCss(), $selector); + return $this->css->parse($cssCompiled->getCss(), $selector); } }