diff --git a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs index 309489f7130..fa017c5ea0a 100644 --- a/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs +++ b/src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs @@ -25,7 +25,7 @@ private static string GetMarkdownText(XElement elem) foreach (var node in nodes) { if (node.NeedEmptyLineBefore()) - node.EnsureEmptyLineBefore(); + node.EnsureEmptyLineBefore(node.Parent != elem); if (node.NeedEmptyLineAfter()) node.EnsureEmptyLineAfter(); @@ -262,8 +262,8 @@ public static XElement[] GetBlockTags(this XElement elem) public static bool NeedEmptyLineBefore(this XElement node) => NeedEmptyLine(node, Direction.Before); - public static void EnsureEmptyLineBefore(this XElement node) - => EnsureEmptyLine(node, Direction.Before); + public static void EnsureEmptyLineBefore(this XElement node, bool nested) + => EnsureEmptyLine(node, Direction.Before, nested); public static bool NeedEmptyLineAfter(this XElement node) => NeedEmptyLine(node, Direction.After); @@ -296,7 +296,7 @@ private static bool NeedEmptyLine(this XElement node, Direction direction) return NeedEmptyLineRules.TryGetValue((leftKind, rightKind), out var result) && result; } - private static void EnsureEmptyLine(this XElement node, Direction direction) + private static void EnsureEmptyLine(this XElement node, Direction direction, bool nested = false) { var adjacentNode = node.GetAdjacentNode(direction); @@ -329,7 +329,12 @@ private static void EnsureEmptyLine(this XElement node, Direction direction) return; } - textNode.Value = textNode.Value.Insert(insertIndex, $"{newLineChars}{indent}"); + // A new blank line ends the containing HTML block. Align the nested + // tag with its container so its XML indentation cannot become code. + if (nested && direction == Direction.Before && !textNode.IsWhitespaceNode()) + textNode.Value = textNode.Value[..insertIndex] + newLineChars + GetLineIndent(node.Parent!); + else + textNode.Value = textNode.Value.Insert(insertIndex, $"{newLineChars}{indent}"); return; default: @@ -455,6 +460,11 @@ private static string GetIndentToInsert(XElement node, Direction direction) if (node.TryGetCurrentIndent(direction, out _)) return ""; + return GetLineIndent(node); + } + + private static string GetLineIndent(XElement node) + { // Inline children inherit the indentation of the line containing their parent. for (XElement? current = node; current != null; current = current.Parent) { diff --git a/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Blocks.cs b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Blocks.cs new file mode 100644 index 00000000000..c17afc4fc92 --- /dev/null +++ b/test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Blocks.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Markdig; +using Xunit; + +namespace Docfx.Dotnet.Tests; + +public partial class XmlCommentSummaryTest +{ + [Theory] + [InlineData(" ", "Indented")] + [InlineData("", "Non-indented")] + public void Blocks_List_Issue11173(string indent, string description) + { + var summary = XmlComment.Parse($""" + + + {indent}{description} paragraph with a list: + {indent} + {indent} see , + {indent} second item. + {indent} + + + """).Summary; + + var html = Markdown.ToHtml(summary); + + Assert.Contains("", html); + Assert.DoesNotContain("
", html);
+    }
+
+    [Theory]
+    [InlineData("bullet", "ul")]
+    [InlineData("number", "ol")]
+    [InlineData("table", "table")]
+    public void Blocks_OnlyListIsIndented(string type, string tag)
+    {
+        var summary = XmlComment.Parse($"""
+            
+            
+            Unindented text before an indented list:
+                
+                    see .
+                    second item.
+                
+            Following **markdown**.
+            
+            
+            """).Summary;
+
+        var html = Markdown.ToHtml(summary);
+
+        Assert.Contains($"<{tag}>", html);
+        Assert.Contains("Following markdown.

", html); + Assert.DoesNotContain("
", html);
+    }
+
+    [Fact]
+    public void Blocks_NestedListsWithInlineReferences()
+    {
+        var summary = XmlComment.Parse("""
+            
+            
+                See :
+                
+                    
+                        First item with :
+                        
+                            Nested item.
+                        
+                    
+                    Second item.
+                
+            
+            
+            """).Summary;
+
+        var html = Markdown.ToHtml(summary);
+
+        Assert.Contains("
  • ", html); + Assert.Contains("
    1. Nested item.
    ", html); + Assert.Contains("
  • Second item.
", html); + Assert.Contains("", html); + } + + [Theory] + [InlineData("{0}")] + [InlineData("
{0}
")] + [InlineData("{0}")] + [InlineData("{0}")] + [InlineData("
{0}
")] + public void Blocks_NewSeparatorPreservesNestedXml(string container) + { + foreach (var rootIndent in new[] { "", " " }) + foreach (var indent in new[] { "", " ", " ", " ", "\t" }) + foreach (var separator in new[] { "", "\n" }) + { + var content = $"\n{rootIndent}{indent}A list:{separator}{rootIndent}{indent}see .\n{rootIndent}"; + var input = $"\n{rootIndent}Before **bold**.\n\n{rootIndent}{string.Format(container, content)}\n\n{rootIndent}After **bold**.\n"; + + var html = Markdown.ToHtml(XmlComment.Parse(input).Summary); + + Assert.Contains("
  1. see Before bold.

    ", html); + Assert.Contains("

    After bold.

    ", html); + Assert.DoesNotContain("
    ", html);
    +                }
    +    }
    +
    +    [Theory]
    +    [InlineData("bullet", "ul")]
    +    [InlineData("number", "ol")]
    +    [InlineData("table", "table")]
    +    public void Blocks_NewSeparatorPreservesFollowingTextIndent(string type, string tag)
    +    {
    +        var summary = XmlComment.Parse($$""""
    +            
    +            
    +                See :
    +                Item.
    +                IndentedCode();
    +
    +            Following  and **bold**.
    +
    +            ```csharp
    +            if (ready)
    +                Run();
    +            ```
    +            
    +            if (ready)
    +            {
    +
    +                Run();
    +            }
    +            
    +            
    +            
    +            """").Summary;
    +
    +        var html = Markdown.ToHtml(summary);
    +
    +        Assert.Contains($"<{tag}>", html);
    +        Assert.Contains("
    IndentedCode();\n
    ", html); + Assert.Contains("and bold.

    ", html); + Assert.Contains("
    if (ready)\n    Run();\n
    ", html); + Assert.Contains("
    if (ready)\n{\n\n    Run();\n}
    ", html); + Assert.DoesNotContain($"<{tag}>", html); + } + + [Fact] + public void Blocks_NewSeparatorOnlyChangesTagIndent() + { + ValidateSummary( + """ + + + A list: + Item. + Code(); + + + """, + """ +

    + A list: + +

    • Item.
    + + Code(); +

    + """); + } + + [Fact] + public void Blocks_ExistingBlankLineKeepsIndent() + { + ValidateSummary( + """ + + + Description. + + Item. + + + """, + """ +

    + Description. + +

    • Item.
    +

    + """); + } + + [Fact] + public void Blocks_TopLevelMarkdownKeepsIndent() + { + ValidateSummary( + """ + + Description. + Item. + + """, + """ + Description. + +
    • Item.
    + """); + } +}