1
1
using System ;
2
+ using System . Collections ;
2
3
using System . Collections . Generic ;
3
4
using System . Linq ;
4
5
using System . Text ;
5
6
using System . Text . RegularExpressions ;
6
7
using Microsoft . VisualStudio . TestTools . UnitTesting ;
8
+ using Uno . Toolkit . RuntimeTests . Extensions ;
9
+ using static Uno . UI . FeatureConfiguration ;
10
+
7
11
8
12
#if IS_WINUI
9
13
using Microsoft . UI . Xaml . Markup ;
@@ -15,6 +19,15 @@ namespace Uno.Toolkit.RuntimeTests.Helpers
15
19
{
16
20
internal static class XamlHelper
17
21
{
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
+
18
31
/// <summary>
19
32
/// Matches right before the > or \> tail of any tag.
20
33
/// </summary>
@@ -28,56 +41,159 @@ internal static class XamlHelper
28
41
/// </summary>
29
42
private static readonly Regex NonXmlnsTagRegex = new Regex ( @"<\w+[ />]" ) ;
30
43
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 ( "<[^>]+(>|$)" ) ;
39
49
40
50
/// <summary>
41
- /// XamlReader.Load the xaml and type-check result .
51
+ /// Auto complete any unclosed tag .
42
52
/// </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 )
46
56
{
47
- var xmlnses = new Dictionary < string , string > ( ) ;
57
+ var buffer = new StringBuilder ( ) ;
48
58
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 )
50
68
{
51
- foreach ( var xmlns in KnownXmlnses )
69
+ while ( stack . TryPop ( out var item ) )
52
70
{
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 )
59
90
{
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
+ }
61
116
}
62
117
}
118
+ buffer . AppendLine ( line ) ;
63
119
}
64
120
65
- return LoadXaml < T > ( xaml , xmlnses ) ;
121
+ PopStack ( stack ) ;
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 ) ;
66
176
}
67
177
68
178
/// <summary>
69
179
/// XamlReader.Load the xaml and type-check result.
70
180
/// </summary>
71
181
/// <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
74
186
{
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 ) ;
78
188
79
- xaml = EndOfTagRegex . Replace ( xaml , injection . TrimEnd ( ) , 1 ) ;
189
+ return LoadXaml < T > ( xaml , xmlnses ) ;
190
+ }
80
191
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
+ {
81
197
var result = XamlReader . Load ( xaml ) ;
82
198
Assert . IsNotNull ( result , "XamlReader.Load returned null" ) ;
83
199
Assert . IsInstanceOfType ( result , typeof ( T ) , "XamlReader.Load did not return the expected type" ) ;
0 commit comments