File: MS\Internal\Data\PathParser.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
//
// Description: Parser for the Path of a (CLR) binding
//
 
using System.Collections;
using System.Text;          // StringBuilder
using System.Windows;
using MS.Utility;           // FrugalList
 
namespace MS.Internal.Data
{
    internal enum SourceValueType { Property, Indexer, Direct };
    internal enum DrillIn { Never, IfNeeded, Always };
 
    internal struct SourceValueInfo
    {
        public SourceValueType type;
        public DrillIn drillIn;
        public string name;                 // the name the user supplied - could be "(0)"
        public FrugalObjectList<IndexerParamInfo> paramList;    // params for indexer
        public string propertyName;         // the real name - could be "Width"
 
        public SourceValueInfo(SourceValueType t, DrillIn d, string n)
        {
            type = t;
            drillIn = d;
            name = n;
            paramList = null;
            propertyName = null;
        }
 
        public SourceValueInfo(SourceValueType t, DrillIn d, FrugalObjectList<IndexerParamInfo> list)
        {
            type = t;
            drillIn = d;
            name = null;
            paramList = list;
            propertyName = null;
        }
    }
 
    internal struct IndexerParamInfo
    {
        // parse each indexer param "(abc)xyz" into two pieces - either can be empty
        public string parenString;
        public string valueString;
 
        public IndexerParamInfo(string paren, string value)
        {
            parenString = paren;
            valueString = value;
        }
    }
 
    internal class PathParser
    {
        string _error;
        public String Error { get { return _error; } }
        void SetError(string id, params object[] args) { _error = SR.Format(SR.GetResourceString(id), args); }
 
        enum State { Init, DrillIn, Prop, Done };
 
        // Each level of the path consists of
        //      a property or indexer:
        //                  .propname
        //                  /propname
        //                  [index]
        //                  /[index]
        //          (The . or / is optional in the very first level.)
        // The parser is a finite-state machine with two states corresponding
        // to the two-character lookahead above, plus two more states for the begining
        // and end.  The state transistions are done explicitly in the code below.
        //
        // The parser returns a 0-length array if it finds a syntax error.
        // It sets the Error property, so the caller can find out what happened.
 
        public SourceValueInfo[] Parse(string path)
        {
            _path = (path != null) ? path.Trim() : String.Empty;
            _n = _path.Length;
 
            if (_n == 0)
            {
                // When no path string is specified, use value directly and do not drill-in. (same as Path=".")
                // ClrBindingWorker needs this information to tell XmlBindingWorker about collectionMode.
                return new SourceValueInfo[] { new SourceValueInfo(SourceValueType.Direct, DrillIn.Never, (string)null) };
            }
 
            _index = 0;
            _drillIn = DrillIn.IfNeeded;
 
            _al.Clear();
            _error = null;
            _state = State.Init;
 
            while (_state != State.Done)
            {
                char c = (_index < _n) ? _path[_index] : NullChar;
                if (Char.IsWhiteSpace(c))
                {
                    ++_index;
                    continue;
                }
 
                switch (_state)
                {
                    case State.Init:
                        switch (c)
                        {
                            case '/':
                            case '.':
                            case NullChar:
                                _state = State.DrillIn;
                                break;
                            default:
                                _state = State.Prop;
                                break;
                        }
                        break;
 
                    case State.DrillIn:
                        switch (c)
                        {
                            case '/':
                                _drillIn = DrillIn.Always;
                                ++_index;
                                break;
                            case '.':
                                _drillIn = DrillIn.Never;
                                ++_index;
                                break;
                            case '[':
                            case NullChar:
                                break;
                            default:
                                SetError(nameof(SR.PathSyntax), _path.Substring(0, _index), _path.Substring(_index));
                                return EmptyInfo;
                        }
                        _state = State.Prop;
                        break;
 
                    case State.Prop:
                        bool isIndexer = false;
                        switch (c)
                        {
                            case '[':
                                isIndexer = true;
                                break;
                            default:
                                break;
                        }
 
                        if (isIndexer)
                            AddIndexer();
                        else
                            AddProperty();
 
                        break;
                }
            }
 
 
            SourceValueInfo[] result;
 
            if (_error == null)
            {
                result = new SourceValueInfo[_al.Count];
                _al.CopyTo(result);
            }
            else
            {
                result = EmptyInfo;
            }
 
            return result;
        }
 
        void AddProperty()
        {
            int start = _index;
            int level = 0;
 
            // include leading dots in the path (for XLinq)
            while (_index < _n && _path[_index] == '.')
                ++_index;
 
            while (_index < _n && (level > 0 || !IsSpecialChar(_path[_index])))
            {
                if (_path[_index] == '(')
                    ++level;
                else if (_path[_index] == ')')
                    --level;
 
                ++_index;
            }
 
            if (level > 0)
            {
                SetError(nameof(SR.UnmatchedParen), _path.Substring(start));
                return;
            }
 
            if (level < 0)
            {
                SetError(nameof(SR.UnmatchedParen), _path.Substring(0, _index));
                return;
            }
 
            string name = _path.Substring(start, _index - start).Trim();
 
            SourceValueInfo info = (name.Length > 0)
                ? new SourceValueInfo(SourceValueType.Property, _drillIn, name)
                : new SourceValueInfo(SourceValueType.Direct, _drillIn, (string)null);
 
            _al.Add(info);
 
            StartNewLevel();
        }
 
 
        enum IndexerState { BeginParam, ParenString, ValueString, Done }
 
        void AddIndexer()
        {
            // indexer args are parsed by a (sub-) state machine with four
            // states.  The string is a comma-separated list of params, each
            // of which has two parts:  a "paren string" and a "value string"
            // (both parts are optional).  The character ^ can be used to
            // escape any of the special characters:  comma, parens, ], ^,
            // and white space.
 
            int start = ++_index;       // skip over initial [
            int level = 1;              // level of nested []
 
            bool escaped = false;       // true if current char is escaped
            bool trimRight = false;     // true if value string has trailing white space
 
            StringBuilder parenStringBuilder = new StringBuilder();
            StringBuilder valueStringBuilder = new StringBuilder();
 
            FrugalObjectList<IndexerParamInfo> paramList = new FrugalObjectList<IndexerParamInfo>();
 
            IndexerState state = IndexerState.BeginParam;
            while (state != IndexerState.Done)
            {
                if (_index >= _n)
                {
                    SetError(nameof(SR.UnmatchedBracket), _path.Substring(start - 1));
                    return;
                }
 
                Char c = _path[_index++];
 
                // handle the escape character - set the flag for the next character
                if (c == EscapeChar && !escaped)
                {
                    escaped = true;
                    continue;
                }
 
                switch (state)
                {
                    case IndexerState.BeginParam:   // look for optional (...)
                        if (escaped)
                        {
                            // no '(', go parse the value
                            state = IndexerState.ValueString;
                            goto case IndexerState.ValueString;
                        }
                        else if (c == '(')
                        {
                            // '(' introduces optional paren string
                            state = IndexerState.ParenString;
                        }
                        else if (Char.IsWhiteSpace(c))
                        {
                            // ignore leading white space
                        }
                        else
                        {
                            // no '(', go parse the value
                            state = IndexerState.ValueString;
                            goto case IndexerState.ValueString;
                        }
                        break;
 
                    case IndexerState.ParenString:  // parse (...)
                        if (escaped)
                        {
                            // add an escaped character without question
                            parenStringBuilder.Append(c);
                        }
                        else if (c == ')')
                        {
                            // end of (...), start to parse value
                            state = IndexerState.ValueString;
                        }
                        else
                        {
                            // add normal characters inside (...)
                            parenStringBuilder.Append(c);
                        }
                        break;
 
                    case IndexerState.ValueString:  // parse value
                        if (escaped)
                        {
                            // add an escaped character without question
                            valueStringBuilder.Append(c);
                            trimRight = false;
                        }
                        else if (level > 1)
                        {
                            // inside nested [], add characters without question
                            valueStringBuilder.Append(c);
                            trimRight = false;
 
                            if (c == ']')
                            {
                                --level;
                            }
                        }
                        else if (Char.IsWhiteSpace(c))
                        {
                            // add white space, but trim it later if it's trailing
                            valueStringBuilder.Append(c);
                            trimRight = true;
                        }
                        else if (c == ',' || c == ']')
                        {
                            // end of current paramater - assemble the two parts
                            string parenString = parenStringBuilder.ToString();
                            string valueString = valueStringBuilder.ToString();
                            if (trimRight)
                            {
                                valueString = valueString.TrimEnd();
                            }
 
                            // add the parts to the final result
                            paramList.Add(new IndexerParamInfo(parenString, valueString));
 
                            // reset for the next parameter
                            parenStringBuilder.Length = 0;
                            valueStringBuilder.Length = 0;
                            trimRight = false;
 
                            // after ',' parse next parameter;  after ']' we're done
                            state = (c == ']') ? IndexerState.Done : IndexerState.BeginParam;
                        }
                        else
                        {
                            // add normal characters
                            valueStringBuilder.Append(c);
                            trimRight = false;
 
                            // keep track of nested []
                            if (c == '[')
                            {
                                ++level;
                            }
                        }
                        break;
                }
 
                // after processing each character, clear the escape flag
                escaped = false;
            }
 
            // assemble the final result
            SourceValueInfo info = new SourceValueInfo(
                                        SourceValueType.Indexer,
                                        _drillIn, paramList);
            _al.Add(info);
 
            StartNewLevel();
        }
 
        void StartNewLevel()
        {
            _state = (_index < _n) ? State.DrillIn : State.Done;
            _drillIn = DrillIn.Never;
        }
 
        private static bool IsSpecialChar(char ch)
        {
            return ch == '.' || ch == '/' || ch == '[' || ch == ']';
        }
 
        State _state;
        string _path;
        int _index;
        int _n;
        DrillIn _drillIn;
        ArrayList _al = new ArrayList();
        const char NullChar = Char.MinValue;
        const char EscapeChar = '^';
        static SourceValueInfo[] EmptyInfo = Array.Empty<SourceValueInfo>();
    }
}