55using System . Diagnostics ;
66using System . Globalization ;
77using System . Text ;
8- using System . Text . Encodings . Web ;
9- using System . Text . Unicode ;
108using Bunit . Rendering ;
119
1210namespace Bunit ;
@@ -22,30 +20,34 @@ internal static class Htmlizer
2220 internal const string BlazorAttrPrefix = "blazor:" ;
2321 internal const string ElementReferenceAttrName = BlazorAttrPrefix + "elementReference" ;
2422
25- private static readonly HashSet < string > SelfClosingElements = new ( StringComparer . OrdinalIgnoreCase )
26- {
27- "area" ,
28- "base" ,
29- "br" ,
30- "col" ,
31- "embed" ,
32- "hr" ,
33- "img" ,
34- "input" ,
35- "link" ,
36- "meta" ,
37- "param" ,
38- "source" ,
39- "track" ,
40- "wbr" ,
41- } ;
23+ private static readonly HashSet < string > SelfClosingElements =
24+ new ( StringComparer . OrdinalIgnoreCase )
25+ {
26+ "area" ,
27+ "base" ,
28+ "br" ,
29+ "col" ,
30+ "embed" ,
31+ "hr" ,
32+ "img" ,
33+ "input" ,
34+ "link" ,
35+ "meta" ,
36+ "param" ,
37+ "source" ,
38+ "track" ,
39+ "wbr" ,
40+ } ;
4241
4342 public static bool IsBlazorAttribute ( string attributeName )
4443 {
4544 if ( attributeName is null )
45+ {
4646 throw new ArgumentNullException ( nameof ( attributeName ) ) ;
47- return attributeName . StartsWith ( BlazorAttrPrefix , StringComparison . Ordinal ) ||
48- attributeName . StartsWith ( BlazorCssScopeAttrPrefix , StringComparison . Ordinal ) ;
47+ }
48+
49+ return attributeName . StartsWith ( BlazorAttrPrefix , StringComparison . Ordinal )
50+ || attributeName . StartsWith ( BlazorCssScopeAttrPrefix , StringComparison . Ordinal ) ;
4951 }
5052
5153 public static string ToBlazorAttribute ( string attributeName )
@@ -58,11 +60,19 @@ public static string GetHtml(int componentId, RenderTreeFrameDictionary framesCo
5860 var context = new HtmlRenderingContext ( framesCollection ) ;
5961 var frames = context . GetRenderTreeFrames ( componentId ) ;
6062 var newPosition = RenderFrames ( context , frames , 0 , frames . Length ) ;
61- Debug . Assert ( newPosition == frames . Length , $ "frames.Length = { frames . Length } . newPosition = { newPosition } ") ;
63+ Debug . Assert (
64+ newPosition == frames . Length ,
65+ $ "frames.Length = { frames . Length } . newPosition = { newPosition } "
66+ ) ;
6267 return context . Result . ToString ( ) ;
6368 }
6469
65- private static int RenderFrames ( HtmlRenderingContext context , ReadOnlySpan < RenderTreeFrame > frames , int position , int maxElements )
70+ private static int RenderFrames (
71+ HtmlRenderingContext context ,
72+ ReadOnlySpan < RenderTreeFrame > frames ,
73+ int position ,
74+ int maxElements
75+ )
6676 {
6777 var nextPosition = position ;
6878 var endPosition = position + maxElements ;
@@ -84,17 +94,20 @@ private static int RenderFrames(HtmlRenderingContext context, ReadOnlySpan<Rende
8494 private static int RenderCore (
8595 HtmlRenderingContext context ,
8696 ReadOnlySpan < RenderTreeFrame > frames ,
87- int position )
97+ int position
98+ )
8899 {
89100 var frame = frames [ position ] ;
90101 switch ( frame . FrameType )
91102 {
92103 case RenderTreeFrameType . Element :
93104 return RenderElement ( context , frames , position ) ;
94105 case RenderTreeFrameType . Attribute :
95- throw new InvalidOperationException ( $ "Attributes should only be encountered within { nameof ( RenderElement ) } ") ;
106+ throw new InvalidOperationException (
107+ $ "Attributes should only be encountered within { nameof ( RenderElement ) } "
108+ ) ;
96109 case RenderTreeFrameType . Text :
97- context . Result . Append ( frame . TextContent ) ;
110+ AppendEscapeText ( context , frame . TextContent ) ;
98111 return position + 1 ;
99112 case RenderTreeFrameType . Markup :
100113 context . Result . Append ( frame . MarkupContent ) ;
@@ -107,14 +120,17 @@ private static int RenderCore(
107120 case RenderTreeFrameType . ComponentReferenceCapture :
108121 return position + 1 ;
109122 default :
110- throw new InvalidOperationException ( $ "Invalid element frame type '{ frame . FrameType } '.") ;
123+ throw new InvalidOperationException (
124+ $ "Invalid element frame type '{ frame . FrameType } '."
125+ ) ;
111126 }
112127 }
113128
114129 private static int RenderChildComponent (
115130 HtmlRenderingContext context ,
116131 ReadOnlySpan < RenderTreeFrame > frames ,
117- int position )
132+ int position
133+ )
118134 {
119135 var frame = frames [ position ] ;
120136 var childFrames = context . GetRenderTreeFrames ( frame . ComponentId ) ;
@@ -125,20 +141,33 @@ private static int RenderChildComponent(
125141 private static int RenderElement (
126142 HtmlRenderingContext context ,
127143 ReadOnlySpan < RenderTreeFrame > frames ,
128- int position )
144+ int position
145+ )
129146 {
130147 var frame = frames [ position ] ;
131148 var result = context . Result ;
132149 result . Append ( '<' ) ;
133150 result . Append ( frame . ElementName ) ;
134- var afterAttributes = RenderAttributes ( context , frames , position + 1 , frame . ElementSubtreeLength - 1 , out var capturedValueAttribute ) ;
151+ var afterAttributes = RenderAttributes (
152+ context ,
153+ frames ,
154+ position + 1 ,
155+ frame . ElementSubtreeLength - 1 ,
156+ out var capturedValueAttribute
157+ ) ;
135158
136159 // When we see an <option> as a descendant of a <select>, and the option's "value" attribute matches the
137160 // "value" attribute on the <select>, then we auto-add the "selected" attribute to that option. This is
138161 // a way of converting Blazor's select binding feature to regular static HTML.
139- if ( context . ClosestSelectValueAsString != null
162+ if (
163+ context . ClosestSelectValueAsString != null
140164 && string . Equals ( frame . ElementName , "option" , StringComparison . OrdinalIgnoreCase )
141- && string . Equals ( capturedValueAttribute , context . ClosestSelectValueAsString , StringComparison . Ordinal ) )
165+ && string . Equals (
166+ capturedValueAttribute ,
167+ context . ClosestSelectValueAsString ,
168+ StringComparison . Ordinal
169+ )
170+ )
142171 {
143172 result . Append ( " selected" ) ;
144173 }
@@ -148,7 +177,11 @@ private static int RenderElement(
148177 {
149178 result . Append ( '>' ) ;
150179
151- var isSelect = string . Equals ( frame . ElementName , "select" , StringComparison . OrdinalIgnoreCase ) ;
180+ var isSelect = string . Equals (
181+ frame . ElementName ,
182+ "select" ,
183+ StringComparison . OrdinalIgnoreCase
184+ ) ;
152185 if ( isSelect )
153186 {
154187 context . ClosestSelectValueAsString = capturedValueAttribute ;
@@ -181,11 +214,19 @@ private static int RenderElement(
181214 result . Append ( '>' ) ;
182215 }
183216
184- Debug . Assert ( afterAttributes == position + frame . ElementSubtreeLength , $ "afterAttributes = { afterAttributes } . position = { position } . frame.ElementSubtreeLength = { frame . ElementSubtreeLength } ") ;
217+ Debug . Assert (
218+ afterAttributes == position + frame . ElementSubtreeLength ,
219+ $ "afterAttributes = { afterAttributes } . position = { position } . frame.ElementSubtreeLength = { frame . ElementSubtreeLength } "
220+ ) ;
185221 return afterAttributes ;
186222 }
187223
188- private static int RenderChildren ( HtmlRenderingContext context , ReadOnlySpan < RenderTreeFrame > frames , int position , int maxElements )
224+ private static int RenderChildren (
225+ HtmlRenderingContext context ,
226+ ReadOnlySpan < RenderTreeFrame > frames ,
227+ int position ,
228+ int maxElements
229+ )
189230 {
190231 if ( maxElements == 0 )
191232 {
@@ -195,13 +236,13 @@ private static int RenderChildren(HtmlRenderingContext context, ReadOnlySpan<Ren
195236 return RenderFrames ( context , frames , position , maxElements ) ;
196237 }
197238
198- [ SuppressMessage ( "Design" , "MA0051:Method is too long" , Justification = "TODO: Refactor" ) ]
199239 private static int RenderAttributes (
200240 HtmlRenderingContext context ,
201241 ReadOnlySpan < RenderTreeFrame > frames ,
202242 int position ,
203243 int maxElements ,
204- out string ? capturedValueAttribute )
244+ out string ? capturedValueAttribute
245+ )
205246 {
206247 capturedValueAttribute = null ;
207248
@@ -251,11 +292,19 @@ private static int RenderAttributes(
251292
252293 switch ( frame . AttributeValue )
253294 {
254- case bool flag when flag && frame . AttributeName . StartsWith ( BlazorInternalAttrPrefix , StringComparison . Ordinal ) :
295+ case bool flag
296+ when flag
297+ && frame . AttributeName . StartsWith (
298+ BlazorInternalAttrPrefix ,
299+ StringComparison . Ordinal
300+ ) :
255301 // NOTE: This was added to make it more obvious
256302 // that this is a generated/special blazor attribute
257303 // for internal usage
258- var nameParts = frame . AttributeName . Split ( '_' , StringSplitOptions . RemoveEmptyEntries ) ;
304+ var nameParts = frame . AttributeName . Split (
305+ '_' ,
306+ StringSplitOptions . RemoveEmptyEntries
307+ ) ;
259308 result . Append ( ' ' ) ;
260309 result . Append ( BlazorAttrPrefix ) ;
261310 result . Append ( nameParts [ 2 ] ) ;
@@ -271,7 +320,7 @@ private static int RenderAttributes(
271320 result . Append ( frame . AttributeName ) ;
272321 result . Append ( '=' ) ;
273322 result . Append ( '"' ) ;
274- result . Append ( Escape ( value ) ) ;
323+ AppendEscapeAttributeValue ( context , value ) ;
275324 result . Append ( '"' ) ;
276325 break ;
277326 default :
@@ -282,10 +331,66 @@ private static int RenderAttributes(
282331 return position + maxElements ;
283332 }
284333
285- private static string Escape ( string value ) =>
286- value
287- . Replace ( "&" , "&" , StringComparison . OrdinalIgnoreCase )
288- . Replace ( "\" " , """ , StringComparison . OrdinalIgnoreCase ) ;
334+ private static void AppendEscapeText ( HtmlRenderingContext context , string value )
335+ {
336+ var valueSpan = value . AsSpan ( ) ;
337+ var copyFromIndex = 0 ;
338+ for ( var index = 0 ; index < valueSpan . Length ; index ++ )
339+ {
340+ var c = valueSpan [ index ] ;
341+ if ( c is '<' or '>' or '&' )
342+ {
343+ context . Result . Append ( valueSpan . Slice ( copyFromIndex , index - copyFromIndex ) ) ;
344+ switch ( c )
345+ {
346+ case '<' :
347+ context . Result . Append ( "<" ) ;
348+ break ;
349+ case '>' :
350+ context . Result . Append ( ">" ) ;
351+ break ;
352+ case '&' :
353+ context . Result . Append ( "&" ) ;
354+ break ;
355+ }
356+ copyFromIndex = index + 1 ;
357+ }
358+ }
359+
360+ if ( copyFromIndex < valueSpan . Length )
361+ {
362+ context . Result . Append ( valueSpan . Slice ( copyFromIndex ) ) ;
363+ }
364+ }
365+
366+ private static void AppendEscapeAttributeValue ( HtmlRenderingContext context , string value )
367+ {
368+ var valueSpan = value . AsSpan ( ) ;
369+ var copyFromIndex = 0 ;
370+ for ( var index = 0 ; index < valueSpan . Length ; index ++ )
371+ {
372+ var c = valueSpan [ index ] ;
373+ if ( c is '"' or '&' )
374+ {
375+ context . Result . Append ( valueSpan . Slice ( copyFromIndex , index - copyFromIndex ) ) ;
376+ switch ( c )
377+ {
378+ case '"' :
379+ context . Result . Append ( """ ) ;
380+ break ;
381+ case '&' :
382+ context . Result . Append ( "&" ) ;
383+ break ;
384+ }
385+ copyFromIndex = index + 1 ;
386+ }
387+ }
388+
389+ if ( copyFromIndex < valueSpan . Length )
390+ {
391+ context . Result . Append ( valueSpan . Slice ( copyFromIndex ) ) ;
392+ }
393+ }
289394
290395 private sealed class HtmlRenderingContext
291396 {
@@ -296,8 +401,8 @@ public HtmlRenderingContext(RenderTreeFrameDictionary frames)
296401 this . frames = frames ;
297402 }
298403
299- public ReadOnlySpan < RenderTreeFrame > GetRenderTreeFrames ( int componentId )
300- => new ( frames [ componentId ] . Array , 0 , frames [ componentId ] . Count ) ;
404+ public ReadOnlySpan < RenderTreeFrame > GetRenderTreeFrames ( int componentId ) =>
405+ new ( frames [ componentId ] . Array , 0 , frames [ componentId ] . Count ) ;
301406
302407 public StringBuilder Result { get ; } = new ( ) ;
303408
0 commit comments