File: XmlAttributePreservingWriter.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.IO;
using System.Diagnostics;
using System.Globalization;

namespace Microsoft.Web.XmlTransform
{
    internal class XmlAttributePreservingWriter : XmlWriter
    {
        private XmlTextWriter xmlWriter;
        private AttributeTextWriter textWriter;

        public XmlAttributePreservingWriter(string fileName, Encoding encoding)
            : this(encoding == null ? new StreamWriter(fileName) : new StreamWriter(fileName, false, encoding)) {
        }

        public XmlAttributePreservingWriter(Stream w, Encoding encoding)
            : this(encoding == null ? new StreamWriter(w) : new StreamWriter(w, encoding))
        {
        }

        public XmlAttributePreservingWriter(TextWriter textWriter) {
            this.textWriter = new AttributeTextWriter(textWriter);
            this.xmlWriter = new XmlTextWriter(this.textWriter);
        }

        public void WriteAttributeWhitespace(string whitespace) {
            Debug.Assert(IsOnlyWhitespace(whitespace));

            // Make sure we're in the right place to write
            // whitespace between attributes
            if (WriteState == WriteState.Attribute) {
                WriteEndAttribute();
            }
            else if (WriteState != WriteState.Element) {
                throw new InvalidOperationException();
            }

            // We don't write right away. We're going to wait until an
            // attribute is being written
            textWriter.AttributeLeadingWhitespace = whitespace;
        }

        public void WriteAttributeTrailingWhitespace(string whitespace) {
            Debug.Assert(IsOnlyWhitespace(whitespace));

            if (WriteState == WriteState.Attribute) {
                WriteEndAttribute();
            }
            else if (WriteState != WriteState.Element) {
                throw new InvalidOperationException();
            }

            textWriter.Write(whitespace);
        }

        public string SetAttributeNewLineString(string newLineString) {
            string old = textWriter.AttributeNewLineString;

            if (newLineString == null && xmlWriter.Settings != null) {
                newLineString = xmlWriter.Settings.NewLineChars;
            }
            if (newLineString == null) {
                newLineString = "\r\n";
            }
            textWriter.AttributeNewLineString = newLineString;

            return old;
        }

        private bool IsOnlyWhitespace(string whitespace) {
            foreach (char whitespaceCharacter in whitespace) {
                if (!Char.IsWhiteSpace(whitespaceCharacter)) {
                    return false;
                }
            }
            return true;
        }

        #region SkippingTextWriter class
        private class AttributeTextWriter : TextWriter
        {
            enum State
            {
                Writing,
                WaitingForAttributeLeadingSpace,
                ReadingAttribute,
                Buffering,
                FlushingBuffer,
                WritingComment,
            }

            #region private data members
            State state = State.Writing;
            StringBuilder writeBuffer = null;

            private TextWriter baseWriter;
            string leadingWhitespace = null;

            int lineNumber = 1;
            int linePosition = 1;
            int maxLineLength = 160;
            string newLineString = "\r\n";
            #endregion

            public AttributeTextWriter(TextWriter baseWriter)
                : base(CultureInfo.InvariantCulture) {
                this.baseWriter = baseWriter;
            }

            public string AttributeLeadingWhitespace {
                set {
                    leadingWhitespace = value;
                }
            }

            public string AttributeNewLineString {
                get {
                    return newLineString;
                }
                set {
                    newLineString = value;
                }
            }

            public void StartAttribute() {
                Debug.Assert(state == State.Writing);

                ChangeState(State.WaitingForAttributeLeadingSpace);
            }

            public void EndAttribute() {
                Debug.Assert(state == State.ReadingAttribute);

                WriteQueuedAttribute();
            }

            public void StartComment() {
                Debug.Assert(state == State.Writing || state == State.Buffering);

                ChangeState(State.WritingComment);
            }

            public void EndComment() {
                Debug.Assert(state == State.WritingComment);

                ChangeState(State.Writing);
            }

            public int MaxLineLength {
                get {
                    return maxLineLength;
                }
                set {
                    maxLineLength = value;
                }
            }

            public override void Write(char value) {
                UpdateState(value);

                switch (state) {
                    case State.WaitingForAttributeLeadingSpace:
                        if (value == ' ') {
                            ChangeState(State.ReadingAttribute);
                            break;
                        }
                        goto case State.Writing;
                    case State.Writing:
                    case State.WritingComment:
                    case State.FlushingBuffer:
                        ReallyWriteCharacter(value);
                        break;
                    case State.ReadingAttribute:
                    case State.Buffering:
                        writeBuffer.Append(value);
                        break;
                }
            }

            private void UpdateState(char value) {

                if (state == State.WritingComment) {
                    // We never want to do any formatting while writing a comment,
                    // so don't leave this state.
                    return;
                }

                // This logic prevents writing the leading space that
                // XmlTextWriter wants to put before "/>". 
                switch (value) {
                    case ' ':
                        if (state == State.Writing) {
                            ChangeState(State.Buffering);
                        }
                        break;
                    case '/':
                        break;
                    case '>':
                        if (state == State.Buffering) {
                            string currentBuffer = writeBuffer.ToString();
                            if (currentBuffer.EndsWith(" /", StringComparison.Ordinal)) {
                                // We've identified the string " />" at the
                                // end of the buffer, so remove the space
                                writeBuffer.Remove(currentBuffer.LastIndexOf(' '), 1);
                            }
                            ChangeState(State.Writing);
                        }
                        break;
                    default:
                        if (state == State.Buffering) {
                            ChangeState(State.Writing);
                        }
                        break;
                }
            }

            private void ChangeState(State newState) {
                if (state != newState) {
                    State oldState = state;
                    state = newState;

                    // Handle buffer management for different states
                    if (StateRequiresBuffer(newState)) {
                        CreateBuffer();
                    }
                    else if (StateRequiresBuffer(oldState)) {
                        FlushBuffer();
                    }
                }
            }

            private bool StateRequiresBuffer(State state) {
                return state == State.Buffering || state == State.ReadingAttribute;
            }

            private void CreateBuffer() {
                Debug.Assert(writeBuffer == null);
                if (writeBuffer == null) {
                    writeBuffer = new StringBuilder();
                }
            }

            private void FlushBuffer() {
                Debug.Assert(writeBuffer != null);
                if (writeBuffer != null) {
                    State oldState = state;
                    try {
                        state = State.FlushingBuffer;

                        Write(writeBuffer.ToString());
                        writeBuffer = null;
                    }
                    finally {
                        state = oldState;
                    }
                }
            }

            private void ReallyWriteCharacter(char value) {
                baseWriter.Write(value);

                if (value == '\n') {
                    lineNumber++;
                    linePosition = 1;
                }
                else {
                    linePosition++;
                }
            }

            private void WriteQueuedAttribute() {
                // Write leading whitespace
                if (leadingWhitespace != null) {
                    writeBuffer.Insert(0, leadingWhitespace);
                    leadingWhitespace = null;
                }
                else {
                    int lineLength = linePosition + writeBuffer.Length + 1;
                    if (lineLength > MaxLineLength) {
                        writeBuffer.Insert(0, AttributeNewLineString);
                    }
                    else {
                        writeBuffer.Insert(0, ' ');
                    }
                }

                // Flush the buffer and start writing characters again
                ChangeState(State.Writing);
            }

            public override Encoding Encoding {
                get {
                    return baseWriter.Encoding;
                }
            }

            public override void Flush() {
                baseWriter.Flush();
            }

            public override void Close() {
                baseWriter.Close();
            }
        }
        #endregion

        #region XmlWriter implementation
        public override void Close() {
            xmlWriter.Close();
        }

        public override void Flush() {
            xmlWriter.Flush();
        }

        public override string LookupPrefix(string ns) {
            return xmlWriter.LookupPrefix(ns);
        }

        public override void WriteBase64(byte[] buffer, int index, int count) {
            xmlWriter.WriteBase64(buffer, index, count);
        }

        public override void WriteCData(string text) {
            xmlWriter.WriteCData(text);
        }

        public override void WriteCharEntity(char ch) {
            xmlWriter.WriteCharEntity(ch);
        }

        public override void WriteChars(char[] buffer, int index, int count) {
            xmlWriter.WriteChars(buffer, index, count);
        }

        public override void WriteComment(string text) {
            textWriter.StartComment();

            xmlWriter.WriteComment(text);

            textWriter.EndComment();
        }

        public override void WriteDocType(string name, string pubid, string sysid, string subset) {
            xmlWriter.WriteDocType(name, pubid, sysid, subset);
        }

        public override void WriteEndAttribute() {
            xmlWriter.WriteEndAttribute();

            textWriter.EndAttribute();
        }

        public override void WriteEndDocument() {
            xmlWriter.WriteEndDocument();
        }

        public override void WriteEndElement() {
            xmlWriter.WriteEndElement();
        }

        public override void WriteEntityRef(string name) {
            xmlWriter.WriteEntityRef(name);
        }

        public override void WriteFullEndElement() {
            xmlWriter.WriteFullEndElement();
        }

        public override void WriteProcessingInstruction(string name, string text) {
            xmlWriter.WriteProcessingInstruction(name, text);
        }

        public override void WriteRaw(string data) {
            xmlWriter.WriteRaw(data);
        }

        public override void WriteRaw(char[] buffer, int index, int count) {
            xmlWriter.WriteRaw(buffer, index, count);
        }

        public override void WriteStartAttribute(string prefix, string localName, string ns) {
            textWriter.StartAttribute();

            xmlWriter.WriteStartAttribute(prefix, localName, ns);
        }

        public override void WriteStartDocument(bool standalone) {
            xmlWriter.WriteStartDocument(standalone);
        }

        public override void WriteStartDocument() {
            xmlWriter.WriteStartDocument();
        }

        public override void WriteStartElement(string prefix, string localName, string ns) {
            xmlWriter.WriteStartElement(prefix, localName, ns);
        }

        public override WriteState WriteState {
            get {
                return xmlWriter.WriteState;
            }
        }

        public override void WriteString(string text) {
            xmlWriter.WriteString(text);
        }

        public override void WriteSurrogateCharEntity(char lowChar, char highChar) {
            xmlWriter.WriteSurrogateCharEntity(lowChar, highChar);
        }

        public override void WriteWhitespace(string ws) {
            xmlWriter.WriteWhitespace(ws);
        }
        #endregion
    }
}