From 74420ae8bb13d235bbc1368e214df78ac9de11d0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 17 Jan 2026 18:18:34 +0000
Subject: [PATCH 1/3] Initial plan
From f448360b7b84c41a53b87673ed9a26fc1c57486f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 17 Jan 2026 18:25:10 +0000
Subject: [PATCH 2/3] Add C# Documentation spector tests
Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
---
.../Http/Documentation/ListsTests.cs | 39 +++++++++++++++++++
.../Http/Documentation/TextFormattingTests.cs | 36 +++++++++++++++++
.../TestProjects.Spector.Tests.csproj | 1 +
3 files changed, 76 insertions(+)
create mode 100644 packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/ListsTests.cs
create mode 100644 packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/TextFormattingTests.cs
diff --git a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/ListsTests.cs b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/ListsTests.cs
new file mode 100644
index 00000000000..1e04f8a0e65
--- /dev/null
+++ b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/ListsTests.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+using Documentation;
+using Documentation._Lists;
+using NUnit.Framework;
+
+namespace TestProjects.Spector.Tests.Http.Documentation
+{
+ public class ListsTests : SpectorTestBase
+ {
+ [SpectorTest]
+ public Task BulletPointsOp() => Test(async (host) =>
+ {
+ var client = new DocumentationClient(host, new DocumentationClientOptions());
+ var response = await client.GetListsClient().BulletPointsOpAsync();
+ Assert.AreEqual(204, response.GetRawResponse().Status);
+ });
+
+ [SpectorTest]
+ [Ignore("https://github.com/microsoft/typespec/issues/9173")]
+ public Task BulletPointsModel() => Test(async (host) =>
+ {
+ var client = new DocumentationClient(host, new DocumentationClientOptions());
+ var input = new BulletPointsModel(BulletPointsEnum.Simple);
+ var response = await client.GetListsClient().BulletPointsModelAsync(input);
+ Assert.AreEqual(200, response.GetRawResponse().Status);
+ });
+
+ [SpectorTest]
+ public Task Numbered() => Test(async (host) =>
+ {
+ var client = new DocumentationClient(host, new DocumentationClientOptions());
+ var response = await client.GetListsClient().NumberedAsync();
+ Assert.AreEqual(204, response.GetRawResponse().Status);
+ });
+ }
+}
diff --git a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/TextFormattingTests.cs b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/TextFormattingTests.cs
new file mode 100644
index 00000000000..045bf58feb1
--- /dev/null
+++ b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/Http/Documentation/TextFormattingTests.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+using Documentation;
+using NUnit.Framework;
+
+namespace TestProjects.Spector.Tests.Http.Documentation
+{
+ public class TextFormattingTests : SpectorTestBase
+ {
+ [SpectorTest]
+ public Task BoldText() => Test(async (host) =>
+ {
+ var client = new DocumentationClient(host, new DocumentationClientOptions());
+ var response = await client.GetTextFormattingClient().BoldTextAsync();
+ Assert.AreEqual(204, response.GetRawResponse().Status);
+ });
+
+ [SpectorTest]
+ public Task ItalicText() => Test(async (host) =>
+ {
+ var client = new DocumentationClient(host, new DocumentationClientOptions());
+ var response = await client.GetTextFormattingClient().ItalicTextAsync();
+ Assert.AreEqual(204, response.GetRawResponse().Status);
+ });
+
+ [SpectorTest]
+ public Task CombinedFormatting() => Test(async (host) =>
+ {
+ var client = new DocumentationClient(host, new DocumentationClientOptions());
+ var response = await client.GetTextFormattingClient().CombinedFormattingAsync();
+ Assert.AreEqual(204, response.GetRawResponse().Status);
+ });
+ }
+}
diff --git a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj
index 62e00182caf..467b32720de 100644
--- a/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj
+++ b/packages/http-client-csharp/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj
@@ -32,6 +32,7 @@
+
From 06f04e2810fa1be20ef1ed3c144de0f0d7115a3a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 17 Jan 2026 18:45:27 +0000
Subject: [PATCH 3/3] Implement markdown to XML doc conversion for C#
documentation
Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
---
.../src/Statements/XmlDocStatement.cs | 29 +++
.../src/Utilities/DocHelpers.cs | 132 +++++++++++++-
.../test/Utilities/DocHelpersTests.cs | 166 ++++++++++++++++++
.../src/Generated/Models/BulletPointsEnum.cs | 13 +-
4 files changed, 331 insertions(+), 9 deletions(-)
create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/DocHelpersTests.cs
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs
index 2baa4239530..e0ee861268c 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Statements/XmlDocStatement.cs
@@ -208,11 +208,40 @@ public static string EscapeLine(string s)
private static bool SkipValidTag(ref ReadOnlySpan span, ref int i)
{
var slice = span.Slice(i);
+ // Check for tags
if (slice.StartsWith(SeeCrefStart.AsSpan(), StringComparison.Ordinal) || slice.StartsWith(SeeCrefEnd.AsSpan(), StringComparison.Ordinal))
{
i += slice.IndexOf('>');
return true;
}
+
+ // Check for markdown-converted XML tags
+ // Bold: ,
+ if (slice.StartsWith("".AsSpan(), StringComparison.Ordinal) || slice.StartsWith("".AsSpan(), StringComparison.Ordinal))
+ {
+ i += slice.IndexOf('>');
+ return true;
+ }
+
+ // Italic: ,
+ if (slice.StartsWith("".AsSpan(), StringComparison.Ordinal) || slice.StartsWith("".AsSpan(), StringComparison.Ordinal))
+ {
+ i += slice.IndexOf('>');
+ return true;
+ }
+
+ // List and related tags: ".AsSpan(), StringComparison.Ordinal) ||
+ slice.StartsWith("- ".AsSpan(), StringComparison.Ordinal) ||
+ slice.StartsWith("
".AsSpan(), StringComparison.Ordinal) ||
+ slice.StartsWith("".AsSpan(), StringComparison.Ordinal) ||
+ slice.StartsWith("".AsSpan(), StringComparison.Ordinal))
+ {
+ i += slice.IndexOf('>');
+ return true;
+ }
+
return false;
}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DocHelpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DocHelpers.cs
index 1f9196e2106..dd4329c5655 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DocHelpers.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DocHelpers.cs
@@ -2,6 +2,9 @@
// Licensed under the MIT License.
using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
namespace Microsoft.TypeSpec.Generator.Utilities
{
@@ -9,17 +12,144 @@ public class DocHelpers
{
public static string? GetDescription(string? summary, string? doc)
{
- return (summary, doc) switch
+ var description = (summary, doc) switch
{
(null or "", null or "") => null,
(string s, null or "") => s,
_ => doc,
};
+
+ return description != null ? ConvertMarkdownToXml(description) : null;
}
public static FormattableString? GetFormattableDescription(string? summary, string? doc)
{
return FormattableStringHelpers.FromString(GetDescription(summary, doc));
}
+
+ ///
+ /// Converts markdown syntax to C# XML documentation syntax.
+ /// Handles bold (**text**), italic (*text*), bullet lists (- item), and numbered lists (1. item).
+ ///
+ internal static string ConvertMarkdownToXml(string markdown)
+ {
+ if (string.IsNullOrEmpty(markdown))
+ return markdown;
+
+ var lines = markdown.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ var result = new StringBuilder();
+ var inList = false;
+ var listType = "";
+ var listItems = new List();
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ var line = lines[i];
+ var trimmedLine = line.TrimStart();
+
+ // Check for bullet list item
+ if (trimmedLine.StartsWith("- "))
+ {
+ if (!inList || listType != "bullet")
+ {
+ // Flush previous list if different type
+ if (inList)
+ {
+ AppendList(result, listType, listItems);
+ listItems.Clear();
+ }
+ inList = true;
+ listType = "bullet";
+ }
+ // Remove the "- " prefix and add to list items
+ listItems.Add(ConvertInlineMarkdown(trimmedLine.Substring(2)));
+ }
+ // Check for numbered list item (e.g., "1. ", "2. ")
+ else if (Regex.IsMatch(trimmedLine, @"^\d+\.\s"))
+ {
+ if (!inList || listType != "number")
+ {
+ // Flush previous list if different type
+ if (inList)
+ {
+ AppendList(result, listType, listItems);
+ listItems.Clear();
+ }
+ inList = true;
+ listType = "number";
+ }
+ // Remove the number prefix and add to list items
+ var match = Regex.Match(trimmedLine, @"^\d+\.\s+(.*)");
+ listItems.Add(ConvertInlineMarkdown(match.Groups[1].Value));
+ }
+ else
+ {
+ // Not a list item, flush any pending list
+ if (inList)
+ {
+ AppendList(result, listType, listItems);
+ listItems.Clear();
+ inList = false;
+ }
+
+ // Process inline markdown (bold, italic) for regular lines
+ var processedLine = ConvertInlineMarkdown(line);
+
+ // Add line to result
+ if (result.Length > 0 && !string.IsNullOrWhiteSpace(processedLine))
+ {
+ result.AppendLine();
+ }
+ result.Append(processedLine);
+ }
+ }
+
+ // Flush any remaining list
+ if (inList)
+ {
+ AppendList(result, listType, listItems);
+ }
+
+ return result.ToString();
+ }
+
+ private static void AppendList(StringBuilder result, string listType, List items)
+ {
+ if (items.Count == 0)
+ return;
+
+ if (result.Length > 0)
+ {
+ result.AppendLine();
+ }
+
+ result.Append($"");
+ foreach (var item in items)
+ {
+ result.Append($"- {item}
");
+ }
+ result.Append("
");
+ }
+
+ ///
+ /// Converts inline markdown (bold and italic) to XML tags.
+ /// Handles: **bold**, ***bold italic***, *italic*
+ ///
+ private static string ConvertInlineMarkdown(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ return text;
+
+ // Handle ***bold italic*** (must be done before ** and *)
+ text = Regex.Replace(text, @"\*\*\*([^*]+?)\*\*\*", "$1");
+
+ // Handle **bold**
+ text = Regex.Replace(text, @"\*\*([^*]+?)\*\*", "$1");
+
+ // Handle *italic* (but not already processed bold markers)
+ text = Regex.Replace(text, @"(?$1");
+
+ return text;
+ }
}
}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/DocHelpersTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/DocHelpersTests.cs
new file mode 100644
index 00000000000..09b4eeac2c4
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/DocHelpersTests.cs
@@ -0,0 +1,166 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.TypeSpec.Generator.Utilities;
+using NUnit.Framework;
+
+namespace Microsoft.TypeSpec.Generator.Tests.Utilities
+{
+ public class DocHelpersTests
+ {
+ [Test]
+ public void TestBoldText()
+ {
+ var result = DocHelpers.GetDescription(null, "This is **bold text** in the middle.");
+ Assert.AreEqual("This is bold text in the middle.", result);
+ }
+
+ [Test]
+ public void TestMultipleBold()
+ {
+ var result = DocHelpers.GetDescription(null, "This has **multiple bold** sections and **another bold** section.");
+ Assert.AreEqual("This has multiple bold sections and another bold section.", result);
+ }
+
+ [Test]
+ public void TestItalicText()
+ {
+ var result = DocHelpers.GetDescription(null, "This is *italic text* in the middle.");
+ Assert.AreEqual("This is italic text in the middle.", result);
+ }
+
+ [Test]
+ public void TestMultipleItalic()
+ {
+ var result = DocHelpers.GetDescription(null, "This has *multiple italic* sections and *another italic* section.");
+ Assert.AreEqual("This has multiple italic sections and another italic section.", result);
+ }
+
+ [Test]
+ public void TestBoldItalicCombined()
+ {
+ var result = DocHelpers.GetDescription(null, "This has **bold**, *italic*, and ***bold italic*** text.");
+ Assert.AreEqual("This has bold, italic, and bold italic text.", result);
+ }
+
+ [Test]
+ public void TestNestedFormatting()
+ {
+ var result = DocHelpers.GetDescription(null, "You can combine them like **bold with *italic inside* bold**.");
+ // This is a complex case - the current implementation will handle the outermost first
+ // The expected behavior should convert ** first, then * inside
+ Assert.IsNotNull(result);
+ Assert.That(result, Does.Contain("").Or.Contain(""));
+ }
+
+ [Test]
+ public void TestBulletList()
+ {
+ var markdown = @"This tests:
+- First bullet point
+- Second bullet point
+- Third bullet point";
+ var result = DocHelpers.GetDescription(null, markdown);
+
+ Assert.That(result, Does.Contain(""));
+ Assert.That(result, Does.Contain("- First bullet point
"));
+ Assert.That(result, Does.Contain("- Second bullet point
"));
+ Assert.That(result, Does.Contain("- Third bullet point
"));
+ Assert.That(result, Does.Contain("
"));
+ }
+
+ [Test]
+ public void TestBulletListWithFormatting()
+ {
+ var markdown = @"This tests:
+- Simple bullet point
+- Bullet with **bold text**
+- Bullet with *italic text*";
+ var result = DocHelpers.GetDescription(null, markdown);
+
+ Assert.That(result, Does.Contain(""));
+ Assert.That(result, Does.Contain("- Simple bullet point
"));
+ Assert.That(result, Does.Contain("- Bullet with bold text
"));
+ Assert.That(result, Does.Contain("- Bullet with italic text
"));
+ }
+
+ [Test]
+ public void TestNumberedList()
+ {
+ var markdown = @"Steps to follow:
+1. First step
+2. Second step
+3. Third step";
+ var result = DocHelpers.GetDescription(null, markdown);
+
+ Assert.That(result, Does.Contain(""));
+ Assert.That(result, Does.Contain("- First step
"));
+ Assert.That(result, Does.Contain("- Second step
"));
+ Assert.That(result, Does.Contain("- Third step
"));
+ Assert.That(result, Does.Contain("
"));
+ }
+
+ [Test]
+ public void TestNumberedListWithFormatting()
+ {
+ var markdown = @"Steps:
+1. First step with **important** note
+2. Second step with *emphasis*
+3. Third step combining **bold** and *italic*";
+ var result = DocHelpers.GetDescription(null, markdown);
+
+ Assert.That(result, Does.Contain(""));
+ Assert.That(result, Does.Contain("- First step with important note
"));
+ Assert.That(result, Does.Contain("- Second step with emphasis
"));
+ }
+
+ [Test]
+ public void TestMixedContent()
+ {
+ var markdown = @"This is a paragraph with **bold** text.
+- First bullet
+- Second bullet
+Another paragraph with *italic* text.";
+ var result = DocHelpers.GetDescription(null, markdown);
+
+ Assert.That(result, Does.Contain("bold"));
+ Assert.That(result, Does.Contain(""));
+ Assert.That(result, Does.Contain("italic"));
+ }
+
+ [Test]
+ public void TestEmptyString()
+ {
+ var result = DocHelpers.GetDescription(null, "");
+ Assert.IsNull(result);
+ }
+
+ [Test]
+ public void TestNullString()
+ {
+ var result = DocHelpers.GetDescription(null, null);
+ Assert.IsNull(result);
+ }
+
+ [Test]
+ public void TestPlainText()
+ {
+ var result = DocHelpers.GetDescription(null, "This is plain text without any markdown.");
+ Assert.AreEqual("This is plain text without any markdown.", result);
+ }
+
+ [Test]
+ public void TestSummaryPreferredOverDoc()
+ {
+ var result = DocHelpers.GetDescription("Summary text", "Doc text");
+ Assert.AreEqual("Doc text", result);
+ }
+
+ [Test]
+ public void TestSummaryUsedWhenDocEmpty()
+ {
+ var result = DocHelpers.GetDescription("Summary with **bold**", "");
+ Assert.AreEqual("Summary with bold", result);
+ }
+ }
+}
diff --git a/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/Models/BulletPointsEnum.cs b/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/Models/BulletPointsEnum.cs
index 1487e435c2e..afed70b8573 100644
--- a/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/Models/BulletPointsEnum.cs
+++ b/packages/http-client-csharp/generator/TestProjects/Spector/http/documentation/src/Generated/Models/BulletPointsEnum.cs
@@ -8,20 +8,17 @@ public enum BulletPointsEnum
{
///
/// Simple bullet point. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- /// - One: one. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- /// - Two: two. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
+ /// - One: one. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- Two: two. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
///
Simple,
///
- /// Bullet point with **bold text**. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- /// - **One**: one. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- /// - **Two**: two. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
+ /// Bullet point with bold text. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
+ /// - One: one. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- Two: two. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
///
Bold,
///
- /// Bullet point with *italic text*. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- /// - *One*: one. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- /// - *Two*: two. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
+ /// Bullet point with italic text. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
+ /// - One: one. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
- Two: two. This line is intentionally long to test text wrapping in bullet points within enum documentation comments. It should properly indent the wrapped lines.
///
Italic
}