// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Blazor.Shared; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; using System; using System.Collections.Generic; namespace Microsoft.AspNetCore.Blazor.Razor { /// /// Keeps track of the nesting of elements/containers while writing out the C# source code /// for a component. This allows us to detect mismatched start/end tags, as well as inject /// additional C# source to capture component descendants in a lambda. /// internal class ScopeStack { private readonly Stack _stack = new Stack(); private int _builderVarNumber = 1; public string BuilderVarName { get; private set; } = "builder"; public void OpenScope(string tagName, bool isComponent) { _stack.Push(new ScopeEntry(tagName, isComponent)); } public void CloseScope(CodeRenderingContext context, string tagName, bool isComponent, SourceSpan? source) { if (_stack.Count == 0) { var diagnostic = BlazorDiagnosticFactory.Create_UnexpectedClosingTag(source ?? SourceSpan.Undefined, tagName); throw new RazorCompilerException(diagnostic); } var currentScope = _stack.Pop(); if (!tagName.Equals(currentScope.TagName, StringComparison.Ordinal)) { var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTag(source, currentScope.TagName, tagName); throw new RazorCompilerException(diagnostic); } // Note: there's no unit test to cover the following, because there's no known way of // triggering it from user code (i.e., Razor source code). But the check is here anyway // just in case one day it turns out there is some way of causing this error. if (isComponent != currentScope.IsComponent) { var kind = isComponent ? "component" : "element"; var expectedKind = currentScope.IsComponent ? "component" : "element"; var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTagKind(source, tagName, kind, expectedKind); throw new RazorCompilerException(diagnostic); } // When closing the scope for a component with children, it's time to close the lambda if (currentScope.LambdaScope != null) { currentScope.LambdaScope.Dispose(); context.CodeWriter.Write(")"); context.CodeWriter.WriteEndMethodInvocation(); OffsetBuilderVarNumber(-1); } } public void IncrementCurrentScopeChildCount(CodeRenderingContext context) { if (_stack.Count > 0) { var currentScope = _stack.Peek(); if (currentScope.IsComponent && currentScope.ChildCount == 0) { // When we're about to insert the first child into a component, // it's time to open a new lambda var blazorNodeWriter = (BlazorNodeWriter)context.NodeWriter; blazorNodeWriter.BeginWriteAttribute(context.CodeWriter, BlazorApi.RenderTreeBuilder.ChildContent); OffsetBuilderVarNumber(1); context.CodeWriter.Write($"({BlazorApi.RenderFragment.FullTypeName})("); currentScope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName); } currentScope.ChildCount++; } } private void OffsetBuilderVarNumber(int delta) { _builderVarNumber += delta; BuilderVarName = _builderVarNumber == 1 ? "builder" : $"builder{_builderVarNumber}"; } private class ScopeEntry { public readonly string TagName; public readonly bool IsComponent; public int ChildCount; public IDisposable LambdaScope; public ScopeEntry(string tagName, bool isComponent) { TagName = tagName; IsComponent = isComponent; ChildCount = 0; } } } }