206 lines
6.9 KiB
C#
206 lines
6.9 KiB
C#
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 == "<Clone>$"))
|
|
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");
|
|
}
|