File: Language\Components\ComponentWhitespacePass.cs
Web Access
Project: src\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.Linq;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
 
namespace Microsoft.AspNetCore.Razor.Language.Components;
 
internal sealed class ComponentWhitespacePass : ComponentIntermediateNodePassBase, IRazorDirectiveClassifierPass
{
    protected override void ExecuteCore(
        RazorCodeDocument codeDocument,
        DocumentIntermediateNode documentNode,
        CancellationToken cancellationToken)
    {
        if (!IsComponentDocument(documentNode))
        {
            return;
        }
 
        if (documentNode.Options.SuppressPrimaryMethodBody)
        {
            // There's no benefit running the whitespace trimmer if we're not emitting
            // the method bodies.
            return;
        }
 
        // There's no benefit running the whitespace trimmer during design-time builds
        if (documentNode.Options.DesignTime)
        {
            return;
        }
 
        var razorLanguageVersion = codeDocument.ParserOptions.LanguageVersion;
        var useLegacyBehavior = razorLanguageVersion < RazorLanguageVersion.Version_5_0;
        if (useLegacyBehavior)
        {
            // Prior to 5.0, the whitespace pass only applied to the BuildRenderTree method, and
            // only removed the top-level leading and trailing whitespace
 
            var method = documentNode.FindPrimaryMethod();
            if (method != null)
            {
                RemoveContiguousWhitespace(method.Children, TraversalDirection.Forwards);
                RemoveContiguousWhitespace(method.Children, TraversalDirection.Backwards);
            }
        }
        else
        {
            // From 5.0 onwards, the whitespace pass applies as broadly as possible. It removes leading
            // and trailing whitespace from all methods, elements, and child component blocks. There's
            // also a directive that can disable it.
 
            // Respect @preservewhitespace directives
            if (PreserveWhitespaceIsEnabled(documentNode))
            {
                return;
            }
 
            var @class = documentNode.FindPrimaryClass();
            if (@class != null)
            {
                var visitor = new Visitor();
                visitor.Visit(@class);
            }
        }
    }
 
    private static bool PreserveWhitespaceIsEnabled(DocumentIntermediateNode documentNode)
    {
        // If there's no @preservewhitespace attribute, the default is that we *don't* preserve whitespace
        var shouldPreserveWhitespace = false;
 
        foreach (var preserveWhitespaceDirective in documentNode.FindDirectiveReferences(ComponentPreserveWhitespaceDirective.Directive))
        {
            var token = preserveWhitespaceDirective.Node.Tokens.FirstOrDefault();
            var shouldPreserveWhitespaceContent = token?.Content;
            if (shouldPreserveWhitespaceContent != null)
            {
                shouldPreserveWhitespace = string.Equals(shouldPreserveWhitespaceContent, "true", StringComparison.Ordinal);
            }
        }
 
        return shouldPreserveWhitespace;
    }
 
    private static int RemoveContiguousWhitespace(IntermediateNodeCollection nodes, TraversalDirection direction, int? startIndex = null)
    {
        var position = startIndex.GetValueOrDefault(direction == TraversalDirection.Forwards ? 0 : nodes.Count - 1);
        var countRemoved = 0;
        while (position >= 0 && position < nodes.Count)
        {
            var node = nodes[position];
            bool shouldRemoveNode;
            bool shouldContinueIteration;
 
            switch (node)
            {
                case IntermediateToken intermediateToken:
                    shouldRemoveNode = string.IsNullOrWhiteSpace(intermediateToken.Content);
                    shouldContinueIteration = shouldRemoveNode;
                    break;
 
                case HtmlContentIntermediateNode htmlContentIntermediateNode:
                    RemoveContiguousWhitespace(htmlContentIntermediateNode.Children, direction);
                    shouldRemoveNode = htmlContentIntermediateNode.Children.Count == 0;
                    shouldContinueIteration = shouldRemoveNode;
                    break;
 
                case MarkupElementIntermediateNode _:
                case CSharpExpressionIntermediateNode _:
                case TagHelperIntermediateNode _:
                    // These node types may produce non-whitespace output at runtime
                    shouldRemoveNode = false;
                    shouldContinueIteration = false;
                    break;
 
                case CSharpCodeIntermediateNode _:
                    shouldRemoveNode = false;
                    shouldContinueIteration = false;
                    break;
 
                default:
                    shouldRemoveNode = false;
                    shouldContinueIteration = true; // Because other types of nodes don't produce output
                    break;
            }
 
            if (shouldRemoveNode)
            {
                nodes.RemoveAt(position);
                countRemoved++;
                if (direction == TraversalDirection.Forwards)
                {
                    position--;
                }
            }
 
            position += direction == TraversalDirection.Forwards ? 1 : -1;
 
            if (!shouldContinueIteration)
            {
                break;
            }
        }
 
        return countRemoved;
    }
 
    enum TraversalDirection
    {
        Forwards,
        Backwards
    }
 
    class Visitor : IntermediateNodeWalker
    {
        public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode node)
        {
            RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards);
            RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards);
            VisitDefault(node);
        }
 
        public override void VisitMarkupElement(MarkupElementIntermediateNode node)
        {
            RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards);
            RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards);
            VisitDefault(node);
        }
 
        public override void VisitTagHelperBody(TagHelperBodyIntermediateNode node)
        {
            // The goal here is to remove leading/trailing whitespace inside component child content. However,
            // at the time this whitespace pass runs, ComponentChildContent is still TagHelperBody in the tree.
            RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards);
            RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards);
            VisitDefault(node);
        }
 
        public override void VisitDefault(IntermediateNode node)
        {
            // For any CSharpCodeIntermediateNode children, remove their preceding and trailing whitespace
            for (var childIndex = 0; childIndex < node.Children.Count; childIndex++)
            {
                if (node.Children[childIndex] is CSharpCodeIntermediateNode)
                {
                    childIndex -= RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards, childIndex - 1);
                    RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards, childIndex + 1);
                }
            }
 
            base.VisitDefault(node);
        }
    }
}