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.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 @@ + 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 }