|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System.Text;
namespace System.Xaml
{
class XamlMarkupExtensionWriter : XamlWriter
{
StringBuilder sb;
Stack<Node> nodes;
WriterState currentState;
XamlXmlWriter xamlXmlWriter;
XamlXmlWriterSettings settings;
XamlMarkupExtensionWriterSettings meSettings;
bool failed;
public XamlMarkupExtensionWriter(XamlXmlWriter xamlXmlWriter)
{
Initialize(xamlXmlWriter);
}
public XamlMarkupExtensionWriter(XamlXmlWriter xamlXmlWriter, XamlMarkupExtensionWriterSettings meSettings)
{
this.meSettings = meSettings;
Initialize(xamlXmlWriter);
}
void Initialize(XamlXmlWriter xamlXmlWriter)
{
this.xamlXmlWriter = xamlXmlWriter;
settings = xamlXmlWriter.Settings; // This will clone, only want to do this once
meSettings = meSettings ?? new XamlMarkupExtensionWriterSettings();
currentState = Start.State;
sb = new StringBuilder();
nodes = new Stack<Node>();
failed = false;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
public override XamlSchemaContext SchemaContext
{
get
{
return xamlXmlWriter.SchemaContext;
}
}
public void Reset()
{
currentState = Start.State;
sb = new StringBuilder();
nodes.Clear();
failed = false;
}
// MarkupExtensionString is used to obtain the curly-formatted markup extension string.
// It should be called after calling the final WriteEndObject().
// If MarkupExtensionString is not called before writing the next markup extension string
// in curly syntax, the previous markup extension string is lost.
public string MarkupExtensionString
{
get
{
if (nodes.Count == 0)
{
return sb.ToString();
}
else
{
return null;
}
}
}
// This is set to true when the Markup Extension Writer fails to write
// the given node stream in curly form.
public bool Failed
{
get
{
return failed;
}
}
string LookupPrefix(XamlType type)
{
string prefix = xamlXmlWriter.LookupPrefix(type.GetXamlNamespaces(), out _);
if (prefix is null)
{
if (!meSettings.ContinueWritingWhenPrefixIsNotFound)
{
// the prefix is not found and curly syntax has no way of defining a prefix
failed = true;
return string.Empty; // what we return here is not important, since Failed has set to be true
}
}
return prefix;
}
string LookupPrefix(XamlMember property)
{
string prefix = xamlXmlWriter.LookupPrefix(property.GetXamlNamespaces(), out _);
if (prefix is null)
{
if (!meSettings.ContinueWritingWhenPrefixIsNotFound)
{
failed = true;
// the prefix is not found and curly syntax has no way of defining a prefix
return string.Empty; // what we return here is not important, since Failed has set to be true
}
}
return prefix;
}
void CheckMemberForUniqueness(Node objectNode, XamlMember property)
{
if (!settings.AssumeValidInput)
{
if (objectNode.Members is null)
{
objectNode.Members = new XamlPropertySet();
}
else if (objectNode.Members.Contains(property))
{
throw new InvalidOperationException(SR.Format(SR.XamlMarkupExtensionWriterDuplicateMember, property.Name));
}
objectNode.Members.Add(property);
}
}
public override void WriteStartObject(XamlType type)
{
currentState.WriteStartObject(this, type);
}
public override void WriteGetObject()
{
currentState.WriteGetObject(this);
}
public override void WriteEndObject()
{
currentState.WriteEndObject(this);
}
public override void WriteStartMember(XamlMember property)
{
currentState.WriteStartMember(this, property);
}
public override void WriteEndMember()
{
currentState.WriteEndMember(this);
}
public override void WriteNamespace(NamespaceDeclaration namespaceDeclaration)
{
currentState.WriteNamespace(this, namespaceDeclaration);
}
public override void WriteValue(object value)
{
string s = value as string;
if (s is null)
{
throw new ArgumentException(SR.XamlMarkupExtensionWriterCannotWriteNonstringValue);
}
currentState.WriteValue(this, s);
}
class Node
{
public XamlMember XamlProperty
{
get;
set;
}
public XamlPropertySet Members
{
get;
set;
}
public XamlNodeType NodeType
{
get;
set;
}
public XamlType XamlType
{
get;
set;
}
}
abstract class WriterState
{
//according to the BNF, CharactersToEscape ::= ['",={}\]
static char[] specialChars = new char[] { '\'', '"', ',', '=', '{', '}', '\\', ' ' };
public virtual void WriteStartObject(XamlMarkupExtensionWriter writer, XamlType type)
{
writer.failed = true;
}
public virtual void WriteGetObject(XamlMarkupExtensionWriter writer)
{
writer.failed = true;
}
public virtual void WriteEndObject(XamlMarkupExtensionWriter writer)
{
writer.failed = true;
}
public virtual void WriteStartMember(XamlMarkupExtensionWriter writer, XamlMember property)
{
writer.failed = true;
}
public virtual void WriteEndMember(XamlMarkupExtensionWriter writer)
{
writer.failed = true;
}
public virtual void WriteValue(XamlMarkupExtensionWriter writer, string value)
{
writer.failed = true;
}
public virtual void WriteNamespace(XamlMarkupExtensionWriter writer, NamespaceDeclaration namespaceDeclaration)
{
writer.failed = true;
}
protected static bool ContainCharacterToEscape(string s)
{
return s.IndexOfAny(specialChars) >= 0;
}
protected static string FormatStringInCorrectSyntax(string s)
{
StringBuilder sb = new StringBuilder("\"");
for (int i = 0; i < s.Length; i++)
{
// BNF: DoubleQuotedValue ::= '"' ((Char - ["\]) | '\"' | '\\')+ '"'
// so the only characters we need to skip are the backslash and the double quote.
if (s[i] == '\\' || s[i] == '"')
{
sb.Append('\\');
}
sb.Append(s[i]);
}
sb.Append('\"');
return sb.ToString();
}
protected void WritePrefix(XamlMarkupExtensionWriter writer, string prefix)
{
if (!string.IsNullOrEmpty(prefix))
{
writer.sb.Append(prefix);
writer.sb.Append(':');
}
}
public void WriteString(XamlMarkupExtensionWriter writer, string value)
{
if (ContainCharacterToEscape(value) || value.Length == 0)
{
value = FormatStringInCorrectSyntax(value);
}
writer.sb.Append(value);
}
}
// XamlMarkupExtensionWriter returns to this state after a markup extension has been completed,
// i.e. when the number of closing curly bracket "}" matches the number of opening curly bracket "{".
// At this state, XamlMarkupExtensionWriter is ready to start writing a markup extension
class Start : WriterState
{
static WriterState state = new Start();
Start()
{
}
public static WriterState State
{
get { return state; }
}
public override void WriteStartObject(XamlMarkupExtensionWriter writer, XamlType type)
{
writer.Reset();
string prefix = writer.LookupPrefix(type);
writer.sb.Append('{');
WritePrefix(writer, prefix);
writer.sb.Append(XamlXmlWriter.GetTypeName(type));
writer.nodes.Push(new Node { NodeType = XamlNodeType.StartObject, XamlType = type });
writer.currentState = InObjectBeforeMember.State;
}
}
abstract class InObject : WriterState
{
protected InObject()
{
}
public abstract string Delimiter
{
get;
}
public override void WriteEndObject(XamlMarkupExtensionWriter writer)
{
if (writer.nodes.Count == 0)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
Node node = writer.nodes.Pop();
if (node.NodeType != XamlNodeType.StartObject)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
writer.sb.Append('}');
if (writer.nodes.Count == 0)
{
writer.currentState = Start.State;
}
else
{
Node member = writer.nodes.Peek();
if (member.NodeType != XamlNodeType.StartMember)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
if (member.XamlProperty == XamlLanguage.PositionalParameters)
{
writer.currentState = InPositionalParametersAfterValue.State;
}
else
{
writer.currentState = InMemberAfterValueOrEndObject.State;
}
}
}
protected void UpdateStack(XamlMarkupExtensionWriter writer, XamlMember property)
{
if (writer.nodes.Count == 0)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
Node objectNode = writer.nodes.Peek();
if (objectNode.NodeType != XamlNodeType.StartObject)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
writer.CheckMemberForUniqueness(objectNode, property);
writer.nodes.Push(new Node
{
NodeType = XamlNodeType.StartMember,
XamlType = objectNode.XamlType,
XamlProperty = property
});
}
protected void WriteNonPositionalParameterMember(XamlMarkupExtensionWriter writer, XamlMember property)
{
if (XamlXmlWriter.IsImplicit(property) ||
(property.IsDirective && (property.Type.IsCollection || property.Type.IsDictionary)))
{
writer.failed = true;
return;
}
if (property.IsDirective)
{
writer.sb.Append(Delimiter);
WritePrefix(writer, writer.LookupPrefix(property));
writer.sb.Append(property.Name);
}
else if (property.IsAttachable)
{
writer.sb.Append(Delimiter);
WritePrefix(writer, writer.LookupPrefix(property));
string local = $"{property.DeclaringType.Name}.{property.Name}";
writer.sb.Append(local);
}
else
{
writer.sb.Append(Delimiter);
writer.sb.Append(property.Name);
}
writer.sb.Append('=');
writer.currentState = InMember.State;
}
}
class InObjectBeforeMember : InObject
{
static WriterState state = new InObjectBeforeMember();
InObjectBeforeMember()
{
}
public static WriterState State
{
get { return state; }
}
public override string Delimiter
{
get { return " "; }
}
public override void WriteStartMember(XamlMarkupExtensionWriter writer, XamlMember property)
{
UpdateStack(writer, property);
if (property == XamlLanguage.PositionalParameters)
{
writer.currentState = InPositionalParametersBeforeValue.State;
}
else
{
WriteNonPositionalParameterMember(writer, property);
}
}
}
class InObjectAfterMember : InObject
{
static WriterState state = new InObjectAfterMember();
InObjectAfterMember()
{
}
public static WriterState State
{
get { return state; }
}
public override string Delimiter
{
get { return ", "; }
}
public override void WriteStartMember(XamlMarkupExtensionWriter writer, XamlMember property)
{
UpdateStack(writer, property);
WriteNonPositionalParameterMember(writer, property);
}
}
abstract class InPositionalParameters : WriterState
{
protected InPositionalParameters()
{
}
public abstract string Delimiter
{
get;
}
public override void WriteValue(XamlMarkupExtensionWriter writer, string value)
{
writer.sb.Append(Delimiter);
WriteString(writer, value);
writer.currentState = InPositionalParametersAfterValue.State;
}
public override void WriteStartObject(XamlMarkupExtensionWriter writer, XamlType type)
{
writer.sb.Append(Delimiter);
writer.currentState = InMember.State;
writer.currentState.WriteStartObject(writer, type);
}
}
class InPositionalParametersBeforeValue : InPositionalParameters
{
static WriterState state = new InPositionalParametersBeforeValue();
InPositionalParametersBeforeValue()
{
}
public static WriterState State
{
get { return state; }
}
public override string Delimiter
{
get { return " "; }
}
}
class InPositionalParametersAfterValue : InPositionalParameters
{
static WriterState state = new InPositionalParametersAfterValue();
InPositionalParametersAfterValue()
{
}
public static WriterState State
{
get { return state; }
}
public override string Delimiter
{
get { return ", "; }
}
public override void WriteEndMember(XamlMarkupExtensionWriter writer)
{
Node node = writer.nodes.Pop();
if (node.NodeType != XamlNodeType.StartMember || node.XamlProperty != XamlLanguage.PositionalParameters)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
writer.currentState = InObjectAfterMember.State;
}
}
class InMember : WriterState
{
static WriterState state = new InMember();
InMember()
{
}
public static WriterState State
{
get { return state; }
}
public override void WriteValue(XamlMarkupExtensionWriter writer, string value)
{
WriteString(writer, value);
writer.currentState = InMemberAfterValueOrEndObject.State;
}
public override void WriteStartObject(XamlMarkupExtensionWriter writer, XamlType type)
{
if (!type.IsMarkupExtension)
{
// can not write a non-ME object in this state in curly form
writer.failed = true;
return;
}
string prefix = writer.LookupPrefix(type);
writer.sb.Append('{');
WritePrefix(writer, prefix);
writer.sb.Append(XamlXmlWriter.GetTypeName(type));
writer.nodes.Push(new Node { NodeType = XamlNodeType.StartObject, XamlType = type });
writer.currentState = InObjectBeforeMember.State;
}
}
class InMemberAfterValueOrEndObject : WriterState
{
static WriterState state = new InMemberAfterValueOrEndObject();
InMemberAfterValueOrEndObject()
{
}
public static WriterState State
{
get { return state; }
}
public override void WriteEndMember(XamlMarkupExtensionWriter writer)
{
if (writer.nodes.Count == 0)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
Node member = writer.nodes.Pop();
if (member.NodeType != XamlNodeType.StartMember)
{
throw new InvalidOperationException(SR.XamlMarkupExtensionWriterInputInvalid);
}
writer.currentState = InObjectAfterMember.State;
}
}
}
internal class XamlMarkupExtensionWriterSettings
{
public bool ContinueWritingWhenPrefixIsNotFound
{
get;
set;
}
}
}
|