diff --git a/docs/decorations.mdx b/docs/decorations.mdx index 6c309da..c12c66e 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,53 @@ $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'),
+    );
+```
+
+## 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/Contracts/TransformerInterface.php b/src/Contracts/TransformerInterface.php
index d4823af..38f4644 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 2f228ec..1406da7 100644
--- a/src/Output/Html/PendingHtmlOutput.php
+++ b/src/Output/Html/PendingHtmlOutput.php
@@ -7,14 +7,18 @@
 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\GutterDecoration;
 use Phiki\Transformers\Decorations\LineDecoration;
+use Phiki\Transformers\Decorations\PreDecoration;
 use Phiki\Transformers\Meta;
 use Psr\SimpleCache\CacheInterface;
 use Stringable;
@@ -87,7 +91,7 @@ public function transformer(TransformerInterface $transformer): self
         return $this;
     }
 
-    public function decoration(LineDecoration ...$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);
@@ -205,15 +209,14 @@ 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');
             $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;
@@ -224,6 +227,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/Phast/ClassList.php b/src/Phast/ClassList.php
index 2f3f80a..1edb906 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 6254398..3bda408 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 1990b60..6ed7468 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/AbstractTransformer.php b/src/Transformers/AbstractTransformer.php
index 0cd23b6..10d706f 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.
      */
diff --git a/src/Transformers/Decorations/CodeDecoration.php b/src/Transformers/Decorations/CodeDecoration.php
new file mode 100644
index 0000000..85b33ac
--- /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 78e0229..6b5e02b 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;
             }
@@ -26,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 0000000..bd9d1bc
--- /dev/null
+++ b/src/Transformers/Decorations/GutterDecoration.php
@@ -0,0 +1,24 @@
+classes->add(...$classes);
+
+        return $this;
+    }
+}
diff --git a/src/Transformers/Decorations/PreDecoration.php b/src/Transformers/Decorations/PreDecoration.php
new file mode 100644
index 0000000..84136ea
--- /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 9a10ea3..f663fef 100644
--- a/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
+++ b/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
@@ -4,7 +4,10 @@
 use Phiki\Phast\ClassList;
 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;
 
 it('can apply decorations to a line', function () {
     $output = (new Phiki)
@@ -47,3 +50,37 @@
 
     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('');
+});
+
+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('