11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Buffers ;
45using System . Diagnostics ;
56using System . Text ;
67using System . Xml ;
@@ -64,6 +65,7 @@ public static string GetInnerXml(this XElement elem)
6465 xml = RemoveCommonIndent ( xml ) ;
6566
6667 // Trim beginning spaces/lines if text starts with HTML tag.
68+ // It is necessary to avoid it being handled as a markdown code block.
6769 var firstNode = nodes . FirstOrDefault ( x => ! x . IsWhitespaceNode ( ) ) ;
6870 if ( firstNode != null && firstNode . NodeType == XmlNodeType . Element )
6971 xml = xml . TrimStart ( ) ;
@@ -103,7 +105,7 @@ private static string RemoveCommonIndent(string text)
103105 minIndent = 0 ;
104106
105107 // 2nd pass: build result
106- var sb = new StringBuilder ( text . Length + 8 ) ;
108+ var sb = new StringBuilder ( text . Length ) ;
107109
108110 inPre = false ;
109111 pos = 0 ;
@@ -192,7 +194,7 @@ static file class XNodeExtensions
192194 /// </summary>
193195 private static readonly Dictionary < ( NodeKind prev , NodeKind next ) , bool > NeedEmptyLineRules = new ( )
194196 {
195- //Block-> *
197+ //Block -> *
196198 [ ( NodeKind . Block , NodeKind . Other ) ] = false ,
197199 [ ( NodeKind . Block , NodeKind . Block ) ] = false ,
198200 [ ( NodeKind . Block , NodeKind . Pre ) ] = true ,
@@ -294,9 +296,9 @@ private static bool NeedEmptyLine(this XElement node, Direction direction)
294296 return NeedEmptyLineRules . TryGetValue ( ( leftKind , rightKind ) , out var result ) && result ;
295297 }
296298
297- private static void EnsureEmptyLine ( this XNode node , Direction direction )
299+ private static void EnsureEmptyLine ( this XElement node , Direction direction )
298300 {
299- var adjacentNode = GetAdjacentNode ( node , direction ) ;
301+ var adjacentNode = node . GetAdjacentNode ( direction ) ;
300302
301303 switch ( adjacentNode )
302304 {
@@ -309,22 +311,26 @@ private static void EnsureEmptyLine(this XNode node, Direction direction)
309311 return ;
310312
311313 case XText textNode :
314+ int count = textNode . CountConsecutiveNewLines ( direction , out var insertIndex ) ;
315+ var indent = GetIndentToInsert ( node , direction , insertIndex ) ;
312316
313- int count = textNode . CountNewLines ( direction , out var insertIndex ) ;
317+ var newLineChars = count switch
318+ {
319+ 0 => "\n \n " ,
320+ 1 => "\n " ,
321+ _ => "" ,
322+ } ;
314323
315- switch ( count )
324+ if ( newLineChars == "" )
316325 {
317- case 0 :
318- textNode . Value = textNode . Value . Insert ( insertIndex , "\n \n " ) ;
319- return ;
320- case 1 :
321- textNode . Value = textNode . Value . Insert ( insertIndex , "\n " ) ;
322- return ;
323- default :
324- Debug . Assert ( textNode . HasEmptyLine ( direction ) ) ;
325- return ;
326+ // It's not expected to be called. Because it's skipped by NeedEmptyLine check.
327+ Debug . Assert ( textNode . HasEmptyLine ( direction ) ) ;
328+ return ;
326329 }
327330
331+ textNode . Value = textNode . Value . Insert ( insertIndex , $ "{ newLineChars } { indent } ") ;
332+ return ;
333+
328334 default :
329335 return ;
330336 }
@@ -364,13 +370,35 @@ private static NodeKind GetNodeKind(XNode node)
364370 return current ;
365371 }
366372
373+ private static T ? FindNeighbor < T > ( this XNode node , Direction direction )
374+ where T : XNode
375+ {
376+ var current = node . GetAdjacentNode ( direction ) ;
377+
378+ while ( current != null && current is not T )
379+ current = current . GetAdjacentNode ( direction ) ;
380+
381+ return ( T ? ) current ;
382+ }
383+
367384 private static bool HasEmptyLine ( this XText node , Direction direction )
368- => CountNewLines ( node , direction , out _ ) >= 2 ;
385+ => node . CountConsecutiveNewLines ( direction , out _ ) >= 2 ;
369386
370387 /// <summary>
371- /// Get count of new lines. space and tabs are ignored.
388+ /// Counts consecutive '\n' characters that exist before/after
372389 /// </summary>
373- private static int CountNewLines ( this XText node , Direction direction , out int insertIndex )
390+ /// <param name="direction">
391+ /// Direction.Before scans from the end
392+ /// Direction.After scans from the beginning.
393+ /// </param>
394+ /// <param name="insertIndex">
395+ /// The position where new content should be inserted.
396+ /// It's determined from the first newline found.
397+ /// </param>
398+ /// <returns>
399+ /// The number of consecutive newline characters found.
400+ /// </returns>
401+ private static int CountConsecutiveNewLines ( this XText node , Direction direction , out int insertIndex )
374402 {
375403 var span = node . Value . AsSpan ( ) ;
376404 int count = 0 ;
@@ -420,6 +448,110 @@ private static int CountNewLines(this XText node, Direction direction, out int i
420448 }
421449 }
422450
451+ private static string GetIndentToInsert ( XElement node , Direction direction , int insertIndex )
452+ {
453+ // Check whether there is an existing indent.
454+ if ( node . TryGetCurrentIndent ( direction , out _ ) )
455+ return "" ;
456+
457+ // Try to get indent from text node that is placed before.
458+ var beforeTextNode = node . FindNeighbor < XText > ( Direction . Before ) ;
459+ if ( beforeTextNode != null )
460+ return beforeTextNode . GetIndentFromLastLine ( ) ;
461+
462+ return "" ;
463+ }
464+
465+ private static bool TryGetCurrentIndent ( this XElement node , Direction direction , out string indent )
466+ {
467+ indent = "" ;
468+
469+ var adjacentNode = node . GetAdjacentNode ( direction ) ;
470+ if ( adjacentNode == null || adjacentNode is not XText textNode )
471+ return false ;
472+
473+ ReadOnlySpan < char > result = direction switch
474+ {
475+ Direction . Before => GetIndentBefore ( textNode . Value ) ,
476+ Direction . After => GetIndentAfter ( textNode . Value ) ,
477+ _ => throw new UnreachableException ( )
478+ } ;
479+
480+ if ( result . IsEmpty )
481+ return false ;
482+
483+ indent = result . ToString ( ) ;
484+ return true ;
485+ }
486+
487+ private static string GetIndentBefore ( ReadOnlySpan < char > span )
488+ {
489+ int lastNewLine = span . LastIndexOf ( '\n ' ) ;
490+ if ( lastNewLine < 0 )
491+ return "" ;
492+
493+ var lastLineSpan = span [ ( lastNewLine + 1 ) ..] ;
494+ if ( lastLineSpan . Length == 0 )
495+ return "" ;
496+
497+ if ( lastLineSpan . ContainsAnyExcept ( [ ' ' , '\t ' ] ) )
498+ return "" ;
499+
500+ return lastLineSpan . ToString ( ) ;
501+ }
502+
503+ private static string GetIndentAfter ( ReadOnlySpan < char > span )
504+ {
505+ int i = 0 ;
506+
507+ while ( i < span . Length )
508+ {
509+ int lineStart = i ;
510+
511+ int indentLength = span [ i ..] . IndexOfAnyExcept ( [ ' ' , '\t ' ] ) ;
512+ if ( indentLength < 0 )
513+ return "" ;
514+
515+ i += indentLength ;
516+ int indentEnd = i ;
517+
518+ // Skip empty line
519+ if ( span [ i ] == '\n ' )
520+ {
521+ i ++ ;
522+ continue ;
523+ }
524+
525+ // Return indent
526+ return span . Slice ( lineStart , indentEnd - lineStart ) . ToString ( ) ;
527+ }
528+
529+ return "" ;
530+ }
531+
532+ private static string GetIndentFromLastLine ( this XText textNode )
533+ {
534+ ReadOnlySpan < char > text = textNode . Value ;
535+
536+ int lastNewLineIndex = text . LastIndexOf ( '\n ' ) ;
537+ if ( lastNewLineIndex < 0 )
538+ return "" ;
539+
540+ var line = text . Slice ( lastNewLineIndex + 1 ) ;
541+
542+ if ( line . IsEmpty )
543+ return "" ;
544+
545+ if ( ! line . ContainsAnyExcept ( [ ' ' , '\t ' ] ) )
546+ return line . ToString ( ) ;
547+
548+ int index = line . IndexOfAnyExcept ( [ ' ' , '\t ' ] ) ;
549+ if ( index <= 0 )
550+ return "" ;
551+
552+ return line . Slice ( 0 , index ) . ToString ( ) ;
553+ }
554+
423555 private static bool IsPreTag ( this XElement elem )
424556 => elem . Name . LocalName == "pre" ;
425557
0 commit comments