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

9.8 KiB
Raw Permalink Blame History

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

Оркестратор, который:

  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: Реализовать интерфейс

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);

Правила выбора приоритета

Диапазон Когда использовать
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 в трансляторе

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;
    }
}