667 lines
27 KiB
C#
667 lines
27 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
|
|
{
|
|
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<dynamic>(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");
|
|
}
|
|
|
|
}
|
|
} |