File: ExpandMarkupsVisitor.cs
Web Access
Project: src\src\Controls\src\Build.Tasks\Controls.Build.Tasks.csproj (Microsoft.Maui.Controls.Build.Tasks)
using System;
using System.Collections.Generic;
using System.Xml;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Controls.Xaml.Internals;
 
namespace Microsoft.Maui.Controls.Build.Tasks
{
	class ExpandMarkupsVisitor : IXamlNodeVisitor
	{
		readonly IList<XmlName> _skips = new List<XmlName>
		{
			XmlName.xKey,
			XmlName.xTypeArguments,
			XmlName.xFactoryMethod,
			XmlName.xName,
		};
 
		public ExpandMarkupsVisitor(ILContext context) => Context = context;
 
		ILContext 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)
		{
			var parentVar = Context.Variables[(IElementNode)node];
			return parentVar.VariableType.FullName == "Microsoft.Maui.Controls.ResourceDictionary"
				|| parentVar.VariableType.Resolve().BaseType?.FullName == "Microsoft.Maui.Controls.ResourceDictionary";
		}
 
		public void Visit(ValueNode node, INode parentNode)
		{
		}
 
		public void Visit(MarkupNode markupnode, INode parentNode)
		{
			if (!TryGetProperyName(markupnode, parentNode, out XmlName propertyName))
				return;
			if (_skips.Contains(propertyName))
				return;
			if (parentNode is IElementNode && ((IElementNode)parentNode).SkipProperties.Contains(propertyName))
				return;
			var markupString = markupnode.MarkupString;
			if (ParseExpression(ref markupString, Context, markupnode.NamespaceResolver, markupnode) is IElementNode node)
			{
				((IElementNode)parentNode).Properties[propertyName] = node;
				node.Accept(new XamlNodeVisitor((n, parent) => n.Parent = parent), parentNode);
			}
		}
 
		public void Visit(ElementNode node, INode parentNode)
		{
		}
 
		public void Visit(RootNode node, INode parentNode)
		{
		}
 
		public void Visit(ListNode node, INode parentNode)
		{
		}
 
		public static bool TryGetProperyName(INode node, INode parentNode, out XmlName name)
		{
			name = default(XmlName);
			if (!(parentNode is IElementNode parentElement))
				return false;
			foreach (var kvp in parentElement.Properties)
			{
				if (kvp.Value != node)
					continue;
				name = kvp.Key;
				return true;
			}
			return false;
		}
 
		static INode ParseExpression(ref string expression, ILContext context, IXmlNamespaceResolver nsResolver,
			IXmlLineInfo xmlLineInfo)
		{
			if (expression.StartsWith("{}", StringComparison.Ordinal))
				return new ValueNode(expression.Substring(2), null);
 
			if (expression[expression.Length - 1] != '}')
				throw new BuildException(BuildExceptionCode.MarkupNotClosed, xmlLineInfo, null);
 
			if (!MarkupExpressionParser.MatchMarkup(out var match, expression, out var len))
				throw new BuildException(BuildExceptionCode.MarkupParsingFailed, xmlLineInfo, null);
			expression = expression.Substring(len).TrimStart();
			if (expression.Length == 0)
				throw new BuildException(BuildExceptionCode.MarkupNotClosed, xmlLineInfo, null);
 
			var provider = new XamlServiceProvider(null, null);
			provider.Add(typeof(ILContextProvider), new ILContextProvider(context));
			provider.Add(typeof(IXmlNamespaceResolver), nsResolver);
			provider.Add(typeof(IXmlLineInfoProvider), new XmlLineInfoProvider(xmlLineInfo));
 
			return new MarkupExpansionParser().Parse(match, ref expression, provider);
		}
 
		class ILContextProvider
		{
			public ILContextProvider(ILContext context) => Context = context;
 
			public ILContext Context { get; }
		}
 
		class MarkupExpansionParser : MarkupExpressionParser, IExpressionParser<INode>
		{
			IElementNode _node;
 
			object IExpressionParser.Parse(string match, ref string remaining, IServiceProvider serviceProvider) => 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 contextProvider = serviceProvider.GetService(typeof(ILContextProvider)) as ILContextProvider;
 
				var split = match.Split(':');
				if (split.Length > 2)
					throw new ArgumentException();
 
				var (prefix, name) = ParseName(match);
 
				var namespaceuri = nsResolver.LookupNamespace(prefix) ?? "";
				if (!string.IsNullOrEmpty(prefix) && string.IsNullOrEmpty(namespaceuri))
					throw new BuildException(BuildExceptionCode.XmlnsUndeclared, xmlLineInfo, null, 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
					{
						try
						{
							parsed = ParseProperty(serviceProvider, ref remaining);
						}
						catch (XamlParseException xpe)
						{
							throw new BuildException(BuildExceptionCode.MarkupParsingFailed, xmlLineInfo, xpe);
						}
						XmlName childname;
 
						if (parsed.name == null)
						{
							childname = contentname;
						}
						else
						{
							var (propertyPrefix, propertyName) = ParseName(parsed.name);
 
							childname = XamlParser.ParsePropertyName(new XmlName(
								propertyPrefix == "" ? "" : 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);
				}
 
				//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.
				XmlType type = new XmlType(namespaceuri, name + "Extension", typeArguments);
				if (!type.TryGetTypeReference(contextProvider.Context.Cache, contextProvider.Context.Module, null, out _))
					type = new XmlType(namespaceuri, name, typeArguments);
 
				if (type == null)
					throw new NotSupportedException();
 
				_node = xmlLineInfo == null
					? new ElementNode(type, "", nsResolver)
					: new ElementNode(type, "", nsResolver, xmlLineInfo.LineNumber, xmlLineInfo.LinePosition);
 
				foreach (var (childname, childnode) in childnodes)
				{
					if (childname == contentname)
					{
						//ContentProperty
						_node.CollectionItems.Add(childnode);
					}
					else
					{
						_node.Properties[childname] = childnode;
					}
				}
 
				return _node;
			}
		}
	}
}