diff --git a/global.json b/global.json deleted file mode 100644 index 391ba3c2..00000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "8.0.100", - "rollForward": "latestFeature" - } -} diff --git a/src/XmlDocMarkdown.Core/ArgsReader.cs b/src/XmlDocMarkdown.Core/ArgsReader.cs index adb73ed6..fbaf75e4 100644 --- a/src/XmlDocMarkdown.Core/ArgsReader.cs +++ b/src/XmlDocMarkdown.Core/ArgsReader.cs @@ -18,7 +18,7 @@ internal sealed class ArgsReader /// args is null. public ArgsReader(IEnumerable args) { - m_args = (args ?? throw new ArgumentNullException(nameof(args))).ToList(); + m_args = [.. args ?? throw new ArgumentNullException(nameof(args))]; } /// diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index c9f445e0..29396e65 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; +using System.Xml.Linq; namespace XmlDocMarkdown.Core { @@ -31,7 +32,7 @@ internal sealed class MarkdownGenerator public IReadOnlyList? ExternalDocs { get; set; } public IReadOnlyList GenerateOutput(Assembly assembly, XmlDocAssembly xmlDocAssembly) => - DoGenerateOutput(assembly, xmlDocAssembly).ToList(); + [.. DoGenerateOutput(assembly, xmlDocAssembly)]; public static string GetCodeGenComment(string assemblyName) => $""; @@ -326,7 +327,7 @@ private string GetPermalink(string path) return path.Replace("\\", "/"); } - private NamedText WriteMemberPage(string path, string parent, string title, IReadOnlyList memberGroup, MarkdownContext context) + private NamedText WriteMemberPage(string path, string parent, string title, List memberGroup, MarkdownContext context) { var extension = GetFileExtension(); @@ -682,7 +683,7 @@ private bool IsVisible(MemberInfo memberInfo) public static XmlDocVisibilityLevel GetMostPrivate(params XmlDocVisibilityLevel[] visibilityLevels) => (XmlDocVisibilityLevel) visibilityLevels.Min(x => (int) x); - private static string GetMemberHeading(IReadOnlyList membersInfos, int index) + private static string GetMemberHeading(List membersInfos, int index) { var heading = $"{GetFullMemberName(membersInfos[index])} {GetMemberGroupNoun([membersInfos[index]])}"; if (membersInfos.Count > 1) @@ -690,7 +691,7 @@ private static string GetMemberHeading(IReadOnlyList membersInfos, i return heading; } - private static string GetMemberGroupNoun(IReadOnlyList memberInfos) + private static string GetMemberGroupNoun(List memberInfos) { var plural = memberInfos.Count != 1; @@ -709,11 +710,11 @@ private static string GetMemberGroupNoun(IReadOnlyList memberInfos) if (memberInfos.All(x => x is FieldInfo)) return plural ? "fields" : "field"; if (memberInfos.All(x => x is TypeInfo)) - return GetTypeGroupNoun(memberInfos.Cast().ToList()); + return GetTypeGroupNoun([.. memberInfos.Cast()]); return plural ? "members" : "member"; } - private static string GetTypeGroupNoun(IReadOnlyList typeInfos) + private static string GetTypeGroupNoun(List typeInfos) { var plural = typeInfos.Count != 1; @@ -1250,7 +1251,7 @@ private IEnumerable GetFullSignatureParts(MemberInfo memberInfo, ICollec yield return RenderTypeName(parameterInfo.ParameterType.GetTypeInfo(), seeAlso: seeAlsoMembers, - attributes: parameterInfo.GetCustomAttributes().ToList(), + attributes: [.. parameterInfo.GetCustomAttributes()], nullableContextFlags: nullableContextFlags); yield return " "; @@ -1974,7 +1975,7 @@ private static Type[] GetGenericArguments(MemberInfo memberInfo) return type.GenericTypeParameters; var method = memberInfo as MethodInfo; - return method?.GetGenericArguments() ?? Array.Empty(); + return method?.GetGenericArguments() ?? []; } private static ParameterInfo[] GetParameters(MemberInfo memberInfo) @@ -1988,7 +1989,7 @@ private static ParameterInfo[] GetParameters(MemberInfo memberInfo) return propertyInfo.GetIndexParameters(); var method = memberInfo as MethodBase; - return method?.GetParameters() ?? Array.Empty(); + return method?.GetParameters() ?? []; } private static string GetParameterShortNames(MemberInfo memberInfo) => @@ -2119,7 +2120,7 @@ private string MakeRelative(string baseUri, string path) private string? ToMarkdown(IEnumerable? inlines, MarkdownContext context) => inlines == null ? null : string.Concat(inlines.Select(x => ToMarkdown(x, context))).Trim(); - private IEnumerable ToMarkdown(IReadOnlyList blocks, MarkdownContext context) + private IEnumerable ToMarkdown(Collection blocks, MarkdownContext context) { for (var index = 0; index < blocks.Count; index++) { @@ -2158,6 +2159,61 @@ private IEnumerable ToMarkdown(IReadOnlyList blocks, Markdo index++; } } + else if (block.ListKind == XmlDocListKind.Table) + { + if (string.IsNullOrWhiteSpace(block.Inlines[0].Text)) + throw new ArgumentException("Input XML cannot be null or empty.", nameof(blocks)); + + if (block.Inlines[0].Text is { } text) + { + var doc = XDocument.Parse(text); + var table = (doc.Root?.Name.LocalName == "table" + ? doc.Root + : doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "table")) ?? throw new InvalidOperationException("No element was found."); + var thead = table.Elements().FirstOrDefault(e => e.Name.LocalName == "thead"); + var tbody = table.Elements().FirstOrDefault(e => e.Name.LocalName == "tbody"); + + var headerRow = thead?.Descendants().FirstOrDefault(e => e.Name.LocalName == "tr"); + var headers = headerRow != null ? GetCells(headerRow) : new List(); + + // Fallback: if there is no , use first data row as header. + var bodyRows = (tbody?.Elements().Where(e => e.Name.LocalName == "tr") + ?? table.Elements().Where(e => e.Name.LocalName == "tr")).ToList(); + + if (headers.Count == 0 && bodyRows.Count > 0) + { + headers = GetCells(bodyRows[0]); + bodyRows.RemoveAt(0); + } + + if (headers.Count == 0) + throw new InvalidOperationException("No header row could be determined."); + + var sb = new StringBuilder(); + + sb.AppendLine("| " + string.Join(" | ", headers.Select(EscapeCell)) + " |"); + sb.AppendLine("| " + string.Join(" | ", headers.Select(_ => "---")) + " |"); + + foreach (var cells in bodyRows.Select(GetCells)) + { + var list = cells; + + // Keep markdown shape stable if rows have fewer/more columns. + if (cells.Count < headers.Count) + { + list.AddRange(Enumerable.Repeat("", headers.Count - cells.Count)); + } + else if (cells.Count > headers.Count) + { + list = [.. cells.Take(headers.Count)]; + } + + sb.AppendLine("| " + string.Join(" | ", list.Select(EscapeCell)) + " |"); + } + + yield return sb.ToString(); + } + } else { if (block.IsCode) @@ -2178,6 +2234,24 @@ private IEnumerable ToMarkdown(IReadOnlyList blocks, Markdo } } + private static List GetCells(XElement row) => + [ + .. row.Elements() + .Where(e => e.Name.LocalName == "th" || e.Name.LocalName == "td") + .Select(e => NormalizeWhitespace(e.Value)) + ]; + + private static string NormalizeWhitespace(string value) => + Regex.Replace(value ?? "", @"\s+", " ").Trim(); + + private static string EscapeCell(string value) + { + value ??= ""; + value = value.Replace("|", "\\|"); + value = value.Replace("\r\n", "
").Replace("\n", "
"); + return value; + } + private sealed class MarkdownContext { public MarkdownContext(XmlDocAssembly xmlDocAssembly, IReadOnlyDictionary membersByXmlDocName, string assemblyFileName, string? sourceCodePath, string rootNamespace, string pageLocation) diff --git a/src/XmlDocMarkdown.Core/XmlDocMember.cs b/src/XmlDocMarkdown.Core/XmlDocMember.cs index ab44c814..a6b57832 100644 --- a/src/XmlDocMarkdown.Core/XmlDocMember.cs +++ b/src/XmlDocMarkdown.Core/XmlDocMember.cs @@ -208,6 +208,15 @@ private void AddElement(XElement xElement) m_block?.Inlines.Add(new XmlDocInline { Text = (string) xElement.Attribute("name"), IsTypeParamRef = true }); break; + case "table": + // Insert the raw table + m_listKinds.Push(XmlDocListKind.Table); + NextBlock(); + m_block?.Inlines.Add(new XmlDocInline { Text = xElement.ToString() }); + m_listKinds.Pop(); + NextBlock(); + break; + default: AddNodes(xElement.Nodes()); break; @@ -241,7 +250,7 @@ private static XmlDocListKind GetListKind(XElement xElement) => private static string TrimCode(string text) { // trimming logic adapted from https://github.com/kzu/NuDoq - var lines = text.Split(new[] { Environment.NewLine, "\n" }, StringSplitOptions.None).ToList(); + var lines = text.Split([Environment.NewLine, "\n"], StringSplitOptions.None).ToList(); if (lines.Count != 0 && lines[0].Trim().Length == 0) lines.RemoveAt(0); diff --git a/src/XmlDocMarkdown.Core/XmlDocToc.cs b/src/XmlDocMarkdown.Core/XmlDocToc.cs index 4fdc1532..5409ef00 100644 --- a/src/XmlDocMarkdown.Core/XmlDocToc.cs +++ b/src/XmlDocMarkdown.Core/XmlDocToc.cs @@ -67,7 +67,7 @@ private XmlDocToc GetOrCreate(string path, string title) internal void Save(string tocPath) { - Directory.CreateDirectory(System.IO.Path.GetDirectoryName(tocPath)!); + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(tocPath) ?? ""); using (var writer = new StreamWriter(tocPath, false, Encoding.UTF8)) { writer.WriteLine("toc:"); diff --git a/tests/XmlDocMarkdown.Tests/MarkdownGeneratorTests.cs b/tests/XmlDocMarkdown.Tests/MarkdownGeneratorTests.cs index ee384d57..590b9063 100644 --- a/tests/XmlDocMarkdown.Tests/MarkdownGeneratorTests.cs +++ b/tests/XmlDocMarkdown.Tests/MarkdownGeneratorTests.cs @@ -5,7 +5,7 @@ namespace XmlDocMarkdown.Tests { - public class MarkdownGeneratorTests + internal sealed class MarkdownGeneratorTests { [Test] public void ExampleAssembly() diff --git a/tools/ExampleAssembly/IsExternalInit.cs b/tools/ExampleAssembly/IsExternalInit.cs index 50093a56..64e185f1 100644 --- a/tools/ExampleAssembly/IsExternalInit.cs +++ b/tools/ExampleAssembly/IsExternalInit.cs @@ -1,6 +1,8 @@ using System.ComponentModel; +#pragma warning disable IDE0130 namespace System.Runtime.CompilerServices +#pragma warning restore IDE0130 { /// /// Bug fix for C# 9 record when not using .NET 5.