/// <summary>
        /// Advances the parser until the specified object is closed i.e. has a closing tag.
        /// </summary>
        /// <param name="parser">A spine parser. Its state will be modified.</param>
        /// <param name="ob">The object to complete</param>
        /// <param name="text">The text snapshot corresponding to the parser.</param>
        /// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
        /// <returns>Whether the object was successfully completed</returns>
        public static bool AdvanceUntilClosed(this XmlSpineParser parser, XObject ob, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
        {
            var el = ob as XElement;

            if (el == null)
            {
                return(AdvanceUntilEnded(parser, ob, text, maximumReadahead));
            }

            var startingDepth = parser.Spine.Count;

            var end = Math.Min(text.Length - parser.Position, maximumReadahead) + parser.Position;

            while (parser.Position < end)
            {
                parser.Push(text[parser.Position]);
                if (el.IsClosed)
                {
                    return(true);
                }
                // just in case, bail if we pop out past the element's parent
                if (parser.Spine.Count < startingDepth - 1)
                {
                    return(false);
                }
            }
            return(false);
        }
        /// <summary>
        /// Advances the parser until the specified object is ended.
        /// </summary>
        /// <param name="parser">A spine parser. Its state will be modified.</param>
        /// <param name="ob">The object to complete</param>
        /// <param name="text">The text snapshot corresponding to the parser.</param>
        /// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
        /// <returns>Whether the object was successfully completed</returns>
        public static bool AdvanceUntilEnded(this XmlSpineParser parser, XObject ob, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
        {
            var startingDepth = parser.Spine.Count;

            var end = Math.Min(text.Length - parser.Position, maximumReadahead) + parser.Position;

            while (parser.Position < end)
            {
                parser.Push(text[parser.Position]);
                if (ob.IsEnded)
                {
                    return(true);
                }
                if (parser.Spine.Count < startingDepth)
                {
                    return(false);
                }
            }
            // if at end of document, consider text nodes to be ended anyways
            if (parser.Position == text.Length && ob is XText xt)
            {
                xt.End(text.GetTextBetween(xt.Span.Start, text.Length));
            }
            return(false);
        }
        /// <summary>
        /// Advances the parser to end the node at the current position and gets that node's path.
        /// </summary>
        /// <param name="parser">A spine parser. Its state will be modified.</param>
        /// <param name="text">The text snapshot corresponding to the parser.</param>
        /// <returns></returns>
        public static List <XObject> AdvanceToNodeEndAndGetNodePath(this XmlSpineParser parser, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
        {
            var context = parser.GetContext();

            int startOffset = parser.Position;
            int startDepth  = parser.Spine.Count;

            //if in potential start of a state, advance into the next state
            var end = Math.Min(text.Length - context.Position, maximumReadahead) + context.Position;

            if (parser.Position < end && (XmlRootState.IsNotFree(context) || (context.CurrentState is XmlRootState && text[parser.Position] == '<')))
            {
                do
                {
                    parser.Push(text[parser.Position]);
                } while (parser.Position < end && XmlRootState.IsNotFree(context));

                //if it transitioned to another state, eat until we get a new node on the stack
                if (parser.Position < end && !(context.CurrentState is XmlRootState) && context.Nodes.Count <= startDepth)
                {
                    parser.Push(text[parser.Position]);
                }
            }

            var path = parser.Spine.ToNodePath();

            // make sure the leaf node is ended
            if (path.Count > 0)
            {
                var leaf = path[path.Count - 1];
                if (!(leaf is XDocument))
                {
                    AdvanceUntilEnded(parser, leaf, text, maximumReadahead - (parser.Position - startOffset));
                }
                //the leaf node might have a child that's a better match for the offset
                if (leaf is XContainer c && c.FindAtOffset(startOffset) is XObject o)
                {
                    path.Add(o);
                }
            }

            return(path);
        }