From 32cdde1d079a21a2faf52a993cd5eb55a56658b2 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Tue, 14 May 2024 18:13:06 +0200 Subject: [PATCH 1/4] Highlight PHP attributes --- src/Renderers/CodeNodeRenderer.php | 60 +++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Renderers/CodeNodeRenderer.php b/src/Renderers/CodeNodeRenderer.php index cfa46a1..00a080e 100644 --- a/src/Renderers/CodeNodeRenderer.php +++ b/src/Renderers/CodeNodeRenderer.php @@ -70,9 +70,10 @@ public function render(): string $highLighter = new Highlighter(); $highlightedCode = $highLighter->highlight($languageMapping, $code)->value; + } - // this allows to highlight the $ in PHP variable names - $highlightedCode = str_replace('$', '$', $highlightedCode); + if ('php' === $language) { + $highlightedCode = $this->processHighlightedPhpCode($highlightedCode); } if ('terminal' === $language) { @@ -139,4 +140,59 @@ private function escapeForbiddenCharactersInsideCodeBlock(string $code): string return strtr($codeEscaped, ['<' => '<', '>' => '>', '"' => '"']); } + + private function processHighlightedPhpCode(string $highlightedCode): string + { + // this allows to highlight the $ in PHP variable names + $highlightedCode = str_replace('$', '$', $highlightedCode); + + // this highlights PHP attributes, which can be defined in many different ways: + // + // #[AttributeName] + // #[AttributeName()] + // #[AttributeName('value')] + // #[AttributeName('value', option: 'value')] + // #[AttributeName(['value' => 'value'])] + // #[AttributeName( + // 'value', + // option: 'value' + // )] + // + // The attribute name is mandatory, but the parentheses and the arguments are optional. + $highlightedCode = preg_replace_callback( + // '/#\[\s*((?[a-zA-Z_\\\\][\w\\\\]*)(?\((?:[^\(\)]*|\((?:[^\(\)]*|\([^)]*\))*\))*\))?)\s*\]<\/span>/', + '/#\[\s*(?[a-zA-Z_\\\\][\w\\\\]*)(?\(.*\))?\s*\]<\/span>/', + static function (array $matches) { + $attributeName = $matches['name']; + $attributeArguments = $matches['arguments'] ?? ''; + + if ('' === $attributeArguments) { + return sprintf('#[%s]', $attributeName); + } + + // the tricky part is to highlight the values and options; so we + // use the highlighter to highlight the whole attribute wrapped with + // some contents to make it valid PHP code + // Original string to highlight: AttributeName('value', option: 'value') + // String passed to highlight: $attribute = new AttributeName('value', option: 'value'); + // After highlighting, we remove the `$attribute = new ` prefix and the trailing `;` + $highlighter = new Highlighter(); + $highlightedAttribute = $highlighter->highlight('php', sprintf('$attribute = new %s%s;', $attributeName, $attributeArguments))->value; + $highlightedAttribute = preg_replace('/^\$attribute<\/span> = new<\/span> (.*);$/', '$1', $highlightedAttribute); + + // $highlightedAttribute is like 'Route('/posts/{id}')' + // remove the attribute name and the parenthesis from the highlighted code + $highlightedAttribute = preg_replace( + sprintf('/^%s\((.*)\)$/', preg_quote($attributeName, '/')), + '$1', + $highlightedAttribute + ); +echo sprintf('#[%s(%s)]', $attributeName, $highlightedAttribute)."\n\n"; + return sprintf('#[%s%s]', $attributeName, $highlightedAttribute); + }, + $highlightedCode + ); + + return $highlightedCode; + } } From d75a0ff09cc57883358129696979b018a3ced732 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 15 May 2024 15:03:40 +0200 Subject: [PATCH 2/4] Highlight PHP attributes --- src/Renderers/CodeNodeRenderer.php | 61 +++++--- tests/IntegrationTest.php | 4 + .../blocks/code-blocks/php-attributes.html | 136 ++++++++++++++++++ .../blocks/code-blocks/php-attributes.rst | 48 +++++++ 4 files changed, 229 insertions(+), 20 deletions(-) create mode 100644 tests/fixtures/expected/blocks/code-blocks/php-attributes.html create mode 100644 tests/fixtures/source/blocks/code-blocks/php-attributes.rst diff --git a/src/Renderers/CodeNodeRenderer.php b/src/Renderers/CodeNodeRenderer.php index 00a080e..a62b042 100644 --- a/src/Renderers/CodeNodeRenderer.php +++ b/src/Renderers/CodeNodeRenderer.php @@ -59,8 +59,8 @@ public function render(): string } $language = $this->codeNode->getLanguage() ?? 'php'; - $languageMapping = self::LANGUAGES_MAPPING[$language] ?? $language; - $languages = array_unique([$language, $languageMapping]); + $highlightingLanguage = self::LANGUAGES_MAPPING[$language] ?? $language; + $languages = array_unique([$language, $highlightingLanguage]); if ('text' === $language) { // Highlighter escapes correctly the code, we need to manually escape only for "text" code @@ -69,10 +69,10 @@ public function render(): string $this->configureHighlighter(); $highLighter = new Highlighter(); - $highlightedCode = $highLighter->highlight($languageMapping, $code)->value; + $highlightedCode = $highLighter->highlight($highlightingLanguage, $code)->value; } - if ('php' === $language) { + if ('php' === $highlightingLanguage) { $highlightedCode = $this->processHighlightedPhpCode($highlightedCode); } @@ -146,6 +146,10 @@ private function processHighlightedPhpCode(string $highlightedCode): string // this allows to highlight the $ in PHP variable names $highlightedCode = str_replace('$', '$', $highlightedCode); + if (!str_contains($highlightedCode, '#[')) { + return $highlightedCode; + } + // this highlights PHP attributes, which can be defined in many different ways: // // #[AttributeName] @@ -160,35 +164,52 @@ private function processHighlightedPhpCode(string $highlightedCode): string // // The attribute name is mandatory, but the parentheses and the arguments are optional. $highlightedCode = preg_replace_callback( - // '/#\[\s*((?[a-zA-Z_\\\\][\w\\\\]*)(?\((?:[^\(\)]*|\((?:[^\(\)]*|\([^)]*\))*\))*\))?)\s*\]<\/span>/', - '/#\[\s*(?[a-zA-Z_\\\\][\w\\\\]*)(?\(.*\))?\s*\]<\/span>/', + '/#\[\s*(?[a-zA-Z_\\\\][\w\\\\]*)(?\(.*\))?\s*\]/Us', static function (array $matches) { $attributeName = $matches['name']; $attributeArguments = $matches['arguments'] ?? ''; if ('' === $attributeArguments) { - return sprintf('#[%s]', $attributeName); + return sprintf('#[%s]', $attributeName); } // the tricky part is to highlight the values and options; so we // use the highlighter to highlight the whole attribute wrapped with // some contents to make it valid PHP code // Original string to highlight: AttributeName('value', option: 'value') - // String passed to highlight: $attribute = new AttributeName('value', option: 'value'); + // String passed to highlighter: $attribute = new AttributeName('value', option: 'value'); // After highlighting, we remove the `$attribute = new ` prefix and the trailing `;` $highlighter = new Highlighter(); - $highlightedAttribute = $highlighter->highlight('php', sprintf('$attribute = new %s%s;', $attributeName, $attributeArguments))->value; - $highlightedAttribute = preg_replace('/^\$attribute<\/span> = new<\/span> (.*);$/', '$1', $highlightedAttribute); - - // $highlightedAttribute is like 'Route('/posts/{id}')' - // remove the attribute name and the parenthesis from the highlighted code - $highlightedAttribute = preg_replace( - sprintf('/^%s\((.*)\)$/', preg_quote($attributeName, '/')), - '$1', - $highlightedAttribute - ); -echo sprintf('#[%s(%s)]', $attributeName, $highlightedAttribute)."\n\n"; - return sprintf('#[%s%s]', $attributeName, $highlightedAttribute); + + // this is needed because when using 'class' as the name of an attribute argument, the highlighter + // confuses it for a new class instantiation and highlights it as such + $attributeArguments = str_replace('class:', 'klass:', $attributeArguments); + + // this happens in multiline attributes, where the highlighter already highlighted each line of the attribute (except the attribute name) + if (str_contains($attributeArguments, '(.*)\)$/s', '$1', $attributeArguments); + } else { + $highlightedAttribute = $highlighter->highlight('php', sprintf('$hljsAttribute = new %s%s;', $attributeName, $attributeArguments))->value; + $highlightedAttribute = preg_replace('/^\$hljsAttribute<\/span> = new<\/span> (.*);$/', '$1', $highlightedAttribute); + + // fix the double transformation of < to &< and > to &> caused by the highlighter + $highlightedAttribute = str_replace('&lt;', '<', $highlightedAttribute); + $highlightedAttribute = str_replace('&gt;', '>', $highlightedAttribute); + + // $highlightedAttribute is like 'Route('/posts/{id}')' + // remove the attribute name and the parenthesis from the highlighted code + $highlightedAttribute = substr( + $highlightedAttribute, + strlen($attributeName) + 1, + -1 + ); + } + + // reverse the previous change needed to avoid highlighting 'class' as a new class instantiation + $highlightedAttribute = str_replace('klass:', 'class:', $highlightedAttribute); + + return sprintf('#[%s(%s)]', $attributeName, $highlightedAttribute); }, $highlightedCode ); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index cf34cde..69b3681 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -290,6 +290,10 @@ public function parserUnitBlockProvider() 'blockName' => 'code-blocks/php-annotations', ]; + yield 'code-block-php-attributes' => [ + 'blockName' => 'code-blocks/php-attributes', + ]; + yield 'code-block-text' => [ 'blockName' => 'code-blocks/text', ]; diff --git a/tests/fixtures/expected/blocks/code-blocks/php-attributes.html b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html new file mode 100644 index 0000000..e1ca366 --- /dev/null +++ b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html @@ -0,0 +1,136 @@ +
+
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+
+            
+                // src/SomePath/SomeClass.php
+namespace App\SomePath;
+                useSymfony\Component\Validator\Constraints as Assert;
+                
+                    class
+                    SomeClass
+                
+                {
+                #[AttributeName]
+                private
+                
+                    $ property1
+                ;
+                #[AttributeName(
+                )]
+                private
+                
+                    $ property2
+                ;
+                #[AttributeName(
+                'value'
+                )]
+                private
+                
+                    $ property3
+                ;
+                #[AttributeName(
+                'value'
+                , option:
+                'value'
+                )]
+                private
+                
+                    $ property4
+                ;
+                #[AttributeName(
+['value' => 'value'])]
+                private
+                
+                    $ property5
+                ;
+                #[AttributeName(
+                'value'
+                , option:
+                'value'
+                )]
+                private
+                
+                    $ property6
+                ;
+                #[Assert\AttributeName(
+                'value'
+                )]
+                private
+                
+                    $ property7
+                ;
+                #[Assert\AttributeName(
+                'value'
+                , option:
+                'value'
+                )]
+                private
+                
+                    $ property8
+                ;
+                #[Route(
+                '/blog/{page<\d+>}'
+                , name:
+                'blog_list'
+                )]
+                private
+                
+                $ property9
+                ;
+                #[Assert\GreaterThanOrEqual(
+                value:
+                18
+                ,
+                )]
+                private
+                
+                $ property10
+                ;
+}
+
+
diff --git a/tests/fixtures/source/blocks/code-blocks/php-attributes.rst b/tests/fixtures/source/blocks/code-blocks/php-attributes.rst new file mode 100644 index 0000000..658333c --- /dev/null +++ b/tests/fixtures/source/blocks/code-blocks/php-attributes.rst @@ -0,0 +1,48 @@ +.. code-block:: php-attributes + + // src/SomePath/SomeClass.php + namespace App\SomePath; + + use Symfony\Component\Validator\Constraints as Assert; + + class SomeClass + { + #[AttributeName] + private $property1; + + #[AttributeName()] + private $property2; + + #[AttributeName('value')] + private $property3; + + #[AttributeName('value', option: 'value')] + private $property4; + + #[AttributeName(['value' => 'value'])] + private $property5; + + #[AttributeName( + 'value', + option: 'value' + )] + private $property6; + + #[Assert\AttributeName('value')] + private $property7; + + #[Assert\AttributeName( + 'value', + option: 'value' + )] + private $property8; + + #[Route('/blog/{page<\d+>}', name: 'blog_list')] + private $property9; + + #[Assert\GreaterThanOrEqual( + value: 18, + )] + private $property10; + } + From 6b633b2c09550a92b1d2bd021011194434e821cd Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 15 May 2024 15:15:15 +0200 Subject: [PATCH 3/4] More tests --- src/Renderers/CodeNodeRenderer.php | 23 ++++++++----------- .../blocks/code-blocks/php-attributes.html | 15 ++++++++++-- .../blocks/code-blocks/php-attributes.rst | 3 +++ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Renderers/CodeNodeRenderer.php b/src/Renderers/CodeNodeRenderer.php index a62b042..fc674bd 100644 --- a/src/Renderers/CodeNodeRenderer.php +++ b/src/Renderers/CodeNodeRenderer.php @@ -146,6 +146,7 @@ private function processHighlightedPhpCode(string $highlightedCode): string // this allows to highlight the $ in PHP variable names $highlightedCode = str_replace('$', '$', $highlightedCode); + // the rest of this method highlights PHP attributes, so if we can't find this pattern, return early if (!str_contains($highlightedCode, '#[')) { return $highlightedCode; } @@ -173,16 +174,10 @@ static function (array $matches) { return sprintf('#[%s]', $attributeName); } - // the tricky part is to highlight the values and options; so we - // use the highlighter to highlight the whole attribute wrapped with - // some contents to make it valid PHP code - // Original string to highlight: AttributeName('value', option: 'value') - // String passed to highlighter: $attribute = new AttributeName('value', option: 'value'); - // After highlighting, we remove the `$attribute = new ` prefix and the trailing `;` $highlighter = new Highlighter(); // this is needed because when using 'class' as the name of an attribute argument, the highlighter - // confuses it for a new class instantiation and highlights it as such + // confuses it for a new class instantiation and highlights it as such (this is later reverted) $attributeArguments = str_replace('class:', 'klass:', $attributeArguments); // this happens in multiline attributes, where the highlighter already highlighted each line of the attribute (except the attribute name) @@ -190,20 +185,22 @@ static function (array $matches) { // don't trim the result to keep the leading and trailing \n $highlightedAttribute = preg_replace('/\(<\/span>(.*)\)$/s', '$1', $attributeArguments); } else { + // the tricky part is to highlight the values and options; so we + // use the highlighter to highlight the whole attribute wrapped with + // some contents to make it valid PHP code: + // Original string to highlight: AttributeName('value', option: 'value') + // String passed to highlighter: $hljsAttribute = new AttributeName('value', option: 'value'); + // After highlighting, we remove the `$hljsAttribute = new ` prefix and the trailing `;` $highlightedAttribute = $highlighter->highlight('php', sprintf('$hljsAttribute = new %s%s;', $attributeName, $attributeArguments))->value; $highlightedAttribute = preg_replace('/^\$hljsAttribute<\/span> = new<\/span> (.*);$/', '$1', $highlightedAttribute); - // fix the double transformation of < to &< and > to &> caused by the highlighter + // fix the transformation of < to &< and > to &> caused by the highlighter $highlightedAttribute = str_replace('&lt;', '<', $highlightedAttribute); $highlightedAttribute = str_replace('&gt;', '>', $highlightedAttribute); // $highlightedAttribute is like 'Route('/posts/{id}')' // remove the attribute name and the parenthesis from the highlighted code - $highlightedAttribute = substr( - $highlightedAttribute, - strlen($attributeName) + 1, - -1 - ); + $highlightedAttribute = substr($highlightedAttribute, strlen($attributeName) + 1, -1); } // reverse the previous change needed to avoid highlighting 'class' as a new class instantiation diff --git a/tests/fixtures/expected/blocks/code-blocks/php-attributes.html b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html index e1ca366..5b73756 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php-attributes.html +++ b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html @@ -1,4 +1,4 @@ -
+
1
 2
@@ -44,7 +44,10 @@
 42
 43
 44
-45
+45 +46 +47 +48
             
                 // src/SomePath/SomeClass.php
@@ -131,6 +134,14 @@
                 
                 $ property10
                 ;
+                #[ORM\CustomIdGenerator(
+                class:
+                'doctrine.uuid_generator'
+                )]
+                private
+                
+                $ property11
+                ;
 }
diff --git a/tests/fixtures/source/blocks/code-blocks/php-attributes.rst b/tests/fixtures/source/blocks/code-blocks/php-attributes.rst index 658333c..fa69511 100644 --- a/tests/fixtures/source/blocks/code-blocks/php-attributes.rst +++ b/tests/fixtures/source/blocks/code-blocks/php-attributes.rst @@ -44,5 +44,8 @@ value: 18, )] private $property10; + + #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] + private $property11; } From 9bcaad1978069275a7624773d64fe8a6049928da Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 15 May 2024 15:16:18 +0200 Subject: [PATCH 4/4] - --- .../blocks/code-blocks/php-attributes.rst | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/fixtures/source/blocks/code-blocks/php-attributes.rst b/tests/fixtures/source/blocks/code-blocks/php-attributes.rst index fa69511..d43f8ad 100644 --- a/tests/fixtures/source/blocks/code-blocks/php-attributes.rst +++ b/tests/fixtures/source/blocks/code-blocks/php-attributes.rst @@ -31,21 +31,21 @@ #[Assert\AttributeName('value')] private $property7; - #[Assert\AttributeName( + #[Assert\AttributeName( 'value', option: 'value' - )] - private $property8; + )] + private $property8; - #[Route('/blog/{page<\d+>}', name: 'blog_list')] - private $property9; + #[Route('/blog/{page<\d+>}', name: 'blog_list')] + private $property9; - #[Assert\GreaterThanOrEqual( - value: 18, - )] - private $property10; + #[Assert\GreaterThanOrEqual( + value: 18, + )] + private $property10; - #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] - private $property11; + #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] + private $property11; }