File: Operations\BalancedNesting.cs
Web Access
Project: src\src\sdk\src\TemplateEngine\Microsoft.TemplateEngine.Core\Microsoft.TemplateEngine.Core.csproj (Microsoft.TemplateEngine.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Core.Contracts;

namespace Microsoft.TemplateEngine.Core.Operations
{
    // TODO: Determine how to reset this if the stream is lead token heavy.
    // Or better yet force balancing, in which case nothing needs to be done.
    // but that requires a lookahead.
    //
    // If (when) more general use cases are found for this type of thing, this class will
    // probably need to be reworked to deal with more generalized balancing
    // and more generalized actions.

    public class BalancedNesting : IOperationProvider
    {
        public static readonly string OperationName = "balancednesting";

        private readonly ITokenConfig _startToken;
        private readonly ITokenConfig _realEndToken;
        private readonly ITokenConfig _pseudoEndToken;
        private readonly string? _resetFlag;
        private readonly bool _initialState;

        public BalancedNesting(ITokenConfig startToken, ITokenConfig realEndToken, ITokenConfig pseudoEndToken, string? id, string? resetFlag, bool initialState)
        {
            _startToken = startToken;
            _realEndToken = realEndToken;
            _pseudoEndToken = pseudoEndToken;
            Id = id;
            _resetFlag = resetFlag;
            _initialState = initialState;
        }

        public string? Id { get; }

        public IOperation GetOperation(Encoding encoding, IProcessorState processorState)
        {
            IToken startToken = _startToken.ToToken(encoding);
            IToken realEndToken = _realEndToken.ToToken(encoding);
            IToken pseudoEndToken = _pseudoEndToken.ToToken(encoding);

            return new Implementation(startToken, realEndToken, pseudoEndToken, Id, _resetFlag, _initialState);
        }

        private class Implementation : IOperation
        {
            // the order they're added to this.Tokens in the constructor must be the same as this!
            private const int StartTokenIndex = 0;

            private const int RealEndTokenIndex = 1;
            private const int PseudoEndTokenIndex = 2;
            private readonly IToken _realEndToken;
            private readonly IToken _pseudoEndToken;
            private readonly string? _resetFlag;
            private int _depth;

            public Implementation(IToken start, IToken realEnd, IToken pseudoEnd, string? id, string? resetFlag, bool initialState)
            {
                _realEndToken = realEnd;
                _pseudoEndToken = pseudoEnd;
                Id = id;
                _resetFlag = resetFlag;
                Tokens = new[] { start, _realEndToken, _pseudoEndToken };
                _depth = 0;
                IsInitialStateOn = string.IsNullOrEmpty(id) || initialState;
            }

            public string? Id { get; }

            public IReadOnlyList<IToken> Tokens { get; }

            public bool IsInitialStateOn { get; }

            public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token)
            {
                // check if this operation has been reset. If so, set _depth = 0
                // this fixes the reset problem, but not the trailing unbalanced pseudo comment problem, i.e. it won't turn this:
                //      <!-- <!-- comment -- >
                // into this:
                //      <!-- <!-- comment -->
                if (processor.Config.Flags.TryGetValue(_resetFlag!, out bool resetFlagValue) && resetFlagValue)
                {
                    processor.Config.Flags.Remove(_resetFlag!);
                    _depth = 0;
                }

                if (token == StartTokenIndex)
                {
                    ++_depth;
                }
                else if (token is RealEndTokenIndex or PseudoEndTokenIndex)
                {
                    --_depth;
                }

                if (_depth < 0)
                {
                    // TODO: determine a better way to deal with this.
                    // The reset operation should limit the scope of the problem.
                    // The depth-zero pseudo-comment is the only one that will be "fixed".
                    // But this could be changed to also fix any where depth < 0.
                    processor.Config.Logger.LogInformation($"Balanced nesting depth < 0. CurrentBufferPosition = {currentBufferPosition}");
                }

                if (_depth == 0 && token == PseudoEndTokenIndex)
                {
                    processor.WriteToTarget(_realEndToken.Value, _realEndToken.Start, _realEndToken.Length);
                    return _pseudoEndToken.Length;  // the source buffer needs to skip over this token.
                }
                else
                {
                    processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length);
                }

                return 0;
            }
        }
    }
}