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 GenerateDocumentation(IEnumerable 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(); // 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 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 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 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 { switch (settings.OutputFormat) { case OutputFormat.Json: return JsonConvert.SerializeObject(documentation, Formatting.Indented, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }); case OutputFormat.Xml: return GenerateXmlOutput(documentation); case OutputFormat.Yaml: default: return GenerateYamlOutput(documentation); } } catch (Exception ex) { throw new InvalidOperationException($"Error al generar vista previa: {ex.Message}", ex); } } private string GenerateXmlOutput(DocumentationModel documentation) { var doc = new XDocument( new XDeclaration("1.0", "utf-8", null), new XElement("doc", new XElement("assembly", new XElement("name", "DocumentationForLLM") ), new XElement("members") ) ); var membersElement = doc.Root.Element("members"); // Agregar tipos foreach (var ns in documentation.Namespaces) { foreach (var type in ns.Types) { // Agregar elemento de tipo var typeMember = new XElement("member", new XAttribute("name", $"T:{type.FullName}"), new XElement("summary", type.Description) ); // Agregar información de base y interfaces si corresponde if (type.BaseTypes.Count > 0 || type.Interfaces.Count > 0) { var remarks = new XElement("remarks"); if (type.BaseTypes.Count > 0) { remarks.Add(new XText($"Inherits from {type.BaseTypes[0]}")); } if (type.Interfaces.Count > 0) { if (type.BaseTypes.Count > 0) remarks.Add(new XText("\n")); remarks.Add(new XText($"Implements: {string.Join(", ", type.Interfaces)}")); } typeMember.Add(remarks); } membersElement.Add(typeMember); // Agregar miembros foreach (var member in type.Members) { XElement memberElement; // Agregar con formato correcto según el tipo switch (member.MemberType) { case "Method": memberElement = CreateMethodElement(type, member); break; case "Property": memberElement = CreatePropertyElement(type, member); break; case "Event": memberElement = CreateEventElement(type, member); break; case "Field": memberElement = CreateFieldElement(type, member); break; default: continue; // Tipo no soportado } membersElement.Add(memberElement); } } } // Usar un StringWriter para convertir el XML a texto using (var stringWriter = new StringWriter()) { using (var writer = new XmlTextWriter(stringWriter)) { writer.Formatting = (System.Xml.Formatting)Formatting.Indented; writer.Indentation = 4; doc.Save(writer); } return stringWriter.ToString(); } } private XElement CreateMethodElement(TypeDocumentation type, MemberDocumentation method) { // Crear ID XML para el método string methodId; if (method.Parameters.Count > 0) { var paramTypes = string.Join(",", method.Parameters.Select(p => p.Type)); methodId = $"M:{type.FullName}.{method.Name}({paramTypes})"; } else { methodId = $"M:{type.FullName}.{method.Name}"; } var element = new XElement("member", new XAttribute("name", methodId), new XElement("summary", method.Description) ); // Agregar parámetros foreach (var param in method.Parameters) { element.Add(new XElement("param", new XAttribute("name", param.Name), new XText(param.Description ?? $"A {param.Type} parameter.") )); } // Agregar información de retorno si no es void if (!string.IsNullOrEmpty(method.ReturnType) && method.ReturnType != "void") { element.Add(new XElement("returns", method.ReturnDescription ?? $"A {method.ReturnType} value." )); } // Agregar ejemplos si hay foreach (var example in method.Examples) { element.Add(new XElement("example", example)); } return element; } private XElement CreatePropertyElement(TypeDocumentation type, MemberDocumentation property) { var element = new XElement("member", new XAttribute("name", $"P:{type.FullName}.{property.Name}"), new XElement("summary", property.Description) ); if (!string.IsNullOrEmpty(property.ReturnType)) { element.Add(new XElement("value", $"A {property.ReturnType} representing the property value." )); } return element; } private XElement CreateEventElement(TypeDocumentation type, MemberDocumentation eventMember) { var element = new XElement("member", new XAttribute("name", $"E:{type.FullName}.{eventMember.Name}"), new XElement("summary", eventMember.Description) ); return element; } private XElement CreateFieldElement(TypeDocumentation type, MemberDocumentation field) { var element = new XElement("member", new XAttribute("name", $"F:{type.FullName}.{field.Name}"), new XElement("summary", field.Description) ); return element; } private string GenerateYamlOutput(DocumentationModel documentation) { // Implementación simplificada de YAML (mantenemos la existente) // Deserialize JSON primero, luego a YAML var json = JsonConvert.SerializeObject(documentation, Formatting.None, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }); // Convierte a YAML usando una implementación simple var yaml = new System.Text.StringBuilder(); yaml.AppendLine("namespaces:"); var obj = JsonConvert.DeserializeObject(json); foreach (var ns in obj.Namespaces) { yaml.AppendLine($" - name: {ns.Name}"); yaml.AppendLine($" description: {ns.Description}"); yaml.AppendLine(" types:"); foreach (var type in ns.Types) { yaml.AppendLine($" - name: {type.Name}"); yaml.AppendLine($" fullName: {type.FullName}"); yaml.AppendLine($" typeKind: {type.TypeKind}"); yaml.AppendLine($" description: {EscapeYamlString(type.Description)}"); yaml.AppendLine(" members:"); foreach (var member in type.Members) { yaml.AppendLine($" - name: {member.Name}"); yaml.AppendLine($" memberType: {member.MemberType}"); yaml.AppendLine($" signature: \"{EscapeYamlString(member.Signature)}\""); yaml.AppendLine($" description: \"{EscapeYamlString(member.Description)}\""); if (member.Parameters != null && member.Parameters.Count > 0) { yaml.AppendLine(" parameters:"); foreach (var param in member.Parameters) { yaml.AppendLine($" - name: {param.Name}"); yaml.AppendLine($" type: {param.Type}"); yaml.AppendLine($" description: \"{EscapeYamlString(param.Description)}\""); yaml.AppendLine($" isOptional: {param.IsOptional.ToString().ToLower()}"); if (!string.IsNullOrEmpty(param.DefaultValue)) yaml.AppendLine($" defaultValue: \"{EscapeYamlString(param.DefaultValue)}\""); } } if (!string.IsNullOrEmpty(member.ReturnType)) { yaml.AppendLine($" returnType: {member.ReturnType}"); if (!string.IsNullOrEmpty(member.ReturnDescription)) yaml.AppendLine($" returnDescription: \"{EscapeYamlString(member.ReturnDescription)}\""); } if (member.Examples != null && member.Examples.Count > 0) { yaml.AppendLine(" examples:"); foreach (var example in member.Examples) { yaml.AppendLine($" - |\n {example.Replace("\n", "\n ")}"); } } } } } return yaml.ToString(); } private string EscapeYamlString(string input) { if (string.IsNullOrEmpty(input)) return ""; return input .Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\n", "\\n") .Replace("\r", "\\r") .Replace("\t", "\\t"); } } }