File: XmlAttributePreservationDict.cs
Web Access
Project: src\src\xdt\src\Microsoft.Web.XmlTransform\Microsoft.Web.XmlTransform.csproj (Microsoft.Web.XmlTransform)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.IO;

namespace Microsoft.Web.XmlTransform
{
    internal class XmlAttributePreservationDict
    {
        #region Private data members
        private List<string> orderedAttributes = new List<string>();
        private Dictionary<string, string> leadingSpaces = new Dictionary<string, string>();

        private string attributeNewLineString = null;
        private bool computedOneAttributePerLine = false;
        private bool oneAttributePerLine = false;
        #endregion

        private bool OneAttributePerLine {
            get {
                if (!computedOneAttributePerLine) {
                    computedOneAttributePerLine = true;
                    oneAttributePerLine = ComputeOneAttributePerLine();
                }
                return oneAttributePerLine;
            }
        }

        internal void ReadPreservationInfo(string elementStartTag) {
            Debug.Assert(elementStartTag.StartsWith("<", StringComparison.Ordinal) && elementStartTag.EndsWith(">", StringComparison.Ordinal), "Expected string containing exactly a single tag");
            WhitespaceTrackingTextReader whitespaceReader = new WhitespaceTrackingTextReader(new StringReader(elementStartTag));

            int lastCharacter = EnumerateAttributes(elementStartTag, 
                (int line, int linePosition, string attributeName) => 
                {
                    orderedAttributes.Add(attributeName);
                    if (whitespaceReader.ReadToPosition(line, linePosition))
                    {
                        leadingSpaces.Add(attributeName, whitespaceReader.PrecedingWhitespace);
                    }
                    else
                    {
                        Debug.Fail("Couldn't get leading whitespace for attribute");
                    }
                }
                );

            if (whitespaceReader.ReadToPosition(lastCharacter)) {
                leadingSpaces.Add(String.Empty, whitespaceReader.PrecedingWhitespace);
            }
            else {
                Debug.Fail("Couldn't get trailing whitespace for tag");
            }
        }

        private int EnumerateAttributes(string elementStartTag, Action<int, int, string> onAttributeSpotted)
        {
            bool selfClosed = (elementStartTag.EndsWith("/>", StringComparison.Ordinal));
            string xmlDocString = elementStartTag;
            if (!selfClosed)
            {
                xmlDocString = elementStartTag.Substring(0, elementStartTag.Length-1) + "/>" ;
            }

            XmlTextReader xmlReader = new XmlTextReader(new StringReader(xmlDocString));

            xmlReader.Namespaces = false;
            xmlReader.Read();

            bool hasMoreAttributes = xmlReader.MoveToFirstAttribute();
            while (hasMoreAttributes)
            {
                onAttributeSpotted(xmlReader.LineNumber, xmlReader.LinePosition, xmlReader.Name);
                hasMoreAttributes = xmlReader.MoveToNextAttribute();
            }

            int lastCharacter = elementStartTag.Length;
            if (selfClosed)
            {
                lastCharacter--;
            }
            return lastCharacter;
        }

        internal void WritePreservedAttributes(XmlAttributePreservingWriter writer, XmlAttributeCollection attributes) {
            string oldNewLineString = null;
            if (attributeNewLineString != null) {
                oldNewLineString = writer.SetAttributeNewLineString(attributeNewLineString);
            }

            try {
                foreach (string attributeName in orderedAttributes) {
                    XmlAttribute attr = attributes[attributeName];
                    if (attr != null) {
                        if (leadingSpaces.ContainsKey(attributeName)) {
                            writer.WriteAttributeWhitespace(leadingSpaces[attributeName]);
                        }

                        attr.WriteTo(writer);
                    }
                }

                if (leadingSpaces.ContainsKey(String.Empty)) {
                    writer.WriteAttributeTrailingWhitespace(leadingSpaces[String.Empty]);
                }
            }
            finally {
                if (oldNewLineString != null) {
                    writer.SetAttributeNewLineString(oldNewLineString);
                }
            }
        }

        internal void UpdatePreservationInfo(XmlAttributeCollection updatedAttributes, XmlFormatter formatter) {
            if (updatedAttributes.Count == 0) {
                if (orderedAttributes.Count > 0) {
                    // All attributes were removed, clear preservation info
                    leadingSpaces.Clear();
                    orderedAttributes.Clear();
                }
            }
            else {
                Dictionary<string, bool> attributeExists = new Dictionary<string, bool>();

                // Prepopulate the list with attributes that existed before
                foreach (string attributeName in orderedAttributes) {
                    attributeExists[attributeName] = false;
                }

                // Update the list with attributes that exist now
                foreach (XmlAttribute attribute in updatedAttributes) {
                    if (!attributeExists.ContainsKey(attribute.Name)) {
                        orderedAttributes.Add(attribute.Name);
                    }
                    attributeExists[attribute.Name] = true;
                }

                bool firstAttribute = true;
                string keepLeadingWhitespace = null;
                foreach (string key in orderedAttributes) {
                    bool exists = attributeExists[key];

                    // Handle removal
                    if (!exists) {
                        // We need to figure out whether to keep the leading
                        // or trailing whitespace. The logic is:
                        //   1. The whitespace before the first attribute is
                        //      always kept, so remove trailing space.
                        //   2. We want to keep newlines, so if the leading
                        //      space contains a newline then remove trailing
                        //      space. If multiple newlines are being removed,
                        //      keep the last one.
                        //   3. Otherwise, remove leading space.
                        //
                        // In order to remove trailing space, we have to 
                        // remove the leading space of the next attribute, so
                        // we store this leading space to replace the next.
                        if (leadingSpaces.ContainsKey(key)) {
                            string leadingSpace = leadingSpaces[key];
                            if (firstAttribute) {
                                if (keepLeadingWhitespace == null) {
                                    keepLeadingWhitespace = leadingSpace;
                                }
                            }
                            else if (ContainsNewLine(leadingSpace)) {
                                keepLeadingWhitespace = leadingSpace;
                            }

                            leadingSpaces.Remove(key);
                        }
                    }

                    else if (keepLeadingWhitespace != null) {
                        // Exception to rule #2 above: Don't replace an existing
                        // newline with one that was removed
                        if (firstAttribute || !leadingSpaces.ContainsKey(key) || !ContainsNewLine(leadingSpaces[key])) {
                            leadingSpaces[key] = keepLeadingWhitespace;
                        }
                        keepLeadingWhitespace = null;
                    }

                    // Handle addition
                    else if (!leadingSpaces.ContainsKey(key)) {
                        if (firstAttribute) {
                            // This will prevent the textwriter from writing a
                            // newline before the first attribute
                            leadingSpaces[key] = " ";
                        }
                        else if (OneAttributePerLine) {
                            // Add the indent space between each attribute
                            leadingSpaces[key] = GetAttributeNewLineString(formatter);
                        }
                        else {
                            // Don't add any hard-coded spaces. All new attributes
                            // should be at the end, so they'll be formatted while
                            // writing. Make sure we have the right indent string,
                            // though.
                            EnsureAttributeNewLineString(formatter);
                        }
                    }

                    // firstAttribute remains true until we find the first
                    // attribute that actually exists
                    firstAttribute = firstAttribute && !exists;
                }
            }
        }

        private bool ComputeOneAttributePerLine() {
            if (leadingSpaces.Count > 1) {
                // If there is a newline between each pair of attributes, then
                // we'll continue putting newlines between all attributes. If
                // there's no newline between one pair, then we won't.
                bool firstAttribute = true;
                foreach (string attributeName in orderedAttributes) {
                    // The space in front of the first attribute doesn't count,
                    // because that space isn't between attributes.
                    if (firstAttribute) {
                        firstAttribute = false;
                    }
                    else if (leadingSpaces.ContainsKey(attributeName) &&
                        !ContainsNewLine(leadingSpaces[attributeName])) {
                        // This means there are two attributes on one line
                        return false;
                    }
                }

                return true;
            }
            else {
                // If there aren't at least two original attributes on this
                // tag, then it's not possible to tell if more than one would
                // be on a line. Default to more than one per line.
                // TODO(jodavis): Should we look at sibling tags to decide?
                return false;
            }
        }

        private bool ContainsNewLine(string space) {
            return space.IndexOf("\n", StringComparison.Ordinal) >= 0;
        }

        public string GetAttributeNewLineString(XmlFormatter formatter) {
            if (attributeNewLineString == null) {
                attributeNewLineString = ComputeAttributeNewLineString(formatter);
            }
            return attributeNewLineString;
        }

        private string ComputeAttributeNewLineString(XmlFormatter formatter) {
            string lookAheadString = LookAheadForNewLineString();
            if (lookAheadString != null) {
                return lookAheadString;
            }
            else if (formatter != null) {
                return formatter.CurrentAttributeIndent;
            }
            else {
                return null;
            }
        }

        private string LookAheadForNewLineString() {
            foreach (string space in leadingSpaces.Values) {
                if (ContainsNewLine(space)) {
                    return space;
                }
            }
            return null;
        }

        private void EnsureAttributeNewLineString(XmlFormatter formatter) {
            GetAttributeNewLineString(formatter);
        }
    }
}