11using System ;
2+ using System . Collections ;
23using System . Collections . Generic ;
34using System . Linq ;
45using System . Text ;
56using System . Text . RegularExpressions ;
67using Microsoft . VisualStudio . TestTools . UnitTesting ;
8+ using Uno . Toolkit . RuntimeTests . Extensions ;
79
810#if IS_WINUI
911using Microsoft . UI . Xaml . Markup ;
@@ -15,6 +17,15 @@ namespace Uno.Toolkit.RuntimeTests.Helpers
1517{
1618 internal static class XamlHelper
1719 {
20+ public static readonly IReadOnlyDictionary < string , string > KnownXmlnses = new Dictionary < string , string >
21+ {
22+ [ string . Empty ] = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" ,
23+ [ "x" ] = "http://schemas.microsoft.com/winfx/2006/xaml" ,
24+ [ "toolkit" ] = "using:Uno.UI.Toolkit" , // uno utilities
25+ [ "utu" ] = "using:Uno.Toolkit.UI" , // this library
26+ [ "muxc" ] = "using:Microsoft.UI.Xaml.Controls" ,
27+ } ;
28+
1829 /// <summary>
1930 /// Matches right before the > or \> tail of any tag.
2031 /// </summary>
@@ -28,56 +39,159 @@ internal static class XamlHelper
2839 /// </summary>
2940 private static readonly Regex NonXmlnsTagRegex = new Regex ( @"<\w+[ />]" ) ;
3041
31- private static readonly IReadOnlyDictionary < string , string > KnownXmlnses = new Dictionary < string , string >
32- {
33- [ string . Empty ] = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" ,
34- [ "x" ] = "http://schemas.microsoft.com/winfx/2006/xaml" ,
35- [ "toolkit" ] = "using:Uno.UI.Toolkit" , // uno utilities
36- [ "utu" ] = "using:Uno.Toolkit.UI" , // this library
37- [ "muxc" ] = "using:Microsoft.UI.Xaml.Controls" ,
38- } ;
42+ /// <summary>
43+ /// Matches any open/open-hanging/self-close/close tag.
44+ /// </summary>
45+ /// <remarks>open-hanging refers to xml tag that opens, but span on multiple lines.</remarks>
46+ private static readonly Regex XmlTagRegex = new Regex ( "<[^>]+(>|$)" ) ;
3947
4048 /// <summary>
41- /// XamlReader.Load the xaml and type-check result .
49+ /// Auto complete any unclosed tag .
4250 /// </summary>
43- /// <param name="xaml">Xaml with single or double quotes </param>
44- /// <param name="autoInjectXmlns">Toggle automatic detection of xmlns required and inject to the xaml</param >
45- public static T LoadXaml < T > ( string xaml , bool autoInjectXmlns = true ) where T : class
51+ /// <param name="xaml"></param>
52+ /// <returns></returns >
53+ internal static string XamlAutoFill ( string xaml )
4654 {
47- var xmlnses = new Dictionary < string , string > ( ) ;
55+ var buffer = new StringBuilder ( ) ;
4856
49- if ( autoInjectXmlns )
57+ // we assume the input is either space or tab indented, not mixed.
58+ // it doesnt really matter here if we count the depth in 1 or 2 or 4,
59+ // since they will be compared against themselves, which hopefully follow the same "style".
60+ var stack = new Stack < ( string Indent , string Name ) > ( ) ;
61+ void PopFrame ( ( string Indent , string Name ) frame )
5062 {
51- foreach ( var xmlns in KnownXmlnses )
63+ buffer . AppendLine ( $ "{ frame . Indent } </{ frame . Name } >") ;
64+ }
65+ void PopStack ( Stack < ( string Indent , string Name ) > stack )
66+ {
67+ while ( stack . TryPop ( out var item ) )
5268 {
53- var match = xmlns . Key == string . Empty
54- ? NonXmlnsTagRegex . IsMatch ( xaml )
55- // naively match the xmlns-prefix regardless if it is quoted,
56- // since false positive doesn't matter.
57- : xaml . Contains ( $ "{ xmlns . Key } :") ;
58- if ( match )
69+ PopFrame ( item ) ;
70+ }
71+ }
72+
73+ var lines = string . Concat ( xaml . Split ( '\r ' ) ) . Split ( '\n ' ) ;
74+ foreach ( var line in lines )
75+ {
76+ if ( line . TrimStart ( ) is { Length : > 0 } content )
77+ {
78+ var depth = line . Length - content . Length ;
79+ var indent = line [ 0 ..depth ] ;
80+
81+ // we should parse all tags on this line: Open OpenHanging SelfClose Close
82+ // then close all 'open/open-hanging' tags in the stack with higher depth
83+ // while pairing `Close` in the left-most part of current line with whats in stack that match name and depth, and eliminate them
84+
85+ var overflows = new Stack < ( string Indent , string Name ) > ( stack . PopWhile ( x => x . Indent . Length >= depth ) . Reverse ( ) ) ;
86+ var tags = XmlTagRegex . Matches ( content ) . Select ( x => x . Value ) . ToArray ( ) ;
87+ foreach ( var tag in tags )
5988 {
60- xmlnses . Add ( xmlns . Key , xmlns . Value ) ;
89+ if ( tag . StartsWith ( "<!" ) )
90+ {
91+ PopStack ( overflows ) ;
92+ }
93+ else if ( tag . EndsWith ( "/>" ) )
94+ {
95+ PopStack ( overflows ) ;
96+ }
97+ else if ( tag . StartsWith ( "</" ) )
98+ {
99+ var name = tag . Split ( ' ' , '>' ) [ 0 ] [ 2 ..] ;
100+ while ( overflows . TryPop ( out var overflow ) )
101+ {
102+ if ( overflow . Name == name ) break ;
103+
104+ PopFrame ( overflow ) ;
105+ }
106+ }
107+ else
108+ {
109+ PopStack ( overflows ) ;
110+
111+ var name = tag . Split ( ' ' , '/' , '>' ) [ 0 ] [ 1 ..] ;
112+ stack . Push ( ( indent , name ) ) ;
113+ }
61114 }
62115 }
116+ buffer . AppendLine ( line ) ;
63117 }
64118
65- return LoadXaml < T > ( xaml , xmlnses ) ;
119+ PopStack ( new ( stack . Reverse ( ) ) ) ;
120+ return buffer . ToString ( ) ;
121+ }
122+
123+ /// <summary>
124+ /// Inject any required xmlns.
125+ /// </summary>
126+ /// <param name="xaml"></param>
127+ /// <param name="xmlnses">Optional; used to override <see cref="KnownXmlnses"/>.</param>
128+ /// <param name="complementaryXmlnses">Completary xmlnses that adds to <paramref name="xmlnses"/></param>
129+ /// <returns></returns>
130+ internal static string InjectXmlns ( string xaml , IDictionary < string , string > ? xmlnses = null , IDictionary < string , string > ? complementaryXmlnses = null )
131+ {
132+ var xmlnsLookup = ( xmlnses ? . AsReadOnly ( ) ?? KnownXmlnses ) . Combine ( complementaryXmlnses ? . AsReadOnly ( ) ) ;
133+ var injectables = new Dictionary < string , string > ( ) ;
134+
135+ foreach ( var xmlns in xmlnsLookup )
136+ {
137+ var match = xmlns . Key == string . Empty
138+ ? NonXmlnsTagRegex . IsMatch ( xaml )
139+ // naively match the xmlns-prefix regardless if it is quoted,
140+ // since false positive doesn't matter.
141+ : xaml . Contains ( $ "{ xmlns . Key } :") ;
142+ if ( match )
143+ {
144+ injectables . Add ( xmlns . Key , xmlns . Value ) ;
145+ }
146+ }
147+
148+ if ( injectables . Any ( ) )
149+ {
150+ var injection = " " + string . Join ( " " , injectables
151+ . Select ( x => $ "xmlns{ ( string . IsNullOrEmpty ( x . Key ) ? "" : $ ":{ x . Key } ") } =\" { x . Value } \" ")
152+ ) ;
153+
154+ xaml = EndOfTagRegex . Replace ( xaml , injection . TrimEnd ( ) , 1 ) ;
155+ }
156+
157+ return xaml ;
158+ }
159+
160+ /// <summary>
161+ /// Load partial xaml with omittable closing tags.
162+ /// </summary>
163+ /// <param name="xaml">Xaml with single or double quotes</param>
164+ /// <param name="xmlnses">Optional; xmlns that may be needed. <see cref="KnownXmlnses"/> will be used if null.</param>
165+ /// <param name="complementaryXmlnses">Completary xmlnses that adds to <paramref name="xmlnses"/></param>
166+ /// <returns></returns>
167+ public static T LoadPartialXaml < T > ( string xaml , IDictionary < string , string > ? xmlnses = null , IDictionary < string , string > ? complementaryXmlnses = null )
168+ where T : class
169+ {
170+ xaml = XamlAutoFill ( xaml ) ;
171+ xaml = InjectXmlns ( xaml , xmlnses , complementaryXmlnses ) ;
172+
173+ return LoadXaml < T > ( xaml ) ;
66174 }
67175
68176 /// <summary>
69177 /// XamlReader.Load the xaml and type-check result.
70178 /// </summary>
71179 /// <param name="xaml">Xaml with single or double quotes</param>
72- /// <param name="xmlnses">Xmlns to inject; use string.Empty for the default xmlns' key</param>
73- public static T LoadXaml < T > ( string xaml , Dictionary < string , string > xmlnses ) where T : class
180+ /// <param name="xmlnses">Optional; xmlns that may be needed. <see cref="KnownXmlnses"/> will be used if null.</param>
181+ /// <param name="complementaryXmlnses">Completary xmlnses that adds to <paramref name="xmlnses"/></param>
182+ public static T LoadXaml < T > ( string xaml , IDictionary < string , string > ? xmlnses = null , IDictionary < string , string > ? complementaryXmlnses = null )
183+ where T : class
74184 {
75- var injection = " " + string . Join ( " " , xmlnses
76- . Select ( x => $ "xmlns{ ( string . IsNullOrEmpty ( x . Key ) ? "" : $ ":{ x . Key } ") } =\" { x . Value } \" ")
77- ) ;
185+ xaml = InjectXmlns ( xaml , xmlnses , complementaryXmlnses ) ;
78186
79- xaml = EndOfTagRegex . Replace ( xaml , injection . TrimEnd ( ) , 1 ) ;
187+ return LoadXaml < T > ( xaml , xmlnses ) ;
188+ }
80189
190+ /// <summary>
191+ /// XamlReader.Load the xaml and type-check result.
192+ /// </summary>
193+ private static T LoadXaml < T > ( string xaml ) where T : class
194+ {
81195 var result = XamlReader . Load ( xaml ) ;
82196 Assert . IsNotNull ( result , "XamlReader.Load returned null" ) ;
83197 Assert . IsInstanceOfType ( result , typeof ( T ) , "XamlReader.Load did not return the expected type" ) ;
0 commit comments