|
// 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;
using System.Xaml;
using System.Xaml.MS.Impl;
using System.Xaml.Schema;
using MS.Internal.Xaml.Context;
namespace MS.Internal.Xaml.Parser
{
// Markup Extension Tokenizer AKA Scanner.
enum MeTokenType
{
None,
Open = '{',
Close = '}',
EqualSign = '=',
Comma = ',',
TypeName, // String - Follows a '{' space delimited
PropertyName, // String - Preceeds a '='. {},= delimited, can (but shouldn't) contain spaces.
String, // String - all other strings, {},= delimited can contain spaces.
QuotedMarkupExtension // String - must be recursivly parsed as a MarkupExtension.
};
// 1) Value and (propertynames for compatibility with WPF 3.0) can also have
// escaped character with '\' to include '{' '}' ',' '=', and '\'.
// 2) Value strings can also be quoted (w/ ' or ") in their entirity to escape all
// uses of the above characters.
// 3) All strings are trimmed of whitespace front and back unless they were quoted.
// 4) Quote characters can only appear at the start and end of strings.
// 5) TypeNames cannot be quoted.
internal class MeScanner
{
public const char Space = ' ';
public const char OpenCurlie = '{';
public const char CloseCurlie = '}';
public const char Comma = ',';
public const char EqualSign = '=';
public const char Quote1 = '\'';
public const char Quote2 = '\"';
public const char Backslash = '\\';
public const char NullChar = '\0';
enum StringState { Value, Type, Property };
XamlParserContext _context;
string _inputText;
int _idx;
MeTokenType _token;
XamlType _tokenXamlType;
XamlMember _tokenProperty;
string _tokenNamespace;
string _tokenText;
StringState _state;
bool _hasTrailingWhitespace;
int _lineNumber;
int _startPosition;
private string _currentParameterName;
private SpecialBracketCharacters _currentSpecialBracketCharacters;
public MeScanner(XamlParserContext context, string text, int lineNumber, int linePosition)
{
_context = context;
_inputText = text;
_lineNumber = lineNumber;
_startPosition = linePosition;
_idx = -1;
_state = StringState.Value;
_currentParameterName = null;
_currentSpecialBracketCharacters = null;
}
public int LineNumber
{
get { return _lineNumber; }
}
public int LinePosition
{
get
{
int offset = (_idx < 0) ? 0 : _idx;
return _startPosition + offset;
}
}
public string Namespace
{
get { return _tokenNamespace; }
}
public MeTokenType Token
{
get { return _token; }
}
public XamlType TokenType
{
get { return _tokenXamlType; }
}
public XamlMember TokenProperty
{
get { return _tokenProperty; }
}
public string TokenText
{
get { return _tokenText; }
}
public bool IsAtEndOfInput
{
get { return (_idx >= _inputText.Length); }
}
public bool HasTrailingWhitespace
{
get { return _hasTrailingWhitespace; }
}
public void Read()
{
bool isQuotedMarkupExtension = false;
bool readString = false;
_tokenText = string.Empty;
_tokenXamlType = null;
_tokenProperty = null;
_tokenNamespace = null;
Advance();
AdvanceOverWhitespace();
if (IsAtEndOfInput)
{
_token = MeTokenType.None;
return;
}
switch (CurrentChar)
{
case OpenCurlie:
if (NextChar == CloseCurlie) // the {} escapes the ME. return the string.
{
_token = MeTokenType.String;
_state = StringState.Value;
readString = true; // ReadString() will strip the leading {}
}
else
{
_token = MeTokenType.Open;
_state = StringState.Type; // types follow '{'
}
break;
case Quote1:
case Quote2:
if (NextChar == OpenCurlie)
{
Advance(); // read ahead one character
if (NextChar != CloseCurlie) // check for the '}' of a {}
{
isQuotedMarkupExtension = true;
}
PushBack(); // put back the read-ahead.
}
readString = true; // read substring"
break;
case CloseCurlie:
_token = MeTokenType.Close;
_state = StringState.Value;
break;
case EqualSign:
_token = MeTokenType.EqualSign;
_state = StringState.Value;
_context.CurrentBracketModeParseParameters.IsConstructorParsingMode = false;
break;
case Comma:
_token = MeTokenType.Comma;
_state = StringState.Value;
if (_context.CurrentBracketModeParseParameters.IsConstructorParsingMode)
{
_context.CurrentBracketModeParseParameters.IsConstructorParsingMode =
++_context.CurrentBracketModeParseParameters.CurrentConstructorParam <
_context.CurrentBracketModeParseParameters.MaxConstructorParams;
}
break;
default:
readString = true;
break;
}
if(readString)
{
if (_context.CurrentType.IsMarkupExtension
&& _context.CurrentBracketModeParseParameters is not null
&& _context.CurrentBracketModeParseParameters.IsConstructorParsingMode)
{
int currentCtrParam = _context.CurrentBracketModeParseParameters.CurrentConstructorParam;
_currentParameterName = _context.CurrentLongestConstructorOfMarkupExtension[currentCtrParam].Name;
_currentSpecialBracketCharacters = GetBracketCharacterForProperty(_currentParameterName);
}
string str = ReadString();
_token = (isQuotedMarkupExtension) ? MeTokenType.QuotedMarkupExtension : MeTokenType.String;
switch (_state)
{
case StringState.Value:
break;
case StringState.Type:
_token = MeTokenType.TypeName;
ResolveTypeName(str);
break;
case StringState.Property:
_token = MeTokenType.PropertyName;
ResolvePropertyName(str);
break;
}
_state = StringState.Value;
_tokenText = RemoveEscapes(str);
}
}
private static string RemoveEscapes(string value)
{
if (value.StartsWith("{}", StringComparison.OrdinalIgnoreCase))
{
value = value.Substring(2);
}
if (!value.Contains(Backslash))
{
return value;
}
StringBuilder builder = new StringBuilder(value.Length);
int start = 0;
int idx;
do
{
idx = value.IndexOf(Backslash, start);
if (idx < 0)
{
builder.Append(value, start, value.Length - start);
break;
}
else
{
int clearTextLength = idx - start;
// Copy Clear Text
builder.Append(value, start, clearTextLength);
// Add the character after the backslash
if (idx + 1 < value.Length)
{
builder.Append(value[idx + 1]);
}
// pick up again after that
start = idx + 2;
}
} while (start < value.Length);
string result = builder.ToString();
return result;
}
private void ResolveTypeName(string longName)
{
string error;
XamlTypeName typeName = XamlTypeName.ParseInternal(longName, _context.FindNamespaceByPrefix, out error);
if (typeName is null)
{
throw new XamlParseException(this, error);
}
// In curly form, we search for TypeName + 'Extension' before TypeName
string bareTypeName = typeName.Name;
typeName.Name = typeName.Name + KnownStrings.Extension;
XamlType xamlType = _context.GetXamlType(typeName, false);
// This would be cleaner if we moved the Extension fallback logic out of XSC
if (xamlType is null ||
// Guard against Extension getting added twice
(xamlType.UnderlyingType is not null &&
KS.Eq(xamlType.UnderlyingType.Name, typeName.Name + KnownStrings.Extension)))
{
typeName.Name = bareTypeName;
xamlType = _context.GetXamlType(typeName, true);
}
_tokenXamlType = xamlType;
_tokenNamespace = typeName.Namespace;
}
private void ResolvePropertyName(string longName)
{
XamlPropertyName propName = XamlPropertyName.Parse(longName);
if (propName is null)
{
throw new ArgumentException(SR.MalformedPropertyName);
}
XamlMember prop = null;
XamlType declaringType;
XamlType tagType = _context.CurrentType;
string tagNamespace = _context.CurrentTypeNamespace;
if (propName.IsDotted)
{
prop = _context.GetDottedProperty(tagType, tagNamespace, propName, false /*tagIsRoot*/);
}
// Regular property p
else
{
string ns = _context.GetAttributeNamespace(propName, Namespace);
declaringType = _context.CurrentType;
prop = _context.GetNoDotAttributeProperty(declaringType, propName, Namespace, ns, false /*tagIsRoot*/);
}
_tokenProperty = prop;
}
private string ReadString()
{
bool escaped = false;
char quoteChar = NullChar;
bool atStart = true;
bool wasQuoted = false;
uint braceCount = 0; // To be compat with v3 which allowed balanced {} inside of strings
StringBuilder sb = new StringBuilder();
char ch;
while(!IsAtEndOfInput)
{
ch = CurrentChar;
// handle escaping and quoting first.
if(escaped)
{
sb.Append(Backslash);
sb.Append(ch);
escaped = false;
}
else if (quoteChar != NullChar)
{
if (ch == Backslash)
{
escaped = true;
}
else if (ch != quoteChar)
{
sb.Append(ch);
}
else
{
ch = CurrentChar;
quoteChar = NullChar;
break; // we are done.
}
}
// If we are inside of MarkupExtensionBracketCharacters for a particular property or position parameter,
// scoop up everything inside one by one, and keep track of nested Bracket Characters in the stack.
else if (_context.CurrentBracketModeParseParameters is not null && _context.CurrentBracketModeParseParameters.IsBracketEscapeMode)
{
Stack<char> bracketCharacterStack = _context.CurrentBracketModeParseParameters.BracketCharacterStack;
if (_currentSpecialBracketCharacters.StartsEscapeSequence(ch))
{
bracketCharacterStack.Push(ch);
}
else if (_currentSpecialBracketCharacters.EndsEscapeSequence(ch))
{
if (_currentSpecialBracketCharacters.Match(bracketCharacterStack.Peek(), ch))
{
bracketCharacterStack.Pop();
}
else
{
throw new XamlParseException(this, SR.Format(SR.InvalidClosingBracketCharacers, ch.ToString()));
}
}
else if (ch == Backslash)
{
escaped = true;
}
if (bracketCharacterStack.Count == 0)
{
_context.CurrentBracketModeParseParameters.IsBracketEscapeMode = false;
}
if (!escaped)
{
sb.Append(ch);
}
}
else
{
bool done = false;
switch (ch)
{
case Space:
if (_state == StringState.Type)
{
done = true; // we are done.
break;
}
sb.Append(ch);
break;
case OpenCurlie:
braceCount++;
sb.Append(ch);
break;
case CloseCurlie:
if (braceCount == 0)
{
done = true;
}
else
{
braceCount--;
sb.Append(ch);
}
break;
case Comma:
done = true; // we are done.
break;
case EqualSign:
_state = StringState.Property;
done = true; // we are done.
break;
case Backslash:
escaped = true;
break;
case Quote1:
case Quote2:
if (!atStart)
{
throw new XamlParseException(this, SR.QuoteCharactersOutOfPlace);
}
quoteChar = ch;
wasQuoted = true;
break;
default: // All other character (including whitespace)
if (_currentSpecialBracketCharacters is not null && _currentSpecialBracketCharacters.StartsEscapeSequence(ch))
{
Stack<char> bracketCharacterStack =
_context.CurrentBracketModeParseParameters.BracketCharacterStack;
bracketCharacterStack.Clear();
bracketCharacterStack.Push(ch);
_context.CurrentBracketModeParseParameters.IsBracketEscapeMode = true;
}
sb.Append(ch);
break;
}
if (done)
{
if (braceCount > 0)
{
throw new XamlParseException(this, SR.UnexpectedTokenAfterME);
}
else
{
if (_context.CurrentBracketModeParseParameters?.BracketCharacterStack.Count > 0)
{
throw new XamlParseException(this, SR.Format(SR.MalformedBracketCharacters, ch.ToString()));
}
}
PushBack();
break; // we are done.
}
}
atStart = false;
Advance();
}
if (quoteChar != NullChar)
{
throw new XamlParseException(this, SR.UnclosedQuote);
}
string result = sb.ToString();
if (!wasQuoted)
{
result = result.TrimEnd(KnownStrings.WhitespaceChars);
result = result.TrimStart(KnownStrings.WhitespaceChars);
}
if (_state == StringState.Property)
{
_currentParameterName = result;
_currentSpecialBracketCharacters = GetBracketCharacterForProperty(_currentParameterName);
}
return result;
}
private char CurrentChar
{
get { return _inputText[_idx]; }
}
private char NextChar
{
get
{
if (_idx + 1 < _inputText.Length)
{
return _inputText[_idx + 1];
}
return NullChar;
}
}
private bool Advance()
{
++_idx;
if (IsAtEndOfInput)
{
_idx = _inputText.Length;
return false;
}
return true;
}
private static bool IsWhitespaceChar(char ch)
{
Debug.Assert(KnownStrings.WhitespaceChars.Length == 5);
if (ch == KnownStrings.WhitespaceChars[0] ||
ch == KnownStrings.WhitespaceChars[1] ||
ch == KnownStrings.WhitespaceChars[2] ||
ch == KnownStrings.WhitespaceChars[3] ||
ch == KnownStrings.WhitespaceChars[4])
{
return true;
}
return false;
}
private void AdvanceOverWhitespace()
{
bool sawWhitespace = false;
while (!IsAtEndOfInput && IsWhitespaceChar(CurrentChar))
{
sawWhitespace = true;
Advance();
}
// WFP 3.0 errors on trailing whitespace.
// [note: very first compat workaround in the new XAML parser]
// Noticing trailing whitespace is not very natural in this parser.
// so this extra code is here to implement this error.
if (IsAtEndOfInput && sawWhitespace)
{
_hasTrailingWhitespace = true;
}
}
private void PushBack()
{
_idx -= 1;
}
private SpecialBracketCharacters GetBracketCharacterForProperty(string propertyName)
{
SpecialBracketCharacters bracketCharacters = null;
if (_context.CurrentEscapeCharacterMapForMarkupExtension is not null &&
_context.CurrentEscapeCharacterMapForMarkupExtension.ContainsKey(propertyName))
{
bracketCharacters = _context.CurrentEscapeCharacterMapForMarkupExtension[propertyName];
}
return bracketCharacters;
}
}
internal class BracketModeParseParameters
{
internal BracketModeParseParameters(XamlParserContext context)
{
CurrentConstructorParam = 0;
IsBracketEscapeMode = false;
BracketCharacterStack = new Stack<char>();
if (context.CurrentLongestConstructorOfMarkupExtension is not null)
{
IsConstructorParsingMode = context.CurrentLongestConstructorOfMarkupExtension.Length > 0;
MaxConstructorParams = context.CurrentLongestConstructorOfMarkupExtension.Length;
}
else
{
IsConstructorParsingMode = false;
MaxConstructorParams = 0;
}
}
internal int CurrentConstructorParam { get; set; }
internal int MaxConstructorParams { get; set; }
internal bool IsConstructorParsingMode { get; set; }
internal bool IsBracketEscapeMode { get; set; }
internal Stack<char> BracketCharacterStack { get; set; }
}
}
|