File: ExpandMarkupsVisitor.cs
Web Access
Project: src\src\Controls\src\Xaml\Controls.Xaml.csproj (Microsoft.Maui.Controls.Xaml)
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Xml;
using Microsoft.Maui.Controls.Xaml.Internals;
 
namespace Microsoft.Maui.Controls.Xaml
{
	[RequiresUnreferencedCode(TrimmerConstants.XamlRuntimeParsingNotSupportedWarning)]
#if !NETSTANDARD
	[RequiresDynamicCode(TrimmerConstants.XamlRuntimeParsingNotSupportedWarning)]
#endif
	class ExpandMarkupsVisitor : IXamlNodeVisitor
	{
		public ExpandMarkupsVisitor(HydrationContext context) => Context = context;
 
		public static readonly IList<XmlName> Skips = new List<XmlName>
		{
			XmlName.xKey,
			XmlName.xTypeArguments,
			XmlName.xFactoryMethod,
			XmlName.xName,
			XmlName.xDataType
		};
 
		Dictionary<INode, object> Values
		{
			get { return Context.Values; }
		}
 
		HydrationContext Context { get; }
 
		public TreeVisitingMode VisitingMode => TreeVisitingMode.BottomUp;
		public bool StopOnDataTemplate => false;
		public bool StopOnResourceDictionary => false;
		public bool VisitNodeOnDataTemplate => true;
		public bool SkipChildren(INode node, INode parentNode) => false;
		public bool IsResourceDictionary(ElementNode node) => false;
 
		public void Visit(ValueNode node, INode parentNode)
		{
		}
 
		public void Visit(MarkupNode markupnode, INode parentNode)
		{
			var parentElement = parentNode as IElementNode;
			XmlName propertyName;
			if (!markupnode.TryGetPropertyName(parentNode, out propertyName))
				return;
			if (Skips.Contains(propertyName))
				return;
			if (parentElement.SkipProperties.Contains(propertyName))
				return;
 
			var markupString = markupnode.MarkupString;
			var node =
				ParseExpression(ref markupString, markupnode.NamespaceResolver, markupnode, markupnode, parentNode) as IElementNode;
			if (node != null)
			{
				((IElementNode)parentNode).Properties[propertyName] = node;
				node.Parent = parentNode;
			}
		}
 
		public void Visit(ElementNode node, INode parentNode)
		{
		}
 
		public void Visit(RootNode node, INode parentNode)
		{
		}
 
		public void Visit(ListNode node, INode parentNode)
		{
		}
 
		INode ParseExpression(ref string expression, IXmlNamespaceResolver nsResolver, IXmlLineInfo xmlLineInfo, INode node,
			INode parentNode)
		{
			if (expression.StartsWith("{}", StringComparison.Ordinal))
				return new ValueNode(expression.Substring(2), null);
 
			if (expression[expression.Length - 1] != '}')
			{
				var ex = new XamlParseException("Expression must end with '}'", xmlLineInfo);
				if (Context.ExceptionHandler != null)
				{
					Context.ExceptionHandler(ex);
					return null;
				}
				throw ex;
			}
 
			if (!MarkupExpressionParser.MatchMarkup(out var match, expression, out var len))
				throw new Exception();
 
			expression = expression.Substring(len).TrimStart();
			if (expression.Length == 0)
			{
				var ex = new XamlParseException("Expression did not end in '}'", xmlLineInfo);
				if (Context.ExceptionHandler != null)
				{
					Context.ExceptionHandler(ex);
					return null;
				}
				throw ex;
			}
			var serviceProvider = new XamlServiceProvider(node, Context);
			serviceProvider.Add(typeof(IXmlNamespaceResolver), nsResolver);
 
			return new MarkupExpansionParser { ExceptionHandler = Context.ExceptionHandler }.Parse(match, ref expression, serviceProvider);
		}
 
		[RequiresUnreferencedCode(TrimmerConstants.XamlRuntimeParsingNotSupportedWarning)]
		public class MarkupExpansionParser : MarkupExpressionParser, IExpressionParser<INode>
		{
			IElementNode _node;
			internal Action<Exception> ExceptionHandler { get; set; }
			object IExpressionParser.Parse(string match, ref string remaining, IServiceProvider serviceProvider)
			{
				return Parse(match, ref remaining, serviceProvider);
			}
 
			public INode Parse(string match, ref string remaining, IServiceProvider serviceProvider)
			{
				if (!(serviceProvider.GetService(typeof(IXmlNamespaceResolver)) is IXmlNamespaceResolver nsResolver))
					throw new ArgumentException();
				IXmlLineInfo xmlLineInfo = null;
				if (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider xmlLineInfoProvider)
					xmlLineInfo = xmlLineInfoProvider.XmlLineInfo;
 
				var (prefix, name) = ParseName(match);
 
				var namespaceuri = nsResolver.LookupNamespace(prefix) ?? "";
 
				IList<XmlType> typeArguments = null;
				var childnodes = new List<(XmlName, INode)>();
				var contentname = new XmlName(null, null);
 
				if (remaining.StartsWith("}", StringComparison.Ordinal))
				{
					remaining = remaining.Substring(1);
				}
				else
				{
					Property parsed;
					do
					{
						parsed = ParseProperty(serviceProvider, ref remaining);
						XmlName childname;
 
						if (parsed.name == null)
						{
							childname = contentname;
						}
						else
						{
							var (propertyPrefix, propertyName) = ParseName(parsed.name);
 
							childname = XamlParser.ParsePropertyName(new XmlName(
								propertyPrefix == "" ? null : nsResolver.LookupNamespace(propertyPrefix),
								propertyName));
 
							if (childname.NamespaceURI == null && childname.LocalName == null)
								continue;
						}
 
						if (childname == XmlName.xTypeArguments)
						{
							typeArguments = TypeArgumentsParser.ParseExpression(parsed.strValue, nsResolver, xmlLineInfo);
							childnodes.Add((childname, new ValueNode(typeArguments, nsResolver)));
						}
						else
						{
							var childnode = parsed.value as INode ?? new ValueNode(parsed.strValue, nsResolver);
							childnodes.Add((childname, childnode));
						}
					}
					while (!parsed.last);
				}
 
 
				if (!(serviceProvider.GetService(typeof(IXamlTypeResolver)) is XamlTypeResolver typeResolver))
					throw new NotSupportedException();
 
				var xmltype = new XmlType(namespaceuri, name + "Extension", typeArguments);
 
				//The order of lookup is to look for the Extension-suffixed class name first and then look for the class name without the Extension suffix.
				if (!typeResolver.TryResolve(xmltype, out _))
				{
					xmltype = new XmlType(namespaceuri, name, typeArguments);
					if (!typeResolver.TryResolve(xmltype, out _))
					{
						var ex = new XamlParseException($"MarkupExtension not found for {match}", serviceProvider);
						if (ExceptionHandler != null)
						{
							ExceptionHandler(ex);
							return null;
						}
						throw ex;
					}
				}
 
				_node = xmlLineInfo == null
					? new ElementNode(xmltype, null, nsResolver)
					: new ElementNode(xmltype, null, nsResolver, xmlLineInfo.LineNumber, xmlLineInfo.LinePosition);
 
				foreach (var (childname, childnode) in childnodes)
				{
					childnode.Parent = _node;
 
					if (childname == contentname)
					{
						//ContentProperty
						_node.CollectionItems.Add(childnode);
					}
					else
					{
						_node.Properties[childname] = childnode;
					}
				}
 
				return _node;
			}
		}
	}
}