479 lines
18 KiB
C#
479 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using DocumentFormat.OpenXml;
|
|
using DocumentFormat.OpenXml.Packaging;
|
|
using DocumentFormat.OpenXml.Wordprocessing;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
public class DocumentAssemblerTool : IAgentTool
|
|
{
|
|
public string Name => "document_assemble";
|
|
|
|
public string Description => "Assemble multiple individually-written sections into a single complete document. Use this after writing each section separately with document_plan. Supports HTML, DOCX, and Markdown output. Automatically adds table of contents, cover page, and section numbering for HTML. After assembly, the document is auto-validated for quality issues.";
|
|
|
|
public ToolParameterSchema Parameters
|
|
{
|
|
get
|
|
{
|
|
ToolParameterSchema toolParameterSchema = new ToolParameterSchema();
|
|
Dictionary<string, ToolProperty> obj = new Dictionary<string, ToolProperty>
|
|
{
|
|
["path"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Output file path. Relative to work folder."
|
|
},
|
|
["title"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Document title"
|
|
},
|
|
["sections"] = new ToolProperty
|
|
{
|
|
Type = "array",
|
|
Description = "Array of section objects: [{\"heading\": \"1. 개요\", \"content\": \"HTML or markdown body...\", \"level\": 1}]. content should be the detailed text for each section.",
|
|
Items = new ToolProperty
|
|
{
|
|
Type = "object"
|
|
}
|
|
}
|
|
};
|
|
ToolProperty obj2 = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Output format: html, docx, markdown. Default: html"
|
|
};
|
|
int num = 3;
|
|
List<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = "html";
|
|
span[1] = "docx";
|
|
span[2] = "markdown";
|
|
obj2.Enum = list;
|
|
obj["format"] = obj2;
|
|
obj["mood"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Design theme for HTML output: modern, professional, creative, corporate, dashboard, etc. Default: professional"
|
|
};
|
|
obj["toc"] = new ToolProperty
|
|
{
|
|
Type = "boolean",
|
|
Description = "Auto-generate table of contents. Default: true"
|
|
};
|
|
obj["cover_subtitle"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Subtitle for cover page. If provided, a cover page is added."
|
|
};
|
|
obj["header"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Header text for DOCX output."
|
|
};
|
|
obj["footer"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Footer text for DOCX output. Use {page} for page number."
|
|
};
|
|
toolParameterSchema.Properties = obj;
|
|
num = 3;
|
|
List<string> list2 = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list2, num);
|
|
Span<string> span2 = CollectionsMarshal.AsSpan(list2);
|
|
span2[0] = "path";
|
|
span2[1] = "title";
|
|
span2[2] = "sections";
|
|
toolParameterSchema.Required = list2;
|
|
return toolParameterSchema;
|
|
}
|
|
}
|
|
|
|
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
|
|
{
|
|
string path = args.GetProperty("path").GetString() ?? "";
|
|
string title = args.GetProperty("title").GetString() ?? "Document";
|
|
JsonElement fmt;
|
|
string format = (args.TryGetProperty("format", out fmt) ? (fmt.GetString() ?? "html") : "html");
|
|
JsonElement m;
|
|
string mood = (args.TryGetProperty("mood", out m) ? (m.GetString() ?? "professional") : "professional");
|
|
JsonElement tocVal;
|
|
bool useToc = !args.TryGetProperty("toc", out tocVal) || tocVal.GetBoolean();
|
|
JsonElement cs;
|
|
string coverSubtitle = (args.TryGetProperty("cover_subtitle", out cs) ? cs.GetString() : null);
|
|
JsonElement hdr;
|
|
string headerText = (args.TryGetProperty("header", out hdr) ? hdr.GetString() : null);
|
|
JsonElement ftr;
|
|
string footerText = (args.TryGetProperty("footer", out ftr) ? ftr.GetString() : null);
|
|
if (!args.TryGetProperty("sections", out var sectionsEl) || sectionsEl.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return ToolResult.Fail("sections 배열이 필요합니다.");
|
|
}
|
|
List<(string Heading, string Content, int Level)> sections = new List<(string, string, int)>();
|
|
foreach (JsonElement sec in sectionsEl.EnumerateArray())
|
|
{
|
|
JsonElement h;
|
|
string heading = (sec.TryGetProperty("heading", out h) ? (h.GetString() ?? "") : "");
|
|
JsonElement c;
|
|
string content = (sec.TryGetProperty("content", out c) ? (c.GetString() ?? "") : "");
|
|
JsonElement lv;
|
|
int level = ((!sec.TryGetProperty("level", out lv)) ? 1 : lv.GetInt32());
|
|
if (!string.IsNullOrWhiteSpace(heading) || !string.IsNullOrWhiteSpace(content))
|
|
{
|
|
sections.Add((heading, content, level));
|
|
}
|
|
h = default(JsonElement);
|
|
c = default(JsonElement);
|
|
lv = default(JsonElement);
|
|
}
|
|
if (sections.Count == 0)
|
|
{
|
|
return ToolResult.Fail("조립할 섹션이 없습니다.");
|
|
}
|
|
string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
|
|
if (context.ActiveTab == "Cowork")
|
|
{
|
|
fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
|
}
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
string text = format;
|
|
string text2 = ((text == "docx") ? ".docx" : ((!(text == "markdown")) ? ".html" : ".md"));
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
string ext = text2;
|
|
if (!fullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
fullPath += ext;
|
|
}
|
|
if (!context.IsPathAllowed(fullPath))
|
|
{
|
|
return ToolResult.Fail("경로 접근 차단: " + fullPath);
|
|
}
|
|
if (!(await context.CheckWritePermissionAsync(Name, fullPath)))
|
|
{
|
|
return ToolResult.Fail("쓰기 권한 거부: " + fullPath);
|
|
}
|
|
string dir = Path.GetDirectoryName(fullPath);
|
|
if (!string.IsNullOrEmpty(dir))
|
|
{
|
|
Directory.CreateDirectory(dir);
|
|
}
|
|
try
|
|
{
|
|
string text3 = format;
|
|
text2 = text3;
|
|
string resultMsg = ((text2 == "docx") ? AssembleDocx(fullPath, title, sections, headerText, footerText) : ((!(text2 == "markdown")) ? AssembleHtml(fullPath, title, sections, mood, useToc, coverSubtitle) : AssembleMarkdown(fullPath, title, sections)));
|
|
int totalChars = sections.Sum<(string, string, int)>(((string Heading, string Content, int Level) s) => s.Content.Length);
|
|
int totalWords = sections.Sum<(string, string, int)>(((string Heading, string Content, int Level) s) => EstimateWordCount(s.Content));
|
|
int pageEstimate = Math.Max(1, totalWords / 500);
|
|
return ToolResult.Ok($"✅ 문서 조립 완료: {Path.GetFileName(fullPath)}\n 섹션: {sections.Count}개 | 글자: {totalChars:N0} | 단어: ~{totalWords:N0} | 예상 페이지: ~{pageEstimate}\n{resultMsg}", fullPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail("문서 조립 실패: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private string AssembleHtml(string path, string title, List<(string Heading, string Content, int Level)> sections, string mood, bool toc, string? coverSubtitle)
|
|
{
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
string css = TemplateService.GetCss(mood);
|
|
TemplateMood mood2 = TemplateService.GetMood(mood);
|
|
stringBuilder.AppendLine("<!DOCTYPE html>");
|
|
stringBuilder.AppendLine("<html lang=\"ko\">");
|
|
stringBuilder.AppendLine("<head>");
|
|
stringBuilder.AppendLine("<meta charset=\"utf-8\">");
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder2);
|
|
handler.AppendLiteral("<title>");
|
|
handler.AppendFormatted(Escape(title));
|
|
handler.AppendLiteral("</title>");
|
|
stringBuilder3.AppendLine(ref handler);
|
|
stringBuilder.AppendLine("<style>");
|
|
stringBuilder.AppendLine(css);
|
|
stringBuilder.AppendLine("\n.assembled-doc { max-width: 900px; margin: 0 auto; padding: 40px 30px 60px; }\n.assembled-doc h1 { font-size: 28px; margin-bottom: 8px; }\n.assembled-doc h2 { font-size: 22px; margin-top: 36px; margin-bottom: 12px; border-bottom: 2px solid var(--accent, #4B5EFC); padding-bottom: 6px; }\n.assembled-doc h3 { font-size: 18px; margin-top: 24px; margin-bottom: 8px; }\n.assembled-doc .section-content { line-height: 1.8; margin-bottom: 20px; }\n.assembled-doc .toc { background: #f8f9fa; border-radius: 12px; padding: 20px 28px; margin: 24px 0 32px; }\n.assembled-doc .toc h3 { margin-top: 0; }\n.assembled-doc .toc a { color: inherit; text-decoration: none; }\n.assembled-doc .toc a:hover { text-decoration: underline; }\n.assembled-doc .toc ul { list-style: none; padding-left: 0; }\n.assembled-doc .toc li { padding: 4px 0; }\n.cover-page { text-align: center; padding: 120px 40px 80px; page-break-after: always; }\n.cover-page h1 { font-size: 36px; margin-bottom: 16px; }\n.cover-page .subtitle { font-size: 18px; color: #666; margin-bottom: 40px; }\n.cover-page .date { font-size: 14px; color: #999; }\n@media print { .cover-page { page-break-after: always; } .assembled-doc h2 { page-break-before: auto; } }\n");
|
|
stringBuilder.AppendLine("</style>");
|
|
stringBuilder.AppendLine("</head>");
|
|
stringBuilder.AppendLine("<body>");
|
|
if (!string.IsNullOrWhiteSpace(coverSubtitle))
|
|
{
|
|
stringBuilder.AppendLine("<div class=\"cover-page\">");
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2);
|
|
handler.AppendLiteral("<h1>");
|
|
handler.AppendFormatted(Escape(title));
|
|
handler.AppendLiteral("</h1>");
|
|
stringBuilder4.AppendLine(ref handler);
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder5 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(28, 1, stringBuilder2);
|
|
handler.AppendLiteral("<div class=\"subtitle\">");
|
|
handler.AppendFormatted(Escape(coverSubtitle));
|
|
handler.AppendLiteral("</div>");
|
|
stringBuilder5.AppendLine(ref handler);
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder6 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(24, 1, stringBuilder2);
|
|
handler.AppendLiteral("<div class=\"date\">");
|
|
handler.AppendFormatted(DateTime.Now, "yyyy년 MM월 dd일");
|
|
handler.AppendLiteral("</div>");
|
|
stringBuilder6.AppendLine(ref handler);
|
|
stringBuilder.AppendLine("</div>");
|
|
}
|
|
stringBuilder.AppendLine("<div class=\"assembled-doc\">");
|
|
if (string.IsNullOrWhiteSpace(coverSubtitle))
|
|
{
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder7 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2);
|
|
handler.AppendLiteral("<h1>");
|
|
handler.AppendFormatted(Escape(title));
|
|
handler.AppendLiteral("</h1>");
|
|
stringBuilder7.AppendLine(ref handler);
|
|
}
|
|
if (toc && sections.Count > 1)
|
|
{
|
|
stringBuilder.AppendLine("<div class=\"toc\">");
|
|
stringBuilder.AppendLine("<h3>\ud83d\udccb 목차</h3>");
|
|
stringBuilder.AppendLine("<ul>");
|
|
for (int i = 0; i < sections.Count; i++)
|
|
{
|
|
string value = ((sections[i].Level > 1) ? " style=\"padding-left:20px\"" : "");
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder8 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(33, 3, stringBuilder2);
|
|
handler.AppendLiteral("<li");
|
|
handler.AppendFormatted(value);
|
|
handler.AppendLiteral("><a href=\"#section-");
|
|
handler.AppendFormatted(i + 1);
|
|
handler.AppendLiteral("\">");
|
|
handler.AppendFormatted(Escape(sections[i].Heading));
|
|
handler.AppendLiteral("</a></li>");
|
|
stringBuilder8.AppendLine(ref handler);
|
|
}
|
|
stringBuilder.AppendLine("</ul>");
|
|
stringBuilder.AppendLine("</div>");
|
|
}
|
|
for (int j = 0; j < sections.Count; j++)
|
|
{
|
|
(string Heading, string Content, int Level) tuple = sections[j];
|
|
string item = tuple.Heading;
|
|
string item2 = tuple.Content;
|
|
int item3 = tuple.Level;
|
|
string value2 = ((item3 <= 1) ? "h2" : "h3");
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder9 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(19, 4, stringBuilder2);
|
|
handler.AppendLiteral("<");
|
|
handler.AppendFormatted(value2);
|
|
handler.AppendLiteral(" id=\"section-");
|
|
handler.AppendFormatted(j + 1);
|
|
handler.AppendLiteral("\">");
|
|
handler.AppendFormatted(Escape(item));
|
|
handler.AppendLiteral("</");
|
|
handler.AppendFormatted(value2);
|
|
handler.AppendLiteral(">");
|
|
stringBuilder9.AppendLine(ref handler);
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder10 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(35, 1, stringBuilder2);
|
|
handler.AppendLiteral("<div class=\"section-content\">");
|
|
handler.AppendFormatted(item2);
|
|
handler.AppendLiteral("</div>");
|
|
stringBuilder10.AppendLine(ref handler);
|
|
}
|
|
stringBuilder.AppendLine("</div>");
|
|
stringBuilder.AppendLine("</body>");
|
|
stringBuilder.AppendLine("</html>");
|
|
File.WriteAllText(path, stringBuilder.ToString(), Encoding.UTF8);
|
|
List<string> list = ValidateBasic(stringBuilder.ToString());
|
|
return (list.Count > 0) ? $" ⚠ 품질 검증 이슈 {list.Count}건: {string.Join("; ", list)}" : " ✓ 품질 검증 통과";
|
|
}
|
|
|
|
private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections, string? headerText, string? footerText)
|
|
{
|
|
using WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Create(path, WordprocessingDocumentType.Document);
|
|
MainDocumentPart mainDocumentPart = wordprocessingDocument.AddMainDocumentPart();
|
|
mainDocumentPart.Document = new Document();
|
|
Body body = mainDocumentPart.Document.AppendChild(new Body());
|
|
Paragraph paragraph = new Paragraph();
|
|
Run run = new Run();
|
|
run.AppendChild(new RunProperties
|
|
{
|
|
Bold = new Bold(),
|
|
FontSize = new FontSize
|
|
{
|
|
Val = "48"
|
|
}
|
|
});
|
|
run.AppendChild(new Text(title));
|
|
paragraph.AppendChild(run);
|
|
body.AppendChild(paragraph);
|
|
body.AppendChild(new Paragraph());
|
|
foreach (var section in sections)
|
|
{
|
|
string item = section.Heading;
|
|
string item2 = section.Content;
|
|
int item3 = section.Level;
|
|
Paragraph paragraph2 = new Paragraph();
|
|
Run run2 = new Run();
|
|
run2.AppendChild(new RunProperties
|
|
{
|
|
Bold = new Bold(),
|
|
FontSize = new FontSize
|
|
{
|
|
Val = ((item3 <= 1) ? "32" : "28")
|
|
},
|
|
Color = new Color
|
|
{
|
|
Val = "2B579A"
|
|
}
|
|
});
|
|
run2.AppendChild(new Text(item));
|
|
paragraph2.AppendChild(run2);
|
|
body.AppendChild(paragraph2);
|
|
string[] array = StripHtmlTags(item2).Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
|
string[] array2 = array;
|
|
foreach (string text in array2)
|
|
{
|
|
Paragraph paragraph3 = new Paragraph();
|
|
Run run3 = new Run();
|
|
run3.AppendChild(new Text(text.Trim())
|
|
{
|
|
Space = SpaceProcessingModeValues.Preserve
|
|
});
|
|
paragraph3.AppendChild(run3);
|
|
body.AppendChild(paragraph3);
|
|
}
|
|
body.AppendChild(new Paragraph());
|
|
}
|
|
return " ✓ DOCX 조립 완료";
|
|
}
|
|
|
|
private string AssembleMarkdown(string path, string title, List<(string Heading, string Content, int Level)> sections)
|
|
{
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2);
|
|
handler.AppendLiteral("# ");
|
|
handler.AppendFormatted(title);
|
|
stringBuilder3.AppendLine(ref handler);
|
|
stringBuilder.AppendLine();
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder2);
|
|
handler.AppendLiteral("*작성일: ");
|
|
handler.AppendFormatted(DateTime.Now, "yyyy-MM-dd");
|
|
handler.AppendLiteral("*");
|
|
stringBuilder4.AppendLine(ref handler);
|
|
stringBuilder.AppendLine();
|
|
if (sections.Count > 1)
|
|
{
|
|
stringBuilder.AppendLine("## 목차");
|
|
stringBuilder.AppendLine();
|
|
foreach (var section in sections)
|
|
{
|
|
string item = section.Heading;
|
|
string value = item.Replace(" ", "-").ToLowerInvariant();
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder5 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(7, 2, stringBuilder2);
|
|
handler.AppendLiteral("- [");
|
|
handler.AppendFormatted(item);
|
|
handler.AppendLiteral("](#");
|
|
handler.AppendFormatted(value);
|
|
handler.AppendLiteral(")");
|
|
stringBuilder5.AppendLine(ref handler);
|
|
}
|
|
stringBuilder.AppendLine();
|
|
stringBuilder.AppendLine("---");
|
|
stringBuilder.AppendLine();
|
|
}
|
|
foreach (var section2 in sections)
|
|
{
|
|
string item2 = section2.Heading;
|
|
string item3 = section2.Content;
|
|
int item4 = section2.Level;
|
|
string value2 = ((item4 <= 1) ? "##" : "###");
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder6 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(1, 2, stringBuilder2);
|
|
handler.AppendFormatted(value2);
|
|
handler.AppendLiteral(" ");
|
|
handler.AppendFormatted(item2);
|
|
stringBuilder6.AppendLine(ref handler);
|
|
stringBuilder.AppendLine();
|
|
stringBuilder.AppendLine(StripHtmlTags(item3));
|
|
stringBuilder.AppendLine();
|
|
}
|
|
File.WriteAllText(path, stringBuilder.ToString(), Encoding.UTF8);
|
|
return " ✓ Markdown 조립 완료";
|
|
}
|
|
|
|
private static int EstimateWordCount(string text)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return 0;
|
|
}
|
|
string source = StripHtmlTags(text);
|
|
int num = source.Count((char c) => c == ' ');
|
|
int num2 = source.Count((char c) => c >= '가' && c <= '힣');
|
|
return num + 1 + num2 / 3;
|
|
}
|
|
|
|
private static string StripHtmlTags(string html)
|
|
{
|
|
if (string.IsNullOrEmpty(html))
|
|
{
|
|
return "";
|
|
}
|
|
return Regex.Replace(html, "<[^>]+>", " ").Replace(" ", " ").Replace("&", "&")
|
|
.Replace("<", "<")
|
|
.Replace(">", ">")
|
|
.Replace(" ", " ")
|
|
.Trim();
|
|
}
|
|
|
|
private static List<string> ValidateBasic(string html)
|
|
{
|
|
List<string> list = new List<string>();
|
|
if (html.Length < 500)
|
|
{
|
|
list.Add("문서 내용이 매우 짧습니다 (500자 미만)");
|
|
}
|
|
Regex regex = new Regex("<h[23][^>]*>[^<]+</h[23]>\\s*<div class=\"section-content\">\\s*</div>", RegexOptions.IgnoreCase);
|
|
MatchCollection matchCollection = regex.Matches(html);
|
|
if (matchCollection.Count > 0)
|
|
{
|
|
list.Add($"빈 섹션 {matchCollection.Count}개 발견");
|
|
}
|
|
if (html.Contains("[TODO]", StringComparison.OrdinalIgnoreCase) || html.Contains("[PLACEHOLDER]", StringComparison.OrdinalIgnoreCase) || html.Contains("Lorem ipsum", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
list.Add("플레이스홀더 텍스트가 남아있습니다");
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static string Escape(string text)
|
|
{
|
|
return text.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
|
}
|
|
}
|