From f942c09ba4363e57b92e91dc3ae8ac764f7a8833 Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Mon, 25 Aug 2025 19:26:22 +0100 Subject: [PATCH 1/4] Add support for pre/code decorations --- src/Output/Html/PendingHtmlOutput.php | 7 ++-- src/Phast/ClassList.php | 5 +++ src/Phast/Element.php | 4 ++- src/Phast/Properties.php | 2 +- .../Decorations/CodeDecoration.php | 24 ++++++++++++++ .../Decorations/DecorationTransformer.php | 32 ++++++++++++++++++- .../Decorations/PreDecoration.php | 24 ++++++++++++++ .../Decorations/DecorationTransformerTest.php | 24 ++++++++++++++ 8 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 src/Transformers/Decorations/CodeDecoration.php create mode 100644 src/Transformers/Decorations/PreDecoration.php diff --git a/src/Output/Html/PendingHtmlOutput.php b/src/Output/Html/PendingHtmlOutput.php index 2f228ec2..20941283 100644 --- a/src/Output/Html/PendingHtmlOutput.php +++ b/src/Output/Html/PendingHtmlOutput.php @@ -7,14 +7,17 @@ use Phiki\Grammar\ParsedGrammar; use Phiki\Phast\ClassList; use Phiki\Phast\Element; +use Phiki\Phast\Properties; use Phiki\Phast\Root; use Phiki\Phast\Text; use Phiki\Support\Arr; use Phiki\Theme\ParsedTheme; use Phiki\Token\HighlightedToken; use Phiki\Token\Token; +use Phiki\Transformers\Decorations\CodeDecoration; use Phiki\Transformers\Decorations\DecorationTransformer; use Phiki\Transformers\Decorations\LineDecoration; +use Phiki\Transformers\Decorations\PreDecoration; use Phiki\Transformers\Meta; use Psr\SimpleCache\CacheInterface; use Stringable; @@ -87,7 +90,7 @@ public function transformer(TransformerInterface $transformer): self return $this; } - public function decoration(LineDecoration ...$decorations): self + public function decoration(LineDecoration | PreDecoration | CodeDecoration ...$decorations): self { if (! Arr::any($this->transformers, fn (TransformerInterface $transformer) => $transformer instanceof DecorationTransformer)) { $this->transformers[] = new DecorationTransformer($this->decorations); @@ -205,7 +208,7 @@ public function __toString(): string $pre->properties->set('style', implode(';', $preStyles)); - $code = new Element('code'); + $code = new Element('code', new Properties(['class' => new ClassList])); foreach ($highlightedTokens as $index => $lineTokens) { $line = new Element('span'); diff --git a/src/Phast/ClassList.php b/src/Phast/ClassList.php index 2f3f80a7..1edb906d 100644 --- a/src/Phast/ClassList.php +++ b/src/Phast/ClassList.php @@ -45,6 +45,11 @@ public function all(): array return $this->classes; } + public function isEmpty(): bool + { + return empty($this->classes); + } + public function __toString(): string { return implode(' ', array_filter($this->classes, fn (string $class) => trim($class) !== '')); diff --git a/src/Phast/Element.php b/src/Phast/Element.php index 62543981..3bda4086 100644 --- a/src/Phast/Element.php +++ b/src/Phast/Element.php @@ -17,10 +17,12 @@ public function __construct( public function __toString(): string { + $properties = (string) $this->properties; + $element = sprintf( '<%s%s>', $this->tagName, - count($this->properties->properties) > 0 ? ' '.(string) $this->properties : '' + $properties ? ' '.$properties : '' ); foreach ($this->children as $child) { diff --git a/src/Phast/Properties.php b/src/Phast/Properties.php index 1990b604..6ed7468f 100644 --- a/src/Phast/Properties.php +++ b/src/Phast/Properties.php @@ -39,7 +39,7 @@ public function remove(string $key): self public function __toString(): string { - $properties = array_filter($this->properties, fn ($value) => (bool) $value); + $properties = array_filter($this->properties, fn ($value) => $value instanceof ClassList ? (! $value->isEmpty()) : (!! $value)); return implode(' ', array_map( fn ($key, $value) => sprintf('%s="%s"', $key, $value), diff --git a/src/Transformers/Decorations/CodeDecoration.php b/src/Transformers/Decorations/CodeDecoration.php new file mode 100644 index 00000000..85b33acd --- /dev/null +++ b/src/Transformers/Decorations/CodeDecoration.php @@ -0,0 +1,24 @@ +classes->add(...$classes); + + return $this; + } +} diff --git a/src/Transformers/Decorations/DecorationTransformer.php b/src/Transformers/Decorations/DecorationTransformer.php index 78e0229d..82cd29ec 100644 --- a/src/Transformers/Decorations/DecorationTransformer.php +++ b/src/Transformers/Decorations/DecorationTransformer.php @@ -8,15 +8,45 @@ class DecorationTransformer extends AbstractTransformer { /** - * @param array $decorations + * @param array $decorations */ public function __construct( public array &$decorations, ) {} + public function pre(Element $pre): Element + { + foreach ($this->decorations as $decoration) { + if (! $decoration instanceof PreDecoration) { + continue; + } + + $pre->properties->get('class')->add(...$decoration->classes->all()); + } + + return $pre; + } + + public function code(Element $code): Element + { + foreach ($this->decorations as $decoration) { + if (! $decoration instanceof CodeDecoration) { + continue; + } + + $code->properties->get('class')->add(...$decoration->classes->all()); + } + + return $code; + } + public function line(Element $span, array $tokens, int $index): Element { foreach ($this->decorations as $decoration) { + if (! $decoration instanceof LineDecoration) { + continue; + } + if (! $decoration->appliesToLine($index)) { continue; } diff --git a/src/Transformers/Decorations/PreDecoration.php b/src/Transformers/Decorations/PreDecoration.php new file mode 100644 index 00000000..84136ea8 --- /dev/null +++ b/src/Transformers/Decorations/PreDecoration.php @@ -0,0 +1,24 @@ +classes->add(...$classes); + + return $this; + } +} diff --git a/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php b/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php index 9a10ea3b..433d8367 100644 --- a/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php +++ b/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php @@ -4,7 +4,9 @@ use Phiki\Phast\ClassList; use Phiki\Phiki; use Phiki\Theme\Theme; +use Phiki\Transformers\Decorations\CodeDecoration; use Phiki\Transformers\Decorations\LineDecoration; +use Phiki\Transformers\Decorations\PreDecoration; it('can apply decorations to a line', function () { $output = (new Phiki) @@ -47,3 +49,25 @@ expect(substr_count($output, 'multi-line'))->toBe(3); }); + +it('can apply decorations to pre', function () { + $output = (new Phiki) + ->codeToHtml(<<<'PHP' + echo "Hello, world!"; + PHP, Grammar::Php, Theme::GithubLight) + ->decoration(new PreDecoration(new ClassList(['pre-class']))) + ->toString(); + + expect($output)->toContain('
codeToHtml(<<<'PHP'
+        echo "Hello, world!";
+        PHP, Grammar::Php, Theme::GithubLight)
+        ->decoration(new CodeDecoration(new ClassList(['code-class'])))
+        ->toString();
+
+    expect($output)->toContain('');
+});

From c6c952527fab8731f56bcc72a5d4cacdadd2934c Mon Sep 17 00:00:00 2001
From: Ryan Chandler 
Date: Mon, 25 Aug 2025 19:27:52 +0100
Subject: [PATCH 2/4] Add docs for pre/code decorations

---
 docs/decorations.mdx | 36 +++++++++++++++++++++++++++++++++---
 1 file changed, 33 insertions(+), 3 deletions(-)

diff --git a/docs/decorations.mdx b/docs/decorations.mdx
index 6c309dae..87ef6dbd 100644
--- a/docs/decorations.mdx
+++ b/docs/decorations.mdx
@@ -2,11 +2,11 @@
 title: Decorations
 ---
 
-Decorations allow you to apply custom classes to specific lines in the generated HTML. This is useful if you want to style certain regions of your code blocks differently, such as focusing attention on specific lines or adding custom colors.
+Decorations allow you to apply custom classes to specific elements in the generated HTML. This is useful if you want to style certain regions of your code blocks differently, such as focusing attention on specific lines or adding custom colors.
 
-## Usage
+## Line decorations
 
-To use decorations, you can pass a `LineDecoration` instance to the `PendingHtmlOutput::decoration()` method.
+To add additional classes to line elements, you can pass a `LineDecoration` instance to the `PendingHtmlOutput::decoration()` method.
 
 ```php
 use Phiki\Transformers\Decorations\LineDecoration;
@@ -20,6 +20,36 @@ $output = (new Phiki)
 
 This will add the `focus` class to the first line of the code block, since lines are zero-indexed.
 
+## Pre decorations
+
+To add additional classes to the `
` element that wraps the entire code block, you can pass a `PreDecoration` instance to the `PendingHtmlOutput::decoration()` method.
+
+```php
+use Phiki\Transformers\Decorations\PreDecoration;
+
+$output = (new Phiki)
+    ->codeToHtml('decoration(
+        PreDecoration::make()->class('pre-class'),
+    );
+```
+
+This will add the `pre-class` class to the `
` element.
+
+## Code decorations
+
+To add additional classes to the `` element that wraps the entire code block, you can pass a `CodeDecoration` instance to the `PendingHtmlOutput::decoration()` method.
+
+```php
+use Phiki\Transformers\Decorations\CodeDecoration;
+
+$output = (new Phiki)
+    ->codeToHtml('decoration(
+        CodeDecoration::make()->class('code-class'),
+    );
+```
+
 ## Substring decorations
 
 At the time of writing, you can only decorate entire lines. 

From aeb3017963cba079b1b660951ee1db7930eb5265 Mon Sep 17 00:00:00 2001
From: Ryan Chandler 
Date: Mon, 25 Aug 2025 19:30:00 +0100
Subject: [PATCH 3/4] Support transforming line numbers/gutter elements

---
 src/Contracts/TransformerInterface.php   | 5 +++++
 src/Output/Html/PendingHtmlOutput.php    | 7 +++++--
 src/Transformers/AbstractTransformer.php | 8 ++++++++
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/src/Contracts/TransformerInterface.php b/src/Contracts/TransformerInterface.php
index d4823af5..38f46449 100644
--- a/src/Contracts/TransformerInterface.php
+++ b/src/Contracts/TransformerInterface.php
@@ -56,6 +56,11 @@ public function line(Element $span, array $line, int $index): Element;
      */
     public function token(Element $span, HighlightedToken $token, int $index, int $line): Element;
 
+    /**
+     * Modify the  for each gutter element.
+     */
+    public function gutter(Element $span, int $lineNumber): Element;
+
     /**
      * Modify the HTML output after the AST has been converted.
      */
diff --git a/src/Output/Html/PendingHtmlOutput.php b/src/Output/Html/PendingHtmlOutput.php
index 20941283..0922cc67 100644
--- a/src/Output/Html/PendingHtmlOutput.php
+++ b/src/Output/Html/PendingHtmlOutput.php
@@ -215,8 +215,7 @@ public function __toString(): string
             $line->properties->set('class', new ClassList(['line']));
 
             if ($this->withGutter) {
-                $line->children[] = $gutter = new Element('span');
-
+                $gutter = new Element('span');
                 $gutter->properties->set('class', new ClassList(['line-number']));
 
                 $lineNumberColor = $this->getDefaultTheme()->colors['editorLineNumber.foreground'] ?? null;
@@ -227,6 +226,10 @@ public function __toString(): string
                 ])));
 
                 $gutter->children[] = new Text(sprintf('%2d', $this->startingLineNumber + $index));
+                
+                [$gutter] = $this->callTransformerMethod('gutter', $gutter, $index);
+
+                $line->children[] = $gutter;
             }
 
             foreach ($lineTokens as $j => $token) {
diff --git a/src/Transformers/AbstractTransformer.php b/src/Transformers/AbstractTransformer.php
index 0cd23b6f..10d706f0 100644
--- a/src/Transformers/AbstractTransformer.php
+++ b/src/Transformers/AbstractTransformer.php
@@ -85,6 +85,14 @@ public function token(Element $span, HighlightedToken $token, int $index, int $l
         return $span;
     }
 
+    /**
+     * Modify the  for line number.
+     */
+    public function gutter(Element $span, int $lineNumber): Element
+    {
+        return $span;
+    }
+
     /**
      * Modify the HTML output after the AST has been converted.
      */

From 44e2f34968fe4fd315ea3f2c158d491e3f1671e3 Mon Sep 17 00:00:00 2001
From: Ryan Chandler 
Date: Mon, 25 Aug 2025 19:32:26 +0100
Subject: [PATCH 4/4] Add support for gutter decorations

---
 docs/decorations.mdx                          | 17 +++++++++++++
 src/Output/Html/PendingHtmlOutput.php         |  3 ++-
 .../Decorations/DecorationTransformer.php     | 15 +++++++++++-
 .../Decorations/GutterDecoration.php          | 24 +++++++++++++++++++
 .../Decorations/DecorationTransformerTest.php | 13 ++++++++++
 5 files changed, 70 insertions(+), 2 deletions(-)
 create mode 100644 src/Transformers/Decorations/GutterDecoration.php

diff --git a/docs/decorations.mdx b/docs/decorations.mdx
index 87ef6dbd..c12c66e2 100644
--- a/docs/decorations.mdx
+++ b/docs/decorations.mdx
@@ -50,6 +50,23 @@ $output = (new Phiki)
     );
 ```
 
+## Gutter decorations
+
+To add additional classes to the gutter element that contains line numbers, you can pass a `GutterDecoration` instance to the `PendingHtmlOutput::decoration()` method.
+
+```php
+use Phiki\Transformers\Decorations\GutterDecoration;
+
+$output = (new Phiki)
+    ->codeToHtml('withGutter()
+    ->decoration(
+        GutterDecoration::make()->class('gutter-class'),
+    );
+```
+
+This will add the `gutter-class` class to the gutter element.
+
 ## Substring decorations
 
 At the time of writing, you can only decorate entire lines. 
diff --git a/src/Output/Html/PendingHtmlOutput.php b/src/Output/Html/PendingHtmlOutput.php
index 0922cc67..1406da7f 100644
--- a/src/Output/Html/PendingHtmlOutput.php
+++ b/src/Output/Html/PendingHtmlOutput.php
@@ -16,6 +16,7 @@
 use Phiki\Token\Token;
 use Phiki\Transformers\Decorations\CodeDecoration;
 use Phiki\Transformers\Decorations\DecorationTransformer;
+use Phiki\Transformers\Decorations\GutterDecoration;
 use Phiki\Transformers\Decorations\LineDecoration;
 use Phiki\Transformers\Decorations\PreDecoration;
 use Phiki\Transformers\Meta;
@@ -90,7 +91,7 @@ public function transformer(TransformerInterface $transformer): self
         return $this;
     }
 
-    public function decoration(LineDecoration | PreDecoration | CodeDecoration ...$decorations): self
+    public function decoration(LineDecoration | PreDecoration | CodeDecoration | GutterDecoration ...$decorations): self
     {
         if (! Arr::any($this->transformers, fn (TransformerInterface $transformer) => $transformer instanceof DecorationTransformer)) {
             $this->transformers[] = new DecorationTransformer($this->decorations);
diff --git a/src/Transformers/Decorations/DecorationTransformer.php b/src/Transformers/Decorations/DecorationTransformer.php
index 82cd29ec..6b5e02b7 100644
--- a/src/Transformers/Decorations/DecorationTransformer.php
+++ b/src/Transformers/Decorations/DecorationTransformer.php
@@ -8,7 +8,7 @@
 class DecorationTransformer extends AbstractTransformer
 {
     /**
-     * @param  array  $decorations
+     * @param  array  $decorations
      */
     public function __construct(
         public array &$decorations,
@@ -56,4 +56,17 @@ public function line(Element $span, array $tokens, int $index): Element
 
         return $span;
     }
+
+    public function gutter(Element $span, int $lineNumber): Element
+    {
+        foreach ($this->decorations as $decoration) {
+            if (! $decoration instanceof GutterDecoration) {
+                continue;
+            }
+
+            $span->properties->get('class')->add(...$decoration->classes->all());
+        }
+
+        return $span;
+    }
 }
diff --git a/src/Transformers/Decorations/GutterDecoration.php b/src/Transformers/Decorations/GutterDecoration.php
new file mode 100644
index 00000000..bd9d1bc1
--- /dev/null
+++ b/src/Transformers/Decorations/GutterDecoration.php
@@ -0,0 +1,24 @@
+classes->add(...$classes);
+
+        return $this;
+    }
+}
diff --git a/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php b/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
index 433d8367..f663fef9 100644
--- a/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
+++ b/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
@@ -5,6 +5,7 @@
 use Phiki\Phiki;
 use Phiki\Theme\Theme;
 use Phiki\Transformers\Decorations\CodeDecoration;
+use Phiki\Transformers\Decorations\GutterDecoration;
 use Phiki\Transformers\Decorations\LineDecoration;
 use Phiki\Transformers\Decorations\PreDecoration;
 
@@ -71,3 +72,15 @@
 
     expect($output)->toContain('');
 });
+
+it('can apply decorations to gutter', function () {
+    $output = (new Phiki)
+        ->codeToHtml(<<<'PHP'
+        echo "Hello, world!";
+        PHP, Grammar::Php, Theme::GithubLight, true)
+        ->withGutter()
+        ->decoration(GutterDecoration::make()->class('gutter-class'))
+        ->toString();
+
+    expect($output)->toContain('