/// <summary>
        /// Extracts a block of text delimited by the specified open and close
        /// characters. It is assumed the parser is positioned at an
        /// occurrence of the open character. The open and closing characters
        /// are not included in the returned string. On return, the parser is
        /// positioned at the closing character or at the end of the text if
        /// the closing character was not found.
        /// </summary>
        /// <param name="parser">ParsingHelper object</param>
        /// <param name="openChar">Start-of-block delimiter</param>
        /// <param name="closeChar">End-of-block delimiter</param>
        /// <returns>The extracted text</returns>
        internal static string ExtractBlock(ParsingHelper parser, char openChar, char closeChar)
        {
            // Track delimiter depth
            int depth = 1;

            // Extract characters between delimiters
            parser.MoveAhead();
            int start = parser.Index;

            while (!parser.EndOfText)
            {
                char ch = parser.Peek();
                if (ch == openChar)
                {
                    // Increase block depth
                    depth++;
                }
                else if (ch == closeChar)
                {
                    // Decrease block depth
                    depth--;
                    // Test for end of block
                    if (depth == 0)
                    {
                        break;
                    }
                }
                else if (ch == '"' || ch == '\'')
                {
                    // Don't count delimiters within quoted text
                    parser.MoveAhead();
                    parser.SkipWhile(c => c != ch);
                }
                // Move to next character
                parser.MoveAhead();
            }
            return(parser.Extract(start, parser.Index));
        }
        /// <summary>
        /// Parses a query segment and converts it to an expression
        /// tree.
        /// </summary>
        /// <param name="query">Query segment to be converted.</param>
        /// <param name="defaultConjunction">Implicit conjunction type.</param>
        /// <returns>Root node of expression tree.</returns>
        internal INode?ParseNode(string?query, ConjunctionType defaultConjunction)
        {
            ConjunctionType conjunction = defaultConjunction;
            TermForm        termForm    = TermForm.Inflectional;
            bool            termExclude = false;
            bool            resetState  = true;
            INode?          root        = null;
            INode?          node;
            string          term;

            ParsingHelper parser = new ParsingHelper(query);

            while (!parser.EndOfText)
            {
                if (resetState)
                {
                    // Reset modifiers
                    conjunction = defaultConjunction;
                    termForm    = TermForm.Inflectional;
                    termExclude = false;
                    resetState  = false;
                }

                parser.SkipWhitespace();
                if (parser.EndOfText)
                {
                    break;
                }

                char ch = parser.Peek();
                if (Punctuation.Contains(ch))
                {
                    switch (ch)
                    {
                    case '"':
                    case '\'':
                        termForm = TermForm.Literal;
                        parser.MoveAhead();
                        term       = parser.ParseWhile(c => c != ch);
                        root       = AddNode(root, term.Trim(), termForm, termExclude, conjunction);
                        resetState = true;
                        break;

                    case '(':
                        // Parse parentheses block
                        term       = ExtractBlock(parser, '(', ')');
                        node       = ParseNode(term, defaultConjunction);
                        root       = AddNode(root, node, conjunction, true);
                        resetState = true;
                        break;

                    case '<':
                        // Parse angle brackets block
                        term       = ExtractBlock(parser, '<', '>');
                        node       = ParseNode(term, ConjunctionType.Near);
                        root       = AddNode(root, node, conjunction);
                        resetState = true;
                        break;

                    case '-':
                        // Match when next term is not present
                        termExclude = true;
                        break;

                    case '+':
                        // Match next term exactly
                        termForm = TermForm.Literal;
                        break;

                    case '~':
                        // Match synonyms of next term
                        termForm = TermForm.Thesaurus;
                        break;

                    default:
                        break;
                    }
                    // Advance to next character
                    parser.MoveAhead();
                }
                else
                {
                    // Parse this query term
                    term = parser.ParseWhile(c => !Punctuation.Contains(c) && !char.IsWhiteSpace(c));

                    // Allow trailing wildcard
                    if (parser.Peek() == '*')
                    {
                        term += parser.Peek();
                        parser.MoveAhead();
                        termForm = TermForm.Literal;
                    }

                    // Interpret term
                    StringComparer comparer = StringComparer.OrdinalIgnoreCase;
                    if (comparer.Compare(term, "AND") == 0)
                    {
                        conjunction = ConjunctionType.And;
                    }
                    else if (comparer.Compare(term, "OR") == 0)
                    {
                        conjunction = ConjunctionType.Or;
                    }
                    else if (comparer.Compare(term, "NEAR") == 0)
                    {
                        conjunction = ConjunctionType.Near;
                    }
                    else if (comparer.Compare(term, "NOT") == 0)
                    {
                        termExclude = true;
                    }
                    else
                    {
                        root       = AddNode(root, term, termForm, termExclude, conjunction);
                        resetState = true;
                    }
                }
            }
            return(root);
        }