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 { 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); } } } }