private static ContainerOrTerminalNode GetNodeToAdjust(Container parent, TerminalNode comment, int index, int first, int last)
        {
            var containers = parent.Children;

            if (index == first && index == last)
            {
                return(parent);
            }

            if (index == first)
            {
                return(containers[index + 1]);
            }

            if (index == last)
            {
                return(containers[index - 1]);
            }

            var next   = containers[index + 1];
            var before = containers[index - 1];

            var trailing = comment.LocationSpan.Start.LineNumber == before.LocationSpan.End.LineNumber;

            return(trailing ? before : next);
        }
        private static void Parse(XmlTextReader reader, Container parent, CharacterPositionFinder finder, IXmlFlavor flavor)
        {
            var nodeType = reader.NodeType;

            switch (nodeType)
            {
            case XmlNodeType.Element:
            {
                ParseElement(reader, parent, finder, flavor);
                break;
            }

            case XmlNodeType.ProcessingInstruction:
            case XmlNodeType.Comment:
            case XmlNodeType.XmlDeclaration:
            case XmlNodeType.CDATA:
            case XmlNodeType.Text:
            {
                ParseTerminalNode(reader, parent, finder, flavor);
                break;
            }

            default:
            {
                reader.Read();
                break;
            }
            }
        }
        private static LineInfo FixRoot(Container root, Container dummyRoot)
        {
            if (dummyRoot.Children.Any())
            {
                // there might be no declaration, such as when trying to parse XAML files
                var xmlDeclaration = dummyRoot.Children.OfType <TerminalNode>().FirstOrDefault(_ => _.Type == NodeType.XmlDeclaration);
                if (xmlDeclaration != null)
                {
                    // let root include the XML declaration
                    AdjustRoot(root, xmlDeclaration);
                }
                else
                {
                    // there might be a comment
                    var xmlComment = dummyRoot.Children.OfType <TerminalNode>().FirstOrDefault(_ => _.Type == NodeType.Comment);
                    if (xmlComment != null)
                    {
                        // so is it the first element?
                        if (dummyRoot.Children.IndexOf(xmlComment) == 0)
                        {
                            // let root include the comment
                            AdjustRoot(root, xmlComment);
                        }
                    }
                }
            }

            return(root.LocationSpan.End);
        }
        private static void ParseElement(XmlTextReader reader, Container parent, CharacterPositionFinder finder, IXmlFlavor flavor)
        {
            var name    = flavor.GetName(reader);
            var type    = flavor.GetType(reader);
            var content = flavor.GetContent(reader);

            var container = new Container
            {
                Type    = type,
                Name    = name,
                Content = content,
            };

            var isEmpty = reader.IsEmptyElement;

            var startingSpan = GetLocationSpanWithParseAttributes(reader, container, finder, flavor);
            var headerSpan   = GetCharacterSpan(startingSpan, finder);

            if (isEmpty)
            {
                // there is no content, so we have to get away of the footer by just using the '/>' characters as footer
                var headerSpanCorrected = new CharacterSpan(headerSpan.Start, Math.Max(headerSpan.Start, headerSpan.End - 2));
                var footerSpanCorrected = new CharacterSpan(Math.Max(0, headerSpan.End - 1), headerSpan.End);

                container.LocationSpan = startingSpan;
                container.HeaderSpan   = headerSpanCorrected;
                container.FooterSpan   = footerSpanCorrected;
            }
            else
            {
                while (reader.NodeType != XmlNodeType.EndElement)
                {
                    Parse(reader, container, finder, flavor);

                    // we had a side effect (reading further on stream to get the location span), so we have to check whether we found already an element or end element
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        continue;
                    }

                    if (reader.NodeType == XmlNodeType.EndElement)
                    {
                        break;
                    }
                }

                var endingSpan = GetLocationSpan(reader);
                var footerSpan = GetCharacterSpan(endingSpan, finder);

                container.LocationSpan = new LocationSpan(startingSpan.Start, endingSpan.End);
                container.HeaderSpan   = headerSpan;
                container.FooterSpan   = footerSpan;
            }

            // check whether we can use a terminal node instead
            var child = flavor.FinalAdjustAfterParsingComplete(container);

            parent.Children.Add(child);
        }
        public void PrepareTest()
        {
            var parentDirectory = Directory.GetParent(new Uri(GetType().Assembly.Location).LocalPath).FullName;
            var fileName        = Path.Combine(parentDirectory, "Resources", "CommentWithoutDeclaration.xml");

            _objectUnderTest = Parser.Parse(fileName);
            _root            = _objectUnderTest.Children.Single();
        }
        public void PrepareTest()
        {
            var parentDirectory = Directory.GetParent(new Uri(GetType().Assembly.Location).LocalPath).FullName;
            var fileName        = Path.Combine(parentDirectory, "Resources", "test_with_umlaut_and_wrong_encoding.xml");

            _objectUnderTest = Parser.Parse(fileName);
            _root            = _objectUnderTest.Children.Single();
        }
        private static void AdjustRoot(Container root, TerminalNode node)
        {
            var rootStart = node.LocationSpan.Start;

            // adjust positions
            root.LocationSpan = new LocationSpan(rootStart, root.LocationSpan.End);
            root.HeaderSpan   = new CharacterSpan(node.Span.Start, root.HeaderSpan.End);
        }
        public void Xml_without_declaration_can_be_read()
        {
            var parentDirectory = Directory.GetParent(new Uri(GetType().Assembly.Location).LocalPath).FullName;
            var fileName        = Path.Combine(parentDirectory, "Resources", "test_without_declaration.xml");

            _objectUnderTest = Parser.Parse(fileName);
            _root            = _objectUnderTest.Children.Single();

            Assert.That(_objectUnderTest.LocationSpan.Start, Is.EqualTo(new LineInfo(1, 0)), "Wrong start");
            Assert.That(_objectUnderTest.LocationSpan.End, Is.EqualTo(new LineInfo(28, 0)), "Wrong end");

            Assert.That(_objectUnderTest.FooterSpan, Is.EqualTo(new CharacterSpan(495, 496)), "Wrong footer");
        }
        public void PrepareTest()
        {
            var parentDirectory = Directory.GetParent(new Uri(GetType().Assembly.Location).LocalPath).FullName;
            var fileName        = Path.Combine(parentDirectory, "Resources", "Xaml_ResourceDictionary.xml");

            // we need to adjust line breaks because Git checkout on AppVeyor (or elsewhere) will adjust the line breaks
            var originalContent = File.ReadAllText(fileName);

            File.WriteAllText(fileName, originalContent.Replace(Environment.NewLine, "\n"));

            _objectUnderTest = Parser.Parse(fileName);
            _root            = _objectUnderTest.Children.Single();
        }
        private static void ParseAttributes(XmlTextReader reader, Container parent, CharacterPositionFinder finder, IXmlFlavor flavor)
        {
            if (flavor.ParseAttributesEnabled)
            {
                reader.MoveToFirstAttribute();

                // read all attributes
                do
                {
                    ParseAttribute(reader, parent, finder, flavor);
                }while (reader.MoveToNextAttribute());
            }
        }
        private static void ParseTerminalNode(XmlTextReader reader, Container parent, CharacterPositionFinder finder, IXmlFlavor flavor)
        {
            var name    = flavor.GetName(reader);
            var type    = flavor.GetType(reader);
            var content = flavor.GetContent(reader);

            var locationSpan = GetLocationSpan(reader);
            var span         = GetCharacterSpan(locationSpan, finder);

            var child = AddTerminalNode(parent, type, name, content, locationSpan, span);

            flavor.FinalAdjustAfterParsingComplete(child);
        }
        private static TerminalNode AddTerminalNode(Container parent, string type, string name, string content, LocationSpan locationSpan, CharacterSpan span)
        {
            var child = new TerminalNode
            {
                Type         = type,
                Name         = name,
                Content      = content,
                LocationSpan = locationSpan,
                Span         = span,
            };

            parent.Children.Add(child);

            return(child);
        }
        private static void Clean(Container parent, bool initial = false)
        {
            var children = parent.Children;

            const int first = 0;
            var       last  = children.Count - 1;

            var i = first;

            while (i <= last)
            {
                var child = children[i];
                if (child is Container c)
                {
                    Clean(c);
                }
                else
                {
                    var comment = child as TerminalNode;
                    if (comment?.Type == NodeType.Comment)
                    {
                        if (initial && i == last)
                        {
                            // special situation, a comment is the last element before the root footer
                            // so we simply adjust the footer instead
                            AdjustLocationSpan(comment, parent);
                            parent.FooterSpan = new CharacterSpan(comment.Span.Start, parent.FooterSpan.End);
                        }
                        else
                        {
                            var node = GetNodeToAdjust(parent, comment, i, first, last);

                            Adjust(comment, node);
                        }

                        children.Remove(comment);

                        last--;
                        i--;
                    }
                }

                i++;
            }
        }
        private static void AdjustSpan(TerminalNode comment, Container nodeToAdjust)
        {
            var commentStart = comment.Span.Start;
            var commentEnd   = comment.Span.End;

            var headerStart = nodeToAdjust.HeaderSpan.Start;
            var headerEnd   = nodeToAdjust.HeaderSpan.End;

            var footerStart = nodeToAdjust.FooterSpan.Start;
            var footerEnd   = nodeToAdjust.FooterSpan.End;

            if (commentStart > headerEnd && commentStart < footerStart)
            {
                // comment if after header
                nodeToAdjust.HeaderSpan = new CharacterSpan(headerStart, commentEnd);
            }
            else
            {
                var min = Math.Min(headerStart, commentStart);
                var max = Math.Max(footerEnd, commentEnd);
                nodeToAdjust.HeaderSpan = new CharacterSpan(min, headerEnd);
                nodeToAdjust.FooterSpan = new CharacterSpan(footerStart, max);
            }
        }
        private static void ParseAttribute(XmlTextReader reader, Container parent, CharacterPositionFinder finder, IXmlFlavor flavor)
        {
            var attributeStartPos = new LineInfo(reader.LineNumber, reader.LinePosition);

            var name = flavor.GetName(reader);
            var type = flavor.GetType(reader);

            // important call to be able to read the attribute value
            reader.ReadAttributeValue();

            var content = flavor.GetContent(reader);

            var attributeEndPos = new LineInfo(reader.LineNumber, reader.LinePosition + content.Length);

            var startPos = finder.GetCharacterPosition(attributeStartPos);
            var endPos   = finder.GetCharacterPosition(attributeEndPos);

            var locationSpan = new LocationSpan(attributeStartPos, attributeEndPos);
            var span         = new CharacterSpan(startPos, endPos);

            var child = AddTerminalNode(parent, type, name, content, locationSpan, span);

            flavor.FinalAdjustAfterParsingComplete(child);
        }
        public static File ParseCore(string filePath, CharacterPositionFinder finder, IXmlFlavor flavor, Encoding encoding)
        {
            using (var reader = new XmlTextReader(new StreamReader(SystemFile.OpenRead(filePath), encoding)))
            {
                var file = new File
                {
                    Name       = filePath,
                    FooterSpan = CharacterSpan.None,                // there is no footer
                };

                var fileBegin = new LineInfo(reader.LineNumber + 1, reader.LinePosition);

                try
                {
                    var dummyRoot = new Container();

                    // Parse the XML and display the text content of each of the elements.
                    // as there are XMLs that have the declaration on same line as the root element, we just loop and parse until the end
                    while (!reader.EOF)
                    {
                        Parse(reader, dummyRoot, finder, flavor);
                    }

                    var root    = dummyRoot.Children.OfType <Container>().Last();
                    var rootEnd = FixRoot(root, dummyRoot);

                    var fileEnd = new LineInfo(reader.LineNumber, reader.LinePosition - 1);

                    var positionAfterLastElement = new LineInfo(rootEnd.LineNumber, rootEnd.LinePosition + 1); // we calculate the next one (either a new line character or a regular one)

                    file.LocationSpan = new LocationSpan(fileBegin, fileEnd);

                    if (positionAfterLastElement < fileEnd)
                    {
                        file.FooterSpan = GetCharacterSpan(new LocationSpan(positionAfterLastElement, fileEnd), finder);
                    }

                    file.Children.Add(root);
                }
                catch (XmlException ex)
                {
                    // try to adjust location span to include full file content
                    // but ignore empty files as parsing errors
                    var lines = SystemFile.ReadLines(filePath).Count();
                    if (lines == 0)
                    {
                        file.LocationSpan = new LocationSpan(LineInfo.None, LineInfo.None);
                    }
                    else
                    {
                        file.ParsingErrors.Add(new ParsingError
                        {
                            ErrorMessage = ex.Message,
                            Location     = new LineInfo(ex.LineNumber, ex.LinePosition),
                        });

                        file.LocationSpan = new LocationSpan(new LineInfo(1, 0), new LineInfo(lines + 1, 0));
                    }
                }

                return(file);
            }
        }
 //// ATTENTION !!!! SIDE EFFECT AS WE READ FURTHER !!!!
 private static LocationSpan GetLocationSpanWithParseAttributes(XmlTextReader reader, Container container, CharacterPositionFinder finder, IXmlFlavor flavor)
 {
     return(GetLocationSpan(reader, _ =>
     {
         if (_.HasAttributes)
         {
             // we have to read the attributes
             ParseAttributes(_, container, finder, flavor);
         }
     }));
 }