diff --git a/src/Output/Html/PendingHtmlOutput.php b/src/Output/Html/PendingHtmlOutput.php
index 15753498..bb07e328 100644
--- a/src/Output/Html/PendingHtmlOutput.php
+++ b/src/Output/Html/PendingHtmlOutput.php
@@ -13,6 +13,9 @@
use Phiki\Theme\ParsedTheme;
use Phiki\Token\HighlightedToken;
use Phiki\Token\Token;
+use Phiki\Transformers\Decorations\DecorationsTransformer;
+use Phiki\Transformers\Decorations\DecorationTransformer;
+use Phiki\Transformers\Decorations\LineDecoration;
use Psr\SimpleCache\CacheInterface;
use Stringable;
@@ -28,6 +31,8 @@ class PendingHtmlOutput implements Stringable
protected array $transformers = [];
+ protected array $decorations = [];
+
protected int $startingLineNumber = 1;
/**
@@ -80,6 +85,17 @@ public function transformer(TransformerInterface $transformer): self
return $this;
}
+ public function decoration(LineDecoration ...$decorations): self
+ {
+ if (! Arr::any($this->transformers, fn (TransformerInterface $transformer) => $transformer instanceof DecorationTransformer)) {
+ $this->transformers[] = new DecorationTransformer($this->decorations);
+ }
+
+ $this->decorations = array_merge($this->decorations, $decorations);
+
+ return $this;
+ }
+
public function startingLine(int $lineNumber): self
{
$this->startingLineNumber = $lineNumber;
diff --git a/src/Phast/ClassList.php b/src/Phast/ClassList.php
index 4c6673ea..2f3f80a7 100644
--- a/src/Phast/ClassList.php
+++ b/src/Phast/ClassList.php
@@ -40,6 +40,11 @@ public function remove(string ...$class): self
return $this;
}
+ public function all(): array
+ {
+ return $this->classes;
+ }
+
public function __toString(): string
{
return implode(' ', array_filter($this->classes, fn (string $class) => trim($class) !== ''));
diff --git a/src/Transformers/Decorations/DecorationTransformer.php b/src/Transformers/Decorations/DecorationTransformer.php
new file mode 100644
index 00000000..1ea23cb2
--- /dev/null
+++ b/src/Transformers/Decorations/DecorationTransformer.php
@@ -0,0 +1,29 @@
+ $decorations
+ */
+ public function __construct(
+ public array &$decorations,
+ ) {}
+
+ public function line(Element $span, array $tokens, int $index): Element
+ {
+ foreach ($this->decorations as $decoration) {
+ if (! $decoration->appliesToLine($index)) {
+ continue;
+ }
+
+ $span->properties->get('class')->add(...$decoration->classes->all());
+ }
+
+ return $span;
+ }
+}
diff --git a/src/Transformers/Decorations/LineDecoration.php b/src/Transformers/Decorations/LineDecoration.php
new file mode 100644
index 00000000..5f05f582
--- /dev/null
+++ b/src/Transformers/Decorations/LineDecoration.php
@@ -0,0 +1,21 @@
+ $line
+ */
+ public function __construct(
+ public int | array $line,
+ public ClassList $classes,
+ ) {}
+
+ public function appliesToLine(int $line): bool
+ {
+ return $this->line === $line || (is_array($this->line) && $line >= $this->line[0] && $line <= $this->line[1]);
+ }
+}
diff --git a/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php b/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
new file mode 100644
index 00000000..9a10ea3b
--- /dev/null
+++ b/tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
@@ -0,0 +1,49 @@
+codeToHtml(<<<'PHP'
+ echo "Hello, world!";
+ PHP, Grammar::Php, Theme::GithubLight)
+ ->decoration(new LineDecoration(0, new ClassList(['test-class'])))
+ ->toString();
+
+ expect($output)->toContain('');
+});
+
+it('can apply decorations to multiple lines with multiple instances', function () {
+ $output = (new Phiki)
+ ->codeToHtml(<<<'PHP'
+ echo "Hello, world!";
+ echo "Goodbye, world!";
+ PHP, Grammar::Php, Theme::GithubLight)
+ ->decoration(
+ new LineDecoration(0, new ClassList(['first-line'])),
+ new LineDecoration(1, new ClassList(['second-line'])),
+ new LineDecoration(0, new ClassList(['also-first-line'])),
+ )
+ ->toString();
+
+ expect($output)
+ ->toContain('')
+ ->toContain('');
+});
+
+it('can apply decorations to a range of lines', function () {
+ $output = (new Phiki)
+ ->codeToHtml(<<<'PHP'
+ echo "Hello, world!";
+ echo "Goodbye, world!";
+ echo "Farewell, world!";
+ PHP, Grammar::Php, Theme::GithubLight)
+ ->decoration(new LineDecoration([0, 2], new ClassList(['multi-line'])))
+ ->toString();
+
+ expect(substr_count($output, 'multi-line'))->toBe(3);
+});