// 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. using System; using System.Text; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Globalization; using MS.Utility; using MS.Internal.Xaml.Parser; #if PBTCOMPILER namespace MS.Internal.Markup #else namespace System.Windows.Markup #endif { /// <summary> /// MarkupExtension parsing helper that provides things like namespace lookups /// and type resolution services. This is implemented by classes like XamlReaderHelper /// and BamlWriter. /// </summary> internal interface IParserHelper { string LookupNamespace(string prefix); bool GetElementType( bool extensionFirst, string localName, string namespaceURI, ref string assemblyName, ref string typeFullName, ref Type baseType, ref Type serializerType); bool CanResolveLocalAssemblies(); } /// <summary> /// MarkupExtension parser class. /// </summary> internal class MarkupExtensionParser { /// <summary> /// Constructor. /// </summary> internal MarkupExtensionParser( IParserHelper parserHelper, ParserContext parserContext) { _parserHelper = parserHelper; _parserContext = parserContext; } /// <summary> /// Return an intialized AttributeData structure if the attribute value adheres to the /// format for MarkupExtensions. Otherwise return null. /// </summary> internal AttributeData IsMarkupExtensionAttribute( Type declaringType, // Type where propIdName is declared string propIdName, // Name of the property ref string attrValue, int lineNumber, int linePosition, int depth, object info) // PropertyInfo or DependencyProperty or MethodInfo for the property { string typeName; string args; if (!GetMarkupExtensionTypeAndArgs(ref attrValue, out typeName, out args)) { return null; } return FillAttributeData(declaringType, propIdName, typeName, args, attrValue, lineNumber, linePosition, depth, info); } /// <summary> /// Return an intialized DefAttributeData structure if the attribute value adheres to the /// format for MarkupExtensions. Otherwise return null. /// </summary> internal DefAttributeData IsMarkupExtensionDefAttribute( Type declaringType, ref string attrValue, int lineNumber, int linePosition, int depth) { string typeName; string args; if (!GetMarkupExtensionTypeAndArgs(ref attrValue, out typeName, out args)) { return null; } return FillDefAttributeData(declaringType, typeName, args, attrValue, lineNumber, linePosition, depth); } /// <summary> /// Applies some quick checks to see if an Attribute Value is a /// Markup Extension. This is meant to be much cheaper than the full /// parse. And this can return true even if the ME has some syntax errors. /// </summary> internal static bool LooksLikeAMarkupExtension(string attrValue) { if (attrValue.Length < 2) return false; if (attrValue[0] != '{') return false; if (attrValue[1] == '}') return false; return true; } #if !PBTCOMPILER /// <summary> /// Given a string that is to be treated as a literal string, check /// to see if it might be mistaken for a markup extension and escape it /// accordingly. /// </summary> /// <remarks> /// Prefixing the string with "{}" will tell GetMarkupExtensionTypeAndArgs() /// that the remainder of the string is to be treated literally. /// </remarks> internal static string AddEscapeToLiteralString( string literalString ) { string returnString = literalString; if (!String.IsNullOrEmpty(returnString) && returnString[0] == '{') { returnString = $"{{}}{returnString}"; } return returnString; } #endif // Returns an known markup extension that can have a simple value. Also returns the property // name of the extension that can be used to set the value. This is used to strip it out of // the args to get the simple value. private KnownElements GetKnownExtensionFromType(Type extensionType, out string propName) { if (KnownTypes.Types[(int)KnownElements.TypeExtension] == extensionType) { propName = "TypeName"; return KnownElements.TypeExtension; } else if (KnownTypes.Types[(int)KnownElements.StaticExtension] == extensionType) { propName = "Member"; return KnownElements.StaticExtension; } else if (KnownTypes.Types[(int)KnownElements.TemplateBindingExtension] == extensionType) { propName = "Property"; return KnownElements.TemplateBindingExtension; } else if (KnownTypes.Types[(int)KnownElements.DynamicResourceExtension] == extensionType) { propName = "ResourceKey"; return KnownElements.DynamicResourceExtension; } else if (KnownTypes.Types[(int)KnownElements.StaticResourceExtension] == extensionType) { propName = "ResourceKey"; return KnownElements.StaticResourceExtension; } propName = string.Empty; return 0; } /// <summary> /// Determine if the argument string passed in can represent a valid /// type in the format /// prefix:Classname or /// TypeName = prefix:Classname /// If so, then change the args string to contain only prefix:Classname and /// return true. Otherwise, return false. /// </summary> private bool IsSimpleTypeExtensionArgs( Type extensionType, int lineNumber, int linePosition, ref string args) { if (KnownTypes.Types[(int)KnownElements.TypeExtension] == extensionType) { return IsSimpleExtensionArgs(lineNumber, linePosition, "TypeName", ref args, extensionType); } return false; } /// <summary> /// Determine if the argument string passed in can represent a valid /// param for a MarkuPExtension in one of these formats: /// prefix:Classname /// TypeName = prefix:Classname (TypeExtension) /// prefix:Classname.MemberName /// Member = prefix:Classname.MemberName (StaticExtension) /// Property = prefix:Classname.MemberName (TemplateBindingExtension) /// {x:Type prefix:Classname} /// {x:Static prefix:Classname.MemberName} /// StringValue /// ResourceKey = {x:Type prefix:Classname} (DynamicResourceExtension) /// ResourceKey = {x:Static prefix:Classname.MemberName} (DynamicResourceExtension) /// ResourceKey = StringValue (DynamicResourceExtension) /// If so, then change the args string to contain only the raw value: /// prefix:Classname or /// prefix:Classname.MemberName or /// StringValue /// and return true. Otherwise, return false. /// isValueNestedExtension = true, if the args value is itself a StaticExtension or TypeExtension. /// isValueTypeExtension = true, if the args value is itself a TypeExtension. /// valueExtensions only apply to DynamicResourceExtension. /// </summary> private bool IsSimpleExtension( Type extensionType, int lineNumber, int linePosition, int depth, out short extensionTypeId, out bool isValueNestedExtension, out bool isValueTypeExtension, ref string args) { bool isSimple = false; string propName; extensionTypeId = 0; isValueNestedExtension = false; isValueTypeExtension = false; // if we support optimizing for custom extensions, this can be generalized // to use a converter\serializer Id for that extension. KnownElements knownExtensionTypeId = GetKnownExtensionFromType(extensionType, out propName); if (knownExtensionTypeId != KnownElements.UnknownElement) { isSimple = IsSimpleExtensionArgs(lineNumber, linePosition, propName, ref args, extensionType); } if (isSimple) { switch (knownExtensionTypeId) { case KnownElements.DynamicResourceExtension: case KnownElements.StaticResourceExtension: if (LooksLikeAMarkupExtension(args)) { // if value may be a possible ME, see if it is a simple Type\StaticExtension. // null is passed for propIdName to indicate this. AttributeData nestedAttrData = IsMarkupExtensionAttribute(extensionType, null, ref args, lineNumber, linePosition, depth, null); isValueTypeExtension = nestedAttrData.IsTypeExtension; isSimple = isValueTypeExtension || nestedAttrData.IsStaticExtension; isValueNestedExtension = isSimple; if (isSimple) { // if nested extension value is simple, take the simple args args = nestedAttrData.Args; } else { // else restore the original args for normal processing args += "}"; } } break; } if (isSimple) { extensionTypeId = (short)knownExtensionTypeId; } } return isSimple; } private bool IsSimpleExtensionArgs(int lineNumber, int linePosition, string propName, ref string args, Type targetType) { // We have a MarkupExtension, so process the argument string to determine // if it is simple. Do this by tokenizing now and extracting the simple // type string. ArrayList tokens = TokenizeAttributes(args, lineNumber, linePosition, targetType); if (tokens == null) { return false; } if (tokens.Count == 1) { args = (String)tokens[0]; return true; } if (tokens.Count == 3 && (string)tokens[0] == propName) { args = (String)tokens[2]; return true; } return false; } /// <summary> /// Parse the attrValue string into a typename and arguments. Return true if /// they parse successfully. /// </summary> /// <remarks> /// Localization API also relys on this method to filter markup extensions, as they are not /// localizable by default. /// </remarks> internal static bool GetMarkupExtensionTypeAndArgs( ref string attrValue, out string typeName, out string args) { int length = attrValue.Length; typeName = string.Empty; args = string.Empty; // MarkupExtensions MUST have '{' as the first character if (length < 1 || attrValue[0] != '{') { return false; } bool gotEscape = false; StringBuilder stringBuilder = null; int i = 1; for (; i<length; i++) { // Skip all whitespace unless we are collecting characters for the // type name. if (Char.IsWhiteSpace(attrValue[i])) { if (stringBuilder != null) { break; } } else { // If there is no string builder, then we haven't encountered the // first non-whitespace character after '{'. if (stringBuilder == null) { // Always escape the first '\' if (!gotEscape && attrValue[i] == '\\') { gotEscape = true; } // We have the first non-whitespace character after '{' else if (attrValue[i] == '}') { // Found the closing '}', so we're done. If this is the // second character, we have an empty MarkupExtension, which // is a way to escape a string. In that case, trim off // the first two characters from attrValue so that the caller // will get the unescaped string. if (i == 1) { attrValue = attrValue.Substring(2); return false; } } else { stringBuilder = new StringBuilder(length - i); stringBuilder.Append(attrValue[i]); gotEscape = false; } } else { // Always escape the first '\' if (!gotEscape && attrValue[i] == '\\') { gotEscape = true; } else if (attrValue[i] == '}') { // Found the closing '}', so we're done. break; } else { // Collect characters that make up the type name stringBuilder.Append(attrValue[i]); gotEscape = false; } } } } // Set typeName and arguments. Note that both may be empty, but having // an empyt typeName will generate an error later on. if (stringBuilder != null) { typeName = stringBuilder.ToString(); } if (i < length-1) { args = attrValue.Substring(i, length-i); } else if( attrValue[length-1] == '}') { args = "}"; } return true; } /// <summary> /// Fill the def attribute data structure with type and attribute string information. /// Note that this is not for general def attributes, but only for the key attribute /// used when storing items in an IDictionary. /// </summary> private DefAttributeData FillDefAttributeData( Type declaringType, // Type where attribute is declared string typename, string args, string attributeValue, int lineNumber, int linePosition, int depth) { string namespaceURI; string targetAssemblyName; string targetFullName; Type targetType; Type serializerType; bool isSimple = false; bool resolvedTag = GetExtensionType(typename, attributeValue, lineNumber, linePosition, out namespaceURI, out targetAssemblyName, out targetFullName, out targetType, out serializerType); if (resolvedTag) { isSimple = IsSimpleTypeExtensionArgs(targetType, lineNumber, linePosition, ref args); } return new DefAttributeData(targetAssemblyName, targetFullName, targetType, args, declaringType, namespaceURI, lineNumber, linePosition, depth, isSimple); } // Fill the attribute data structure with type and attribute string information private AttributeData FillAttributeData( Type declaringType, // Type where propIdName is declared string propIdName, // Name of the property string typename, string args, string attributeValue, int lineNumber, int linePosition, int depth, object info) // PropertyInfo or DependencyProperty or MethodInfo for the property { string namespaceURI; string targetAssemblyName; string targetFullName; Type targetType; Type serializerType; bool isSimple = false; short extensionId = 0; bool isValueNestedExtension = false; bool isValueTypeExtension = false; bool resolvedTag = GetExtensionType(typename, attributeValue, lineNumber, linePosition, out namespaceURI, out targetAssemblyName, out targetFullName, out targetType, out serializerType); // propIdName is an empty string only for the case when args is a ctor param of a MarkupExtension if (resolvedTag && propIdName != string.Empty) { if (propIdName == null) { // If propIdName is null, then we are looking for nested simple extensions and // we allow only Type\StaticExtension. if (KnownTypes.Types[(int)KnownElements.TypeExtension] == targetType) { isSimple = IsSimpleExtensionArgs(lineNumber, linePosition, "TypeName", ref args, targetType); isValueNestedExtension = isSimple; isValueTypeExtension = isSimple; extensionId = (short)KnownElements.TypeExtension; } else if (KnownTypes.Types[(int)KnownElements.StaticExtension] == targetType) { isSimple = IsSimpleExtensionArgs(lineNumber, linePosition, "Member", ref args, targetType); isValueNestedExtension = isSimple; extensionId = (short)KnownElements.StaticExtension; } } else { propIdName = propIdName.Trim(); isSimple = IsSimpleExtension(targetType, lineNumber, linePosition, depth, out extensionId, out isValueNestedExtension, out isValueTypeExtension, ref args); } } return new AttributeData(targetAssemblyName, targetFullName, targetType, args, declaringType, propIdName, info, serializerType, lineNumber, linePosition, depth, namespaceURI, extensionId, isValueNestedExtension, isValueTypeExtension, isSimple); } private bool GetExtensionType( string typename, string attributeValue, int lineNumber, int linePosition, out string namespaceURI, out string targetAssemblyName, out string targetFullName, out Type targetType, out Type serializerType) { targetAssemblyName = null; targetFullName = null; targetType = null; serializerType = null; // lookup the type of the target string fullname = typename; string prefix = String.Empty; int typeIndex = typename.IndexOf(':'); if (typeIndex >= 0) { prefix = typename.Substring(0, typeIndex); typename = typename.Substring(typeIndex + 1); } namespaceURI = _parserHelper.LookupNamespace(prefix); bool resolvedTag = _parserHelper.GetElementType(true, typename, namespaceURI, ref targetAssemblyName, ref targetFullName, ref targetType, ref serializerType); if (!resolvedTag) { if (_parserHelper.CanResolveLocalAssemblies()) { // if local assemblies can be resolved, but the type could not be resolved, then // we need to throw an exception ThrowException(nameof(SR.ParserNotMarkupExtension), attributeValue, typename, namespaceURI, lineNumber, linePosition); } else { // if local assemblies cannot yet be resolved, we record the data that we will need // to write an unknown tag start, and note in the data that the type is an unknown // markup extension. targetFullName = fullname; targetType = typeof(UnknownMarkupExtension); } } else if (!KnownTypes.Types[(int)KnownElements.MarkupExtension].IsAssignableFrom(targetType)) { // if the type is not known, throw an exception ThrowException(nameof(SR.ParserNotMarkupExtension), attributeValue, typename, namespaceURI, lineNumber, linePosition); } return resolvedTag; } /// <summary> /// Fill the attribute data structure with type and attribute string information. /// Note that this is for general properties and not def attributes. /// </summary> internal ArrayList CompileAttributes( ArrayList markupExtensionList, int startingDepth) { ArrayList xamlNodes = new ArrayList(markupExtensionList.Count * 5); for (int i = 0; i<markupExtensionList.Count; i++) { AttributeData data = (AttributeData)markupExtensionList[i]; CompileAttribute(xamlNodes, data); } return xamlNodes; } /// <summary> /// Create nodes for a complex property that surrounds an element tree. /// Note that this is for general properties and not def attributes. /// </summary> internal void CompileAttribute( ArrayList xamlNodes, AttributeData data) { // For MarkupExtension syntax, treat PropertyInfo as a CLR property, but MethodInfo // and DependencyProperty as a DependencyProperty. Note that the MarkupCompiler // will handle the case where a DependencyProperty callback is made if it gets // a MethodInfo for the attached property setter. string declaringTypeAssemblyName = data.DeclaringType.Assembly.FullName; string declaringTypeFullName = data.DeclaringType.FullName; // Find the PropertyRecordType to use in this case Type propertyType; bool propertyCanWrite; XamlTypeMapper.GetPropertyType(data.Info, out propertyType, out propertyCanWrite); BamlRecordType propertyRecordType = BamlRecordManager.GetPropertyStartRecordType(propertyType, propertyCanWrite); // Create the property start and end records XamlNode propertyStart; XamlNode propertyEnd; switch (propertyRecordType) { case BamlRecordType.PropertyArrayStart: { propertyStart = new XamlPropertyArrayStartNode( data.LineNumber, data.LinePosition, data.Depth, data.Info, declaringTypeAssemblyName, declaringTypeFullName, data.PropertyName); propertyEnd = new XamlPropertyArrayEndNode( data.LineNumber, data.LinePosition, data.Depth); break; } case BamlRecordType.PropertyIDictionaryStart: { propertyStart = new XamlPropertyIDictionaryStartNode( data.LineNumber, data.LinePosition, data.Depth, data.Info, declaringTypeAssemblyName, declaringTypeFullName, data.PropertyName); propertyEnd = new XamlPropertyIDictionaryEndNode( data.LineNumber, data.LinePosition, data.Depth); break; } case BamlRecordType.PropertyIListStart: { propertyStart = new XamlPropertyIListStartNode( data.LineNumber, data.LinePosition, data.Depth, data.Info, declaringTypeAssemblyName, declaringTypeFullName, data.PropertyName); propertyEnd = new XamlPropertyIListEndNode( data.LineNumber, data.LinePosition, data.Depth); break; } default: // PropertyComplexStart { propertyStart = new XamlPropertyComplexStartNode( data.LineNumber, data.LinePosition, data.Depth, data.Info, declaringTypeAssemblyName, declaringTypeFullName, data.PropertyName); propertyEnd = new XamlPropertyComplexEndNode( data.LineNumber, data.LinePosition, data.Depth); break; } } // NOTE: Add duplicate property checking here as is done in XamlReaderHelper xamlNodes.Add(propertyStart); CompileAttributeCore(xamlNodes, data); xamlNodes.Add(propertyEnd); } /// <summary> /// Create nodes for an element tree. /// Note that this is for general properties and not def attributes. /// </summary> internal void CompileAttributeCore( ArrayList xamlNodes, AttributeData data) { string typename = null; string namespaceURI = null; ArrayList list = TokenizeAttributes(data.Args, data.LineNumber, data.LinePosition, data.TargetType); // If the list is empty, or the second item on the list is an equal sign, then // we have a simple markup extension that uses the default constructor. In all // other cases we must have at least one constructor parameter. if (data.TargetType == typeof(UnknownMarkupExtension)) { // If the target type is unknown, then we record an unknown tag start, rather // than an element start. typename = data.TargetFullName; string prefix = String.Empty; int typeIndex = typename.IndexOf(':'); if (typeIndex >= 0) { prefix = typename.Substring(0, typeIndex); typename = typename.Substring(typeIndex + 1); } namespaceURI = _parserHelper.LookupNamespace(prefix); xamlNodes.Add(new XamlUnknownTagStartNode( data.LineNumber, data.LinePosition, ++data.Depth, namespaceURI, typename)); } else { xamlNodes.Add(new XamlElementStartNode( data.LineNumber, data.LinePosition, ++data.Depth, data.TargetAssemblyName, data.TargetFullName, data.TargetType, data.SerializerType)); } xamlNodes.Add(new XamlEndAttributesNode( data.LineNumber, data.LinePosition, data.Depth, true)); int listIndex = 0; if (list != null && (list.Count == 1 || (list.Count > 1 && !(list[1] is String) && ((Char)list[1] == ',')))) { // Go through the constructor parameters, writing them out like complex // properties WriteConstructorParams(xamlNodes, list, data, ref listIndex); } // Write properties that come after the element constructor parameters WriteProperties(xamlNodes, list, listIndex, data); // close up if (data.TargetType == typeof(UnknownMarkupExtension)) { xamlNodes.Add(new XamlUnknownTagEndNode( data.LineNumber, data.LinePosition, data.Depth--, typename, namespaceURI)); } else { xamlNodes.Add(new XamlElementEndNode( data.LineNumber, data.LinePosition, data.Depth--)); } } /// <summary> /// Parse the string representation of a set of def attributes in MarkupExtension /// syntax and return a list of xaml nodes that represents those attributes. /// </summary> internal ArrayList CompileDictionaryKeys( ArrayList complexDefAttributesList, int startingDepth) { ArrayList xamlNodes = new ArrayList(complexDefAttributesList.Count * 5); for (int i = 0; i<complexDefAttributesList.Count; i++) { DefAttributeData data = (DefAttributeData)complexDefAttributesList[i]; CompileDictionaryKey(xamlNodes, data); } return xamlNodes; } /// <summary> /// Parse the string representation of a set of def attributes in MarkupExtension /// syntax and return a list of xaml nodes that represents those attributes. /// </summary> internal void CompileDictionaryKey( ArrayList xamlNodes, DefAttributeData data) { ArrayList list = TokenizeAttributes(data.Args, data.LineNumber, data.LinePosition, data.TargetType); // If the list is empty, or the second item on the list is an equal sign, then // we have a simple markup extension that uses the default constructor. In all // other cases we must have at least one constructor parameter. xamlNodes.Add(new XamlKeyElementStartNode( data.LineNumber, data.LinePosition, ++data.Depth, data.TargetAssemblyName, data.TargetFullName, data.TargetType, null)); xamlNodes.Add(new XamlEndAttributesNode( data.LineNumber, data.LinePosition, data.Depth, true)); int listIndex = 0; if (list != null && (list.Count == 1 || (list.Count > 1 && !(list[1] is String) && ((Char)list[1] == ',')))) { // Go through the constructor parameters, writing them out like complex // properties WriteConstructorParams(xamlNodes, list, data, ref listIndex); } // Write properties that come after the element constructor parameters WriteProperties(xamlNodes, list, listIndex, data); // close up xamlNodes.Add(new XamlKeyElementEndNode( data.LineNumber, data.LinePosition, data.Depth--)); } /// <summary> /// The core method that writes out the MarkupExtension itself without the surrounding /// contextual xaml nodes. /// The format of compact syntax is /// "{typename constParam1, constParam2, name = value, name = value, ... }" /// (whitespace is ignored near delimiters). The effect of this is to /// create a new object of the given type, using the provided constructor /// parameters. If they are absent, use the default constructor. Then to set /// its properties. Each name=value pair causes a property /// with the given name to be set to the given value (after type conversion). /// /// For constructor parameters, or on right-hand side of the = sign, some /// characters are treated specially: /// \ - escape charater - quotes the following character (including \) /// , - terminates the clause /// {} - matching braces, meaning a nested MarkupExtension /// '' - matching single quotes /// "" - matching double quotes /// Inside matching delimiters, comma has no special meaning and /// delimiters must nest correctly (unless escaped). Inside matching /// quotes (either kind), no characters are special except \. /// /// If the string really is in "MarkupExtension syntax" form, return true. /// /// Exceptions are thrown for mismatching delimiters, and for errors while /// assigning to properties. /// Major changes in 4.6.2 : /// MarkupExtensionBracketCharacterAttributes : Specified on a particular markup extension property, /// these can be a pair of special characters (like (), [] etc). Anything enclosed inside these characters /// has no special meaning, except \ and other such MarkupExtensionBracketCharacters themselves. /// </summary> private ArrayList TokenizeAttributes ( string args, int lineNumber, int linePosition, Type extensionType) { // As a result of having to rely on Reflection to find whether a property has a MarkupExtensionBracketCharacterAttribute // set on it, we can't parse a locally defined Markup Extension in MarkupCompilePass1. Therefore, if we find an UnknownExtension // here, we just return null. If this was MarkupCompilePass2 and the extension was still unknown, it would have errored out by now // already. if (extensionType == typeof (UnknownMarkupExtension)) { return null; } int maxConstructorParams = 0; ParameterInfo[] constructorParameters = FindLongestConstructor(extensionType, out maxConstructorParams); Dictionary<string, SpecialBracketCharacters> bracketCharacterCache = _parserContext.InitBracketCharacterCacheForType(extensionType); Stack<char> bracketCharacterStack = new Stack<char>(); int currentConstructorParam = 0; bool inCtorParsingMode = constructorParameters != null && maxConstructorParams > 0; bool inBracketCharacterMode = false; ArrayList list = null; int length = args.Length; bool inQuotes = false; bool gotEscape = false; bool nonWhitespaceFound = false; bool gotFinalCloseCurly = false; Char quoteChar = '\''; int leftCurlies = 0; StringBuilder stringBuilder = null; int i = 0; string parameterName = null; SpecialBracketCharacters bracketCharacters = null; if (inCtorParsingMode && bracketCharacterCache != null) { parameterName = maxConstructorParams > 0 ? constructorParameters[currentConstructorParam].Name : null; if (!string.IsNullOrEmpty(parameterName)) { bracketCharacters = GetBracketCharacterForProperty(parameterName, bracketCharacterCache); } } // Loop through the args, creating a list of arguments and known delimiters. // This loop does limited syntax checking, and serves to tokenize the argument // string into chunks that are validated in greater detail in the next phase. for (i=0; i < length && !gotFinalCloseCurly; i++) { // Escape character is always in effect for everything inside // a MarkupExtension. We have to remember that the next character is // escaped, and is not treated as a quote or delimiter. if (!gotEscape && args[i] == '\\') { gotEscape = true; continue; } if (!nonWhitespaceFound && !Char.IsWhiteSpace(args[i])) { nonWhitespaceFound = true; } // Process all characters that are not whitespace or are between quotes if (inQuotes || leftCurlies > 0 || nonWhitespaceFound) { // We have a non-whitespace character, so ensure we have // a string builder to accumulate characters and a list to collect // attributes and delimiters. These are lazily // created so that simple cases that have no parameters do not // create any extra objects. if (stringBuilder == null) { stringBuilder = new StringBuilder(length); list = new ArrayList(1); } // If the character is escaped, then it is part of the attribute // being collected, regardless of its value and is not treated as // a delimiter or special character. Write back the escape // character since downstream processing will need it to determine // whether the value is a MarkupExtension or not, and to prevent // multiple escapes from being lost by recursive processing. if (gotEscape) { stringBuilder.Append('\\'); stringBuilder.Append(args[i]); gotEscape = false; continue; } // If quoted or inside curlies then scoop up everything. // Track yet deeper nestings of curlies. if (inQuotes || leftCurlies > 0) { if (inQuotes && args[i] == quoteChar) { // If we're inside quotes, then only an end quote that is not // escaped is special, and will act as a delimiter. inQuotes = false; nonWhitespaceFound = false; // Don't trim leading and trailing spaces that were inside quotes. AddToTokenList(list, stringBuilder, false); } else { if (leftCurlies > 0 && args[i] == '}') { leftCurlies--; } else if (args[i] == '{') { leftCurlies++; } stringBuilder.Append(args[i]); } } // 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 (inBracketCharacterMode) { stringBuilder.Append(args[i]); if (bracketCharacters.StartsEscapeSequence(args[i])) { bracketCharacterStack.Push(args[i]); } else if (bracketCharacters.EndsEscapeSequence(args[i])) { if (bracketCharacters.Match(bracketCharacterStack.Peek(), args[i])) { bracketCharacterStack.Pop(); } else { ThrowException(nameof(SR.ParserMarkupExtensionInvalidClosingBracketCharacers), args[i].ToString(), lineNumber, linePosition); } } if (bracketCharacterStack.Count == 0) { inBracketCharacterMode = false; } } else // not in quotes or inside nested curlies. Parse the Tokens { // not in special escape mode either switch(args[i]) { case '"': case '\'': // If we're not inside quotes, then a start quote can only // occur as the first non-whitespace character in a name or value. if (stringBuilder.Length != 0) { ThrowException(nameof(SR.ParserMarkupExtensionNoQuotesInName), args, lineNumber, linePosition); } inQuotes = true; quoteChar = args[i]; break; case ',': case '=': if (inCtorParsingMode && args[i] == ',') { inCtorParsingMode = ++currentConstructorParam < maxConstructorParams; if (inCtorParsingMode) { parameterName = constructorParameters[currentConstructorParam].Name; bracketCharacters = GetBracketCharacterForProperty(parameterName, bracketCharacterCache); } } // If we have a token in the stringbuilder, then store it if (stringBuilder != null && stringBuilder.Length > 0) { AddToTokenList(list, stringBuilder, true); if (bracketCharacterStack.Count != 0) { ThrowException(nameof(SR.ParserMarkupExtensionMalformedBracketCharacers), bracketCharacterStack.Peek().ToString(), lineNumber, linePosition); } } else if (list.Count == 0) { // Must have an attribute before you have the first delimeter ThrowException(nameof(SR.ParserMarkupExtensionDelimiterBeforeFirstAttribute), args, lineNumber, linePosition); } else if (list[list.Count - 1] is Char) { // Can't have two delimiters in a row, so check what is on // the list and complain if the last item is a character, or if // a delimiter is the first item. ThrowException(nameof(SR.ParserMarkupExtensionBadDelimiter), args, lineNumber, linePosition); } if (args[i] == '=') { inCtorParsingMode = false; // find BracketCharacterAttribute for the property name that preceeded this = delimiter parameterName = (string) list[list.Count - 1]; bracketCharacters = GetBracketCharacterForProperty(parameterName, bracketCharacterCache); } // Append known delimiters. list.Add(args[i]); nonWhitespaceFound = false; break; case '}': // If we hit the outside right curly brace, then end processing. If // there is a delimiter on the top of the stack and we haven't // hit another non-whitespace character, then its an error gotFinalCloseCurly = true; if (stringBuilder != null) { if (stringBuilder.Length > 0) { AddToTokenList(list, stringBuilder, true); } else if (list.Count > 0 && (list[list.Count-1] is Char)) { ThrowException(nameof(SR.ParserMarkupExtensionBadDelimiter), args, lineNumber, linePosition); } } break; case '{': leftCurlies++; stringBuilder.Append(args[i]); break; default: if (bracketCharacters != null && bracketCharacters.StartsEscapeSequence(args[i])) { bracketCharacterStack.Clear(); bracketCharacterStack.Push(args[i]); inBracketCharacterMode = true; } // Must just be a plain old character, so add it to the stringbuilder stringBuilder.Append(args[i]); break; } } } } // If we've accumulated content but haven't hit a terminating '}' then the // format is bad, so complain. if (!gotFinalCloseCurly) { ThrowException(nameof(SR.ParserMarkupExtensionNoEndCurlie), "}", lineNumber, linePosition); } // If there is junk after the closing '}', complain else if (i < length) { ThrowException(nameof(SR.ParserMarkupExtensionTrailingGarbage), "}", args.Substring(i,length-(i)), lineNumber, linePosition); } return list; } private static void AddToTokenList(ArrayList list, StringBuilder sb, bool trim) { if(trim) { Debug.Assert(sb.Length > 0); Debug.Assert(!Char.IsWhiteSpace(sb[0])); int i = sb.Length-1; while(Char.IsWhiteSpace(sb[i])) --i; sb.Length = i+1; } list.Add(sb.ToString()); sb.Length = 0; } /// <summary> /// Returns the list of parameters of the constructor with the most number /// of arguments. /// </summary> private ParameterInfo[] FindLongestConstructor(Type extensionType, out int maxConstructorArguments) { ParameterInfo[] constructorParameters = null; ConstructorInfo[] constructors = extensionType.GetConstructors(BindingFlags.Public | BindingFlags.Instance); maxConstructorArguments = 0; foreach (ConstructorInfo ctor in constructors) { ParameterInfo[] parInfo = ctor.GetParameters(); if (parInfo.Length >= maxConstructorArguments) { maxConstructorArguments = parInfo.Length; constructorParameters = parInfo; } } return constructorParameters; } /// <summary> /// At this point the start element is written, and we have to process all /// the constructor parameters that follow. Stop when we hit the first /// name=value, or when the end of the attributes is reached. /// </summary> private void WriteConstructorParams( ArrayList xamlNodes, ArrayList list, DefAttributeData data, ref int listIndex) { #if PBTCOMPILER int numberOfConstructorAttributes = 0; #endif if (list != null && listIndex < list.Count) { // Mark the start of the constructor parameter section. Nodes directly // under this one are constructor parameters. Note that we can have // element trees under here. xamlNodes.Add(new XamlConstructorParametersStartNode( data.LineNumber, data.LinePosition, ++data.Depth)); for (; listIndex < list.Count; listIndex+=2) { if (!(list[listIndex] is String)) { ThrowException(nameof(SR.ParserMarkupExtensionBadConstructorParam), data.Args, data.LineNumber, data.LinePosition); } // If the next item after the current one is '=', then we've hit the // start of named parameters, so stop if (list.Count > (listIndex+1) && list[listIndex+1] is Char && (Char)list[listIndex+1] == '=') { break; } #if PBTCOMPILER numberOfConstructorAttributes++; #endif // Handle nested markup extensions by recursing here. If the // value is not a markup extension, just store it as text for // runtime resolution as a constructor parameter. string value = (String)list[listIndex]; AttributeData nestedData = IsMarkupExtensionAttribute(data.DeclaringType, string.Empty, ref value, data.LineNumber, data.LinePosition, data.Depth, null); if (nestedData == null) { RemoveEscapes(ref value); xamlNodes.Add(new XamlTextNode( data.LineNumber, data.LinePosition, data.Depth, value, null)); } else { CompileAttributeCore(xamlNodes, nestedData); } } // End of constructor parameter section. xamlNodes.Add(new XamlConstructorParametersEndNode( data.LineNumber, data.LinePosition, data.Depth--)); #if PBTCOMPILER if (data.TargetType != typeof(UnknownMarkupExtension)) { // For compile mode, check that there is a constructor with the correct // number of arguments. In xaml load scenarios, the BamlRecordReader // will do this, so don't bother doing it here. If the target type is // unknown, then we defer this check until it can be resolved. ConstructorInfo[] infos = data.TargetType.GetConstructors(BindingFlags.Public | BindingFlags.Instance); for (int i=0; i<infos.Length; i++) { ConstructorInfo info = infos[i]; ParameterInfo[] paramInfos = info.GetParameters(); if (paramInfos.Length == numberOfConstructorAttributes) { // Found a constructor with the right number of arguments return; } } // If we get to here, then no matching constructor was found, so complain ThrowException(nameof(SR.ParserBadConstructorParams), data.TargetType.Name, numberOfConstructorAttributes.ToString(CultureInfo.CurrentCulture), data.LineNumber, data.LinePosition); } #endif } } /// <summary> /// At this point the start element is created and all the constructor params /// have been processed. Now handle the name=value pairs as property sets. /// If we have used a regular start element record, then just use regular /// properties. /// </summary> private void WriteProperties( ArrayList xamlNodes, ArrayList list, int listIndex, DefAttributeData data) { if (list != null && listIndex < list.Count) { ArrayList propertyNamesSoFar = new ArrayList(list.Count/4); for (int k = listIndex; k < list.Count; k+=4) { if (k > (list.Count-3) || (list[k+1] is String) || ((Char)list[k+1]) != '=') { ThrowException(nameof(SR.ParserMarkupExtensionNoNameValue), data.Args, data.LineNumber, data.LinePosition); } // See if this is a duplicate property definition, and throw if it is. string propertyName = list[k] as String; propertyName = propertyName.Trim(); if (propertyNamesSoFar.Contains(propertyName)) { ThrowException(nameof(SR.ParserDuplicateMarkupExtensionProperty), propertyName, data.LineNumber, data.LinePosition); } propertyNamesSoFar.Add( propertyName ); // Fetch the property context int nameIndex = propertyName.IndexOf(':'); string localName = (nameIndex < 0) ? propertyName : propertyName.Substring(nameIndex+1); string prefix = (nameIndex < 0) ? String.Empty : propertyName.Substring(0, nameIndex); string attribNamespaceURI = ResolveAttributeNamespaceURI(prefix, localName, data.TargetNamespaceUri); object dynamicObject; string assemblyName; string typeFullName; Type declaringType; string dynamicObjectName; AttributeContext attributeContext = GetAttributeContext( data.TargetType, data.TargetNamespaceUri, attribNamespaceURI, localName, out dynamicObject, out assemblyName, out typeFullName, out declaringType, out dynamicObjectName); // Handle nested markup extensions by recursing here. If the // value is not a markup extension, just store it as text for // runtime resolution. string strValue = list[k+2] as String; AttributeData nestedAttrData = IsMarkupExtensionAttribute( data.TargetType, propertyName, ref strValue, data.LineNumber, data.LinePosition, data.Depth, dynamicObject); list[k+2] = strValue; if (data.IsUnknownExtension) { // For unknown extensions, no more work should be done. // In pass1, we don't yet have the context to make sense of the nested properties, // so recursing into them would lead to spurious parse errors. // In pass2 an unknown extension would have errored out before getting here. return; } if (nestedAttrData != null) { if (nestedAttrData.IsSimple) { CompileProperty(xamlNodes, propertyName, nestedAttrData.Args, data.TargetType, data.TargetNamespaceUri, // xmlns of TargetType nestedAttrData, nestedAttrData.LineNumber, nestedAttrData.LinePosition, nestedAttrData.Depth); } else { // NOTE: Consider checking validity of property by calling GetAttributeContext here. CompileAttribute(xamlNodes, nestedAttrData); } } else { CompileProperty(xamlNodes, propertyName, ((String)list[k+2]), data.TargetType, data.TargetNamespaceUri, // xmlns of TargetType null, data.LineNumber, data.LinePosition, data.Depth); } } } } private string ResolveAttributeNamespaceURI(string prefix, string name, string parentURI) { string attribNamespaceURI; if(!String.IsNullOrEmpty(prefix)) { attribNamespaceURI = _parserHelper.LookupNamespace(prefix); } else { // if the prefix was "" then // 1) normal properties resolve to the parent Tag namespace. // 2) Attached properties resolve to the "" default namespace. #if !NETFX if (!name.Contains('.')) #else if (!name.Contains(".")) #endif attribNamespaceURI = parentURI; else attribNamespaceURI = _parserHelper.LookupNamespace(""); } return attribNamespaceURI; } /// <summary> /// Looks up the already constructed BracketCharacter cache for the BracketCharacters on /// the given property. /// </summary> private SpecialBracketCharacters GetBracketCharacterForProperty(string propertyName, Dictionary<string, SpecialBracketCharacters> bracketCharacterCache) { SpecialBracketCharacters bracketCharacters = null; if (bracketCharacterCache != null && bracketCharacterCache.ContainsKey(propertyName)) { bracketCharacters = bracketCharacterCache[propertyName]; } return bracketCharacters; } /// <summary> /// Represent a single property for a MarkupExtension as a complex property. /// </summary> private void CompileProperty( ArrayList xamlNodes, string name, string value, Type parentType, string parentTypeNamespaceUri, AttributeData data, int lineNumber, int linePosition, int depth) { RemoveEscapes(ref name); RemoveEscapes(ref value); int nameIndex = name.IndexOf(':'); string localName = (nameIndex < 0) ? name : name.Substring(nameIndex+1); string prefix = (nameIndex < 0) ? String.Empty : name.Substring(0, nameIndex); string attribNamespaceURI = ResolveAttributeNamespaceURI(prefix, localName, parentTypeNamespaceUri); object dynamicObject; string assemblyName; string typeFullName; Type declaringType; string dynamicObjectName; if (String.IsNullOrEmpty(attribNamespaceURI)) { ThrowException(nameof(SR.ParserPrefixNSProperty), prefix, name, lineNumber, linePosition); } AttributeContext attributeContext = GetAttributeContext( parentType, parentTypeNamespaceUri, attribNamespaceURI, localName, out dynamicObject, out assemblyName, out typeFullName, out declaringType, out dynamicObjectName); if (attributeContext != AttributeContext.Property) { ThrowException(nameof(SR.ParserMarkupExtensionUnknownAttr), localName, parentType.FullName, lineNumber, linePosition); } MemberInfo info = dynamicObject as MemberInfo; Debug.Assert(null != info, "No property or method info for field Name"); if (data != null && data.IsSimple) { if (data.IsTypeExtension) { string typeValueFullName = value; // set this to original value for error reporting if reqd. string typeValueAssemblyFullName = null; Type typeValue = _parserContext.XamlTypeMapper.GetTypeFromBaseString(value, _parserContext, true); if (typeValue != null) { typeValueFullName = typeValue.FullName; typeValueAssemblyFullName = typeValue.Assembly.FullName; } XamlPropertyWithTypeNode xamlPropertyWithTypeNode = new XamlPropertyWithTypeNode(data.LineNumber, data.LinePosition, data.Depth, dynamicObject, assemblyName, typeFullName, localName, typeValueFullName, typeValueAssemblyFullName, typeValue, string.Empty, string.Empty); xamlNodes.Add(xamlPropertyWithTypeNode); } else { XamlPropertyWithExtensionNode xamlPropertyWithExtensionNode = new XamlPropertyWithExtensionNode(data.LineNumber, data.LinePosition, data.Depth, dynamicObject, assemblyName, typeFullName, localName, value, data.ExtensionTypeId, data.IsValueNestedExtension, data.IsValueTypeExtension); xamlNodes.Add(xamlPropertyWithExtensionNode); } } else { XamlPropertyNode xamlPropertyNode = new XamlPropertyNode(lineNumber, linePosition, depth, dynamicObject, assemblyName, typeFullName, dynamicObjectName, value, BamlAttributeUsage.Default, true); xamlNodes.Add(xamlPropertyNode); } } /// <summary> /// Remove any '\' escape characters from the passed string. This does a simple /// pass through the string and won't do anything if there are no '\' characters. /// </summary> internal static void RemoveEscapes(ref string value) { StringBuilder builder=null; bool noEscape = true; for (int i = 0; i < value.Length; i++) { if (noEscape && value[i] == '\\') { if (builder == null) { builder = new StringBuilder(value.Length); builder.Append(value, 0, i); } noEscape = false; } else if (builder != null) { builder.Append(value[i]); noEscape = true; } } if (builder != null) { value = builder.ToString(); } } /// <summary> /// Get property information for an attribute in a MarkupExtension. This is /// very similar code to what is done in XamlReaderHelper, but we only look for clr /// properties here, since MarkupExtensions don't support events or /// DependencyProperties. /// </summary> AttributeContext GetAttributeContext( Type elementBaseType, string elementBaseTypeNamespaceUri, string attributeNamespaceUri, string attributeLocalName, out Object dynamicObject, // resolved object. out string assemblyName, // assemblyName the declaringType is found in out string typeFullName, // typeFullName of the object that the field is on out Type declaringType, // type of the object that the field is on out string dynamicObjectName) // name of the dynamicObject if found one { AttributeContext attributeContext = AttributeContext.Unknown; dynamicObject = null; assemblyName = null; typeFullName = null; declaringType = null; dynamicObjectName = null; // First, check if this is a CLR property using Static setter name // matching or property info lookups on element base type. MemberInfo mi = _parserContext.XamlTypeMapper.GetClrInfo(false, elementBaseType, attributeNamespaceUri, attributeLocalName, ref dynamicObjectName); if (null != mi) { attributeContext = AttributeContext.Property; dynamicObject = mi; declaringType = mi.DeclaringType; typeFullName = declaringType.FullName; assemblyName = declaringType.Assembly.FullName; } return attributeContext; } /// <summary> /// Throw a XamlParseException /// </summary> void ThrowException( string id, string parameter1, int lineNumber, int linePosition) { string message = SR.Format(SR.GetResourceString(id), parameter1); ThrowExceptionWithLine(message, lineNumber, linePosition); } /// <summary> /// Throw a XamlParseException /// </summary> void ThrowException( string id, string parameter1, string parameter2, int lineNumber, int linePosition) { string message = SR.Format(SR.GetResourceString(id), parameter1, parameter2); ThrowExceptionWithLine(message, lineNumber, linePosition); } /// <summary> /// Throw a XamlParseException /// </summary> void ThrowException( string id, string parameter1, string parameter2, string parameter3, int lineNumber, int linePosition) { string message = SR.Format(SR.GetResourceString(id), parameter1, parameter2, parameter3); ThrowExceptionWithLine(message, lineNumber, linePosition); } /// <summary> /// Throw a XamlParseException /// </summary> void ThrowExceptionWithLine( string message, int lineNumber, int linePosition) { message += " "; message += SR.Format(SR.ParserLineAndOffset, lineNumber.ToString(CultureInfo.CurrentCulture), linePosition.ToString(CultureInfo.CurrentCulture)); XamlParseException parseException = new XamlParseException(message, lineNumber, linePosition); throw parseException; } // Helper that provices namespace and type resolutions private IParserHelper _parserHelper; // Parser Context for the current node being parsed. private ParserContext _parserContext; internal class UnknownMarkupExtension { } } } |