File: Language\Intermediate\IntermediateNodeFormatter.cs
Web Access
Project: src\src\roslyn\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace Microsoft.AspNetCore.Razor.Language.Intermediate;

public sealed class IntermediateNodeFormatter(
    StringBuilder builder,
    IntermediateNodeFormatter.FormatterMode mode = IntermediateNodeFormatter.FormatterMode.PreferContent,
    bool includeSource = false)
{
    // Depending on the usage of the formatter we might prefer thoroughness (properties)
    // or brevity (content). Generally if a node has a single string that provides value
    // it has content.
    //
    // Some nodes have neither: TagHelperBody
    // Some nodes have content: HtmlContent
    // Some nodes have properties: Document
    // Some nodes have both: TagHelperProperty
    public enum FormatterMode
    {
        PreferContent,
        PreferProperties,
    }

    private static readonly Dictionary<Type, string> s_nodeTypeToShortNameMap = [];

    private static readonly char[] s_charsToEscape = ['\r', '\n', '\t'];

    private readonly StringBuilder _builder = builder;
    private readonly FormatterMode Mode = mode;
    private readonly bool _includeSource = includeSource;

    private readonly Dictionary<string, string> _properties = new(StringComparer.Ordinal);
    private string? _content;

    private static string GetNodeShortName(IntermediateNode node)
    {
        lock (s_nodeTypeToShortNameMap)
        {
            return s_nodeTypeToShortNameMap.GetOrAdd(node.GetType(), ComputeShortName);
        }

        static string ComputeShortName(Type type)
        {
            const string ShortNameSuffix = nameof(IntermediateNode);

            var shortName = type.Name;

            if (shortName.EndsWith(ShortNameSuffix, StringComparison.Ordinal))
            {
                shortName = shortName[..^ShortNameSuffix.Length];
            }

            return shortName;
        }
    }

    public void WriteChildren(IntermediateNodeCollection children)
    {
        Debug.Assert(children != null);

        _builder.Append(" \"");

        foreach (var child in children)
        {
            if (child is IntermediateToken token)
            {
                WriteEscaped(token.Content);
            }
        }

        _builder.Append('"');
    }

    public void WriteContent(string? content)
    {
        if (content != null)
        {
            _content = content;
        }
    }

    public void WriteProperty(string key, string? value)
    {
        Debug.Assert(key != null);

        if (value != null)
        {
            _properties.Add(key, value);
        }
    }

    public void FormatNode(IntermediateNode node)
    {
        _builder.Append(GetNodeShortName(node));

        if (_includeSource)
        {
            _builder.Append(' ');
            _builder.Append(node.Source?.ToString() ?? "(n/a)");
        }

        node.FormatNode(this);

        if (ShouldWriteContent())
        {
            _builder.Append(" \"");
            WriteEscaped(_content);
            _builder.Append('"');
        }

        if (ShouldWriteProperties())
        {
            _builder.Append(" { ");

            var first = true;

            foreach (var (key, value) in _properties)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    _builder.Append(", ");
                }

                _builder.Append(key);
                _builder.Append(": \"");
                WriteEscaped(value);
                _builder.Append('"');
            }

            _builder.Append(" }");
        }

        _content = null;
        _properties.Clear();
    }

    [MemberNotNullWhen(true, nameof(_content))]
    private bool ShouldWriteContent()
        => _content != null && (_properties.Count == 0 || Mode == FormatterMode.PreferContent);

    private bool ShouldWriteProperties()
        => _properties.Count > 0 && (_content == null || Mode == FormatterMode.PreferContent);

    public void FormatTree(IntermediateNode node)
    {
        var visitor = new FormatterVisitor(this);
        visitor.Visit(node);
    }

    private void Write(char value, int repeatCount)
    {
        _builder.Append(value, repeatCount);
    }

    private void WriteLine()
    {
        _builder.AppendLine();
    }

    private void WriteEscaped(string content)
    {
        var startIndex = 0;
        int charToEscapeIndex;

        while ((charToEscapeIndex = content.IndexOfAny(s_charsToEscape, startIndex)) >= 0)
        {
            if (startIndex < charToEscapeIndex)
            {
                _builder.Append(content, startIndex, charToEscapeIndex - startIndex);
            }

            switch (content[charToEscapeIndex])
            {
                case '\r':
                    _builder.Append("\\r");
                    break;
                case '\n':
                    _builder.Append("\\n");
                    break;
                case '\t':
                    _builder.Append("\\t");
                    break;
            }

            startIndex = charToEscapeIndex + 1;
        }

        if (startIndex < content.Length)
        {
            _builder.Append(content, startIndex, content.Length - startIndex);
        }
    }

    private sealed class FormatterVisitor(IntermediateNodeFormatter formatter) : IntermediateNodeWalker
    {
        private const int IndentSize = 2;

        private readonly IntermediateNodeFormatter _formatter = formatter;
        private int _indent;

        public override void VisitDefault(IntermediateNode node)
        {
            // Indent
            _formatter.Write(' ', repeatCount: _indent);

            _formatter.FormatNode(node);
            _formatter.WriteLine();

            // Process children
            _indent += IndentSize;
            base.VisitDefault(node);
            _indent -= IndentSize;
        }
    }
}