Files
LazyBearWorks/docs/razorconsole/custom-translators.md

277 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RazorConsole — Кастомные VDOM-трансляторы
## Архитектура
RazorConsole использует Virtual DOM (VDOM) для преобразования Razor-компонентов в Spectre.Console `IRenderable`. Система трансляторов (translators) является расширяемой: можно добавить поддержку новых Spectre.Console-конструкций или построить полностью кастомные компоненты.
### Ключевые компоненты
#### `IVdomElementTranslator`
```csharp
public interface IVdomElementTranslator
{
// Чем меньше значение — тем выше приоритет (обрабатывается раньше).
int Priority { get; }
bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable);
}
```
#### `TranslationContext`
```csharp
public sealed class TranslationContext
{
// Рекурсивный перевод дочерних узлов
public bool TryTranslate(VNode node, out IRenderable? renderable);
}
```
#### `VdomSpectreTranslator`
Оркестратор, который:
1. Получает список трансляторов через DI (отсортированных по приоритету)
2. Пробует каждый по очереди
3. Возвращает первый успешный результат
4. Предоставляет статические вспомогательные методы
### Pipeline трансляции
```
Razor-компонент → ConsoleRenderer → VNode tree → VdomSpectreTranslator
→ [Priority 10] → [Priority 20] → ... → [Priority 1000 (fallback)]
→ IRenderable
```
---
## Встроенные трансляторы
| Приоритет | Транслятор | Обрабатывает |
|---|---|---|
| 10 | TextElementTranslator | `<span data-text="true">` |
| 20 | HtmlInlineTextElementTranslator | `<strong>`, `<em>`, `<code>` |
| 30 | ParagraphElementTranslator | `<p>` |
| 40 | SpacerElementTranslator | `<div data-spacer="true">` |
| 50 | NewlineElementTranslator | `<br>` |
| 60 | SpinnerElementTranslator | `<div class="spinner">` |
| 7080 | Button translators | `<button>` |
| 90 | SyntaxHighlighterElementTranslator | `<div class="syntax-highlighter">` |
| 100190 | Layout translators | Panels, Rows, Columns, Grid, Padding, Align |
| 1000 | FailToRenderElementTranslator | Fallback для необработанных узлов |
---
## Создание кастомного транслятора
### Шаг 1: Реализовать интерфейс
```csharp
using RazorConsole.Core.Rendering.Vdom;
using RazorConsole.Core.Vdom;
using Spectre.Console;
using Spectre.Console.Rendering;
public sealed class OverflowElementTranslator : IVdomElementTranslator
{
public int Priority => 85; // между встроенными 80 и 90
public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
{
renderable = null;
// 1. Быстрые проверки — fail fast
if (node.Kind != VNodeKind.Element) return false;
if (!string.Equals(node.TagName, "div", StringComparison.OrdinalIgnoreCase)) return false;
if (!node.Attributes.TryGetValue("data-overflow", out var overflowType)) return false;
// 2. Трансляция дочерних узлов
if (!VdomSpectreTranslator.TryConvertChildrenToRenderables(
node.Children, context, out var children)) return false;
var content = VdomSpectreTranslator.ComposeChildContent(children);
// 3. Создать IRenderable
renderable = overflowType?.ToLowerInvariant() switch
{
"ellipsis" => new Padder(content).Overflow(Overflow.Ellipsis),
"crop" => new Padder(content).Overflow(Overflow.Crop),
"fold" => new Padder(content).Overflow(Overflow.Fold),
_ => content
};
return true;
}
}
```
### Шаг 2: Зарегистрировать транслятор
```csharp
using Microsoft.Extensions.Hosting;
using RazorConsole.Core;
using RazorConsole.Core.Vdom;
IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args)
.UseRazorConsole<MyComponent>(configure: config =>
{
config.ConfigureServices(services =>
{
// По типу (создаётся через DI)
services.AddVdomTranslator<OverflowElementTranslator>();
// По экземпляру
services.AddVdomTranslator(new OverflowElementTranslator());
// Через фабрику (для инъекции зависимостей)
services.AddVdomTranslator(sp =>
new OverflowElementTranslator(sp.GetRequiredService<IMyService>()));
});
});
```
### Шаг 3: Использовать в Razor-компонентах
```razor
<div data-overflow="ellipsis">
Очень длинный текст, который будет обрезан с многоточием...
</div>
```
---
## Вспомогательные методы `VdomSpectreTranslator`
### Инспекция узла
```csharp
string? value = VdomSpectreTranslator.GetAttribute(node, "data-style");
bool hasClass = VdomSpectreTranslator.HasClass(node, "my-class");
string? text = VdomSpectreTranslator.CollectInnerText(node);
```
### Парсинг атрибутов
```csharp
// bool
if (VdomSpectreTranslator.TryGetBoolAttribute(node, "data-enabled", out bool enabled)) { }
// int с fallback
int count = VdomSpectreTranslator.TryGetIntAttribute(node, "data-count", fallback: 10);
// положительный int
if (VdomSpectreTranslator.TryParsePositiveInt(rawValue, out int result)) { }
// int? (null если невалидно)
int? width = VdomSpectreTranslator.ParseOptionalPositiveInt(rawValue);
// CSS-подобный padding: "1", "1,2", "1,2,3,4"
if (VdomSpectreTranslator.TryParsePadding(rawValue, out Padding padding)) { }
```
### Парсинг выравнивания
```csharp
var hAlign = VdomSpectreTranslator.ParseHorizontalAlignment(value); // Left/Center/Right
var vAlign = VdomSpectreTranslator.ParseVerticalAlignment(value); // Top/Middle/Bottom
```
### Работа с дочерними узлами
```csharp
// Перевести дочерние узлы
if (VdomSpectreTranslator.TryConvertChildrenToRenderables(
node.Children, context, out List<IRenderable> renderables)) { }
// Объединить в один IRenderable
// - 1 элемент → возвращает его напрямую
// - N элементов → Rows
// - 0 элементов → пустой Markup
IRenderable composed = VdomSpectreTranslator.ComposeChildContent(renderables);
```
---
## Правила выбора приоритета
| Диапазон | Когда использовать |
|---|---|
| 19 | Переопределить встроенное поведение |
| 10190 | Вклиниться между конкретными встроенными трансляторами |
| 200999 | Общие кастомные трансляторы |
| 1000+ | Fallback-обработчики |
---
## Лучшие практики
1. **Fail fast** — сразу возвращай `false` если узел не совпадает
2. **Case-insensitive сравнение**`StringComparison.OrdinalIgnoreCase` для `TagName` и атрибутов
3. **Всегда используй `TryConvertChildrenToRenderables`** для рекурсивной трансляции детей
4. **Валидируй атрибуты** — предусматривай defaults, не бросай исключения
5. **Immutability** — создавай новые экземпляры `IRenderable`, не мутируй существующие
6. **Thread safety** — трансляторы должны быть stateless или использовать immutable state
---
## Продвинутые сценарии
### DI в трансляторе
```csharp
public sealed class DatabaseStyleTranslator : IVdomElementTranslator
{
private readonly IStyleProvider _styleProvider;
public DatabaseStyleTranslator(IStyleProvider styleProvider)
{
_styleProvider = styleProvider;
}
public int Priority => 95;
public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
{
renderable = null;
if (!node.Attributes.TryGetValue("data-style-id", out var styleId)) return false;
var style = _styleProvider.GetStyle(styleId);
// ... создать renderable ...
return true;
}
}
// Регистрация
services.AddSingleton<IStyleProvider, MyStyleProvider>();
services.AddVdomTranslator<DatabaseStyleTranslator>();
```
### Условная трансляция (Alert-компонент)
```csharp
public sealed class AlertTranslator : IVdomElementTranslator
{
public int Priority => 105;
public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
{
renderable = null;
if (!VdomSpectreTranslator.HasClass(node, "alert")) return false;
if (!VdomSpectreTranslator.TryConvertChildrenToRenderables(
node.Children, context, out var children)) return false;
var content = VdomSpectreTranslator.ComposeChildContent(children);
renderable = VdomSpectreTranslator.HasClass(node, "alert-danger")
? new Panel(content).BorderColor(Color.Red).Header("[red]⚠ Error[/]")
: VdomSpectreTranslator.HasClass(node, "alert-success")
? new Panel(content).BorderColor(Color.Green).Header("[green]✓ Success[/]")
: new Panel(content).BorderColor(Color.Blue).Header("[blue] Info[/]");
return true;
}
}
```