using System; using System.IO; using System.Text; namespace SabreTools.Library.IO { /// /// ClrMamePro writer patterned heavily off of XmlTextWriter /// /// public class ClrMameProWriter : IDisposable { /// /// State machine state for use in the table /// private enum State { Start, Prolog, Element, Attribute, Content, AttrOnly, Epilog, Error, Closed, } /// /// Potential token types /// private enum Token { None, Standalone, StartElement, EndElement, LongEndElement, StartAttribute, EndAttribute, Content, } /// /// Tag information for the stack /// private struct TagInfo { public string Name; public bool Mixed; public void Init() { Name = null; Mixed = false; } } /// /// Internal stream writer /// private StreamWriter sw; /// /// Stack for tracking current node /// private TagInfo[] stack; /// /// Pointer to current top element in the stack /// private int top; /// /// State table for determining the state machine /// private readonly State[] stateTable = { // State.Start State.Prolog State.Element State.Attribute State.Content State.AttrOnly State.Epilog // /* Token.None */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog, /* Token.Standalone */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog, /* Token.StartElement */ State.Element, State.Element, State.Element, State.Element, State.Element, State.Error, State.Element, /* Token.EndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error, /* Token.LongEndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error, /* Token.StartAttribute */ State.AttrOnly, State.Error, State.Attribute, State.Attribute, State.Error, State.Error, State.Error, /* Token.EndAttribute */ State.Error, State.Error, State.Error, State.Element, State.Error, State.Epilog, State.Error, /* Token.Content */ State.Content, State.Content, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog, }; /// /// Current state in the machine /// private State currentState; /// /// Last seen token /// private Token lastToken; /// /// Get if quotes should surround attribute values /// public bool Quotes { get; set; } /// /// Constructor for opening a write from a file /// public ClrMameProWriter(string filename) { sw = new StreamWriter(filename); Quotes = true; // Element stack stack = new TagInfo[10]; top = 0; stack[top].Init(); } /// /// Constructor for opening a write from a stream and encoding /// public ClrMameProWriter(Stream stream, Encoding encoding) { sw = new StreamWriter(stream, encoding); Quotes = true; // Element stack stack = new TagInfo[10]; top = 0; stack[top].Init(); } /// /// Write the start of an element node /// public void WriteStartElement(string name) { try { // If we're writing quotes, don't write out quote characters internally if (Quotes) name = name.Replace("\"", "''"); AutoComplete(Token.StartElement); PushStack(); stack[top].Name = name; sw.Write(name); sw.Write(" ("); } catch { currentState = State.Error; throw; } } /// /// Write the end of an element node /// public void WriteEndElement() { InternalWriteEndElement(false); } /// /// Write the end of a mixed element node /// public void WriteFullEndElement() { InternalWriteEndElement(true); } /// /// Write a complete element with content /// public void WriteElementString(string name, string value) { WriteStartElement(name); WriteString(value); WriteEndElement(); } /// /// Ensure writing writing null values as empty strings /// public void WriteRequiredElementString(string name, string value) { WriteElementString(name, value ?? string.Empty); } /// /// Write an element, if the value is not null or empty /// public void WriteOptionalElementString(string name, string value) { if (!string.IsNullOrEmpty(value)) WriteElementString(name, value); } /// /// Write the start of an attribute node /// public void WriteStartAttribute(string name) { try { // If we're writing quotes, don't write out quote characters internally if (Quotes) name = name.Replace("\"", "''"); AutoComplete(Token.StartAttribute); sw.Write(name); sw.Write(" "); if (Quotes) sw.Write("\""); } catch { currentState = State.Error; throw; } } /// /// Write the end of an attribute node /// public void WriteEndAttribute() { try { AutoComplete(Token.EndAttribute); } catch { currentState = State.Error; throw; } } /// /// Write a complete attribute with content /// public void WriteAttributeString(string name, string value) { WriteStartAttribute(name); WriteString(value); WriteEndAttribute(); } /// /// Ensure writing writing null values as empty strings /// public void WriteRequiredAttributeString(string name, string value) { WriteAttributeString(name, value ?? string.Empty); } /// /// Write an attribute, if the value is not null or empty /// public void WriteOptionalAttributeString(string name, string value) { if (!string.IsNullOrEmpty(value)) WriteAttributeString(name, value); } /// /// Write a standalone attribute /// public void WriteStandalone(string name, string value, bool? quoteOverride = null) { try { if (string.IsNullOrEmpty(name)) throw new ArgumentException(); // If we're writing quotes, don't write out quote characters internally if ((quoteOverride == null && Quotes) || (quoteOverride == true)) { name = name.Replace("\"", "''"); value = value.Replace("\"", "''"); } AutoComplete(Token.Standalone); sw.Write(name); sw.Write(" "); if ((quoteOverride == null && Quotes) || (quoteOverride == true)) { sw.Write("\""); } sw.Write(value); if ((quoteOverride == null && Quotes) || (quoteOverride == true)) { sw.Write("\""); } } catch { currentState = State.Error; throw; } } /// /// Ensure writing writing null values as empty strings /// public void WriteRequiredStandalone(string name, string value, bool? quoteOverride = null) { WriteStandalone(name, value ?? string.Empty, quoteOverride); } /// /// Write an standalone, if the value is not null or empty /// public void WriteOptionalStandalone(string name, string value, bool? quoteOverride = null) { if (!string.IsNullOrEmpty(value)) WriteStandalone(name, value, quoteOverride); } /// /// Write a string content value /// public void WriteString(string value) { try { if (!string.IsNullOrEmpty(value)) { AutoComplete(Token.Content); // If we're writing quotes, don't write out quote characters internally if (Quotes) value = value.Replace("\"", "''"); sw.Write(value); } } catch { currentState = State.Error; throw; } } /// /// Close the writer /// public void Close() { try { AutoCompleteAll(); } catch { // Don't fail at this step } finally { currentState = State.Closed; sw.Close(); } } /// /// Close and dispose /// public void Dispose() { Close(); sw.Dispose(); } /// /// Flush the base TextWriter /// public void Flush() { sw.Flush(); } /// /// Prepare for the next token to be written /// private void AutoComplete(Token token) { // Handle the error cases if (currentState == State.Closed) throw new InvalidOperationException(); else if (currentState == State.Error) throw new InvalidOperationException(); State newState = stateTable[(int)token * 7 + (int)currentState]; if (newState == State.Error) throw new InvalidOperationException(); // TODO: Figure out how to get attributes on their own lines ONLY if an element contains both attributes and elements switch (token) { case Token.StartElement: case Token.Standalone: if (currentState == State.Attribute) { WriteEndAttributeQuote(); WriteEndStartTag(false); } else if (currentState == State.Element) { WriteEndStartTag(false); } if (currentState != State.Start) Indent(false); break; case Token.EndElement: case Token.LongEndElement: if (currentState == State.Attribute) WriteEndAttributeQuote(); if (currentState == State.Content) token = Token.LongEndElement; else WriteEndStartTag(token == Token.EndElement); break; case Token.StartAttribute: if (currentState == State.Attribute) { WriteEndAttributeQuote(); sw.Write(' '); } else if (currentState == State.Element) { sw.Write(' '); } break; case Token.EndAttribute: WriteEndAttributeQuote(); break; case Token.Content: if (currentState == State.Element && lastToken != Token.Content) WriteEndStartTag(false); if (newState == State.Content) stack[top].Mixed = true; break; default: throw new InvalidOperationException(); } currentState = newState; lastToken = token; } /// /// Autocomplete all open element nodes /// private void AutoCompleteAll() { while (top > 0) { WriteEndElement(); } } /// /// Internal helper to write the end of an element /// private void InternalWriteEndElement(bool longFormat) { try { if (top <= 0) throw new InvalidOperationException(); AutoComplete(longFormat ? Token.LongEndElement : Token.EndElement); if (this.lastToken == Token.LongEndElement) { Indent(true); sw.Write(')'); } top--; } catch { currentState = State.Error; throw; } } /// /// Internal helper to write the end of a tag /// private void WriteEndStartTag(bool empty) { if (empty) sw.Write(" )"); } /// /// Internal helper to write the end of an attribute /// private void WriteEndAttributeQuote() { if (Quotes) sw.Write("\""); } /// /// Internal helper to indent a node, if necessary /// private void Indent(bool beforeEndElement) { if (top == 0) { sw.WriteLine(); } else if (!stack[top].Mixed) { sw.WriteLine(); int i = beforeEndElement ? top - 1 : top; for (; i > 0; i--) { sw.Write('\t'); } } } /// /// Move up one element in the stack /// private void PushStack() { if (top == stack.Length - 1) { TagInfo[] na = new TagInfo[stack.Length + 10]; if (top > 0) Array.Copy(stack, na, top + 1); stack = na; } top++; // Move up stack stack[top].Init(); } } }