using System; using System.IO; using System.Text; namespace SabreTools.Library.Tools { /// /// ClrMamePro writer patterned heavily off of XmlTextWriter /// /// public class ClrMameProWriter { /// /// 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 textWriter; /// /// 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) { textWriter = new StreamWriter(filename); Quotes = true; stack = new TagInfo[10]; top = 0; } /// /// Constructor for opening a write from a stream and encoding /// public ClrMameProWriter(Stream stream, Encoding encoding) { textWriter = new StreamWriter(stream, encoding); Quotes = true; // Element stack stack = new TagInfo[10]; top = 0; stack[top].Init(); } /// /// Base stream for easy access /// public Stream BaseStream { get { return textWriter?.BaseStream ?? null; } } /// /// Write the start of an element node /// public void WriteStartElement(string name) { try { AutoComplete(Token.StartElement); PushStack(); stack[top].Name = name; textWriter.Write(name); textWriter.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(); } /// /// Write the start of an attribute node /// public void WriteStartAttribute(string name) { try { AutoComplete(Token.StartAttribute); textWriter.Write(name); textWriter.Write(" "); if (Quotes) textWriter.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(); } /// /// Write a standalone attribute /// public void WriteStandalone(string name, string value, bool? quoteOverride = null) { try { if (string.IsNullOrEmpty(name)) throw new ArgumentException(); AutoComplete(Token.Standalone); textWriter.Write(name); textWriter.Write(" "); if ((quoteOverride == null && Quotes) || (quoteOverride == true)) { textWriter.Write("\""); } textWriter.Write(value); if ((quoteOverride == null && Quotes) || (quoteOverride == true)) { textWriter.Write("\""); } } catch { currentState = State.Error; throw; } } /// /// Write a string content value /// public void WriteString(string value) { try { if (!string.IsNullOrEmpty(value)) { AutoComplete(Token.Content); textWriter.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; textWriter.Close(); } } /// /// Flush the base TextWriter /// public void Flush() { textWriter.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(); textWriter.Write(' '); } else if (currentState == State.Element) { textWriter.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); textWriter.Write(')'); } top--; } catch { currentState = State.Error; throw; } } /// /// Internal helper to write the end of a tag /// private void WriteEndStartTag(bool empty) { if (empty) textWriter.Write(" )"); } /// /// Internal helper to write the end of an attribute /// private void WriteEndAttributeQuote() { if (Quotes) textWriter.Write("\""); } /// /// Internal helper to indent a node, if necessary /// private void Indent(bool beforeEndElement) { if (top == 0) { textWriter.WriteLine(); } else if (!stack[top].Mixed) { textWriter.WriteLine(); int i = beforeEndElement ? top - 1 : top; for (; i > 0; i--) { textWriter.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(); } } }