diff --git a/SabreTools.Library/Tools/ClrMameProWriter.cs b/SabreTools.Library/Tools/ClrMameProWriter.cs new file mode 100644 index 00000000..c2f55f88 --- /dev/null +++ b/SabreTools.Library/Tools/ClrMameProWriter.cs @@ -0,0 +1,482 @@ +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(); + } + } +}