Files
SabreTools/SabreTools.Library/IO/ClrMameProWriter.cs

487 lines
14 KiB
C#
Raw Normal View History

2020-06-13 12:41:49 -07:00
using System;
using System.IO;
using System.Text;
2020-08-01 22:46:28 -07:00
namespace SabreTools.Library.IO
2020-06-13 12:41:49 -07:00
{
/// <summary>
/// ClrMamePro writer patterned heavily off of XmlTextWriter
/// </summary>
/// <see cref="https://referencesource.microsoft.com/#System.Xml/System/Xml/Core/XmlTextWriter.cs"/>
public class ClrMameProWriter : IDisposable
2020-06-13 12:41:49 -07:00
{
/// <summary>
/// State machine state for use in the table
/// </summary>
private enum State
{
Start,
Prolog,
Element,
Attribute,
Content,
AttrOnly,
Epilog,
Error,
Closed,
}
/// <summary>
/// Potential token types
/// </summary>
private enum Token
{
None,
Standalone,
StartElement,
EndElement,
LongEndElement,
StartAttribute,
EndAttribute,
Content,
}
/// <summary>
/// Tag information for the stack
/// </summary>
private struct TagInfo
{
public string Name;
public bool Mixed;
public void Init()
{
Name = null;
Mixed = false;
}
}
/// <summary>
/// Internal stream writer
/// </summary>
2020-06-15 10:56:47 -07:00
private StreamWriter sw;
2020-06-13 12:41:49 -07:00
/// <summary>
/// Stack for tracking current node
/// </summary>
private TagInfo[] stack;
/// <summary>
/// Pointer to current top element in the stack
/// </summary>
private int top;
/// <summary>
/// State table for determining the state machine
/// </summary>
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,
};
/// <summary>
/// Current state in the machine
/// </summary>
private State currentState;
/// <summary>
/// Last seen token
/// </summary>
private Token lastToken;
/// <summary>
/// Get if quotes should surround attribute values
/// </summary>
public bool Quotes { get; set; }
/// <summary>
/// Constructor for opening a write from a file
/// </summary>
public ClrMameProWriter(string filename)
{
2020-06-15 10:56:47 -07:00
sw = new StreamWriter(filename);
2020-06-13 12:41:49 -07:00
Quotes = true;
2020-06-15 10:56:47 -07:00
// Element stack
2020-06-13 12:41:49 -07:00
stack = new TagInfo[10];
top = 0;
2020-06-15 10:56:47 -07:00
stack[top].Init();
2020-06-13 12:41:49 -07:00
}
/// <summary>
/// Constructor for opening a write from a stream and encoding
/// </summary>
public ClrMameProWriter(Stream stream, Encoding encoding)
{
2020-06-15 10:56:47 -07:00
sw = new StreamWriter(stream, encoding);
2020-06-13 12:41:49 -07:00
Quotes = true;
// Element stack
stack = new TagInfo[10];
top = 0;
stack[top].Init();
}
/// <summary>
/// Write the start of an element node
/// </summary>
public void WriteStartElement(string name)
{
try
{
AutoComplete(Token.StartElement);
PushStack();
stack[top].Name = name;
2020-06-15 10:56:47 -07:00
sw.Write(name);
sw.Write(" (");
2020-06-13 12:41:49 -07:00
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write the end of an element node
/// </summary>
public void WriteEndElement()
{
InternalWriteEndElement(false);
}
/// <summary>
/// Write the end of a mixed element node
/// </summary>
public void WriteFullEndElement()
{
InternalWriteEndElement(true);
}
/// <summary>
/// Write a complete element with content
/// </summary>
public void WriteElementString(string name, string value)
{
WriteStartElement(name);
WriteString(value);
WriteEndElement();
}
/// <summary>
/// Write the start of an attribute node
/// </summary>
public void WriteStartAttribute(string name)
{
try
{
AutoComplete(Token.StartAttribute);
2020-06-15 10:56:47 -07:00
sw.Write(name);
sw.Write(" ");
2020-06-13 12:41:49 -07:00
if (Quotes)
2020-06-15 10:56:47 -07:00
sw.Write("\"");
2020-06-13 12:41:49 -07:00
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write the end of an attribute node
/// </summary>
public void WriteEndAttribute()
{
try
{
AutoComplete(Token.EndAttribute);
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write a complete attribute with content
/// </summary>
public void WriteAttributeString(string name, string value)
{
WriteStartAttribute(name);
WriteString(value);
WriteEndAttribute();
}
/// <summary>
/// Write a standalone attribute
/// </summary>
public void WriteStandalone(string name, string value, bool? quoteOverride = null)
{
try
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException();
AutoComplete(Token.Standalone);
2020-06-15 10:56:47 -07:00
sw.Write(name);
sw.Write(" ");
2020-06-13 12:41:49 -07:00
if ((quoteOverride == null && Quotes)
|| (quoteOverride == true))
{
2020-06-15 10:56:47 -07:00
sw.Write("\"");
2020-06-13 12:41:49 -07:00
}
2020-06-15 10:56:47 -07:00
sw.Write(value);
2020-06-13 12:41:49 -07:00
if ((quoteOverride == null && Quotes)
|| (quoteOverride == true))
{
2020-06-15 10:56:47 -07:00
sw.Write("\"");
2020-06-13 12:41:49 -07:00
}
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write a string content value
/// </summary>
public void WriteString(string value)
{
try
{
if (!string.IsNullOrEmpty(value))
{
AutoComplete(Token.Content);
2020-06-15 10:56:47 -07:00
sw.Write(value);
2020-06-13 12:41:49 -07:00
}
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Close the writer
/// </summary>
public void Close()
{
try
{
AutoCompleteAll();
}
catch
{
// Don't fail at this step
}
finally
{
currentState = State.Closed;
2020-06-15 10:56:47 -07:00
sw.Close();
2020-06-13 12:41:49 -07:00
}
}
/// <summary>
/// Close and dispose
/// </summary>
public void Dispose()
{
Close();
2020-06-15 10:56:47 -07:00
sw.Dispose();
}
2020-06-13 12:41:49 -07:00
/// <summary>
/// Flush the base TextWriter
/// </summary>
public void Flush()
{
2020-06-15 10:56:47 -07:00
sw.Flush();
2020-06-13 12:41:49 -07:00
}
/// <summary>
/// Prepare for the next token to be written
/// </summary>
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();
2020-06-15 10:56:47 -07:00
sw.Write(' ');
2020-06-13 12:41:49 -07:00
}
else if (currentState == State.Element)
{
2020-06-15 10:56:47 -07:00
sw.Write(' ');
2020-06-13 12:41:49 -07:00
}
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;
}
/// <summary>
/// Autocomplete all open element nodes
/// </summary>
private void AutoCompleteAll()
{
while (top > 0)
{
WriteEndElement();
}
}
/// <summary>
/// Internal helper to write the end of an element
/// </summary>
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);
2020-06-15 10:56:47 -07:00
sw.Write(')');
2020-06-13 12:41:49 -07:00
}
top--;
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Internal helper to write the end of a tag
/// </summary>
private void WriteEndStartTag(bool empty)
{
if (empty)
2020-06-15 10:56:47 -07:00
sw.Write(" )");
2020-06-13 12:41:49 -07:00
}
/// <summary>
/// Internal helper to write the end of an attribute
/// </summary>
private void WriteEndAttributeQuote()
{
if (Quotes)
2020-06-15 10:56:47 -07:00
sw.Write("\"");
2020-06-13 12:41:49 -07:00
}
/// <summary>
/// Internal helper to indent a node, if necessary
/// </summary>
private void Indent(bool beforeEndElement)
{
if (top == 0)
{
2020-06-15 10:56:47 -07:00
sw.WriteLine();
2020-06-13 12:41:49 -07:00
}
else if (!stack[top].Mixed)
{
2020-06-15 10:56:47 -07:00
sw.WriteLine();
2020-06-13 12:41:49 -07:00
int i = beforeEndElement ? top - 1 : top;
for (; i > 0; i--)
{
2020-06-15 10:56:47 -07:00
sw.Write('\t');
2020-06-13 12:41:49 -07:00
}
}
}
/// <summary>
/// Move up one element in the stack
/// </summary>
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();
}
}
}