Skip to content

Expose IVdomElementTranslator and replace MEF with Service Provider for custom translators#36

Merged
LittleLittleCloud merged 9 commits intomainfrom
copilot/expose-ivdomelementtranslator
Oct 16, 2025
Merged

Expose IVdomElementTranslator and replace MEF with Service Provider for custom translators#36
LittleLittleCloud merged 9 commits intomainfrom
copilot/expose-ivdomelementtranslator

Conversation

Copy link
Contributor

Copilot AI commented Oct 16, 2025

Overview

This PR addresses #35 by exposing the IVdomElementTranslator interface and related APIs, enabling users to create custom VDOM element translators. It also replaces the MEF (Managed Extensibility Framework) dependency with the standard Microsoft.Extensions.DependencyInjection service provider for a cleaner, more maintainable architecture.

Motivation

Previously, all VDOM translators were internal and used MEF for discovery, making it impossible for users to extend RazorConsole with custom Spectre.Console renderables. This prevented users from implementing features like overflow handling or other custom components that aren't built into the framework.

Changes

Public APIs

The following APIs are now public and documented:

  • VdomSpectreTranslator - Main translator class with public constructor
  • IVdomElementTranslator - Standalone interface for implementing custom translators with built-in Priority property
  • TranslationContext - Standalone context class for recursive translation of child nodes
  • IAnimatedConsoleRenderable - Interface for animated renderables
  • Utility methods - All helper methods (GetAttribute, TryConvertChildrenToRenderables, CollectInnerText, etc.) are now public static methods
  • All built-in translators - All 20 translator classes are now standalone public classes with their own Priority property

Architecture Improvements

  • Standalone translators: All translator classes are now public standalone classes in their own files, no longer nested in VdomSpectreTranslator
  • Built-in Priority: IVdomElementTranslator includes a Priority property, eliminating the need for wrapper classes
  • Simplified registration: Direct registration without priority wrappers - translators implement their own priority

Service Provider Integration

Replaced MEF with Microsoft.Extensions.DependencyInjection:

  • Removed System.Composition package dependency
  • Removed all [Export] attributes from translator classes
  • Created VdomTranslatorServiceCollectionExtensions with fluent registration API
  • Updated AppHost to register default translators via DI
  • Updated ConsoleRenderer to receive VdomSpectreTranslator from DI container

Priority-Based Registration

Users can now register custom translators using the standard DI pattern:

var app = AppHost.Create<MyComponent>(builder =>
{
    // Register custom translator (priority comes from the translator's Priority property)
    builder.Services.AddVdomTranslator<OverflowElementTranslator>();
    
    // Or register by instance
    builder.Services.AddVdomTranslator(new MyTranslator());
    
    // Or use a factory with DI
    builder.Services.AddVdomTranslator(sp => 
        new MyTranslator(sp.GetRequiredService<ISomeService>()));
});

Built-in translators use priorities from 10-1000, allowing users to insert custom translators before, between, or after default handlers by implementing the appropriate Priority value.

Example Usage

Creating a custom translator for overflow handling:

public class OverflowElementTranslator : IVdomElementTranslator
{
    public int Priority => 50; // Higher priority than most built-in translators

    public bool TryTranslate(VNode node, TranslationContext context, 
        out IRenderable? renderable)
    {
        renderable = null;
        
        if (node.Kind != VNodeKind.Element || 
            !string.Equals(node.TagName, "div", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        var overflow = VdomSpectreTranslator.GetAttribute(node, "data-overflow");
        if (string.IsNullOrEmpty(overflow))
        {
            return false;
        }

        // Convert children using public utility method
        if (!VdomSpectreTranslator.TryConvertChildrenToRenderables(
            node.Children, context, out var children))
        {
            return false;
        }

        renderable = VdomSpectreTranslator.ComposeChildContent(children);
        return true;
    }
}

Then use it in a Razor component:

<div data-overflow="ellipsis">
    This text will be handled by the custom overflow translator
</div>

Documentation

Added comprehensive documentation for custom translators:

Design Documentation

Created design-doc/custom-translators.md with detailed coverage of:

  • Architecture overview and core components
  • Complete reference of all 20 built-in translators with priorities
  • Step-by-step guide for creating custom translators
  • Utility methods reference with examples
  • Best practices and advanced scenarios (DI, conditional translation, complex renderables)
  • Troubleshooting guide and testing strategies

README Updates

Added Custom Translators section to README.md including:

  • Quick-start example for creating a custom translator
  • Registration patterns using AddVdomTranslator
  • Usage examples in Razor components
  • Link to comprehensive design documentation

Testing

  • Added CustomTranslatorTests with 3 new tests validating custom translator registration and priority handling
  • Created TestHelpers utility for consistent test renderer setup
  • All existing tests pass (60 tests → 63 tests)
  • Tests run on both .NET 8.0 and .NET 9.0 (126 total test executions)

Breaking Changes

None for users of the public API. The internal MEF-based system has been replaced, but since IVdomElementTranslator was previously internal, no external code should be affected.

Migration Guide

For any internal/custom translators that were using the internal MEF approach:

Before:

[Export(typeof(IVdomElementTranslator))]
internal sealed class MyTranslator : VdomSpectreTranslator.IVdomElementTranslator
{
    public bool TryTranslate(VNode node, VdomSpectreTranslator.TranslationContext context, 
        out IRenderable? renderable)
    {
        // Implementation
    }
}

After:

public sealed class MyTranslator : IVdomElementTranslator
{
    public int Priority => 100; // Define your priority
    
    public bool TryTranslate(VNode node, TranslationContext context, 
        out IRenderable? renderable)
    {
        // Implementation
    }
}

// Register in startup:
services.AddVdomTranslator<MyTranslator>();

Benefits

  1. Extensibility - Users can now implement custom Spectre.Console renderables
  2. Standard DI - No MEF dependency, uses familiar .NET patterns
  3. Priority Control - Fine-grained control over translator ordering via Priority property
  4. Cleaner Architecture - Standalone translator classes without nested types
  5. Better Testing - Easier to test with standard DI practices
  6. Documentation - All public APIs are documented with XML comments, comprehensive design docs, and README examples

Closes #35

Original prompt

This section details on the original issue you should resolve

<issue_title>Expose IVdomElementTranslator and friends</issue_title>
<issue_description>Currently, all translators are internal since IVdomElementTranslator is internal, and some other member within the method of the interface

It seems pretty simple

internal sealed partial class VdomSpectreTranslator
{
    [Export(typeof(IVdomElementTranslator))]
    internal sealed class TextElementTranslator : IVdomElementTranslator
    {
        public bool TryTranslate(VNode node, TranslationContext context, out IRenderable? renderable)
        {
            renderable = null;

            if (node.Kind != VNodeKind.Element)
            {
                return false;
            }

            if (!string.Equals(node.TagName, "span", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            if (!node.Attributes.TryGetValue("data-text", out var value) || !string.Equals(value, "true", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            string? text;
            if (node.Attributes.TryGetValue("data-content", out var inlineContent) && inlineContent is not null)
            {
                if (node.Children.Any())
                {
                    // Prefer explicit content attribute when present and require no additional children.
                    return false;
                }

                text = inlineContent;
            }
            else
            {
                text = CollectInnerText(node);
                if (string.IsNullOrWhiteSpace(text))
                {
                    // Missing required text content.
                    return false;
                }
            }

            var styleAttributes = GetAttribute(node, "data-style");
            if (string.IsNullOrEmpty(styleAttributes))
            {
                renderable = new Markup(text);
            }
            else
            {
                var style = Style.Parse(styleAttributes ?? string.Empty);
                renderable = new Markup(text, style);
            }

            return true;
        }
    }
}

This is the translator for span which is what Markup creates.

Now, I would like to support overflow which exist in Spectre but not implemented.

Generally people will be able to create more with those interfaces exposed maybe even creating their own custom components </issue_description>

Comments on the Issue (you are @copilot in this section)

@LittleLittleCloud Additional requests - get rid of using mef and replace it with service provider for dependency injection - allow user registers their custom vdom translator with priority

Fixes #35


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

- Made IVdomElementTranslator and TranslationContext public
- Made VdomSpectreTranslator public with public constructor
- Exposed utility methods as public static methods
- Made IAnimatedConsoleRenderable public
- Removed MEF (System.Composition) dependency entirely
- Created VdomTranslatorServiceCollectionExtensions for registering translators with priority
- Updated AppHost to register default translators via DI
- Updated ConsoleRenderer to accept VdomSpectreTranslator from DI
- Updated all translator files to remove [Export] attributes
- Updated all Razor components to remove System.Composition using
- Created TestHelpers for test renderer creation
- Added CustomTranslatorTests to validate custom translator registration
- All 63 tests pass successfully

Co-authored-by: LittleLittleCloud <16876986+LittleLittleCloud@users.noreply.github.com>
Copilot AI changed the title [WIP] Expose IVdomElementTranslator for external use Expose IVdomElementTranslator and replace MEF with Service Provider for custom translators Oct 16, 2025
Copy link
Member

@LittleLittleCloud LittleLittleCloud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix comments

- Moved IVdomElementTranslator to its own file with Priority property
- Moved TranslationContext to its own file (no longer nested)
- Converted all translator classes from nested to standalone classes
- Each translator now implements Priority property directly
- Removed PrioritizedTranslator wrapper class
- Removed IPrioritizedTranslator interface
- Updated registration to use Priority property from interface
- Updated all tests and helper methods
- All 63 tests pass successfully

Co-authored-by: LittleLittleCloud <16876986+LittleLittleCloud@users.noreply.github.com>
@LittleLittleCloud LittleLittleCloud marked this pull request as ready for review October 16, 2025 22:27
@LittleLittleCloud
Copy link
Member

@copilot
Create a design doc on IVdomElementTranslator and on how to extend razor console with custom translators

- Created custom-translators.md with detailed guide
- Covers architecture, built-in translators, and extensibility
- Includes examples for creating custom translators
- Documents utility methods and best practices
- Provides troubleshooting and testing guidance

Co-authored-by: LittleLittleCloud <16876986+LittleLittleCloud@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot
Create a design doc on IVdomElementTranslator and on how to extend razor console with custom translators

Done. Created comprehensive design document at design-doc/custom-translators.md covering:

  • Architecture overview and core components
  • Complete list of built-in translators with priorities
  • Step-by-step guide for creating custom translators
  • Utility methods reference
  • Best practices and advanced scenarios
  • Troubleshooting and testing guidance

See commit 94a6d85.

@LittleLittleCloud
Copy link
Member

@copilot Update readme with a section on how to register custom translators and link to the design doc

- Added comprehensive Custom Translators section after Built-in Components
- Includes example of creating a custom translator (OverflowElementTranslator)
- Shows registration pattern using AddVdomTranslator
- Demonstrates usage in Razor components
- Links to detailed design doc for advanced scenarios

Co-authored-by: LittleLittleCloud <16876986+LittleLittleCloud@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot Update readme with a section on how to register custom translators and link to the design doc

Done. Added comprehensive Custom Translators section to README.md with:

  • Example of creating a custom translator
  • Registration pattern using AddVdomTranslator
  • Usage example in Razor components
  • Link to detailed design doc at design-doc/custom-translators.md

See commit 294a293.

@LittleLittleCloud LittleLittleCloud enabled auto-merge (squash) October 16, 2025 22:56
@LittleLittleCloud LittleLittleCloud merged commit b45532a into main Oct 16, 2025
4 checks passed
@LittleLittleCloud LittleLittleCloud deleted the copilot/expose-ivdomelementtranslator branch October 16, 2025 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose IVdomElementTranslator and friends

3 participants