
# Трансформеры

Трансформеры позволяют изменять подсвеченный результат на каждом этапе: от исходной строки через сетку токенов до итогового дерева HAST и HTML.
shikiphp реализует модель трансформеров Shiki, повторяя все хуки, поэтому трансформеры работают так же, как их аналоги в JavaScript.

Передавайте их через параметр [`transformers`](https://shikiphp.inline0.dev/ru/docs/options#transformers):

```php
use Shikiphp\Shikiphp;
use Shikiphp\Transformer\Notation\NotationHighlight;
use Shikiphp\Transformer\Notation\NotationDiff;

$code = <<<'JS'
console.log('this line is highlighted') // [!code highlight]
console.log('old line') // [!code --]
console.log('new line') // [!code ++]
JS;

echo Shikiphp::codeToHtml($code, [
    'lang'         => 'js',
    'theme'        => 'vitesse-dark',
    'transformers' => [
        new NotationHighlight(),
        new NotationDiff(),
    ],
]);
```

## Конвейер

Каждый трансформер — это объект, реализующий `Shikiphp\Transformer\Transformer`.
Конвейер по очереди запускает хуки для каждого этапа создания одного блока:

| Хук | Сигнатура | Когда выполняется |
| --- | --- | --- |
| `preprocess` | `(string $code, array &$options, $context): ?string` | до токенизации; может изменить исходный текст и параметры |
| `tokens` | `(array $tokens, $context): ?array` | после токенизации, для двумерной сетки `ThemedToken` |
| `root` | `(Element $el, $context): ?Element` | для корневого узла HAST |
| `pre` | `(Element $el, $context): ?Element` | для элемента `<pre>` |
| `code` | `(Element $el, $context): ?Element` | для элемента `<code>` |
| `line` | `(Element $el, int $line, $context): ?Element` | для каждого элемента строки (нумерация с 1) |
| `span` | `(Element $el, int $line, int $col, Element $lineEl, ThemedToken $token, $context): ?Element` | для каждого диапазона токена |
| `postprocess` | `(string $html, array $options, $context): ?string` | для итоговой сериализованной строки HTML |

Каждый хук необязателен. Возвращённое значение заменяет входное; возврат `null` оставляет его без изменений.

Параметр `enforce()` задаёт порядок выполнения трансформеров: сначала запускается `'pre'`, последним — `'post'`, а `null` (по умолчанию) — между ними в порядке регистрации.

## Создание трансформера

Расширьте `Shikiphp\Transformer\AbstractTransformer`, чтобы переопределить только нужные хуки; остальные ничего не делают. Контекст трансформера предоставляет `addClassToHast()` для добавления классов элементам.

```php
use Shikiphp\Hast\Element;
use Shikiphp\Shikiphp;
use Shikiphp\Transformer\AbstractTransformer;
use Shikiphp\Transformer\TransformerContext;

final class AddLanguageClass extends AbstractTransformer
{
    public function pre(Element $element, TransformerContext $context): ?Element
    {
        if ($context->lang !== null) {
            $context->addClassToHast($element, 'language-' . $context->lang);
        }

        return $element;
    }
}

echo Shikiphp::codeToHtml('const answer = 42', [
    'lang'         => 'js',
    'theme'        => 'nord',
    'transformers' => [new AddLanguageClass()],
]);
// <pre class="shiki nord language-js" …>
```

## Встроенные трансформеры

### Трансформеры аннотаций

Они находят в исходном тексте специальные комментарии `// [!code …]` (синтаксис комментариев автоматически определяется по языку) и преобразуют их в классы, удаляя сами комментарии. Это прямые порты набора аннотаций `@shikijs/transformers`.

#### `NotationHighlight`

`Shikiphp\Transformer\Notation\NotationHighlight` — `// [!code highlight]` (или `// [!code hl]`) добавляет к строке класс `highlighted`.

```js
console.log('kept plain')
console.log('this line is highlighted') // [!code highlight]
```

#### `NotationDiff`

`Shikiphp\Transformer\Notation\NotationDiff` — `// [!code ++]` помечает строку как добавленную (`diff add`), а `// [!code --]` — как удалённую (`diff remove`).

```js
console.log('old line') // [!code --]
console.log('new line') // [!code ++]
```

#### `NotationFocus`

`Shikiphp\Transformer\Notation\NotationFocus` — `// [!code focus]` добавляет класс `focused`.

```js
console.log('focus on me') // [!code focus]
```

#### `NotationErrorLevel`

`Shikiphp\Transformer\Notation\NotationErrorLevel` — `// [!code error]`, `// [!code warning]` и `// [!code info]` добавляют класс `highlighted`, а также `error` / `warning` / `info`.

```js
throwSomething() // [!code error]
maybeRisky()     // [!code warning]
```

#### `NotationWordHighlight`

`Shikiphp\Transformer\Notation\NotationWordHighlight` — `// [!code word:foo]` (при необходимости укажите `// [!code word:foo:2]`, чтобы ограничить действие следующими N строками) оборачивает каждое вхождение `foo` в элемент span с атрибутом `highlighted-word`.

```js
// [!code word:config]
const config = loadConfig()
```

### Трансформеры метаданных

Они читают **строку метаданных** блока кода, переданную через параметр [`meta`](https://shikiphp.inline0.dev/ru/docs/options#meta) (`meta['__raw']`), подобно тому, как Shiki читает текст после указания языка в блоке кода с тройными обратными кавычками.

#### `MetaHighlight`

`Shikiphp\Transformer\MetaHighlight` — подсвечивает строки, указанные в диапазоне метаданных, например `{1,3-5}`, добавляя класс `highlighted`.

```php
use Shikiphp\Shikiphp;
use Shikiphp\Transformer\MetaHighlight;

$code = "const a = 1\nconst b = 2\nconst c = 3\nconst d = 4\nconst e = 5";

echo Shikiphp::codeToHtml($code, [
    'lang'         => 'js',
    'theme'        => 'nord',
    'meta'         => ['__raw' => '{1,3-5}'],
    'transformers' => [new MetaHighlight()],
]);
```

#### `MetaWordHighlight`

`Shikiphp\Transformer\MetaWordHighlight` — подсвечивает слова, указанные в строке метаданных с помощью синтаксиса `/word/`, и добавляет `highlighted-word`.

### Пробелы и структура

#### `RenderWhitespace`

`Shikiphp\Transformer\RenderWhitespace` — оборачивает пробелы и символы табуляции в элементы span (по умолчанию классы `space` и `tab`), чтобы их можно было отображать явно.
Конструктор принимает `(classSpace, classTab, position)`, где `position` — это `'all'` (по умолчанию), `'boundary'`, `'trailing'` или `'leading'`.

#### `CompactLineOptions`

`Shikiphp\Transformer\CompactLineOptions` — применяет классы к отдельным строкам из списка записей `{line, classes}`.

```php
use Shikiphp\Transformer\CompactLineOptions;

$transformer = new CompactLineOptions([
    ['line' => 1, 'classes' => 'highlighted'],
    ['line' => 3, 'classes' => ['highlighted', 'error']],
]);
```

Нумерация строк начинается с 1. Передайте объект в параметре `transformers`, как и любой другой трансформер.

#### `RemoveNotationEscape`

`Shikiphp\Transformer\RemoveNotationEscape` — снимает экранирование с формы `[\!code …]`, используемой для записи буквального комментария-аннотации без запуска трансформера аннотаций.

#### `RemoveLineBreak`

`Shikiphp\Transformer\RemoveLineBreak` — удаляет текстовые узлы с переносами строк между строками (Shiki: `transformerRemoveLineBreak`).

### `StyleToClass`

`Shikiphp\Transformer\StyleToClass` — переносит встроенные стили в сгенерированные классы и собирает таблицу стилей, которую можно получить через `getCSS()`. Имена классов хешируют стиль с помощью Shiki — `cyrb53`, поэтому результат побайтно совпадает с `@shikijs/transformers` 4.2.0, включая его область действия:

- Стиль `<pre>` всегда преобразуется в класс.
- Диапазоны токенов преобразуются только при наличии предварительно созданного стиля, что бывает с картой `themes`. При одной `theme` диапазоны токенов сохраняют встроенный `style="color:…"`, как и в Shiki.

```php
use Shikiphp\Shikiphp;
use Shikiphp\Transformer\StyleToClass;

$code = 'const answer = 42';
$styleToClass = new StyleToClass();

$html = Shikiphp::codeToHtml($code, [
    'lang'         => 'js',
    'themes'       => ['light' => 'github-light', 'dark' => 'github-dark'],
    'transformers' => [$styleToClass],
]);

$css = $styleToClass->getCSS(); // collected stylesheet for the classes above
```

Конструктор принимает `(classPrefix, classSuffix, classReplacer)`; префикс по умолчанию — `__shiki_`.
