From 0103b4e7cb8b47b1c10cadb159becf3de3f83206 Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Mon, 25 Aug 2025 02:25:29 +0100 Subject: [PATCH] Add basic support for line decorations --- src/Output/Html/PendingHtmlOutput.php | 16 ++++++ src/Phast/ClassList.php | 5 ++ .../Decorations/DecorationTransformer.php | 29 +++++++++++ .../Decorations/LineDecoration.php | 21 ++++++++ .../Decorations/DecorationTransformerTest.php | 49 +++++++++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 src/Transformers/Decorations/DecorationTransformer.php create mode 100644 src/Transformers/Decorations/LineDecoration.php create mode 100644 tests/Unit/Transformers/Decorations/DecorationTransformerTest.php 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); +});