537 lines
22 KiB
C#
537 lines
22 KiB
C#
using NetDocsForLLM.Models;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using System.Xml.Linq;
|
|
using Formatting = Newtonsoft.Json.Formatting;
|
|
|
|
namespace NetDocsForLLM.Services
|
|
{
|
|
public class DocumentationGenerator : IDocumentationGenerator
|
|
{
|
|
private readonly IDocFxService _metadataService;
|
|
|
|
public DocumentationGenerator(IDocFxService metadataService)
|
|
{
|
|
_metadataService = metadataService ?? throw new ArgumentNullException(nameof(metadataService));
|
|
}
|
|
|
|
public async Task<DocumentationModel> GenerateDocumentation(IEnumerable<AssemblyModel> assemblies, ExportSettings settings)
|
|
{
|
|
try
|
|
{
|
|
// Generate metadata using XmlDocGenerator
|
|
var metadataPath = await _metadataService.GenerateMetadataAsync(assemblies);
|
|
|
|
// Create a new documentation model
|
|
var documentation = new DocumentationModel();
|
|
var namespaces = new Dictionary<string, NamespaceDocumentation>();
|
|
|
|
// Process XML files produced by our XmlDocGenerator
|
|
var xmlFiles = Directory.GetFiles(metadataPath, "*.xml");
|
|
foreach (var xmlFile in xmlFiles)
|
|
{
|
|
ProcessXmlDocumentation(xmlFile, documentation, namespaces, settings);
|
|
}
|
|
|
|
return documentation;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException($"Error al generar documentación: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
private void ProcessXmlDocumentation(string xmlFile, DocumentationModel documentation,
|
|
Dictionary<string, NamespaceDocumentation> namespaces,
|
|
ExportSettings settings)
|
|
{
|
|
try
|
|
{
|
|
// Cargar el archivo XML
|
|
var xmlDoc = XDocument.Load(xmlFile);
|
|
|
|
// Obtener el nombre del ensamblado
|
|
string assemblyName = xmlDoc.Root.Element("assembly")?.Element("name")?.Value;
|
|
if (string.IsNullOrEmpty(assemblyName))
|
|
return;
|
|
|
|
// Leer todos los miembros
|
|
var memberElements = xmlDoc.Root.Element("members")?.Elements("member");
|
|
if (memberElements == null)
|
|
return;
|
|
|
|
// Procesar los tipos primero (para la estructura jerárquica)
|
|
var typeMembers = memberElements.Where(m =>
|
|
m.Attribute("name")?.Value?.StartsWith("T:") == true);
|
|
|
|
foreach (var typeMember in typeMembers)
|
|
{
|
|
ProcessTypeMember(typeMember, documentation, namespaces);
|
|
}
|
|
|
|
// Luego procesar miembros de tipos
|
|
var nonTypeMembers = memberElements.Where(m =>
|
|
m.Attribute("name")?.Value?.StartsWith("T:") == false);
|
|
|
|
foreach (var member in nonTypeMembers)
|
|
{
|
|
ProcessMember(member, documentation, namespaces, settings);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error procesando archivo XML {xmlFile}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void ProcessTypeMember(XElement typeMember, DocumentationModel documentation,
|
|
Dictionary<string, NamespaceDocumentation> namespaces)
|
|
{
|
|
try
|
|
{
|
|
string typeId = typeMember.Attribute("name")?.Value;
|
|
if (string.IsNullOrEmpty(typeId) || !typeId.StartsWith("T:"))
|
|
return;
|
|
|
|
// Extraer el nombre completo del tipo
|
|
string fullTypeName = typeId.Substring(2); // Quitar "T:"
|
|
|
|
// Obtener namespace y nombre del tipo
|
|
string namespaceName = "Global";
|
|
string typeName = fullTypeName;
|
|
|
|
int lastDotPos = fullTypeName.LastIndexOf('.');
|
|
if (lastDotPos > 0)
|
|
{
|
|
namespaceName = fullTypeName.Substring(0, lastDotPos);
|
|
typeName = fullTypeName.Substring(lastDotPos + 1);
|
|
}
|
|
|
|
// Buscar o crear el namespace
|
|
if (!namespaces.TryGetValue(namespaceName, out var namespaceDoc))
|
|
{
|
|
namespaceDoc = new NamespaceDocumentation
|
|
{
|
|
Name = namespaceName,
|
|
Description = $"Contains types from the assembly"
|
|
};
|
|
namespaces[namespaceName] = namespaceDoc;
|
|
documentation.Namespaces.Add(namespaceDoc);
|
|
}
|
|
|
|
// Obtener información del tipo desde XML
|
|
string description = typeMember.Element("summary")?.Value?.Trim() ?? "No description available";
|
|
string remarks = typeMember.Element("remarks")?.Value?.Trim();
|
|
|
|
// Determinar el tipo de tipo
|
|
string typeKind = "Class";
|
|
if (description.Contains("interface:"))
|
|
typeKind = "Interface";
|
|
else if (description.Contains("structure:"))
|
|
typeKind = "Struct";
|
|
else if (description.Contains("enumeration:"))
|
|
typeKind = "Enum";
|
|
else if (description.Contains("abstract class:"))
|
|
typeKind = "Abstract Class";
|
|
else if (description.Contains("static class:"))
|
|
typeKind = "Static Class";
|
|
|
|
// Crear documentación de tipo
|
|
var typeDoc = new TypeDocumentation
|
|
{
|
|
Name = typeName,
|
|
FullName = fullTypeName,
|
|
Description = description,
|
|
TypeKind = typeKind
|
|
};
|
|
|
|
// Procesar herencia e interfaces si están disponibles en remarks
|
|
if (!string.IsNullOrEmpty(remarks))
|
|
{
|
|
if (remarks.Contains("Inherits from"))
|
|
{
|
|
int start = remarks.IndexOf("Inherits from") + "Inherits from".Length;
|
|
int end = remarks.IndexOf("\n", start);
|
|
if (end < 0) end = remarks.Length;
|
|
|
|
string baseTypeName = remarks.Substring(start, end - start).Trim();
|
|
typeDoc.BaseTypes.Add(baseTypeName);
|
|
}
|
|
|
|
if (remarks.Contains("Implements:"))
|
|
{
|
|
int start = remarks.IndexOf("Implements:") + "Implements:".Length;
|
|
int end = remarks.IndexOf("\n", start);
|
|
if (end < 0) end = remarks.Length;
|
|
|
|
string interfacesStr = remarks.Substring(start, end - start).Trim();
|
|
string[] interfaces = interfacesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
foreach (var interfaceName in interfaces)
|
|
{
|
|
typeDoc.Interfaces.Add(interfaceName.Trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
namespaceDoc.Types.Add(typeDoc);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error procesando tipo: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void ProcessMember(XElement memberElement, DocumentationModel documentation,
|
|
Dictionary<string, NamespaceDocumentation> namespaces,
|
|
ExportSettings settings)
|
|
{
|
|
try
|
|
{
|
|
string memberId = memberElement.Attribute("name")?.Value;
|
|
if (string.IsNullOrEmpty(memberId))
|
|
return;
|
|
|
|
// Determinar el tipo de miembro
|
|
char memberTypeChar = memberId[0];
|
|
if (memberId.Length < 3 || memberId[1] != ':')
|
|
return;
|
|
|
|
string memberType;
|
|
switch (memberTypeChar)
|
|
{
|
|
case 'M': memberType = "Method"; break;
|
|
case 'P': memberType = "Property"; break;
|
|
case 'F': memberType = "Field"; break;
|
|
case 'E': memberType = "Event"; break;
|
|
default: return; // No es un miembro que procesamos
|
|
}
|
|
|
|
// Extraer información del tipo al que pertenece el miembro
|
|
string fullMemberId = memberId.Substring(2); // Quitar "X:"
|
|
int lastDotPos = fullMemberId.LastIndexOf('.');
|
|
if (lastDotPos <= 0)
|
|
return;
|
|
|
|
string fullTypeName = fullMemberId.Substring(0, lastDotPos);
|
|
string memberName = fullMemberId.Substring(lastDotPos + 1);
|
|
|
|
// Para métodos con parámetros, extraer el nombre real
|
|
if (memberType == "Method" && memberName.Contains("("))
|
|
{
|
|
memberName = memberName.Substring(0, memberName.IndexOf('('));
|
|
}
|
|
|
|
// Encontrar el namespace y tipo
|
|
string namespaceName = "Global";
|
|
string typeName = fullTypeName;
|
|
|
|
int lastTypeNameDotPos = fullTypeName.LastIndexOf('.');
|
|
if (lastTypeNameDotPos > 0)
|
|
{
|
|
namespaceName = fullTypeName.Substring(0, lastTypeNameDotPos);
|
|
typeName = fullTypeName.Substring(lastTypeNameDotPos + 1);
|
|
}
|
|
|
|
// Buscar el namespace
|
|
if (!namespaces.TryGetValue(namespaceName, out var namespaceDoc))
|
|
{
|
|
return; // No encontramos el namespace, lo cual es extraño
|
|
}
|
|
|
|
// Buscar el tipo
|
|
var typeDoc = namespaceDoc.Types.FirstOrDefault(t => t.FullName == fullTypeName || t.Name == typeName);
|
|
if (typeDoc == null)
|
|
{
|
|
return; // No encontramos el tipo, lo cual es extraño
|
|
}
|
|
|
|
// Extraer descripción y otra información
|
|
string description = memberElement.Element("summary")?.Value?.Trim() ?? "No description available";
|
|
string returns = memberElement.Element("returns")?.Value?.Trim();
|
|
string value = memberElement.Element("value")?.Value?.Trim();
|
|
|
|
// Crear documentación del miembro
|
|
var memberDoc = new MemberDocumentation
|
|
{
|
|
Name = memberName,
|
|
MemberType = memberType,
|
|
Description = description,
|
|
ReturnType = DeriveReturnType(memberType, returns, value)
|
|
};
|
|
|
|
// Para métodos, procesar parámetros
|
|
if (memberType == "Method")
|
|
{
|
|
foreach (var paramElement in memberElement.Elements("param"))
|
|
{
|
|
string paramName = paramElement.Attribute("name")?.Value;
|
|
string paramDesc = paramElement.Value?.Trim();
|
|
|
|
if (!string.IsNullOrEmpty(paramName))
|
|
{
|
|
// Extraer tipo del parámetro de la descripción
|
|
string paramType = "object";
|
|
if (!string.IsNullOrEmpty(paramDesc) && paramDesc.StartsWith("A "))
|
|
{
|
|
int spacePos = paramDesc.IndexOf(' ', 2);
|
|
if (spacePos > 0)
|
|
{
|
|
paramType = paramDesc.Substring(2, spacePos - 2);
|
|
}
|
|
}
|
|
|
|
var paramDoc = new ParameterDocumentation
|
|
{
|
|
Name = paramName,
|
|
Type = paramType,
|
|
Description = paramDesc,
|
|
IsOptional = paramDesc?.Contains("Optional") == true
|
|
};
|
|
|
|
memberDoc.Parameters.Add(paramDoc);
|
|
}
|
|
}
|
|
|
|
// Generar firma basada en parámetros
|
|
string returnType = memberDoc.ReturnType ?? "void";
|
|
string parameters = string.Join(", ", memberDoc.Parameters.Select(p => $"{p.Type} {p.Name}"));
|
|
memberDoc.Signature = $"{returnType} {memberName}({parameters})";
|
|
}
|
|
else if (memberType == "Property")
|
|
{
|
|
// Generar firma para propiedades
|
|
string propType = memberDoc.ReturnType ?? "object";
|
|
string accessors = "";
|
|
|
|
if (description.Contains("Gets"))
|
|
{
|
|
accessors += "get; ";
|
|
}
|
|
|
|
if (description.Contains("sets"))
|
|
{
|
|
accessors += "set; ";
|
|
}
|
|
|
|
memberDoc.Signature = $"{propType} {memberName} {{ {accessors}}}";
|
|
}
|
|
else
|
|
{
|
|
// Firma simple para otros miembros
|
|
memberDoc.Signature = $"{memberDoc.ReturnType ?? "void"} {memberName}";
|
|
}
|
|
|
|
// Agregar el miembro al tipo
|
|
typeDoc.Members.Add(memberDoc);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error procesando miembro: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private string DeriveReturnType(string memberType, string returns, string value)
|
|
{
|
|
// Intentar extraer el tipo de retorno de las etiquetas returns o value
|
|
if (!string.IsNullOrEmpty(returns))
|
|
{
|
|
// Intentar extraer de algo como "A string value."
|
|
if (returns.StartsWith("A ") || returns.StartsWith("An "))
|
|
{
|
|
int spacePos = returns.IndexOf(' ', 2);
|
|
if (spacePos > 0)
|
|
{
|
|
return returns.Substring(2, spacePos - 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(value))
|
|
{
|
|
// Intentar extraer de algo como "A string representing..."
|
|
if (value.StartsWith("A ") || value.StartsWith("An "))
|
|
{
|
|
int spacePos = value.IndexOf(' ', 2);
|
|
if (spacePos > 0)
|
|
{
|
|
return value.Substring(2, spacePos - 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Valores por defecto para diferentes tipos de miembros
|
|
switch (memberType)
|
|
{
|
|
case "Method": return "void";
|
|
case "Property": return "object";
|
|
case "Field": return "object";
|
|
case "Event": return "EventHandler";
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
public string GenerateDocumentationPreview(DocumentationModel documentation, ExportSettings settings)
|
|
{
|
|
try
|
|
{
|
|
if (settings.OutputFormat == OutputFormat.Json)
|
|
{
|
|
return JsonConvert.SerializeObject(documentation, Formatting.Indented,
|
|
new JsonSerializerSettings
|
|
{
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
NullValueHandling = NullValueHandling.Ignore
|
|
});
|
|
}
|
|
else // YAML
|
|
{
|
|
// En un escenario real, usaríamos YamlDotNet
|
|
// Para esta implementación, usaremos una conversión manual simplificada a XML
|
|
var doc = new XDocument(
|
|
new XDeclaration("1.0", "utf-8", null),
|
|
new XElement("documentation",
|
|
new XElement("namespaces")
|
|
)
|
|
);
|
|
|
|
var namespacesElement = doc.Root.Element("namespaces");
|
|
|
|
foreach (var ns in documentation.Namespaces)
|
|
{
|
|
var nsElement = new XElement("namespace",
|
|
new XAttribute("name", ns.Name),
|
|
new XElement("description", ns.Description),
|
|
new XElement("types")
|
|
);
|
|
|
|
var typesElement = nsElement.Element("types");
|
|
|
|
foreach (var type in ns.Types)
|
|
{
|
|
var typeElement = new XElement("type",
|
|
new XAttribute("name", type.Name),
|
|
new XAttribute("fullName", type.FullName),
|
|
new XAttribute("kind", type.TypeKind),
|
|
new XElement("description", type.Description),
|
|
new XElement("members")
|
|
);
|
|
|
|
if (type.BaseTypes.Count > 0)
|
|
{
|
|
var baseTypesElement = new XElement("baseTypes");
|
|
foreach (var baseType in type.BaseTypes)
|
|
{
|
|
baseTypesElement.Add(new XElement("baseType", baseType));
|
|
}
|
|
typeElement.Add(baseTypesElement);
|
|
}
|
|
|
|
if (type.Interfaces.Count > 0)
|
|
{
|
|
var interfacesElement = new XElement("interfaces");
|
|
foreach (var iface in type.Interfaces)
|
|
{
|
|
interfacesElement.Add(new XElement("interface", iface));
|
|
}
|
|
typeElement.Add(interfacesElement);
|
|
}
|
|
|
|
var membersElement = typeElement.Element("members");
|
|
|
|
foreach (var member in type.Members)
|
|
{
|
|
var memberElement = new XElement("member",
|
|
new XAttribute("name", member.Name),
|
|
new XAttribute("type", member.MemberType),
|
|
new XElement("description", member.Description),
|
|
new XElement("signature", member.Signature)
|
|
);
|
|
|
|
if (!string.IsNullOrEmpty(member.ReturnType))
|
|
{
|
|
memberElement.Add(new XElement("returnType", member.ReturnType));
|
|
|
|
if (!string.IsNullOrEmpty(member.ReturnDescription))
|
|
{
|
|
memberElement.Add(new XElement("returnDescription", member.ReturnDescription));
|
|
}
|
|
}
|
|
|
|
if (member.Parameters.Count > 0)
|
|
{
|
|
var paramsElement = new XElement("parameters");
|
|
|
|
foreach (var param in member.Parameters)
|
|
{
|
|
var paramElement = new XElement("parameter",
|
|
new XAttribute("name", param.Name),
|
|
new XAttribute("type", param.Type),
|
|
new XElement("description", param.Description ?? "")
|
|
);
|
|
|
|
if (param.IsOptional)
|
|
{
|
|
paramElement.Add(new XAttribute("optional", "true"));
|
|
|
|
if (!string.IsNullOrEmpty(param.DefaultValue))
|
|
{
|
|
paramElement.Add(new XAttribute("defaultValue", param.DefaultValue));
|
|
}
|
|
}
|
|
|
|
paramsElement.Add(paramElement);
|
|
}
|
|
|
|
memberElement.Add(paramsElement);
|
|
}
|
|
|
|
if (member.Examples.Count > 0)
|
|
{
|
|
var examplesElement = new XElement("examples");
|
|
|
|
foreach (var example in member.Examples)
|
|
{
|
|
examplesElement.Add(new XElement("example", example));
|
|
}
|
|
|
|
memberElement.Add(examplesElement);
|
|
}
|
|
|
|
membersElement.Add(memberElement);
|
|
}
|
|
|
|
typesElement.Add(typeElement);
|
|
}
|
|
|
|
namespacesElement.Add(nsElement);
|
|
}
|
|
|
|
// Convertir a string con formato
|
|
using (var stringWriter = new StringWriter())
|
|
{
|
|
using (var xmlWriter = new XmlTextWriter(stringWriter)
|
|
{
|
|
Formatting = (System.Xml.Formatting)Formatting.Indented,
|
|
Indentation = 2
|
|
})
|
|
{
|
|
doc.Save(xmlWriter);
|
|
}
|
|
|
|
return stringWriter.ToString();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException($"Error al generar vista previa: {ex.Message}", ex);
|
|
}
|
|
}
|
|
}
|
|
} |