Added docs RazorConsole
This commit is contained in:
276
docs/razorconsole/custom-translators.md
Normal file
276
docs/razorconsole/custom-translators.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# 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">` |
|
||||
| 70–80 | Button translators | `<button>` |
|
||||
| 90 | SyntaxHighlighterElementTranslator | `<div class="syntax-highlighter">` |
|
||||
| 100–190 | 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);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Правила выбора приоритета
|
||||
|
||||
| Диапазон | Когда использовать |
|
||||
|---|---|
|
||||
| 1–9 | Переопределить встроенное поведение |
|
||||
| 10–190 | Вклиниться между конкретными встроенными трансляторами |
|
||||
| 200–999 | Общие кастомные трансляторы |
|
||||
| 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user