Skip to content
Merged
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
53 changes: 50 additions & 3 deletions docs/decorations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 `<pre>` 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('<?php echo ...', Grammar::Php, Theme::GithubLight)
->decoration(
PreDecoration::make()->class('pre-class'),
);
```

This will add the `pre-class` class to the `<pre>` element.

## Code decorations

To add additional classes to the `<code>` 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('<?php echo ...', Grammar::Php, Theme::GithubLight)
->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('<?php echo ...', Grammar::Php, Theme::GithubLight)
->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.
Expand Down
5 changes: 5 additions & 0 deletions src/Contracts/TransformerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span> for each gutter element.
*/
public function gutter(Element $span, int $lineNumber): Element;

/**
* Modify the HTML output after the AST has been converted.
*/
Expand Down
15 changes: 11 additions & 4 deletions src/Output/Html/PendingHtmlOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions src/Phast/ClassList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) !== ''));
Expand Down
4 changes: 3 additions & 1 deletion src/Phast/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/Phast/Properties.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 8 additions & 0 deletions src/Transformers/AbstractTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ public function token(Element $span, HighlightedToken $token, int $index, int $l
return $span;
}

/**
* Modify the <span> for line number.
*/
public function gutter(Element $span, int $lineNumber): Element
{
return $span;
}

/**
* Modify the HTML output after the AST has been converted.
*/
Expand Down
24 changes: 24 additions & 0 deletions src/Transformers/Decorations/CodeDecoration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Phiki\Transformers\Decorations;

use Phiki\Phast\ClassList;

class CodeDecoration
{
public function __construct(
public ClassList $classes,
) {}

public static function make(): self
{
return new self(new ClassList());
}

public function class(string ...$classes): self
{
$this->classes->add(...$classes);

return $this;
}
}
45 changes: 44 additions & 1 deletion src/Transformers/Decorations/DecorationTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,45 @@
class DecorationTransformer extends AbstractTransformer
{
/**
* @param array<int, LineDecoration> $decorations
* @param array<int, LineDecoration | PreDecoration | CodeDecoration | GutterDecoration> $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;
}
Expand All @@ -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;
}
}
24 changes: 24 additions & 0 deletions src/Transformers/Decorations/GutterDecoration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Phiki\Transformers\Decorations;

use Phiki\Phast\ClassList;

class GutterDecoration
{
public function __construct(
public ClassList $classes,
) {}

public static function make(): self
{
return new self(new ClassList());
}

public function class(string ...$classes): self
{
$this->classes->add(...$classes);

return $this;
}
}
24 changes: 24 additions & 0 deletions src/Transformers/Decorations/PreDecoration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Phiki\Transformers\Decorations;

use Phiki\Phast\ClassList;

class PreDecoration
{
public function __construct(
public ClassList $classes,
) {}

public static function make(): self
{
return new self(new ClassList());
}

public function class(string ...$classes): self
{
$this->classes->add(...$classes);

return $this;
}
}
37 changes: 37 additions & 0 deletions tests/Unit/Transformers/Decorations/DecorationTransformerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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('<pre class="phiki language-php github-light pre-class" data-language="php"');
});

it('can apply decorations to code', function () {
$output = (new Phiki)
->codeToHtml(<<<'PHP'
echo "Hello, world!";
PHP, Grammar::Php, Theme::GithubLight)
->decoration(new CodeDecoration(new ClassList(['code-class'])))
->toString();

expect($output)->toContain('<code class="code-class">');
});

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('<span class="line-number gutter-class"');
});