243 lines
11 KiB
C#
243 lines
11 KiB
C#
using SQLLinter.Common;
|
||
using SQLLinter.Infrastructure.Diagram;
|
||
using System.Text;
|
||
|
||
namespace SQLLinter.Infrastructure.Reporters;
|
||
|
||
public class HtmlReportFormatter : IReportFormatter
|
||
{
|
||
public string Format(List<IRuleViolation> violations)
|
||
{
|
||
if (violations.Count == 0)
|
||
{
|
||
return "<p><em>Нет нарушений</em></p>";
|
||
}
|
||
|
||
var groupedByFile = violations
|
||
.GroupBy(v => v.FileName)
|
||
.OrderBy(g => g.Key);
|
||
|
||
var sb = new StringBuilder();
|
||
GenerateBeginningHtml(sb);
|
||
|
||
int fileIndex = 0;
|
||
foreach (var fileGroup in groupedByFile)
|
||
{
|
||
string divId = $"file_{fileIndex}";
|
||
sb.AppendLine($"<div id=\"{divId}\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
||
sb.AppendLine($"<h1>Файл: {fileGroup.Key}</h1>");
|
||
|
||
var groupedBySeverity = fileGroup
|
||
.GroupBy(v => v.Severity)
|
||
.OrderByDescending(g => g.Key);
|
||
|
||
foreach (var severityGroup in groupedBySeverity)
|
||
{
|
||
string severityClass = severityGroup.Key switch
|
||
{
|
||
RuleViolationSeverity.Critical => "critical",
|
||
RuleViolationSeverity.Warning => "warning",
|
||
RuleViolationSeverity.Info => "info",
|
||
_ => ""
|
||
};
|
||
|
||
sb.AppendLine($"<div class=\"{severityClass}\">");
|
||
sb.AppendLine($"<h3>{severityGroup.Key}</h3>");
|
||
sb.AppendLine("<table>");
|
||
sb.AppendLine("<thead>");
|
||
sb.AppendLine("<tr><th>#</th><th>Строка</th><th>Колонка</th><th>Правило</th><th>Описание</th></tr>");
|
||
sb.AppendLine("</thead>");
|
||
sb.AppendLine("<tbody>");
|
||
|
||
int rowIndex = 1;
|
||
foreach (var v in severityGroup
|
||
.OrderBy(x => x.Line)
|
||
.ThenBy(x => x.Column))
|
||
{
|
||
sb.AppendLine($"<tr><td class=\"index\">{rowIndex}</td><td class=\"line\">{v.Line}</td><td class=\"column\">{v.Column}</td><td class=\"rule\">{v.RuleName}</td><td>{v.Text}</td></tr>");
|
||
rowIndex++;
|
||
}
|
||
|
||
sb.AppendLine("</tbody>");
|
||
sb.AppendLine("</table>");
|
||
sb.AppendLine("</div>");
|
||
}
|
||
|
||
sb.AppendLine("</div>");
|
||
fileIndex++;
|
||
}
|
||
|
||
// Табы снизу
|
||
sb.AppendLine("<div class=\"tabs\">");
|
||
fileIndex = 0;
|
||
foreach (var fileGroup in groupedByFile)
|
||
{
|
||
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('file_{fileIndex}', this)\">{fileGroup.Key}</div>");
|
||
fileIndex++;
|
||
}
|
||
sb.AppendLine("</div>");
|
||
|
||
GenerateEndingHtml(sb);
|
||
return sb.ToString();
|
||
}
|
||
|
||
public string Format(List<IRuleViolation> violations, BpmnDiagram diagram)
|
||
{
|
||
if (violations.Count == 0)
|
||
{
|
||
return "<p><em>Нет нарушений</em></p>";
|
||
}
|
||
|
||
var groupedByFile = violations
|
||
.GroupBy(v => v.FileName)
|
||
.OrderBy(g => g.Key);
|
||
|
||
var sb = new StringBuilder();
|
||
GenerateBeginningHtml(sb);
|
||
|
||
int fileIndex = 0;
|
||
foreach (var fileGroup in groupedByFile)
|
||
{
|
||
string divId = $"file_{fileIndex}";
|
||
sb.AppendLine($"<div id=\"{divId}\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
||
sb.AppendLine($"<h1>Файл: {fileGroup.Key}</h1>");
|
||
|
||
var groupedBySeverity = fileGroup
|
||
.GroupBy(v => v.Severity)
|
||
.OrderByDescending(g => g.Key);
|
||
|
||
foreach (var severityGroup in groupedBySeverity)
|
||
{
|
||
string severityClass = severityGroup.Key switch
|
||
{
|
||
RuleViolationSeverity.Critical => "critical",
|
||
RuleViolationSeverity.Warning => "warning",
|
||
RuleViolationSeverity.Info => "info",
|
||
_ => ""
|
||
};
|
||
|
||
sb.AppendLine($"<div class=\"{severityClass}\">");
|
||
sb.AppendLine($"<h3>{severityGroup.Key}</h3>");
|
||
sb.AppendLine("<table>");
|
||
sb.AppendLine("<thead>");
|
||
sb.AppendLine("<tr><th>#</th><th>Строка</th><th>Колонка</th><th>Правило</th><th>Описание</th></tr>");
|
||
sb.AppendLine("</thead>");
|
||
sb.AppendLine("<tbody>");
|
||
|
||
int rowIndex = 1;
|
||
foreach (var v in severityGroup
|
||
.OrderBy(x => x.Line)
|
||
.ThenBy(x => x.Column))
|
||
{
|
||
sb.AppendLine($"<tr><td class=\"index\">{rowIndex}</td><td class=\"line\">{v.Line}</td><td class=\"column\">{v.Column}</td><td class=\"rule\">{v.RuleName}</td><td>{v.Text}</td></tr>");
|
||
rowIndex++;
|
||
}
|
||
|
||
sb.AppendLine("</tbody>");
|
||
sb.AppendLine("</table>");
|
||
sb.AppendLine("</div>");
|
||
}
|
||
|
||
sb.AppendLine("</div>");
|
||
fileIndex++;
|
||
}
|
||
|
||
//mermaid диаграмма
|
||
sb.AppendLine($"<div id=\"mermaid\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
||
sb.AppendLine($"<h1>Диаграмма</h1>");
|
||
|
||
var mermaid = MermaidRenderer.RenderHtml(diagram);
|
||
sb.AppendLine(mermaid);
|
||
sb.AppendLine("</div>");
|
||
|
||
// Табы снизу
|
||
sb.AppendLine("<div class=\"tabs\">");
|
||
fileIndex = 0;
|
||
foreach (var fileGroup in groupedByFile)
|
||
{
|
||
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('file_{fileIndex}', this)\">{fileGroup.Key}</div>");
|
||
fileIndex++;
|
||
}
|
||
|
||
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('mermaid', this)\">Диграмма</div>");
|
||
sb.AppendLine("</div>");
|
||
|
||
GenerateEndingHtml(sb);
|
||
return sb.ToString();
|
||
}
|
||
|
||
private void GenerateBeginningHtml(StringBuilder sb)
|
||
{
|
||
sb.AppendLine("<!DOCTYPE html>");
|
||
sb.AppendLine("<html lang=\"ru\">");
|
||
sb.AppendLine("<head>");
|
||
sb.AppendLine("<meta charset=\"UTF-8\">");
|
||
sb.AppendLine("<title>Отчёт по SQL‑проверкам</title>");
|
||
sb.AppendLine("<style>");
|
||
sb.AppendLine("body { font-family: 'Segoe UI', Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; color: #000; }");
|
||
sb.AppendLine("h1 { padding: 20px; margin: 0; background-color: #0078d4; color: white; }");
|
||
sb.AppendLine("h2 { margin-top: 20px; color: #333; }");
|
||
sb.AppendLine("h3 { margin-top: 15px; color: #555; }");
|
||
sb.AppendLine("table { border-collapse: collapse; width: 100%; margin: 20px 0; box-shadow: 0 2px 6px rgba(0,0,0,0.1); background-color: white; border-radius: 4px; overflow: hidden; }");
|
||
sb.AppendLine("th, td { border: 1px solid #e0e0e0; padding: 4px 8px; text-align: left; line-height: 1.2; }");
|
||
sb.AppendLine("th { background-color: #fafafa; font-weight: 600; }");
|
||
sb.AppendLine("td.line, td.column { width: 60px; text-align: center; }");
|
||
sb.AppendLine("td.rule { width: 300px; text-align: left; }");
|
||
sb.AppendLine("td.index { width: 40px; text-align: center; }");
|
||
sb.AppendLine(".critical { border-left: 4px solid #d13438; padding: 10px; margin-bottom: 20px; background-color: #fde7e9; }");
|
||
sb.AppendLine(".warning { border-left: 4px solid #ffaa44; padding: 10px; margin-bottom: 20px; background-color: #fff4ce; }");
|
||
sb.AppendLine(".info { border-left: 4px solid #0078d4; padding: 10px; margin-bottom: 20px; background-color: #deecf9; }");
|
||
sb.AppendLine(".tabs { position: fixed; bottom: 0; left: 0; right: 0; background-color: #ffffff; border-top: 1px solid #ccc; padding: 10px; display: flex; overflow-x: auto; scrollbar-width: thin; justify-content: flex-start; box-shadow: 0 -2px 6px rgba(0,0,0,0.1); }");
|
||
sb.AppendLine(".tab { margin-right: 10px; padding: 8px 16px; border-radius: 4px; background-color: #f3f2f1; cursor: pointer; transition: background-color 0.2s; }");
|
||
sb.AppendLine(".tab:hover { background-color: #e1dfdd; }");
|
||
sb.AppendLine(".tab.active { background-color: #0078d4; color: white; }");
|
||
sb.AppendLine(".file-report { display: none; padding: 20px 0; }");
|
||
sb.AppendLine(".file-report.active { display: block; }");
|
||
|
||
// Тёмная тема
|
||
sb.AppendLine("@media (prefers-color-scheme: dark) {");
|
||
sb.AppendLine(" body { background-color: #1e1e1e; color: #ddd; }");
|
||
sb.AppendLine(" h1 { background-color: #005a9e; }");
|
||
sb.AppendLine(" h2, h3 { color: #ddd; }");
|
||
sb.AppendLine(" table { background-color: #2d2d2d; box-shadow: none; }");
|
||
sb.AppendLine(" th { background-color: #3c3c3c; color: #fff; }");
|
||
sb.AppendLine(" td { border: 1px solid #444; }");
|
||
sb.AppendLine(" .tabs { background-color: #2d2d2d; border-top: 1px solid #444; }");
|
||
sb.AppendLine(" .tab { background-color: #3c3c3c; color: #ddd; }");
|
||
sb.AppendLine(" .tab:hover { background-color: #555; }");
|
||
sb.AppendLine(" .tab.active { background-color: #0078d4; color: white; }");
|
||
sb.AppendLine(" .critical { background-color: #4d1f1f; border-left-color: #d13438; }");
|
||
sb.AppendLine(" .warning { background-color: #4d3b1f; border-left-color: #ffaa44; }");
|
||
sb.AppendLine(" .info { background-color: #1f3b4d; border-left-color: #0078d4; }");
|
||
sb.AppendLine("}");
|
||
sb.AppendLine("</style>");
|
||
sb.AppendLine("<script src=\"https://unpkg.com/mermaid@10/dist/mermaid.min.js\"></script>");
|
||
sb.AppendLine("<script>mermaid.initialize({ startOnLoad: true, securityLevel: 'loose' });</script>");
|
||
sb.AppendLine("</head>");
|
||
sb.AppendLine("<body>");
|
||
//sb.AppendLine("<h1>Отчёт по SQL‑проверкам</h1>");
|
||
}
|
||
|
||
private void GenerateEndingHtml(StringBuilder sb)
|
||
{
|
||
|
||
// JS для переключения
|
||
sb.AppendLine("<script>");
|
||
sb.AppendLine("function showReport(id, tab) {");
|
||
sb.AppendLine(" document.querySelectorAll('.file-report').forEach(el => el.classList.remove('active'));");
|
||
sb.AppendLine(" document.getElementById(id).classList.add('active');");
|
||
sb.AppendLine(" document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));");
|
||
sb.AppendLine(" tab.classList.add('active');");
|
||
sb.AppendLine(" window.scrollTo({ top: 0, behavior: 'smooth' });");
|
||
sb.AppendLine("}");
|
||
sb.AppendLine("document.querySelector('.tabs').addEventListener('wheel', function(e) {");
|
||
sb.AppendLine(" e.preventDefault();");
|
||
sb.AppendLine(" this.scrollLeft += e.deltaY;");
|
||
sb.AppendLine("});");
|
||
sb.AppendLine("</script>");
|
||
|
||
sb.AppendLine("</body>");
|
||
sb.AppendLine("</html>");
|
||
}
|
||
}
|