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 ;
9+ using static Uno . UI . FeatureConfiguration ;
10+
711
812#if IS_WINUI
913using Microsoft . UI . Xaml . Markup ;
@@ -15,6 +19,15 @@ namespace Uno.Toolkit.RuntimeTests.Helpers
1519{
1620 internal static class XamlHelper
1721 {
22+ public static readonly IReadOnlyDictionary < string , string > KnownXmlnses = new Dictionary < string , string >
23+ {
24+ [ string . Empty ] = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" ,
25+ [ "x" ] = "http://schemas.microsoft.com/winfx/2006/xaml" ,
26+ [ "toolkit" ] = "using:Uno.UI.Toolkit" , // uno utilities
27+ [ "utu" ] = "using:Uno.Toolkit.UI" , // this library
28+ [ "muxc" ] = "using:Microsoft.UI.Xaml.Controls" ,
29+ } ;
30+
1831 /// <summary>
1932 /// Matches right before the > or \> tail of any tag.
2033 /// </summary>
@@ -28,56 +41,159 @@ internal static class XamlHelper
2841 /// </summary>
2942 private static readonly Regex NonXmlnsTagRegex = new Regex ( @"<\w+[ />]" ) ;
3043
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- } ;
44+ /// <summary>
45+ /// Matches any open/open-hanging/self-close/close tag.
46+ /// </summary>
47+ /// <remarks>open-hanging refers to xml tag that opens, but span on multiple lines.</remarks>
48+ private static readonly Regex XmlTagRegex = new Regex ( "<[^>]+(>|$)" ) ;
3949
4050 /// <summary>
41- /// XamlReader.Load the xaml and type-check result .
51+ /// Auto complete any unclosed tag .
4252 /// </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
53+ /// <param name="xaml"></param>
54+ /// <returns></returns >
55+ internal static string XamlAutoFill ( string xaml )
4656 {
47- var xmlnses = new Dictionary < string , string > ( ) ;
57+ var buffer = new StringBuilder ( ) ;
4858
49- if ( autoInjectXmlns )
59+ // we assume the input is either space or tab indented, not mixed.
60+ // it doesnt really matter here if we count the depth in 1 or 2 or 4,
61+ // since they will be compared against themselves, which hopefully follow the same "style".
62+ var stack = new Stack < ( string Indent , string Name ) > ( ) ;
63+ void PopFrame ( ( string Indent , string Name ) frame )
64+ {
65+ buffer . AppendLine ( $ "{ frame . Indent } </{ frame . Name } >") ;
66+ }
67+ void PopStack ( Stack < ( string Indent , string Name ) > stack )
5068 {
51- foreach ( var xmlns in KnownXmlnses )
69+ while ( stack . TryPop ( out var item ) )
5270 {
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 )
71+ PopFrame ( item ) ;
72+ }
73+ }
74+
75+ var lines = string . Concat ( xaml . Split ( '\r ' ) ) . Split ( '\n ' ) ;
76+ foreach ( var line in lines )
77+ {
78+ if ( line . TrimStart ( ) is { Length : > 0 } content )
79+ {
80+ var depth = line . Length - content . Length ;
81+ var indent = line [ 0 ..depth ] ;
82+
83+ // we should parse all tags on this line: Open OpenHanging SelfClose Close
84+ // then close all 'open/open-hanging' tags in the stack with higher depth
85+ // while pairing `Close` in the left-most part of current line with whats in stack that match name and depth, and eliminate them
86+
87+ var overflows = new Stack < ( string Indent , string Name ) > ( stack . PopWhile ( x => x . Indent . Length >= depth ) . Reverse ( ) ) ;
88+ var tags = XmlTagRegex . Matches ( content ) . Select ( x => x . Value ) . ToArray ( ) ;
89+ foreach ( var tag in tags )
5990 {
60- xmlnses . Add ( xmlns . Key , xmlns . Value ) ;
91+ if ( tag . StartsWith ( "<!" ) )
92+ {
93+ PopStack ( overflows ) ;
94+ }
95+ else if ( tag . EndsWith ( "/>" ) )
96+ {
97+ PopStack ( overflows ) ;
98+ }
99+ else if ( tag . StartsWith ( "</" ) )
100+ {
101+ var name = tag . Split ( ' ' , '>' ) [ 0 ] [ 2 ..] ;
102+ while ( overflows . TryPop ( out var overflow ) )
103+ {
104+ if ( overflow . Name == name ) break ;
105+
106+ PopFrame ( overflow ) ;
107+ }
108+ }
109+ else
110+ {
111+ PopStack ( overflows ) ;
112+
113+ var name = tag . Split ( ' ' , '/' , '>' ) [ 0 ] [ 1 ..] ;
114+ stack . Push ( ( indent , name ) ) ;
115+ }
61116 }
62117 }
118+ buffer . AppendLine ( line ) ;
63119 }
64120
65- return LoadXaml < T > ( xaml , xmlnses ) ;
121+ PopStack ( new ( stack . Reverse ( ) ) ) ;
122+ return buffer . ToString ( ) ;
123+ }
124+
125+ /// <summary>
126+ /// Inject any required xmlns.
127+ /// </summary>
128+ /// <param name="xaml"></param>
129+ /// <param name="xmlnses">Optional; used to override <see cref="KnownXmlnses"/>.</param>
130+ /// <param name="complementaryXmlnses">Completary xmlnses that adds to <paramref name="xmlnses"/></param>
131+ /// <returns></returns>
132+ internal static string InjectXmlns ( string xaml , IDictionary < string , string > ? xmlnses = null , IDictionary < string , string > ? complementaryXmlnses = null )
133+ {
134+ var xmlnsLookup = ( xmlnses ? . AsReadOnly ( ) ?? KnownXmlnses ) . Combine ( complementaryXmlnses ? . AsReadOnly ( ) ) ;
135+ var injectables = new Dictionary < string , string > ( ) ;
136+
137+ foreach ( var xmlns in xmlnsLookup )
138+ {
139+ var match = xmlns . Key == string . Empty
140+ ? NonXmlnsTagRegex . IsMatch ( xaml )
141+ // naively match the xmlns-prefix regardless if it is quoted,
142+ // since false positive doesn't matter.
143+ : xaml . Contains ( $ "{ xmlns . Key } :") ;
144+ if ( match )
145+ {
146+ injectables . Add ( xmlns . Key , xmlns . Value ) ;
147+ }
148+ }
149+
150+ if ( injectables . Any ( ) )
151+ {
152+ var injection = " " + string . Join ( " " , injectables
153+ . Select ( x => $ "xmlns{ ( string . IsNullOrEmpty ( x . Key ) ? "" : $ ":{ x . Key } ") } =\" { x . Value } \" ")
154+ ) ;
155+
156+ xaml = EndOfTagRegex . Replace ( xaml , injection . TrimEnd ( ) , 1 ) ;
157+ }
158+
159+ return xaml ;
160+ }
161+
162+ /// <summary>
163+ /// Load partial xaml with omittable closing tags.
164+ /// </summary>
165+ /// <param name="xaml">Xaml with single or double quotes</param>
166+ /// <param name="xmlnses">Optional; xmlns that may be needed. <see cref="KnownXmlnses"/> will be used if null.</param>
167+ /// <param name="complementaryXmlnses">Completary xmlnses that adds to <paramref name="xmlnses"/></param>
168+ /// <returns></returns>
169+ public static T LoadPartialXaml < T > ( string xaml , IDictionary < string , string > ? xmlnses = null , IDictionary < string , string > ? complementaryXmlnses = null )
170+ where T : class
171+ {
172+ xaml = XamlAutoFill ( xaml ) ;
173+ xaml = InjectXmlns ( xaml , xmlnses , complementaryXmlnses ) ;
174+
175+ return LoadXaml < T > ( xaml ) ;
66176 }
67177
68178 /// <summary>
69179 /// XamlReader.Load the xaml and type-check result.
70180 /// </summary>
71181 /// <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
182+ /// <param name="xmlnses">Optional; xmlns that may be needed. <see cref="KnownXmlnses"/> will be used if null.</param>
183+ /// <param name="complementaryXmlnses">Completary xmlnses that adds to <paramref name="xmlnses"/></param>
184+ public static T LoadXaml < T > ( string xaml , IDictionary < string , string > ? xmlnses = null , IDictionary < string , string > ? complementaryXmlnses = null )
185+ where T : class
74186 {
75- var injection = " " + string . Join ( " " , xmlnses
76- . Select ( x => $ "xmlns{ ( string . IsNullOrEmpty ( x . Key ) ? "" : $ ":{ x . Key } ") } =\" { x . Value } \" ")
77- ) ;
187+ xaml = InjectXmlns ( xaml , xmlnses , complementaryXmlnses ) ;
78188
79- xaml = EndOfTagRegex . Replace ( xaml , injection . TrimEnd ( ) , 1 ) ;
189+ return LoadXaml < T > ( xaml , xmlnses ) ;
190+ }
80191
192+ /// <summary>
193+ /// XamlReader.Load the xaml and type-check result.
194+ /// </summary>
195+ private static T LoadXaml < T > ( string xaml ) where T : class
196+ {
81197 var result = XamlReader . Load ( xaml ) ;
82198 Assert . IsNotNull ( result , "XamlReader.Load returned null" ) ;
83199 Assert . IsInstanceOfType ( result , typeof ( T ) , "XamlReader.Load did not return the expected type" ) ;
0 commit comments