| File: Language\Components\ComponentMarkupBlockPass.cs | Web Access |
| Project: 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.Linq; using System.Text; using System.Threading; using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Razor.Language.Components; // Rewrites contiguous subtrees of HTML into a special node type to reduce the // size of the Render tree. // // Does not preserve insignificant details of the HTML, like tag closing style // or quote style. internal sealed class ComponentMarkupBlockPass : ComponentIntermediateNodePassBase, IRazorOptimizationPass { private readonly RazorLanguageVersion _version; public ComponentMarkupBlockPass(RazorLanguageVersion version) { _version = version; } // Runs LATE because we want to destroy structure. // // We also need to run after ComponentMarkupDiagnosticPass to avoid destroying diagnostics // added in that pass. public override int Order => ComponentMarkupDiagnosticPass.DefaultOrder + 10; protected override void ExecuteCore( RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode, CancellationToken cancellationToken) { if (!IsComponentDocument(documentNode)) { return; } var findVisitor = new FindHtmlTreeVisitor(_version); findVisitor.Visit(documentNode); var trees = findVisitor.Trees; var rewriteVisitor = new RewriteVisitor(trees); while (trees.Count > 0) { // Walk backwards since we did a postorder traversal. var reference = trees[trees.Count - 1]; // Forcibly remove a node to prevent infinite loops. trees.RemoveAt(trees.Count - 1); // We want to fold together siblings where possible. To do this, first we find // the index of the node we're looking at now - then we need to walk backwards // and identify a set of contiguous nodes we can merge. var start = reference.Parent.Children.Count - 1; for (; start >= 0; start--) { if (ReferenceEquals(reference.Node, reference.Parent.Children[start])) { break; } } // This is the current node. Check if the left sibling is always a candidate // for rewriting. Due to the order we processed the nodes, we know that the // left sibling is next in the list to process if it's a candidate. var end = start; while (start - 1 >= 0) { var candidate = reference.Parent.Children[start - 1]; if (trees.Count == 0 || !ReferenceEquals(trees[trees.Count - 1].Node, candidate)) { // This means the we're out of nodes, or the left sibling is not in the list. break; } // This means that the left sibling is valid to merge. start--; // Remove this since we're combining it. trees.RemoveAt(trees.Count - 1); } // As a degenerate case, don't bother rewriting an single HtmlContent node // It doesn't add any value. if (end - start == 0 && reference.Node is HtmlContentIntermediateNode) { continue; } // Now we know the range of nodes to rewrite (end is inclusive) var length = end + 1 - start; while (length > 0) { // Keep using start since we're removing nodes. var node = reference.Parent.Children[start]; reference.Parent.Children.RemoveAt(start); rewriteVisitor.Visit(node); length--; } reference.Parent.Children.Insert(start, new MarkupBlockIntermediateNode() { Content = rewriteVisitor.Builder.ToString(), }); rewriteVisitor.Builder.Clear(); } } // Finds HTML-blocks using a postorder traversal. We store nodes in an // ordered list so we can avoid redundant rewrites. // // Consider a case like: // <div> // <a href="...">click me</a> // </div> // // We would store both the div and a tag in a list, but make sure to visit // the div first. Then when we process the div (recursively), we would remove // the a from the list. private class FindHtmlTreeVisitor : IntermediateNodeWalker { private readonly RazorLanguageVersion _version; public FindHtmlTreeVisitor(RazorLanguageVersion version) { _version = version; } private bool _foundNonHtml; public List<IntermediateNodeReference> Trees { get; } = new List<IntermediateNodeReference>(); public override void VisitDefault(IntermediateNode node) { // If we get here, we found a non-HTML node. Keep traversing. _foundNonHtml = true; base.VisitDefault(node); } public override void VisitMarkupElement(MarkupElementIntermediateNode node) { // We need to restore the state after processing this node. // We might have found a leaf-block of HTML, but that shouldn't // affect our parent's state. var originalState = _foundNonHtml; _foundNonHtml = false; if (node.HasDiagnostics) { // Treat node with errors as non-HTML - don't let the parent rewrite this either. _foundNonHtml = true; } if (_version <= RazorLanguageVersion.Version_7_0 && string.Equals("script", node.TagName, StringComparison.OrdinalIgnoreCase)) { // Treat script tags as non-HTML in .NET 7 and earlier. _foundNonHtml = true; } else if (string.Equals("option", node.TagName, StringComparison.OrdinalIgnoreCase)) { // Also, treat <option>...</option> as non-HTML - we don't want it to be coalesced so that we can support setting "selected" attribute on it. // We only care about option tags that are nested under a select tag. foreach (var ancestor in Ancestors) { if (ancestor is MarkupElementIntermediateNode element && string.Equals("select", element.TagName, StringComparison.OrdinalIgnoreCase)) { _foundNonHtml = true; break; } } } base.VisitDefault(node); if (!_foundNonHtml) { Trees.Add(new IntermediateNodeReference(node, Parent!)); } _foundNonHtml = originalState |= _foundNonHtml; } public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node) { if (node.HasDiagnostics) { // Treat node with errors as non-HTML _foundNonHtml = true; } // Visit Children base.VisitDefault(node); } public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) { if (node.HasDiagnostics) { // Treat node with errors as non-HTML _foundNonHtml = true; } // Visit Children base.VisitDefault(node); } public override void VisitHtml(HtmlContentIntermediateNode node) { // We need to restore the state after processing this node. // We might have found a leaf-block of HTML, but that shouldn't // affect our parent's state. var originalState = _foundNonHtml; _foundNonHtml = false; if (node.HasDiagnostics) { // Treat node with errors as non-HTML _foundNonHtml = true; } // Visit Children base.VisitDefault(node); if (!_foundNonHtml) { Trees.Add(new IntermediateNodeReference(node, Parent!)); } _foundNonHtml = originalState |= _foundNonHtml; } public override void VisitToken(IntermediateToken node) { if (node.HasDiagnostics) { // Treat node with errors as non-HTML _foundNonHtml = true; } if (node is CSharpIntermediateToken) { _foundNonHtml = true; } } } private class RewriteVisitor : IntermediateNodeWalker { private readonly List<IntermediateNodeReference> _trees; public RewriteVisitor(List<IntermediateNodeReference> trees) { _trees = trees; } public StringBuilder Builder { get; } = new StringBuilder(); public override void VisitMarkupElement(MarkupElementIntermediateNode node) { for (var i = 0; i < _trees.Count; i++) { // Remove this node if it's in the list. This ensures that we don't // do redundant operations. if (ReferenceEquals(_trees[i].Node, node)) { _trees.RemoveAt(i); break; } } var isVoid = Legacy.ParserHelpers.VoidElements.Contains(node.TagName); var hasBodyContent = node.Body.Any(); Builder.Append('<'); Builder.Append(node.TagName); foreach (var attribute in node.Attributes) { Visit(attribute); } // If for some reason a void element contains body, then treat it as a // start/end tag. if (!hasBodyContent && isVoid) { // void Builder.Append('>'); return; } else if (!hasBodyContent) { // In HTML5, we can't have self-closing non-void elements, so explicitly // add a close tag Builder.Append("></"); Builder.Append(node.TagName); Builder.Append('>'); return; } // start/end tag with body. Builder.Append('>'); foreach (var item in node.Body) { Visit(item); } Builder.Append("</"); Builder.Append(node.TagName); Builder.Append('>'); } public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node) { Builder.Append(' '); Builder.Append(node.AttributeName); if (node.Children.Count == 0) { // Minimized attribute return; } // We examine the node.Prefix (e.g. " onfocus='" or " on focus=\"") // to preserve the quote type that is used in the original markup. var quoteType = node.Prefix.EndsWith("'", StringComparison.Ordinal) ? "'" : "\""; Builder.Append('='); Builder.Append(quoteType); // Visit Children base.VisitDefault(node); Builder.Append(quoteType); } public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) { for (var i = 0; i < node.Children.Count; i++) { Builder.Append(node.Prefix); if (node.Children[i] is IntermediateToken token) { Builder.Append(token.Content); } } } public override void VisitHtml(HtmlContentIntermediateNode node) { for (var i = 0; i < node.Children.Count; i++) { if (node.Children[i] is IntermediateToken token) { Builder.Append(token.Content); } } } } }