using System.Reflection; using System.Text; using LANCommander.Launcher.Plugins.Extensions; using LANCommander.PluginDocsGenerator; using LANCommander.SDK.Plugins; // Generates the plugin API reference for the documentation site directly from the plugin contract // assemblies and their XML doc comments. Run with: // dotnet run --project LANCommander.PluginDocsGenerator [output-path] // If no output path is supplied, the generator writes to LANCommander.Documentation/Plugins/API Reference.md. // Anchor types pull in the two assemblies that make up the public plugin surface. var assemblies = new[] { typeof(IPlugin).Assembly, // LANCommander.SDK (LANCommander.SDK.Plugins.*) typeof(INavigationPageExtension).Assembly, // LANCommander.Launcher.Plugins.* }; // Only types in these namespaces are considered part of the plugin surface. string[] namespacePrefixes = { "LANCommander.SDK.Plugins", "LANCommander.Launcher.Plugins", }; // Fixed ordering so the reference reads top-down from "what you implement" to host internals. string[] namespaceOrder = { "LANCommander.SDK.Plugins", "LANCommander.SDK.Plugins.Events", "LANCommander.Launcher.Plugins.Extensions", "LANCommander.Launcher.Plugins", }; var docs = new XmlDocLookup(assemblies); var types = assemblies .SelectMany(a => a.GetExportedTypes()) .Where(t => t.Namespace is not null && namespacePrefixes.Any(p => t.Namespace == p || t.Namespace.StartsWith(p + "."))) .Where(t => !t.IsNested) .ToList(); var sb = new StringBuilder(); sb.AppendLine("---"); sb.AppendLine("title: API Reference"); sb.AppendLine("sidebar_label: API Reference"); sb.AppendLine("sidebar_position: 4"); sb.AppendLine("---"); sb.AppendLine(); sb.AppendLine("{/* This file is generated by LANCommander.PluginDocsGenerator. Do not edit by hand. */}"); sb.AppendLine("{/* Regenerate with: dotnet run --project LANCommander.PluginDocsGenerator */}"); sb.AppendLine(); sb.AppendLine("# Plugin API Reference"); sb.AppendLine(); sb.AppendLine("This reference is generated directly from the plugin contract assemblies and their XML"); sb.AppendLine("documentation comments, so it always reflects the extension surface of the installed version."); sb.AppendLine("Types are grouped by namespace. Interfaces you implement in a plugin are listed first within"); sb.AppendLine("each group."); sb.AppendLine(); foreach (var ns in types.Select(t => t.Namespace!).Distinct().OrderBy(NamespaceRank).ThenBy(n => n)) { sb.AppendLine($"## `{ns}`"); sb.AppendLine(); var nsTypes = types .Where(t => t.Namespace == ns) .OrderBy(KindRank) .ThenBy(t => t.Name, StringComparer.Ordinal); foreach (var type in nsTypes) WriteType(sb, type, docs); } var outputPath = args.Length > 0 ? args[0] : ResolveDefaultOutputPath(); Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); File.WriteAllText(outputPath, sb.ToString()); Console.WriteLine($"Wrote {types.Count} types to {outputPath}"); return; int NamespaceRank(string ns) { var index = Array.IndexOf(namespaceOrder, ns); return index < 0 ? int.MaxValue : index; } static int KindRank(Type t) => t switch { { IsInterface: true } => 0, { IsEnum: true } => 3, { IsValueType: true } => 2, _ => 1, }; static void WriteType(StringBuilder sb, Type type, XmlDocLookup docs) { sb.AppendLine($"### {type.Name}"); sb.AppendLine(); sb.AppendLine($"`{Kind(type)}` — `{type.FullName}`"); sb.AppendLine(); var summary = docs.GetSummary(XmlId.ForType(type)); if (summary is not null) { sb.AppendLine(summary); sb.AppendLine(); } if (type.IsEnum) { WriteEnumMembers(sb, type, docs); return; } WriteProperties(sb, type, docs); WriteMethods(sb, type, docs); } static void WriteEnumMembers(StringBuilder sb, Type type, XmlDocLookup docs) { var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static); if (fields.Length == 0) return; sb.AppendLine("| Value | Description |"); sb.AppendLine("| --- | --- |"); foreach (var field in fields) { var summary = docs.GetSummary(XmlId.ForField(field))?.Replace("\n", " ") ?? ""; sb.AppendLine($"| `{field.Name}` = `{Convert.ToInt64(field.GetRawConstantValue())}` | {summary} |"); } sb.AppendLine(); } static void WriteProperties(StringBuilder sb, Type type, XmlDocLookup docs) { var properties = type .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(p => p.GetIndexParameters().Length == 0) .OrderBy(p => p.MetadataToken) .ToList(); if (properties.Count == 0) return; sb.AppendLine("**Properties**"); sb.AppendLine(); foreach (var property in properties) { sb.AppendLine($"- `{Signatures.Property(property)}`"); var summary = docs.GetSummary(XmlId.ForProperty(property)); if (summary is not null) sb.AppendLine($" - {summary.Replace("\n", " ")}"); } sb.AppendLine(); } static void WriteMethods(StringBuilder sb, Type type, XmlDocLookup docs) { var methods = type .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) .Where(m => !m.IsSpecialName) // drop property/event accessors, operators .Where(m => m.DeclaringType != typeof(object)) .Where(m => m.Name is not ("Equals" or "GetHashCode" or "ToString" or "Deconstruct" or "PrintMembers")) .Where(m => !m.Name.StartsWith('<')) // drop compiler-generated (e.g. records) .OrderBy(m => m.MetadataToken) .ToList(); if (methods.Count == 0) return; sb.AppendLine("**Methods**"); sb.AppendLine(); foreach (var method in methods) { sb.AppendLine($"- `{Signatures.Method(method)}`"); var summary = docs.GetSummary(XmlId.ForMethod(method)); if (summary is not null) sb.AppendLine($" - {summary.Replace("\n", " ")}"); } sb.AppendLine(); } static string Kind(Type t) { if (t.IsInterface) return "interface"; if (t.IsEnum) return "enum"; if (t.IsValueType) return "struct"; if (typeof(Attribute).IsAssignableFrom(t)) return "attribute"; if (t.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Any(m => m.Name == "$")) return "record"; return "class"; } static string ResolveDefaultOutputPath() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, "LANCommander.Documentation"))) dir = dir.Parent; if (dir is null) throw new InvalidOperationException("Could not locate the LANCommander.Documentation directory. Pass an output path explicitly."); return Path.Combine(dir.FullName, "LANCommander.Documentation", "Plugins", "API Reference.md"); }