From e66fdc2764e5753e370b36a5c8f6ba220b31847f Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:05:11 +0900 Subject: [PATCH 1/5] fix: issue 10965 --- .../Parsers/XmlComment.Extensions.cs | 35 +++++++++++++++++-- .../XmlCommentSummaryTest.Code.cs | 22 ++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs index c34fb0defd5..d0ec14ac292 100644 --- a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs +++ b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs @@ -43,7 +43,7 @@ private static string GetMarkdownText(XElement elem) node.InsertEmptyLineBefore(); if (node.NeedEmptyLineAfter()) - node.AddAfterSelf(new XText("\n")); + node.InsertEmptyLineAfter(); } return elem.GetInnerXml(); @@ -143,7 +143,23 @@ public static void InsertEmptyLineBefore(this XElement elem) } else { - elem.AddBeforeSelf(new XText("\n")); + if (prevTextNode.Value.EndsWith('\n')) + { + elem.AddBeforeSelf(new XText("\n")); + } + else + { + // HTML block tag is adjacent to markdown without new line. + // In this case, it need to append `\n\n` and copy last line indent chars. + int startIndex = lastLine.IndexOfAnyExcept(' ', '\t'); + ReadOnlySpan indent = startIndex switch + { + < 0 => lastLine, + 0 => [], + _ => lastLine.Slice(0, startIndex), + }; + elem.AddBeforeSelf(new XText($"\n\n{indent}")); + } } } @@ -203,6 +219,21 @@ public static bool NeedEmptyLineAfter(this XElement node) return false; } } + + public static void InsertEmptyLineAfter(this XElement elem) + { + if (!elem.TryGetNonWhitespaceNextNode(out var nextNode)) + return; + + Debug.Assert(nextNode.NodeType == XmlNodeType.Text); + + var nextTextNode = (XText)nextNode; + if (nextTextNode.Value.StartsWith('\n')) + elem.AddAfterSelf(new XText("\n")); + else + elem.AddAfterSelf(new XText("\n\n")); + } + private static bool StartsWithEmptyLine(this ReadOnlySpan span) { var index = span.IndexOfAnyExcept([' ', '\t']); diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs index 51844bbe682..6c99808e747 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs @@ -28,6 +28,28 @@ public void Code_Block() """); } + [Fact] + public void Code_Block_WithoutNewLine() + { + ValidateSummary( + // Input XML + """ + + Paragraph1Paragraph2 + + """, + // Expected Markdown + """ + Paragraph1 + +
DELETE /articles/1 HTTP/1.1
+ + Paragraph2 + """); + } + [Fact] public void Code_Inline() { From 042ccd1bb4d078ebcaf277f64a37f8ee9674f86b Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:41:46 +0900 Subject: [PATCH 2/5] chore: fix code block layout corrupted issue. --- .../Parsers/XmlComment.Extensions.cs | 16 ++++++- .../XmlCommentSummaryTest.Code.cs | 47 +++++++++++++++++++ test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs | 1 + 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs index d0ec14ac292..031f22ff4d7 100644 --- a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs +++ b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs @@ -102,9 +102,15 @@ public static bool NeedEmptyLineBefore(this XElement node) switch (prevNode.NodeType) { - // If prev node is HTML element. No need to insert empty line. case XmlNodeType.Element: - return false; + + // No need to insert empty line, if prev node is HTML element and it's not
 tag. T
+                if (node.Name != "pre")
+                    return false;
+
+                // 
 tag needs empty line before. Without this setting, markdown parser treat code as markdown block.
+                var prevElementNode = (XElement)prevNode;
+                return prevElementNode.Name.LocalName != "pre";
 
             // Ensure empty lines exists before text node.
             case XmlNodeType.Text:
@@ -126,6 +132,12 @@ public static void InsertEmptyLineBefore(this XElement elem)
         if (!elem.TryGetNonWhitespacePrevNode(out var prevNode))
             return;
 
+        if (prevNode.NodeType == XmlNodeType.Element)
+        {
+            elem.AddBeforeSelf(new XText("\n"));
+            return;
+        }
+
         Debug.Assert(prevNode.NodeType == XmlNodeType.Text);
 
         var prevTextNode = (XText)prevNode;
diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs
index 6c99808e747..e0c1fe2c760 100644
--- a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs
+++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs
@@ -69,4 +69,51 @@ public void Code_Inline()
             Paragraph2
             """);
     }
+
+
+    [Fact]
+    public void Code_HtmlTagExistBefore()
+    {
+        ValidateSummary(
+            // Input XML
+            """
+            
+            paragraph1
+            paragraph2
+            
+            
+            
+            """,
+            // Expected Markdown
+            """
+            paragraph1
+
+            

paragraph2

+ +
public class Sample
+            {
+                line1
+           
+                line2
+            }
+
public class Sample2
+            {
+                line1
+            
+                line2
+            }
+ """); + } } diff --git a/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs b/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs index e0c3d69cb92..7b06cda1b9a 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs @@ -581,6 +581,7 @@ public sealed class Issue10385

Paragraph.

+
public sealed class Issue10385
             {
                 public int AAA {get;set;}

From 28062e932f16badba51feaf03c66b6b27e854d3d Mon Sep 17 00:00:00 2001
From: filzrev <103790468+filzrev@users.noreply.github.com>
Date: Mon, 19 Jan 2026 11:17:04 +0900
Subject: [PATCH 3/5] chore: fix code block layout corrupted issue when parent
 tag exists before code tag

---
 .../Parsers/XmlComment.Extensions.cs          | 399 +++++++++++-------
 .../XmlCommentTests/XmlCommentRemarksTest.cs  |  43 ++
 .../XmlCommentSummaryTest.Code.cs             |  59 +++
 .../XmlCommentSummaryTest.List.cs             |   6 +-
 .../XmlCommentSummaryTest.Others.cs           |   4 +-
 .../XmlCommentUnitTest.Issue10553.cs          |   5 +-
 test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs |   4 +-
 7 files changed, 363 insertions(+), 157 deletions(-)

diff --git a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs
index 031f22ff4d7..b8697c13465 100644
--- a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs
+++ b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs
@@ -2,10 +2,14 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
 using System.Xml;
 using System.Xml.Linq;
 using System.Xml.XPath;
 
+#nullable enable
+
 namespace Docfx.Dotnet;
 
 internal partial class XmlComment
@@ -20,6 +24,8 @@ internal partial class XmlComment
         "ul",
 
         // Recommended XML tags for C# documentation comments
+        // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags
+        // Note: Some XML tags(e.g. ``/``) are pre-processed and converted to HTML tags.
         "example",
         
         // Other tags
@@ -40,10 +46,10 @@ private static string GetMarkdownText(XElement elem)
         foreach (var node in nodes)
         {
             if (node.NeedEmptyLineBefore())
-                node.InsertEmptyLineBefore();
+                node.EnsureEmptyLineBefore();
 
             if (node.NeedEmptyLineAfter())
-                node.InsertEmptyLineAfter();
+                node.EnsureEmptyLineAfter();
         }
 
         return elem.GetInnerXml();
@@ -97,236 +103,287 @@ public static string GetInnerXml(this XElement elem)
 
     public static bool NeedEmptyLineBefore(this XElement node)
     {
-        if (!node.TryGetNonWhitespacePrevNode(out var prevNode))
+        // Case 1: There is a previous node that is non-whitespace.
+        if (node.TryGetNonWhitespacePrevNode(out var prevNode))
+        {
+            return prevNode switch
+            {
+                // XElement exists on previous nodes.
+                XElement prevElem =>
+                    node.IsPreTag() && !prevElem.IsPreTag(),
+
+                // XText node exists on previous nodes.
+                XText prevText =>
+                    !prevText.EndsWithEmptyLine(),
+
+                // Other node types is not expected, and no need to insert empty line.
+                _ => false
+            };
+        }
+
+        // Case 2: There is no previous non-whitespace node
+        // Empty Line is not needed except for 
 tag.
+        if (!node.IsPreTag())
             return false;
 
-        switch (prevNode.NodeType)
+        // If previous node is XText. Check text ends with empty line.
+        if (node.PreviousNode is XText whitespaceNode)
+            return !whitespaceNode.EndsWithEmptyLine();
+
+        // Otherwise, empty line is needed when it has a parent node.
+        return node.Parent != null;
+    }
+
+    public static void EnsureEmptyLineBefore(this XNode node)
+    {
+        switch (node.PreviousNode)
         {
-            case XmlNodeType.Element:
+            case null:
+            case XElement:
+                node.AddBeforeSelf(new XText("\n\n"));
+                return;
 
-                // No need to insert empty line, if prev node is HTML element and it's not 
 tag. T
-                if (node.Name != "pre")
-                    return false;
+            case XText textNode:
+                var text = textNode.Value;
 
-                // 
 tag needs empty line before. Without this setting, markdown parser treat code as markdown block.
-                var prevElementNode = (XElement)prevNode;
-                return prevElementNode.Name.LocalName != "pre";
+                switch (CountTrailingNewLines(text, out var insertIndex))
+                {
+                    case 0:
+                        textNode.Value = text.Insert(insertIndex, "\n\n");
+                        return;
 
-            // Ensure empty lines exists before text node.
-            case XmlNodeType.Text:
-                var prevTextNode = (XText)prevNode;
+                    case 1:
+                        textNode.Value = text.Insert(insertIndex, "\n");
+                        return;
 
-                // No need to insert line if prev node ends with empty line.
-                if (prevTextNode.Value.EndsWithEmptyLine())
-                    return false;
+                    default:
+                        // This code path is not expected to be called.
+                        // Because it should be filtered by NeedEmptyLineBefore.
+                        Debug.Assert(textNode.EndsWithEmptyLine());
+                        return;
 
-                return true;
+                }
 
             default:
-                return false;
+                return;
         }
     }
 
-    public static void InsertEmptyLineBefore(this XElement elem)
+    public static bool NeedEmptyLineAfter(this XElement node)
     {
-        if (!elem.TryGetNonWhitespacePrevNode(out var prevNode))
-            return;
-
-        if (prevNode.NodeType == XmlNodeType.Element)
+        // Case 1: There is a next node that is non-whitespace.
+        if (node.TryGetNonWhitespaceNextNode(out var nextNode))
         {
-            elem.AddBeforeSelf(new XText("\n"));
-            return;
+            return nextNode switch
+            {
+                // XElement exists on previous nodes.
+                XElement nextElem =>
+                    node.IsPreTag() && !nextElem.IsPreTag(),
+
+                // XText node exists on previous nodes.
+                XText nextText =>
+                    !nextText.StartsWithEmptyLine(),
+
+                // Other node types is not expected, and no need to insert empty line.
+                _ => false
+            };
         }
 
-        Debug.Assert(prevNode.NodeType == XmlNodeType.Text);
+        // Case 2: There is no next non-whitespace node
+        // Empty Line is not needed except for 
 tag.
+        if (!node.IsPreTag())
+            return false;
 
-        var prevTextNode = (XText)prevNode;
-        var span = prevTextNode.Value.AsSpan();
-        int index = span.LastIndexOf('\n');
+        // If previous node is XText. Check text ends with empty line.
+        if (node.NextNode is XText whitespaceNode)
+            return !whitespaceNode.StartsWithEmptyLine();
 
-        ReadOnlySpan lastLine = index == -1
-            ? span
-            : span.Slice(index + 1);
+        var parentNextNode = node.Parent?.NextNode;
+        if (parentNextNode is XElement)
+            return true;
 
-        if (lastLine.Length > 0 && lastLine.IsWhiteSpace())
-        {
-            // Insert new line before indent of last line.
-            prevTextNode.Value = prevTextNode.Value.Insert(index, "\n");
-        }
-        else
-        {
-            if (prevTextNode.Value.EndsWith('\n'))
-            {
-                elem.AddBeforeSelf(new XText("\n"));
-            }
-            else
-            {
-                // HTML block tag is adjacent to markdown without new line.
-                // In this case, it need to append `\n\n` and copy last line indent chars.
-                int startIndex = lastLine.IndexOfAnyExcept(' ', '\t');
-                ReadOnlySpan indent = startIndex switch
-                {
-                    < 0 => lastLine,
-                    0 => [],
-                    _ => lastLine.Slice(0, startIndex),
-                };
-                elem.AddBeforeSelf(new XText($"\n\n{indent}"));
-            }
-        }
+        if (parentNextNode is XText textNode)
+            return !textNode.StartsWithEmptyLine();
+
+        return node.Parent != null;
     }
 
-    private static bool EndsWithEmptyLine(this ReadOnlySpan span)
+    public static void EnsureEmptyLineAfter(this XNode node)
     {
-        var index = span.LastIndexOfAnyExcept([' ', '\t']);
-        if (index >= 0 && span[index] == '\n')
+        switch (node.NextNode)
         {
-            span = span.Slice(0, index);
-            index = span.LastIndexOfAnyExcept([' ', '\t']);
-            if (index >= 0 && span[index] == '\n')
-                return true;
-        }
+            case XElement:
+                node.AddAfterSelf(new XText("\n\n"));
+                return;
+
+            case XText textNode:
+                var textValue = textNode.Value;
 
-        return false;
+                switch (CountLeadingNewLines(textValue, out var insertIndex))
+                {
+                    case 0:
+                        textNode.Value = textValue.Insert(insertIndex, "\n\n");
+                        return;
+
+                    case 1:
+                        textNode.Value = textValue.Insert(insertIndex, "\n");
+                        return;
+
+                    default:
+                        // This code path is not expected to be called.
+                        // Because it should be filtered by NeedEmptyLineAfter.
+                        Debug.Assert(textNode.StartsWithEmptyLine());
+                        return;
+                }
+
+            default:
+                return;
+        }
     }
 
-    private static bool TryGetNonWhitespacePrevNode(this XElement elem, out XNode result)
+
+    /// 
+    /// Get count of trailing new lines. space and tabs are ignored.
+    /// 
+    private static int CountTrailingNewLines(ReadOnlySpan span, out int insertIndex)
     {
-        var prev = elem.PreviousNode;
-        while (prev != null && prev.IsWhitespaceNode())
-            prev = prev.PreviousNode;
+        insertIndex = span.Length;
+        bool insertIndexUpdated = false;
+        int count = 0;
 
-        if (prev == null)
+        int i = span.Length;
+        while (--i >= 0)
         {
-            result = null;
-            return false;
+            var c = span[i];
+            if (IsIndentChar(c))
+                continue;
+
+            if (c != '\n')
+                return count;
+
+            if (!insertIndexUpdated)
+            {
+                insertIndexUpdated = true;
+                insertIndex = i + 1;
+            }
+            ++count;
         }
 
-        result = prev;
-        return true;
+        return count;
     }
 
-    public static bool NeedEmptyLineAfter(this XElement node)
+    /// 
+    /// Get count of leading new lines. space and tabs are ignored.
+    /// 
+    private static int CountLeadingNewLines(ReadOnlySpan span, out int insertIndex)
     {
-        if (!node.TryGetNonWhitespaceNextNode(out var nextNode))
-            return false;
+        insertIndex = 0;
+        bool insertIndexUpdated = false;
+        int count = 0;
 
-        switch (nextNode.NodeType)
+        for (int i = 0; i < span.Length; ++i)
         {
-            // If next node is HTML element. No need to insert new line.
-            case XmlNodeType.Element:
-                return false;
-
-            // Ensure empty lines exists after node.
-            case XmlNodeType.Text:
-                var nextTextNode = (XText)nextNode;
-                var textSpan = nextTextNode.Value.AsSpan();
+            var c = span[i];
+            if (IsIndentChar(c))
+                continue;
 
-                // No need to insert line if prev node ends with empty line.
-                if (textSpan.StartsWithEmptyLine())
-                    return false;
+            if (c != '\n')
+                return count;
 
-                return true;
-
-            default:
-                return false;
+            if (!insertIndexUpdated)
+            {
+                insertIndexUpdated = true;
+                insertIndex = i;
+            }
+            ++count;
         }
+
+        return count;
     }
 
-    public static void InsertEmptyLineAfter(this XElement elem)
+    private static bool StartsWithEmptyLine(this XNode? node)
     {
-        if (!elem.TryGetNonWhitespaceNextNode(out var nextNode))
-            return;
-
-        Debug.Assert(nextNode.NodeType == XmlNodeType.Text);
+        if (node is not XText textNode)
+            return false;
 
-        var nextTextNode = (XText)nextNode;
-        if (nextTextNode.Value.StartsWith('\n'))
-            elem.AddAfterSelf(new XText("\n"));
-        else
-            elem.AddAfterSelf(new XText("\n\n"));
+        return CountLeadingNewLines(textNode.Value, out _) >= 2;
     }
 
-    private static bool StartsWithEmptyLine(this ReadOnlySpan span)
+    private static bool EndsWithEmptyLine(this XNode? node)
     {
-        var index = span.IndexOfAnyExcept([' ', '\t']);
-        if (index >= 0 && span[index] == '\n')
-        {
-            ++index;
-            if (index > span.Length)
-                return false;
+        if (node is not XText textNode)
+            return false;
 
-            span = span.Slice(index);
-            index = span.IndexOfAnyExcept([' ', '\t']);
+        return CountTrailingNewLines(textNode.Value, out _) >= 2;
+    }
 
-            if (index < 0 || span[index] == '\n')
-                return true; // There is no content or empty line is already exists.
-        }
-        return false;
+    private static bool TryGetNonWhitespacePrevNode(this XElement elem, [NotNullWhen(true)] out XNode? result)
+    {
+        var prev = elem.PreviousNode;
+        while (prev is not null && prev.IsWhitespaceNode())
+            prev = prev.PreviousNode;
+
+        result = prev;
+        return result is not null;
     }
 
-    private static bool TryGetNonWhitespaceNextNode(this XElement elem, out XNode result)
+    private static bool TryGetNonWhitespaceNextNode(this XElement elem, [NotNullWhen(true)] out XNode? result)
     {
         var next = elem.NextNode;
         while (next != null && next.IsWhitespaceNode())
             next = next.NextNode;
 
-        if (next == null)
-        {
-            result = null;
-            return false;
-        }
-
         result = next;
-        return true;
+        return result is not null;
     }
 
     private static string RemoveCommonIndent(string text)
     {
-        var lines = text.Split('\n').ToArray();
+        var lines = text.Split('\n');
 
-        var inPre = false;
-        var indentCounts = new List();
+        // 1st pass: Compute minimum indent (excluding 
 blocks)
+        bool inPre = false;
+        int minIndent = int.MaxValue;
 
-        // Caluculate line's indent chars (
 tag region is excluded)
         foreach (var line in lines)
         {
             if (!inPre && !string.IsNullOrWhiteSpace(line))
             {
-                int indent = line.TakeWhile(c => c == ' ' || c == '\t').Count();
-                indentCounts.Add(indent);
+                int indent = CountIndent(line);
+                minIndent = Math.Min(minIndent, indent);
             }
 
-            var trimmed = line.Trim();
-            if (trimmed.StartsWith("", StringComparison.OrdinalIgnoreCase))
-                inPre = false;
+            UpdatePreFlag(line, ref inPre);
         }
 
-        int minIndent = indentCounts.DefaultIfEmpty(0).Min();
+        if (minIndent == int.MaxValue)
+            minIndent = 0;
 
+        // 2nd pass: remove common indent
+        var sb = new StringBuilder(text.Length);
         inPre = false;
-        var resultLines = new List();
+
         foreach (var line in lines)
         {
-            if (!inPre && line.Length >= minIndent)
-                resultLines.Add(line.Substring(minIndent));
+            if (!inPre && line.Length != 0)
+            {
+                int removeLength = Math.Min(minIndent, CountIndent(line));
+                sb.Append(line.AsSpan().Slice(removeLength));
+                sb.Append('\n');
+            }
             else
-                resultLines.Add(line);
-
-            // Update inPre flag.
-            var trimmed = line.Trim();
-            if (trimmed.StartsWith("
", StringComparison.OrdinalIgnoreCase))
-                inPre = true;
-            if (trimmed.EndsWith("
", StringComparison.OrdinalIgnoreCase)) - inPre = false; + { + sb.Append(line); + sb.Append('\n'); + } + + UpdatePreFlag(line, ref inPre); } - // Insert empty line to append `\n`. - resultLines.Add(""); + // Ensure trailing newline + sb.Append('\n'); - return string.Join("\n", resultLines); + return sb.ToString(); } private static bool IsWhitespaceNode(this XNode node) @@ -336,4 +393,40 @@ private static bool IsWhitespaceNode(this XNode node) return textNode.Value.All(char.IsWhiteSpace); } + + private static bool IsPreTag(this XNode? node) + => node is XElement elem && elem.Name == "pre"; + + private static int CountIndent(ReadOnlySpan line) + { + int i = 0; + while (i < line.Length && IsIndentChar(line[i])) + i++; + return i; + } + + private static void UpdatePreFlag(ReadOnlySpan line, ref bool inPre) + { + var trimmed = line.Trim(); + + // Check start tag (It might contains attribute) + if (!inPre && trimmed.StartsWith("", StringComparison.OrdinalIgnoreCase)) + inPre = false; + } + + private static bool IsIndentChar(char c) + { + switch (c) + { + case ' ': + case '\t': + return true; + default: + return false; + } + } } diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentRemarksTest.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentRemarksTest.cs index 04da4d4184d..f3e785b98fa 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentRemarksTest.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentRemarksTest.cs @@ -30,8 +30,10 @@ public class XmlElement // Expected Markdown """
  • +
    public class XmlElement
                     : XmlLinkedNode
    +
""" ); @@ -64,6 +66,47 @@ public void Remarks_WithCodeBlocks() ); } + // UnitTest for https://github.com/dotnet/docfx/issues/10965 + [Fact] + public void Remarks_WithExample() + { + ValidateRemarks( + // Input XML + """ + + + Message + + + + + + """, + // Expected Markdown + """ + Message + + + +
public class User
+            {
+                aaa
+
+                bbb
+                ccc
+            }
+ +
+ """); + } + private static void ValidateRemarks(string input, string expected) { // Act diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs index e0c1fe2c760..32f9a705910 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs @@ -116,4 +116,63 @@ public class Sample2 }
"""); } + + [Fact] + public void Code_ParentHtmlTagExist() + { + ValidateSummary( + // Input XML + """ + + Paragraph1 +
+ +
+ Paragraph2 +
+ """, + // Expected Markdown + """ +

Paragraph1

+
+ +
public class Sample
+            {
+                line1
+           
+                line2
+            }
+ +
+

Paragraph2

+ """); + } + + [Fact] + public void Code_MultipleBlockWithoutNewLine() + { + ValidateSummary( + // Input XML + """ + + Paragraph1 + var x = 1;var x = 2; + Paragraph2 + + """, + // Expected Markdown + """ + Paragraph1 + +
var x = 1;
var x = 2;
+ + Paragraph2 + """); + } } diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.List.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.List.cs index e7ec9dd3ae8..61845d42dea 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.List.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.List.cs @@ -114,8 +114,10 @@ public class XmlElement Paragraph1
  • +
    public class XmlElement
                     : XmlLinkedNode
    +
Paragraph2 @@ -168,10 +170,12 @@ loose text not wrapped in description example

This is ref a sample of exception node

  • +
    public class XmlElement
                   : XmlLinkedNode
    +
    1. - word inside list->listItem->list->listItem->para.> + word inside list->listItem->list->listItem->para.> the second line.
    2. item2 in numbered list
  • item2 in bullet list
  • diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Others.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Others.cs index 58bc835d583..ffa2b5f9fd2 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Others.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Others.cs @@ -8,7 +8,7 @@ namespace Docfx.Dotnet.Tests; public partial class XmlCommentSummaryTest { [Fact] - public void Example() + public void ExampleWithParagraph() { ValidateSummary( // Input XML @@ -26,7 +26,9 @@ public void Example() Paragraph1 +
    code content
    +
    Paragraph2 diff --git a/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.Issue10553.cs b/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.Issue10553.cs index 132d0972577..dc572f4499f 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.Issue10553.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.Issue10553.cs @@ -29,8 +29,11 @@ Converts action result without parameters into action result with null parameter var expected = """ Converts action result without parameters into action result with null parameter. -
    return NotFound() -> return NotFound(null)
    +                
    +
    +                
    return NotFound() -> return NotFound(null)
                     return NotFound() -> return NotFound(null)
    +
    This ensures our formatter is invoked, where we'll build a JSON:API compliant response. For details, see: diff --git a/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs b/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs index 7b06cda1b9a..da67ef42974 100644 --- a/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs +++ b/test/Docfx.Dotnet.Tests/XmlCommentUnitTest.cs @@ -408,10 +408,12 @@ Classes in assemblies are by definition complete. example

    This is ref a sample of exception node

    • +
      public class XmlElement
                       : XmlLinkedNode
      +
      1. - word inside list->listItem->list->listItem->para.> + word inside list->listItem->list->listItem->para.> the second line.
      2. item2 in numbered list
    • item2 in bullet list
    • From 58c5cbac015e9ccf381ab7694a60f79bb4ec309b Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:54:06 +0900 Subject: [PATCH 4/5] chore: cleanup XML comment related code --- .../Parsers/XmlComment.Extensions.cs | 547 +++++++++--------- 1 file changed, 279 insertions(+), 268 deletions(-) diff --git a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs index b8697c13465..95c2a625310 100644 --- a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs +++ b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs @@ -2,11 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Text; using System.Xml; using System.Xml.Linq; -using System.Xml.XPath; #nullable enable @@ -14,33 +12,13 @@ namespace Docfx.Dotnet; internal partial class XmlComment { - // List of block tags that are defined by CommonMark - // https://spec.commonmark.org/0.31.2/#html-blocks - private static readonly string[] BlockTags = - { - "ol", - "p", - "table", - "ul", - - // Recommended XML tags for C# documentation comments - // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags - // Note: Some XML tags(e.g. ``/``) are pre-processed and converted to HTML tags. - "example", - - // Other tags - "pre", - }; - - private static readonly Lazy BlockTagsXPath = new(string.Join(" | ", BlockTags.Select(tagName => $".//{tagName}"))); - /// /// Gets markdown text from XElement. /// private static string GetMarkdownText(XElement elem) { - // Gets HTML block tags by XPath. - var nodes = elem.XPathSelectElements(BlockTagsXPath.Value).ToArray(); + // Gets HTML block tags from tree. + var nodes = elem.GetBlockTags(); // Insert HTML/Markdown separator lines. foreach (var node in nodes) @@ -59,12 +37,9 @@ private static string GetInnerXml(XElement elem) => elem.GetInnerXml(); } -// Define file scoped extension methods. -static file class XElementExtensions +// Define file scoped extension methods for GetInnerXml. +static file class GetInnerXmlExtensions { - /// - /// Gets inner XML text of XElement. - /// public static string GetInnerXml(this XElement elem) { using var sw = new StringWriter(); @@ -101,138 +76,252 @@ public static string GetInnerXml(this XElement elem) return xml; } - public static bool NeedEmptyLineBefore(this XElement node) + private static string RemoveCommonIndent(string text) { - // Case 1: There is a previous node that is non-whitespace. - if (node.TryGetNonWhitespacePrevNode(out var prevNode)) + ReadOnlySpan span = text.AsSpan(); + + // 1st pass: Compute minimum indent (excluding
       blocks)
      +        bool inPre = false;
      +        int minIndent = int.MaxValue;
      +
      +        int pos = 0;
      +        while (pos < span.Length)
               {
      -            return prevNode switch
      +            var line = ReadLine(span, ref pos);
      +
      +            if (!inPre && !IsWhitespaceLine(line))
                   {
      -                // XElement exists on previous nodes.
      -                XElement prevElem =>
      -                    node.IsPreTag() && !prevElem.IsPreTag(),
      +                int indent = CountIndent(line);
      +                if (indent < minIndent)
      +                    minIndent = indent;
      +            }
       
      -                // XText node exists on previous nodes.
      -                XText prevText =>
      -                    !prevText.EndsWithEmptyLine(),
      +            inPre = UpdatePreFlag(inPre, line);
      +        }
       
      -                // Other node types is not expected, and no need to insert empty line.
      -                _ => false
      -            };
      +        if (minIndent == int.MaxValue)
      +            minIndent = 0;
      +
      +        // 2nd pass: build result
      +        var sb = new StringBuilder(text.Length + 8);
      +
      +        inPre = false;
      +        pos = 0;
      +
      +        while (pos < span.Length)
      +        {
      +            var line = ReadLine(span, ref pos);
      +
      +            if (!inPre && line.Length != 0)
      +            {
      +                int remove = Math.Min(minIndent, CountIndent(line));
      +                sb.Append(line.Slice(remove));
      +            }
      +            else
      +            {
      +                sb.Append(line);
      +            }
      +
      +            sb.Append('\n');
      +
      +            inPre = UpdatePreFlag(inPre, line);
               }
       
      -        // Case 2: There is no previous non-whitespace node
      -        // Empty Line is not needed except for 
       tag.
      -        if (!node.IsPreTag())
      -            return false;
      +        // Ensure trailing newline
      +        sb.Append('\n');
      +
      +        return sb.ToString();
      +    }
      +
      +    private static int CountIndent(ReadOnlySpan line)
      +    {
      +        int i = 0;
      +        while (i < line.Length && HelperMethods.IsIndentChar(line[i]))
      +            i++;
      +        return i;
      +    }
       
      -        // If previous node is XText. Check text ends with empty line.
      -        if (node.PreviousNode is XText whitespaceNode)
      -            return !whitespaceNode.EndsWithEmptyLine();
      +    private static bool UpdatePreFlag(bool inPre, ReadOnlySpan line)
      +    {
      +        var trimmed = line.Trim();
      +
      +        // Check start tag (It might contains attribute)
      +        if (!inPre && trimmed.StartsWith("", StringComparison.OrdinalIgnoreCase))
      +            inPre = false;
      +
      +        return inPre;
           }
       
      -    public static void EnsureEmptyLineBefore(this XNode node)
      +    private static bool IsWhitespaceLine(ReadOnlySpan line)
           {
      -        switch (node.PreviousNode)
      +        foreach (var c in line)
               {
      -            case null:
      -            case XElement:
      -                node.AddBeforeSelf(new XText("\n\n"));
      -                return;
      +            if (!char.IsWhiteSpace(c))
      +                return false;
      +        }
      +        return true;
      +    }
       
      -            case XText textNode:
      -                var text = textNode.Value;
      +    private static ReadOnlySpan ReadLine(ReadOnlySpan text, ref int pos)
      +    {
      +        int start = pos;
      +        while (pos < text.Length && text[pos] != '\n')
      +            ++pos;
       
      -                switch (CountTrailingNewLines(text, out var insertIndex))
      -                {
      -                    case 0:
      -                        textNode.Value = text.Insert(insertIndex, "\n\n");
      -                        return;
      +        int length = pos - start;
       
      -                    case 1:
      -                        textNode.Value = text.Insert(insertIndex, "\n");
      -                        return;
      +        // skip '\n'
      +        if (pos < text.Length && text[pos] == '\n')
      +            ++pos;
       
      -                    default:
      -                        // This code path is not expected to be called.
      -                        // Because it should be filtered by NeedEmptyLineBefore.
      -                        Debug.Assert(textNode.EndsWithEmptyLine());
      -                        return;
      +        return text.Slice(start, length);
      +    }
      +}
       
      -                }
      +// Define file scoped extension methods for XNode/XElement.
      +static file class XNodeExtensions
      +{
      +    /// 
      +    /// The whole spacing rule is defined ONLY here.
      +    /// Key = (left, right)
      +    /// Value = need empty line between them
      +    /// 
      +    private static readonly Dictionary<(NodeKind prev, NodeKind next), bool> NeedEmptyLineRules = new()
      +    {
      +        //Block-> *
      +        [(NodeKind.Block, NodeKind.Other)] = false,
      +        [(NodeKind.Block, NodeKind.Block)] = false,
      +        [(NodeKind.Block, NodeKind.Pre)] = true,
      +        [(NodeKind.Block, NodeKind.Text)] = true,
      +
      +        // Pre -> *
      +        [(NodeKind.Pre, NodeKind.Other)] = true,
      +        [(NodeKind.Pre, NodeKind.Block)] = true,
      +        [(NodeKind.Pre, NodeKind.Pre)] = false,
      +        [(NodeKind.Pre, NodeKind.Text)] = true,
      +
      +        // Other -> *
      +        [(NodeKind.Other, NodeKind.Block)] = false,
      +        [(NodeKind.Other, NodeKind.Pre)] = true,
      +        [(NodeKind.Other, NodeKind.Other)] = false,
      +        [(NodeKind.Other, NodeKind.Text)] = true,
      +
      +        // Text -> *
      +        [(NodeKind.Text, NodeKind.Block)] = true,
      +        [(NodeKind.Text, NodeKind.Pre)] = true,
      +        [(NodeKind.Text, NodeKind.Other)] = true,
      +        [(NodeKind.Text, NodeKind.Text)] = false,
      +    };
       
      -            default:
      -                return;
      -        }
      +    private static readonly HashSet BlockTags = new(StringComparer.OrdinalIgnoreCase)
      +    {
      +        "ol",
      +        "p",
      +        "table",
      +        "ul",
      +
      +        // Recommended XML tags for C# documentation comments
      +        // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags
      +        // Note: Some XML tags(e.g. ``/``) are pre-processed and converted to HTML tags.
      +        "example",
      +        
      +        // Other tags
      +        "pre",
      +    };
      +
      +    private enum NodeKind
      +    {
      +        // XElement
      +        Block, // HTML element that requires empty line before/after tag.
      +        Pre,   // 
       tag. It's handled same as block type. It require additional rules.
      +        Other, // Other HTML tags
      +
      +        // XText
      +        Text,
           }
       
      -    public static bool NeedEmptyLineAfter(this XElement node)
      +    private enum Direction
           {
      -        // Case 1: There is a next node that is non-whitespace.
      -        if (node.TryGetNonWhitespaceNextNode(out var nextNode))
      -        {
      -            return nextNode switch
      -            {
      -                // XElement exists on previous nodes.
      -                XElement nextElem =>
      -                    node.IsPreTag() && !nextElem.IsPreTag(),
      +        Before,
      +        After,
      +    }
       
      -                // XText node exists on previous nodes.
      -                XText nextText =>
      -                    !nextText.StartsWithEmptyLine(),
      +    public static XElement[] GetBlockTags(this XElement elem)
      +    {
      +        return elem.Descendants()
      +                   .Where(e => BlockTags.Contains(e.Name.LocalName))
      +                   .ToArray();
      +    }
       
      -                // Other node types is not expected, and no need to insert empty line.
      -                _ => false
      -            };
      -        }
      +    public static bool NeedEmptyLineBefore(this XElement node)
      +        => NeedEmptyLine(node, Direction.Before);
       
      -        // Case 2: There is no next non-whitespace node
      -        // Empty Line is not needed except for 
       tag.
      -        if (!node.IsPreTag())
      -            return false;
      +    public static void EnsureEmptyLineBefore(this XElement node)
      +        => EnsureEmptyLine(node, Direction.Before);
      +
      +    public static bool NeedEmptyLineAfter(this XElement node)
      +        => NeedEmptyLine(node, Direction.After);
      +
      +    public static void EnsureEmptyLineAfter(this XElement node)
      +        => EnsureEmptyLine(node, Direction.After);
       
      -        // If previous node is XText. Check text ends with empty line.
      -        if (node.NextNode is XText whitespaceNode)
      -            return !whitespaceNode.StartsWithEmptyLine();
      +    private static bool NeedEmptyLine(this XElement node, Direction direction)
      +    {
      +        // Check whitespace text node.
      +        XNode? neighborNode = FindNonWhitespaceNeighbor(node, direction);
       
      -        var parentNextNode = node.Parent?.NextNode;
      -        if (parentNextNode is XElement)
      -            return true;
      +        if (neighborNode == null)
      +            return false;
       
      -        if (parentNextNode is XText textNode)
      -            return !textNode.StartsWithEmptyLine();
      +        NodeKind leftKind;
      +        NodeKind rightKind;
       
      -        return node.Parent != null;
      +        if (direction == Direction.Before)
      +        {
      +            leftKind = GetNodeKind(neighborNode);
      +            rightKind = GetNodeKind(node);
      +        }
      +        else
      +        {
      +            leftKind = GetNodeKind(node);
      +            rightKind = GetNodeKind(neighborNode);
      +        }
      +
      +        return NeedEmptyLineRules.TryGetValue((leftKind, rightKind), out var result) && result;
           }
       
      -    public static void EnsureEmptyLineAfter(this XNode node)
      +    private static void EnsureEmptyLine(this XNode node, Direction direction)
           {
      -        switch (node.NextNode)
      +        var adjacentNode = GetAdjacentNode(node, direction);
      +
      +        switch (adjacentNode)
               {
      +            case null:
                   case XElement:
      -                node.AddAfterSelf(new XText("\n\n"));
      +                if (direction == Direction.Before)
      +                    node.AddBeforeSelf(new XText("\n\n"));
      +                else
      +                    node.AddAfterSelf(new XText("\n\n"));
                       return;
       
                   case XText textNode:
      -                var textValue = textNode.Value;
       
      -                switch (CountLeadingNewLines(textValue, out var insertIndex))
      +                int count = textNode.CountNewLines(direction, out var insertIndex);
      +
      +                switch (count)
                       {
                           case 0:
      -                        textNode.Value = textValue.Insert(insertIndex, "\n\n");
      +                        textNode.Value = textNode.Value.Insert(insertIndex, "\n\n");
                               return;
      -
                           case 1:
      -                        textNode.Value = textValue.Insert(insertIndex, "\n");
      +                        textNode.Value = textNode.Value.Insert(insertIndex, "\n");
                               return;
      -
                           default:
      -                        // This code path is not expected to be called.
      -                        // Because it should be filtered by NeedEmptyLineAfter.
      -                        Debug.Assert(textNode.StartsWithEmptyLine());
      +                        Debug.Assert(textNode.HasEmptyLine(direction));
                               return;
                       }
       
      @@ -241,192 +330,114 @@ public static void EnsureEmptyLineAfter(this XNode node)
               }
           }
       
      -
      -    /// 
      -    /// Get count of trailing new lines. space and tabs are ignored.
      -    /// 
      -    private static int CountTrailingNewLines(ReadOnlySpan span, out int insertIndex)
      +    private static NodeKind GetNodeKind(XNode node)
           {
      -        insertIndex = span.Length;
      -        bool insertIndexUpdated = false;
      -        int count = 0;
      +        if (node is not XElement elem)
      +            return NodeKind.Text;
       
      -        int i = span.Length;
      -        while (--i >= 0)
      -        {
      -            var c = span[i];
      -            if (IsIndentChar(c))
      -                continue;
      +        if (elem.IsPreTag())
      +            return NodeKind.Pre;
       
      -            if (c != '\n')
      -                return count;
      +        if (elem.IsBlockTag())
      +            return NodeKind.Block;
       
      -            if (!insertIndexUpdated)
      -            {
      -                insertIndexUpdated = true;
      -                insertIndex = i + 1;
      -            }
      -            ++count;
      -        }
      -
      -        return count;
      +        return NodeKind.Other;
           }
       
      -    /// 
      -    /// Get count of leading new lines. space and tabs are ignored.
      -    /// 
      -    private static int CountLeadingNewLines(ReadOnlySpan span, out int insertIndex)
      +    private static XNode? GetAdjacentNode(this XNode node, Direction direction)
           {
      -        insertIndex = 0;
      -        bool insertIndexUpdated = false;
      -        int count = 0;
      -
      -        for (int i = 0; i < span.Length; ++i)
      -        {
      -            var c = span[i];
      -            if (IsIndentChar(c))
      -                continue;
      -
      -            if (c != '\n')
      -                return count;
      -
      -            if (!insertIndexUpdated)
      -            {
      -                insertIndexUpdated = true;
      -                insertIndex = i;
      -            }
      -            ++count;
      -        }
      -
      -        return count;
      +        return direction == Direction.Before
      +            ? node.PreviousNode
      +            : node.NextNode;
           }
       
      -    private static bool StartsWithEmptyLine(this XNode? node)
      +    private static XNode? FindNonWhitespaceNeighbor(this XNode node, Direction direction)
           {
      -        if (node is not XText textNode)
      -            return false;
      +        var current = node.GetAdjacentNode(direction);
       
      -        return CountLeadingNewLines(textNode.Value, out _) >= 2;
      -    }
      +        while (current != null && current.IsWhitespaceNode())
      +            current = current.GetAdjacentNode(direction);
       
      -    private static bool EndsWithEmptyLine(this XNode? node)
      -    {
      -        if (node is not XText textNode)
      -            return false;
      +        // If node is not found. Use parent instead.
      +        current ??= node.Parent;
       
      -        return CountTrailingNewLines(textNode.Value, out _) >= 2;
      +        return current;
           }
       
      -    private static bool TryGetNonWhitespacePrevNode(this XElement elem, [NotNullWhen(true)] out XNode? result)
      +    private static bool HasEmptyLine(this XText node, Direction direction)
      +      => CountNewLines(node, direction, out _) >= 2;
      +
      +    /// 
      +    /// Get count of new lines. space and tabs are ignored.
      +    /// 
      +    private static int CountNewLines(this XText node, Direction direction, out int insertIndex)
           {
      -        var prev = elem.PreviousNode;
      -        while (prev is not null && prev.IsWhitespaceNode())
      -            prev = prev.PreviousNode;
      +        var span = node.Value.AsSpan();
      +        int count = 0;
       
      -        result = prev;
      -        return result is not null;
      -    }
      +        switch (direction)
      +        {
      +            case Direction.Before:
      +                insertIndex = span.Length;
      +                for (int i = span.Length - 1; i >= 0; --i)
      +                {
      +                    char c = span[i];
       
      -    private static bool TryGetNonWhitespaceNextNode(this XElement elem, [NotNullWhen(true)] out XNode? result)
      -    {
      -        var next = elem.NextNode;
      -        while (next != null && next.IsWhitespaceNode())
      -            next = next.NextNode;
      +                    if (HelperMethods.IsIndentChar(c))
      +                        continue;
       
      -        result = next;
      -        return result is not null;
      -    }
      +                    if (c != '\n')
      +                        break;
       
      -    private static string RemoveCommonIndent(string text)
      -    {
      -        var lines = text.Split('\n');
      +                    if (count == 0)
      +                        insertIndex = i + 1;
       
      -        // 1st pass: Compute minimum indent (excluding 
       blocks)
      -        bool inPre = false;
      -        int minIndent = int.MaxValue;
      +                    count++;
      +                }
      +                return count;
       
      -        foreach (var line in lines)
      -        {
      -            if (!inPre && !string.IsNullOrWhiteSpace(line))
      -            {
      -                int indent = CountIndent(line);
      -                minIndent = Math.Min(minIndent, indent);
      -            }
      +            case Direction.After:
      +                insertIndex = 0;
      +                for (int i = 0; i < span.Length; ++i)
      +                {
      +                    char c = span[i];
       
      -            UpdatePreFlag(line, ref inPre);
      -        }
      +                    if (HelperMethods.IsIndentChar(c))
      +                        continue;
       
      -        if (minIndent == int.MaxValue)
      -            minIndent = 0;
      +                    if (c != '\n')
      +                        break;
       
      -        // 2nd pass: remove common indent
      -        var sb = new StringBuilder(text.Length);
      -        inPre = false;
      +                    if (count == 0)
      +                        insertIndex = i;
       
      -        foreach (var line in lines)
      -        {
      -            if (!inPre && line.Length != 0)
      -            {
      -                int removeLength = Math.Min(minIndent, CountIndent(line));
      -                sb.Append(line.AsSpan().Slice(removeLength));
      -                sb.Append('\n');
      -            }
      -            else
      -            {
      -                sb.Append(line);
      -                sb.Append('\n');
      -            }
      +                    count++;
      +                }
      +                return count;
       
      -            UpdatePreFlag(line, ref inPre);
      +            default:
      +                throw new UnreachableException();
               }
      +    }
       
      -        // Ensure trailing newline
      -        sb.Append('\n');
      +    private static bool IsPreTag(this XElement elem)
      +        => elem.Name.LocalName == "pre";
       
      -        return sb.ToString();
      -    }
      +    private static bool IsBlockTag(this XElement elem)
      +        => BlockTags.Contains(elem.Name.LocalName);
      +}
       
      -    private static bool IsWhitespaceNode(this XNode node)
      +// Define helper methods that are shared between extensions.
      +static file class HelperMethods
      +{
      +    public static bool IsIndentChar(char c)
      +        => c == ' ' || c == '\t';
      +
      +    public static bool IsWhitespaceNode(this XNode node)
           {
               if (node is not XText textNode)
                   return false;
       
               return textNode.Value.All(char.IsWhiteSpace);
           }
      -
      -    private static bool IsPreTag(this XNode? node)
      -        => node is XElement elem && elem.Name == "pre";
      -
      -    private static int CountIndent(ReadOnlySpan line)
      -    {
      -        int i = 0;
      -        while (i < line.Length && IsIndentChar(line[i]))
      -            i++;
      -        return i;
      -    }
      -
      -    private static void UpdatePreFlag(ReadOnlySpan line, ref bool inPre)
      -    {
      -        var trimmed = line.Trim();
      -
      -        // Check start tag (It might contains attribute)
      -        if (!inPre && trimmed.StartsWith("", StringComparison.OrdinalIgnoreCase))
      -            inPre = false;
      -    }
      -
      -    private static bool IsIndentChar(char c)
      -    {
      -        switch (c)
      -        {
      -            case ' ':
      -            case '\t':
      -                return true;
      -            default:
      -                return false;
      -        }
      -    }
       }
      
      From 3b19f122004326a88995eb4703bad64ebf551ac0 Mon Sep 17 00:00:00 2001
      From: filzrev <103790468+filzrev@users.noreply.github.com>
      Date: Fri, 23 Jan 2026 18:46:39 +0900
      Subject: [PATCH 5/5] chore: fix issue when markdown text and pre tag exists on
       same line
      
      ---
       .../Parsers/XmlComment.Extensions.cs          | 168 ++++++++++++++++--
       .../XmlCommentSummaryTest.Code.cs             | 107 +++++++++++
       2 files changed, 257 insertions(+), 18 deletions(-)
      
      diff --git a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs
      index 95c2a625310..b0584d630b7 100644
      --- a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs
      +++ b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs
      @@ -1,6 +1,7 @@
       // Licensed to the .NET Foundation under one or more agreements.
       // The .NET Foundation licenses this file to you under the MIT license.
       
      +using System.Buffers;
       using System.Diagnostics;
       using System.Text;
       using System.Xml;
      @@ -64,6 +65,7 @@ public static string GetInnerXml(this XElement elem)
               xml = RemoveCommonIndent(xml);
       
               // Trim beginning spaces/lines if text starts with HTML tag.
      +        // It is necessary to avoid it being handled as a markdown code block.
               var firstNode = nodes.FirstOrDefault(x => !x.IsWhitespaceNode());
               if (firstNode != null && firstNode.NodeType == XmlNodeType.Element)
                   xml = xml.TrimStart();
      @@ -103,7 +105,7 @@ private static string RemoveCommonIndent(string text)
                   minIndent = 0;
       
               // 2nd pass: build result
      -        var sb = new StringBuilder(text.Length + 8);
      +        var sb = new StringBuilder(text.Length);
       
               inPre = false;
               pos = 0;
      @@ -192,7 +194,7 @@ static file class XNodeExtensions
           /// 
           private static readonly Dictionary<(NodeKind prev, NodeKind next), bool> NeedEmptyLineRules = new()
           {
      -        //Block-> *
      +        //Block -> *
               [(NodeKind.Block, NodeKind.Other)] = false,
               [(NodeKind.Block, NodeKind.Block)] = false,
               [(NodeKind.Block, NodeKind.Pre)] = true,
      @@ -294,9 +296,9 @@ private static bool NeedEmptyLine(this XElement node, Direction direction)
               return NeedEmptyLineRules.TryGetValue((leftKind, rightKind), out var result) && result;
           }
       
      -    private static void EnsureEmptyLine(this XNode node, Direction direction)
      +    private static void EnsureEmptyLine(this XElement node, Direction direction)
           {
      -        var adjacentNode = GetAdjacentNode(node, direction);
      +        var adjacentNode = node.GetAdjacentNode(direction);
       
               switch (adjacentNode)
               {
      @@ -309,22 +311,26 @@ private static void EnsureEmptyLine(this XNode node, Direction direction)
                       return;
       
                   case XText textNode:
      +                int count = textNode.CountConsecutiveNewLines(direction, out var insertIndex);
      +                var indent = GetIndentToInsert(node, direction, insertIndex);
       
      -                int count = textNode.CountNewLines(direction, out var insertIndex);
      +                var newLineChars = count switch
      +                {
      +                    0 => "\n\n",
      +                    1 => "\n",
      +                    _ => "",
      +                };
       
      -                switch (count)
      +                if (newLineChars == "")
                       {
      -                    case 0:
      -                        textNode.Value = textNode.Value.Insert(insertIndex, "\n\n");
      -                        return;
      -                    case 1:
      -                        textNode.Value = textNode.Value.Insert(insertIndex, "\n");
      -                        return;
      -                    default:
      -                        Debug.Assert(textNode.HasEmptyLine(direction));
      -                        return;
      +                    // It's not expected to be called. Because it's skipped by NeedEmptyLine check.
      +                    Debug.Assert(textNode.HasEmptyLine(direction));
      +                    return;
                       }
       
      +                textNode.Value = textNode.Value.Insert(insertIndex, $"{newLineChars}{indent}");
      +                return;
      +
                   default:
                       return;
               }
      @@ -364,13 +370,35 @@ private static NodeKind GetNodeKind(XNode node)
               return current;
           }
       
      +    private static T? FindNeighbor(this XNode node, Direction direction)
      +        where T : XNode
      +    {
      +        var current = node.GetAdjacentNode(direction);
      +
      +        while (current != null && current is not T)
      +            current = current.GetAdjacentNode(direction);
      +
      +        return (T?)current;
      +    }
      +
           private static bool HasEmptyLine(this XText node, Direction direction)
      -      => CountNewLines(node, direction, out _) >= 2;
      +      => node.CountConsecutiveNewLines(direction, out _) >= 2;
       
           /// 
      -    /// Get count of new lines. space and tabs are ignored.
      +    /// Counts consecutive '\n' characters that exist before/after
           /// 
      -    private static int CountNewLines(this XText node, Direction direction, out int insertIndex)
      +    /// 
      +    /// Direction.Before scans from the end
      +    /// Direction.After scans from the beginning.
      +    /// 
      +    /// 
      +    /// The position where new content should be inserted.
      +    /// It's determined from the first newline found.
      +    /// 
      +    /// 
      +    /// The number of consecutive newline characters found.
      +    /// 
      +    private static int CountConsecutiveNewLines(this XText node, Direction direction, out int insertIndex)
           {
               var span = node.Value.AsSpan();
               int count = 0;
      @@ -420,6 +448,110 @@ private static int CountNewLines(this XText node, Direction direction, out int i
               }
           }
       
      +    private static string GetIndentToInsert(XElement node, Direction direction, int insertIndex)
      +    {
      +        // Check whether there is an existing indent.
      +        if (node.TryGetCurrentIndent(direction, out _))
      +            return "";
      +
      +        // Try to get indent from text node that is placed before.
      +        var beforeTextNode = node.FindNeighbor(Direction.Before);
      +        if (beforeTextNode != null)
      +            return beforeTextNode.GetIndentFromLastLine();
      +
      +        return "";
      +    }
      +
      +    private static bool TryGetCurrentIndent(this XElement node, Direction direction, out string indent)
      +    {
      +        indent = "";
      +
      +        var adjacentNode = node.GetAdjacentNode(direction);
      +        if (adjacentNode == null || adjacentNode is not XText textNode)
      +            return false;
      +
      +        ReadOnlySpan result = direction switch
      +        {
      +            Direction.Before => GetIndentBefore(textNode.Value),
      +            Direction.After => GetIndentAfter(textNode.Value),
      +            _ => throw new UnreachableException()
      +        };
      +
      +        if (result.IsEmpty)
      +            return false;
      +
      +        indent = result.ToString();
      +        return true;
      +    }
      +
      +    private static string GetIndentBefore(ReadOnlySpan span)
      +    {
      +        int lastNewLine = span.LastIndexOf('\n');
      +        if (lastNewLine < 0)
      +            return "";
      +
      +        var lastLineSpan = span[(lastNewLine + 1)..];
      +        if (lastLineSpan.Length == 0)
      +            return "";
      +
      +        if (lastLineSpan.ContainsAnyExcept([' ', '\t']))
      +            return "";
      +
      +        return lastLineSpan.ToString();
      +    }
      +
      +    private static string GetIndentAfter(ReadOnlySpan span)
      +    {
      +        int i = 0;
      +
      +        while (i < span.Length)
      +        {
      +            int lineStart = i;
      +
      +            int indentLength = span[i..].IndexOfAnyExcept([' ', '\t']);
      +            if (indentLength < 0)
      +                return "";
      +
      +            i += indentLength;
      +            int indentEnd = i;
      +
      +            // Skip empty line
      +            if (span[i] == '\n')
      +            {
      +                i++;
      +                continue;
      +            }
      +
      +            // Return indent
      +            return span.Slice(lineStart, indentEnd - lineStart).ToString();
      +        }
      +
      +        return "";
      +    }
      +
      +    private static string GetIndentFromLastLine(this XText textNode)
      +    {
      +        ReadOnlySpan text = textNode.Value;
      +
      +        int lastNewLineIndex = text.LastIndexOf('\n');
      +        if (lastNewLineIndex < 0)
      +            return "";
      +
      +        var line = text.Slice(lastNewLineIndex + 1);
      +
      +        if (line.IsEmpty)
      +            return "";
      +
      +        if (!line.ContainsAnyExcept([' ', '\t']))
      +            return line.ToString();
      +
      +        int index = line.IndexOfAnyExcept([' ', '\t']);
      +        if (index <= 0)
      +            return "";
      +
      +        return line.Slice(0, index).ToString();
      +    }
      +
           private static bool IsPreTag(this XElement elem)
               => elem.Name.LocalName == "pre";
       
      diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs
      index 32f9a705910..0b229682e12 100644
      --- a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs
      +++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs
      @@ -175,4 +175,111 @@ public void Code_MultipleBlockWithoutNewLine()
                   Paragraph2
                   """);
           }
      +
      +    [Fact]
      +    public void Code_StartsWithSameLine()
      +    {
      +        ValidateSummary(
      +            // Input XML
      +            """
      +            
      +              
      +              Paragraph: 
      +              
      +            
      +            """,
      +            // Expected Markdown
      +            """
      +            
      +            Paragraph: 
      +
      +            
      Code
      + +
      + """); + } + + [Fact] + public void Code_SingleLineWithParagraph() + { + ValidateSummary( + // Input XML + """ + + + Paragraph1Code + Paragraph2 + + + """, + // Expected Markdown + """ + + Paragraph1 + +
      Code
      + + Paragraph2 +
      + """); + } + + [Fact] + public void Code_SingleLineWithMultipleParagraphs() + { + ValidateSummary( + // Input XML + """ + + + aaa

      bbb

      cccCodeddd

      eee

      fff +
      +
      + """, + // Expected Markdown + """ + + aaa + +

      bbb

      + + ccc + +
      Code
      + + ddd + +

      eee

      + + fff +
      + """); + } + + [Fact] + public void Code_Indented() + { + ValidateSummary( + // Input XML + """ + + + Paragraph + + Code + + + + """, + // Expected Markdown + """ + + Paragraph + +
      Code
      + +
      + """); + } }