<|start_filename|>src/AngleSharp.XPath/HtmlDocumentNavigator.cs<|end_filename|>
using AngleSharp.Dom;
using System;
using System.Xml;
using System.Xml.XPath;

namespace AngleSharp.XPath
{
    /// <inheritdoc />
    public class HtmlDocumentNavigator : XPathNavigator
	{
		private readonly IDocument _document;
        private INode _currentNode;
        private readonly bool _ignoreNamespaces;

        /// <summary>
        /// Creates a new XPath navigator for the given document using the provided root node.
        /// </summary>
        /// <param name="document">The document to navigate.</param>
        /// <param name="currentNode">The node to start navigation.</param>
        /// <param name="ignoreNamespaces"></param>
        public HtmlDocumentNavigator(IDocument document, INode currentNode, bool ignoreNamespaces)
		{
			_document = document ?? throw new ArgumentNullException(nameof(document));
            NameTable = new NameTable();
            _currentNode = currentNode ?? throw new ArgumentNullException(nameof(currentNode));
            _ignoreNamespaces = ignoreNamespaces;
        }

        /// <inheritdoc />
        public override string BaseURI => _document.BaseUri;

        /// <summary>
        /// Gets the currently selected node.
        /// </summary>
		public INode CurrentNode => _currentNode;

        /// <summary>
        /// Gets the currently selected element.
        /// </summary>
		private IElement CurrentElement => CurrentNode as IElement;

        /// <inheritdoc />
        public override bool HasAttributes => CurrentElement != null && CurrentElement.Attributes.Length > 0;

        /// <inheritdoc />
        public override bool IsEmptyElement => !_currentNode.HasChildNodes;

        /// <inheritdoc />
        public override string LocalName =>
            CurrentNode is IAttr attr
                ? attr.LocalName
                : NameTable.GetOrAdd(CurrentNode is IElement e ? e.LocalName : string.Empty);

        /// <inheritdoc />
        public override string Name
        {
            get
            {
                if (CurrentNode is IAttr attr)
                {
                    return NameTable.GetOrAdd(attr.Name);
                }

                if (CurrentElement != null)
                {
                    return NameTable.GetOrAdd(CurrentElement.LocalName);
                }

                return NameTable.GetOrAdd(_currentNode.NodeName);
            }
        }

        /// <inheritdoc />
        public override string NamespaceURI
        {
            get
            {
                if (_ignoreNamespaces)
                {
                    return string.Empty;
                }

                return CurrentNode is IAttr attr
                    ? NameTable.GetOrAdd(attr.NamespaceUri ?? string.Empty)
                    : NameTable.GetOrAdd(CurrentElement?.NamespaceUri ?? string.Empty);
            }
        }

        /// <inheritdoc />
        public override string Prefix =>
            CurrentNode is IAttr attr
                ? NameTable.GetOrAdd(attr.Prefix ?? string.Empty)
                : NameTable.GetOrAdd(CurrentElement?.Prefix ?? string.Empty);

        /// <inheritdoc />
        public override XmlNameTable NameTable { get; }

        /// <inheritdoc />
        public override XPathNodeType NodeType
		{
			get
			{
				switch (_currentNode.NodeType)
				{
					case Dom.NodeType.Attribute:
						return XPathNodeType.Attribute;

					case Dom.NodeType.CharacterData:
						return XPathNodeType.Text;

					case Dom.NodeType.Comment:
						return XPathNodeType.Comment;

					case Dom.NodeType.Document:
						return XPathNodeType.Element;

					case Dom.NodeType.DocumentType:
						return XPathNodeType.Element;

                    case Dom.NodeType.Element:
						return XPathNodeType.Element;

                    case Dom.NodeType.ProcessingInstruction:
						return XPathNodeType.ProcessingInstruction;

					case Dom.NodeType.Text:
						return XPathNodeType.Text;

                    case Dom.NodeType.Entity:
                    case Dom.NodeType.EntityReference:
                    case Dom.NodeType.Notation:
                    case Dom.NodeType.DocumentFragment:
                    default:
						throw new NotImplementedException();
				}
			}
		}

        /// <inheritdoc />
        public override string Value
		{
			get
			{
				switch (_currentNode.NodeType)
				{
					case Dom.NodeType.Attribute:
						var attr = (IAttr)_currentNode;
						return attr.Value;

					case Dom.NodeType.CharacterData:
						var cdata = (ICharacterData)_currentNode;
						return cdata.Data;

					case Dom.NodeType.Comment:
						var comment = (IComment)_currentNode;
						return comment.Data;

					case Dom.NodeType.Document:
						return _currentNode.TextContent;

					case Dom.NodeType.DocumentFragment:
						return _currentNode.TextContent;

					case Dom.NodeType.DocumentType:
						var documentType = (IDocumentType)_currentNode;
						return documentType.Name;

					case Dom.NodeType.Element:
						return _currentNode.TextContent;

                    case Dom.NodeType.Entity:
						return _currentNode.TextContent;

					case Dom.NodeType.EntityReference:
						return _currentNode.TextContent;

					case Dom.NodeType.Notation:
						return _currentNode.TextContent;

					case Dom.NodeType.ProcessingInstruction:
						var instruction = (IProcessingInstruction)_currentNode;
						return instruction.Target;

					case Dom.NodeType.Text:
						return _currentNode.TextContent;

					default:
						throw new NotImplementedException();
				}
			}
		}

        /// <inheritdoc />
        public override XPathNavigator Clone()
		{
			return new HtmlDocumentNavigator(_document, _currentNode, _ignoreNamespaces);
		}

        /// <inheritdoc />
        public override bool IsSamePosition(XPathNavigator other)
		{
			if (!(other is HtmlDocumentNavigator navigator))
			{
				return false;
			}

			return navigator._currentNode == _currentNode;
		}

        /// <inheritdoc />
        public override bool MoveTo(XPathNavigator other)
		{
			if (!(other is HtmlDocumentNavigator navigator))
			{
				return false;
			}

			if (navigator._document == _document)
			{
				_currentNode = navigator._currentNode;
				return true;
			}

			return false;
		}

        /// <inheritdoc />
        public override bool MoveToFirstAttribute()
		{
			if (HasAttributes)
            {
                _currentNode = CurrentElement.Attributes[0];
				return true;
			}

			return false;
		}

        /// <inheritdoc />
        public override bool MoveToFirstChild()
		{
			if (_currentNode.FirstChild == null)
			{
				return false;
			}

			_currentNode = _currentNode.FirstChild;
			return true;
		}

        /// <inheritdoc />
        public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope)
		{
			return false;
		}

        /// <inheritdoc />
        public override bool MoveToId(string id)
		{
			var elementById = _document.GetElementById(id);

			if (elementById == null)
			{
				return false;
			}

			_currentNode = elementById;
			return true;
		}

        /// <inheritdoc />
        public override bool MoveToNext()
		{
			if (_currentNode.NextSibling == null)
			{
				return false;
			}

			_currentNode = _currentNode.NextSibling;
			return true;
		}

        /// <inheritdoc />
        public override bool MoveToNextAttribute()
		{
			if (CurrentElement == null)
			{
				return false;
			}

            if (!(CurrentNode is IAttr attr))
            {
                return false;
            }

            if (attr.OwnerElement == null)
            {
                return false;
            }

            var attrIndex = attr.OwnerElement.Attributes.Index(attr);

			if (attrIndex >= CurrentElement.Attributes.Length - 1)
			{
                return false;
			}

            _currentNode = attr.OwnerElement.Attributes[attrIndex + 1];
			return true;
		}

        /// <inheritdoc />
        public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope)
		{
			return false;
		}

        /// <inheritdoc />
        public override bool MoveToParent()
		{
            if (CurrentNode is IAttr attr)
            {
                _currentNode = attr.OwnerElement;
                return true;
            }

			if (_currentNode.Parent == null)
			{
				return false;
			}

			_currentNode = _currentNode.Parent;
			return true;
		}

        /// <inheritdoc />
        public override bool MoveToPrevious()
		{
			if (_currentNode.PreviousSibling == null)
			{
				return false;
			}

			_currentNode = _currentNode.PreviousSibling;
			return true;
		}

        /// <inheritdoc />
        public override void MoveToRoot()
		{
			_currentNode = _document;
		}
    }
}


<|start_filename|>src/AngleSharp.XPath.Tests/HtmlDocumentNavigatorTests.cs<|end_filename|>
using System.Xml;
using AngleSharp.Xml.Parser;
using AngleSharp.Html.Parser;
using NUnit.Framework;
using System.Threading.Tasks;
using System.Xml.XPath;
using AngleSharp.Dom;

namespace AngleSharp.XPath.Tests
{
    [TestFixture]
    public class HtmlDocumentNavigatorTests
    {
        [Test, Retry(5)]
        public async Task SelectSingleNodeTest()
        {
            // Arrange
            const string address = "https://stackoverflow.com/questions/39471800/is-anglesharps-htmlparser-threadsafe";
            var config = Configuration.Default.WithDefaultLoader();
            var document = await BrowsingContext.New(config).OpenAsync(address);

            // Act
            var content = document.DocumentElement.SelectSingleNode("//div[@id='content']");

            // Assert
            Assert.That(content, Is.Not.Null);
        }

        [Test]
        public void SelectNodes_SelectList_ShouldReturnList()
        {
            // Arrange
            const string html =
            @"<ol>
				<li>First</li>
				<li>Second</li>
				<li>Third</li>
			</ol>";
            var parser = new HtmlParser();
            var document = parser.ParseDocument(html);

            // Act
            var nodes = document.DocumentElement.SelectNodes("//li");

            // Assert
            Assert.That(nodes, Has.Count.EqualTo(3));
        }

        [Test]
        public void SelectPrecedingNodeInDocumentWithDoctype_ShouldReturnNode()
        {
            // Arrange
            const string html =
            @"<!DOCTYPE html>
			<body>
				<span></span>
				<div></div>
			</body>";
            var parser = new HtmlParser();
            var document = parser.ParseDocument(html);

            // Act
            var node = document.DocumentElement.SelectSingleNode("//div/preceding::span");

            // Assert
            Assert.That(node, Is.Not.Null);
        }

        [Test]
        public void SelectSingleNode_IgnoreNamespaces_ShouldReturnNode()
        {
            // Arrange
            var xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"><url><loc>https://www.test.com/de/accounts/profile</loc><xhtml:link rel=\"alternate\" hreflang=\"fr\" href=\"https://www.test.com/fr/accounts/profile\" /><xhtml:link rel=\"alternate\" hreflang=\"en\" href=\"https://www.test.com/en/accounts/profile\" /><xhtml:link rel=\"alternate\" hreflang=\"it\" href=\"https://www.test.com/it/accounts/profile\" /><changefreq>weekly</changefreq><priority>0.4</priority></url></urlset>";
            var parser = new XmlParser();
            var doc = parser.ParseDocument(xml);

            // Act
            var node = doc.DocumentElement.SelectSingleNode("/urlset/url/link");

            // Assert
            Assert.IsNotNull(node);
            Assert.That(node.NodeName, Is.EqualTo("xhtml:link"));
        }

        [Test]
        public void SelectSingleNode_DontIgnoreNamespaces_ShouldReturnNode()
        {
            // Arrange
            var xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"><url><loc>https://www.test.com/de/accounts/profile</loc><xhtml:link rel=\"alternate\" hreflang=\"fr\" href=\"https://www.test.com/fr/accounts/profile\" /><xhtml:link rel=\"alternate\" hreflang=\"en\" href=\"https://www.test.com/en/accounts/profile\" /><xhtml:link rel=\"alternate\" hreflang=\"it\" href=\"https://www.test.com/it/accounts/profile\" /><changefreq>weekly</changefreq><priority>0.4</priority></url></urlset>";
            var parser = new XmlParser();
            var doc = parser.ParseDocument(xml);
            var namespaceManager = new XmlNamespaceManager(new NameTable());

            namespaceManager.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");
            namespaceManager.AddNamespace("d", "http://www.sitemaps.org/schemas/sitemap/0.9");

            // Act
            var node = doc.DocumentElement.SelectSingleNode("/d:urlset/d:url/xhtml:link", namespaceManager, false);

            // Assert
            Assert.IsNotNull(node);
            Assert.That(node.NodeName, Is.EqualTo("xhtml:link"));
        }

        [Test]
        public void SelectNodes_CanReturnAttribute()
        {
            // Arrange
            var html = "<!DOCTYPE html><html><body><div class=\"one\"><span class=\"two\">hello world</span></div></body></html>";
            var parser = new HtmlParser();
            var doc = parser.ParseDocument(html);

            // Act
            var nodes = doc.DocumentElement.SelectNodes("//@*");

            // Assert
            Assert.IsNotNull(nodes);
            Assert.That(nodes, Has.Count.EqualTo(2));
            Assert.That(nodes, Is.All.InstanceOf<Dom.IAttr>());
        }

        [Test]
        public void TestNameXPathFunctionOnXMLDoc()
        {
            // Arrange
            var xml = @"<html><head><title>Test</title></head><body><h1>Test</h1></body></html>";
            var angleSharpXmlDoc = new XmlParser().ParseDocument(xml);

            // Act
            var xmlNav = angleSharpXmlDoc.CreateNavigator();

            // Assert
            Assert.AreEqual(TagNames.Html, xmlNav.Evaluate("name()"));
        }

        [Test]
        public void TestNameXPathFunctionOnHTMLDoc()
        {
            // Arrange
            var html = @"<html><head><title>Test</title></head><body><h1>Test</h1></body></html>";

            var angleSharpHtmlDoc = new HtmlParser().ParseDocument(html);

            // Act
            var htmlNav = angleSharpHtmlDoc.CreateNavigator();

            // Assert
            Assert.AreEqual(TagNames.Html, htmlNav.Evaluate("name()"));
        }

        [Test]
        public void MoveToParent_CallWhenCurrentNodeIsAttr_ShouldBeMovedToAttrOwnerElement()
        {
            // Arrange
            var xml = @"<root att1='value 1' att2='value 2'><child>foo</child></root>";
            var parser = new XmlParser();
            var doc = parser.ParseDocument(xml);
            var nav = doc.CreateNavigator(false);
            nav.MoveToChild("root", "");

            // Act

            if (nav.MoveToFirstAttribute())
            {
                do
                {
                    Assert.AreEqual(nav.NodeType, XPathNodeType.Attribute);
                }
                while (nav.MoveToNextAttribute());
                nav.MoveToParent();
            }

            // Assert
            Assert.AreEqual(nav.Name, "root");
        }
    }
}
