| File: XmlAttributePreservationDict.cs | Web Access |
| Project: 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); } } }