9.8 KiB
9.8 KiB
RazorConsole — Кастомные VDOM-трансляторы
Архитектура
RazorConsole использует Virtual DOM (VDOM) для преобразования Razor-компонентов в Spectre.Console IRenderable. Система трансляторов (translators) является расширяемой: можно добавить поддержку новых Spectre.Console-конструкций или построить полностью кастомные компоненты.
Ключевые компоненты
IVdomElementTranslator
public interface IVdomElementTranslator
{
// Чем меньше значение — тем выше приоритет (обрабатывается раньше).
int Priority { get; }
bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable);
}
TranslationContext
public sealed class TranslationContext
{
// Рекурсивный перевод дочерних узлов
public bool TryTranslate(VNode node, out IRenderable? renderable);
}
VdomSpectreTranslator
Оркестратор, который:
- Получает список трансляторов через DI (отсортированных по приоритету)
- Пробует каждый по очереди
- Возвращает первый успешный результат
- Предоставляет статические вспомогательные методы
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: Реализовать интерфейс
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: Зарегистрировать транслятор
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-компонентах
<div data-overflow="ellipsis">
Очень длинный текст, который будет обрезан с многоточием...
</div>
Вспомогательные методы VdomSpectreTranslator
Инспекция узла
string? value = VdomSpectreTranslator.GetAttribute(node, "data-style");
bool hasClass = VdomSpectreTranslator.HasClass(node, "my-class");
string? text = VdomSpectreTranslator.CollectInnerText(node);
Парсинг атрибутов
// 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)) { }
Парсинг выравнивания
var hAlign = VdomSpectreTranslator.ParseHorizontalAlignment(value); // Left/Center/Right
var vAlign = VdomSpectreTranslator.ParseVerticalAlignment(value); // Top/Middle/Bottom
Работа с дочерними узлами
// Перевести дочерние узлы
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-обработчики |
Лучшие практики
- Fail fast — сразу возвращай
falseесли узел не совпадает - Case-insensitive сравнение —
StringComparison.OrdinalIgnoreCaseдляTagNameи атрибутов - Всегда используй
TryConvertChildrenToRenderablesдля рекурсивной трансляции детей - Валидируй атрибуты — предусматривай defaults, не бросай исключения
- Immutability — создавай новые экземпляры
IRenderable, не мутируй существующие - Thread safety — трансляторы должны быть stateless или использовать immutable state
Продвинутые сценарии
DI в трансляторе
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-компонент)
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;
}
}