Editor/Serialization/DocumentSerializer.cs
using System.Text;
using Grains.RazorDesigner.Document;
using Grains.RazorDesigner.Projection;

namespace Grains.RazorDesigner.Serialization;

public static class DocumentSerializer
{
    private const int SchemaVersion = 4;

    // Sandbox.UI outline shorthand only accepts 'solid'.
    private const string SelectionRule =
        ".selected { outline: 2px solid #3FA9F5; }";

    private const string PreviewMarkerRules =
        ".preview-image-empty { " +
            "background-image: linear-gradient(to bottom right, rgba(120, 120, 160, 0.5), rgba(60, 60, 90, 0.5)); " +
            "border: 1px solid rgba(180, 180, 200, 0.5); " +
            "min-width: 32px; " +
            "min-height: 32px; " +
        "}\n" +
        ".preview-panel { position: relative; }\n" +
        ".preview-chrome-label { " +
            "position: absolute; " +
            "top: 0; left: 0; right: 0; bottom: 0; " +
            "justify-content: center; " +
            "align-items: center; " +
            "color: rgba(255, 255, 255, 0.4); " +
            "font-size: 20px; " +
            "text-align: center; " +
        "}\n" +
        ".chrome-hidden .preview-chrome-label { display: none; }\n" +
        ".chrome-hidden .preview-image-empty { " +
            "background-image: none; " +
            "border: none; " +
            "min-width: 0; " +
            "min-height: 0; " +
        "}\n" +
        ".chrome-hidden .preview-buttongroup { " +
            "background-color: transparent; " +
            "border: none; " +
            "min-height: 0; " +
        "}\n" +
        ".chrome-hidden .preview-dropdown { " +
            "background-image: none; " +
            "background-color: transparent; " +
            "border: none; " +
            "min-height: 0; " +
            "box-shadow: none; " +
        "}\n" +
        ".chrome-hidden .preview-dropdown > .inner { " +
            "background-color: transparent; " +
            "border-left: none; " +
        "}\n" +
        ".chrome-hidden .selected { outline: 0px solid transparent; }";

    public static string GenerateRazorMarkup( DesignerDocument doc, string irHash = null )
    {
        var sb = new StringBuilder();
        sb.AppendLine( $"@* Grains.RazorDesigner schema={SchemaVersion} *@" );
        if ( irHash is not null )
            sb.AppendLine( $"@* generated-from-ir-hash: {irHash} *@" );
        sb.AppendLine( "@* https://sbox.game/xaz/razordesigner *@" );
        sb.AppendLine( "@using Sandbox;" );
        sb.AppendLine( "@using Sandbox.UI;" );
        sb.AppendLine( "@inherits PanelComponent" );
        sb.AppendLine( "<root class=\"root\">" );
        var ctx = new ProjectionContext( PreviewTheme.Default, ForPreview: false );
        var (razorChildren, _) = Applier.BuildSave( new RecordNode( doc.RootRecord ), "", ctx );
        sb.Append( razorChildren );
        sb.AppendLine( "</root>" );
        return sb.ToString();
    }

    public static string GeneratePreviewStylesheet( DesignerDocument doc, PreviewTheme theme )
    {
        var sb = new StringBuilder();
        sb.AppendLine( SelectionRule );
        sb.AppendLine( ( theme ?? PreviewTheme.Default ).Css );
        sb.AppendLine( PreviewMarkerRules );
        var ctx = new ProjectionContext( theme ?? PreviewTheme.Default, ForPreview: true );
        var (_, scss) = Applier.BuildSave( new RecordNode( doc.RootRecord ), "", ctx );
        sb.Append( scss );
        return sb.ToString();
    }

    public static string GenerateCSharp(
        DesignerDocument doc,
        string classNameFallback,
        string namespaceFallback )
    {
        var view = new Projection.CSharp.WiringEnvelopeView(
            doc.Wiring ?? Wiring.WiringEnvelope.Empty,
            namespaceFallback,
            classNameFallback );

        var result = Projection.CSharp.CSharpProjector.Project(
            view, documentHasAnyBindings: doc.HasAnyBindings() );

        return result.Source; // null = elision; caller skips the write
    }

    public static string GenerateSavedScss( DesignerDocument doc, string className, PreviewTheme theme )
    {
        var effectiveTheme = theme ?? PreviewTheme.Default;
        var sb = new StringBuilder();
        sb.AppendLine( $"// designed against theme: {effectiveTheme.Name}" );
        sb.AppendLine( $"{className} {{" );
        var ctx = new ProjectionContext( effectiveTheme, ForPreview: false );
        var (_, scss) = Applier.BuildSave( new RecordNode( doc.RootRecord ), className, ctx );
        sb.Append( scss );
        sb.AppendLine( "}" );
        return sb.ToString();
    }
}