Skip to content

Highlight PHP attributes #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions src/Renderers/CodeNodeRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -69,10 +69,11 @@ public function render(): string
$this->configureHighlighter();

$highLighter = new Highlighter();
$highlightedCode = $highLighter->highlight($languageMapping, $code)->value;
$highlightedCode = $highLighter->highlight($highlightingLanguage, $code)->value;
}

// this allows to highlight the $ in PHP variable names
$highlightedCode = str_replace('<span class="hljs-variable">$', '<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>', $highlightedCode);
if ('php' === $highlightingLanguage) {
$highlightedCode = $this->processHighlightedPhpCode($highlightedCode);
}

if ('terminal' === $language) {
Expand Down Expand Up @@ -139,4 +140,77 @@ private function escapeForbiddenCharactersInsideCodeBlock(string $code): string

return strtr($codeEscaped, ['<' => '&lt;', '>' => '&gt;', '"' => '&quot;']);
}

private function processHighlightedPhpCode(string $highlightedCode): string
{
// this allows to highlight the $ in PHP variable names
$highlightedCode = str_replace('<span class="hljs-variable">$', '<span class="hljs-variable"><span class="hljs-variable-other-marker">$</span>', $highlightedCode);

// the rest of this method highlights PHP attributes, so if we can't find this pattern, return early
if (!str_contains($highlightedCode, '<span class="hljs-comment">#[')) {
return $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(
'/<span class="hljs-comment">#\[\s*(?<name>[a-zA-Z_\\\\][\w\\\\]*)(?<arguments>\(.*\))?\s*\]/Us',
static function (array $matches) {
$attributeName = $matches['name'];
$attributeArguments = $matches['arguments'] ?? '';

if ('' === $attributeArguments) {
return sprintf('<span class="hljs-php-attribute">#[%s]</span>', $attributeName);
}

$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 (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)
if (str_contains($attributeArguments, '<span class="hljs-string') || str_contains($attributeArguments, '<span class="hljs-number')) {
// 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('/^<span class="hljs-variable">\$hljsAttribute<\/span> = <span class="hljs-keyword">new<\/span> (.*);$/', '$1', $highlightedAttribute);

// fix the transformation of < to &amp;&lt; and > to &amp;&gt; caused by the highlighter
$highlightedAttribute = str_replace('&amp;lt;', '&lt;', $highlightedAttribute);
$highlightedAttribute = str_replace('&amp;gt;', '&gt;', $highlightedAttribute);

// $highlightedAttribute is like 'Route(<span class="hljs-string">'/posts/{id}'</span>)'
// 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('<span class="hljs-php-attribute">#[%s(</span>%s<span class="hljs-php-attribute">)]</span>', $attributeName, $highlightedAttribute);
},
$highlightedCode
);

return $highlightedCode;
}
}
4 changes: 4 additions & 0 deletions tests/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
Expand Down
147 changes: 147 additions & 0 deletions tests/fixtures/expected/blocks/code-blocks/php-attributes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<div translate="no" data-loc="48" class="notranslate codeblock codeblock-length-md codeblock-php-attributes codeblock-php">
<div class="codeblock-scroll">
<pre class="codeblock-lines">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
46
47
48</pre>
<pre class="codeblock-code">
<code>
<span class="hljs-comment">// src/SomePath/SomeClass.php</span>
<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">SomePath</span>;
<span class="hljs-keyword">use</span><span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">Validator</span>\<span class="hljs-title">Constraints</span> <span class="hljs-title">as</span> <span class="hljs-title">Assert</span>;
<span class="hljs-class">
<span class="hljs-keyword">class</span>
<span class="hljs-title">SomeClass</span>
</span>
{
<span class="hljs-php-attribute">#[AttributeName]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property1</span>
;
<span class="hljs-php-attribute">#[AttributeName(</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property2</span>
;
<span class="hljs-php-attribute">#[AttributeName(</span>
<span class="hljs-string">'value'</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property3</span>
;
<span class="hljs-php-attribute">#[AttributeName(</span>
<span class="hljs-string">'value'</span>
, option:
<span class="hljs-string">'value'</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property4</span>
;
<span class="hljs-php-attribute">#[AttributeName(</span>
[<span class="hljs-string">'value'</span> =&gt; <span class="hljs-string">'value'</span>]<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property5</span>
;
<span class="hljs-php-attribute">#[AttributeName(</span>
<span class="hljs-string">'value'</span>
, option:
<span class="hljs-string">'value'</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property6</span>
;
<span class="hljs-php-attribute">#[Assert\AttributeName(</span>
<span class="hljs-string">'value'</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property7</span>
;
<span class="hljs-php-attribute">#[Assert\AttributeName(</span>
<span class="hljs-string">'value'</span>
, option:
<span class="hljs-string">'value'</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property8</span>
;
<span class="hljs-php-attribute">#[Route(</span>
<span class="hljs-string">'/blog/{page&lt;\d+&gt;}'</span>
, name:
<span class="hljs-string">'blog_list'</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property9</span>
;
<span class="hljs-php-attribute">#[Assert\GreaterThanOrEqual(</span>
value:
<span class="hljs-number">18</span>
,
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property10</span>
;
<span class="hljs-php-attribute">#[ORM\CustomIdGenerator(</span>
class:
<span class="hljs-string">'doctrine.uuid_generator'</span>
<span class="hljs-php-attribute">)]</span>
<span class="hljs-keyword">private</span>
<span class="hljs-variable">
<span class="hljs-variable-other-marker">$</span> property11</span>
;
}</code></pre>
</div>
</div>
51 changes: 51 additions & 0 deletions tests/fixtures/source/blocks/code-blocks/php-attributes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.. 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;

#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private $property11;
}