284 lines
11 KiB
C#
284 lines
11 KiB
C#
using SQLLinter.Common;
|
||
using SQLLinter.Infrastructure.Diagram;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
|
||
namespace SQLLinter.Infrastructure.Reporters;
|
||
|
||
public class HtmlReportFormatter : IReportFormatter
|
||
{
|
||
public string Format(List<IRuleViolation> violations)
|
||
=> Format(violations, null);
|
||
|
||
public string Format(List<IRuleViolation> violations, BpmnDiagram? diagram)
|
||
{
|
||
var sb = new StringBuilder();
|
||
|
||
GenerateBeginningHtml(sb);
|
||
|
||
if (violations.Count == 0 && diagram == null)
|
||
{
|
||
sb.AppendLine("<div class=\"no-violations\">");
|
||
sb.AppendLine("<div class=\"no-violations-content\">");
|
||
sb.AppendLine("<div class=\"no-violations-icon\">✅</div>");
|
||
sb.AppendLine("<h3 class=\"no-violations-title\">Проверка завершена</h3>");
|
||
sb.AppendLine("<p class=\"no-violations-description\">Нарушений правил SQL не обнаружено.</p>");
|
||
sb.AppendLine("</div>");
|
||
sb.AppendLine("</div>");
|
||
|
||
GenerateEndingHtml(sb, false);
|
||
return sb.ToString();
|
||
}
|
||
|
||
var groupedByFile = violations
|
||
.GroupBy(v => v.FileName)
|
||
.OrderBy(g => g.Key)
|
||
.ToList();
|
||
|
||
int fileIndex = 0;
|
||
|
||
// --- Отчёты по файлам ---
|
||
foreach (var fileGroup in groupedByFile)
|
||
{
|
||
string fileName = Path.GetFileName(fileGroup.Key);
|
||
|
||
sb.AppendLine($"""
|
||
<div id="file_{fileIndex}" class="file-report{(fileIndex == 0 ? " active" : "")}">
|
||
""");
|
||
|
||
// Заголовок файла - добавляем ПЕРЕД секциями с ошибками
|
||
sb.AppendLine($"""
|
||
<div class="file-title-container">
|
||
<div class="file-title">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
<span class="file-name">{fileName}</span>
|
||
</div>
|
||
</div>
|
||
""");
|
||
|
||
// Группируем по severity и выводим таблицы
|
||
var severityGroups = fileGroup
|
||
.GroupBy(v => v.Severity)
|
||
.OrderByDescending(g => g.Key)
|
||
.ToList();
|
||
|
||
foreach (var severityGroup in severityGroups)
|
||
{
|
||
string severityClass = severityGroup.Key switch
|
||
{
|
||
RuleViolationSeverity.Critical => "critical",
|
||
RuleViolationSeverity.Warning => "warning",
|
||
RuleViolationSeverity.Info => "info",
|
||
_ => ""
|
||
};
|
||
|
||
string severityTitle = severityGroup.Key switch
|
||
{
|
||
RuleViolationSeverity.Critical => "Critical",
|
||
RuleViolationSeverity.Warning => "Warning",
|
||
RuleViolationSeverity.Info => "Info",
|
||
_ => ""
|
||
};
|
||
|
||
sb.AppendLine($"""
|
||
<div class="severity-section {severityClass}">
|
||
<div class="severity-header">
|
||
<div class="severity-title">
|
||
<h3>{severityTitle}</h3>
|
||
<span class="severity-count">{severityGroup.Count()}</span>
|
||
</div>
|
||
</div>
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th class="index">#</th>
|
||
<th class="line">Строка</th>
|
||
<th class="column">Колонка</th>
|
||
<th class="rule">Правило</th>
|
||
<th class="description">Описание</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
""");
|
||
|
||
int row = 1;
|
||
foreach (var v in severityGroup
|
||
.OrderBy(x => x.Line)
|
||
.ThenBy(x => x.Column))
|
||
{
|
||
sb.AppendLine(
|
||
$"""
|
||
<tr>
|
||
<td class="index">{row}</td>
|
||
<td class="line">{v.Line}</td>
|
||
<td class="column">{v.Column}</td>
|
||
<td class="rule">{v.RuleName}</td>
|
||
<td class="description">{EscapeHtml(v.Text)}</td>
|
||
</tr>
|
||
""");
|
||
row++;
|
||
}
|
||
|
||
sb.AppendLine($"""
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
""");
|
||
}
|
||
|
||
sb.AppendLine("</div>");
|
||
fileIndex++;
|
||
}
|
||
|
||
// --- Вкладка с диаграммой ---
|
||
bool hasDiagram = diagram != null;
|
||
if (hasDiagram)
|
||
{
|
||
sb.AppendLine($"""
|
||
<div id="mermaid" class="file-report">
|
||
<div class="diagram-toolbar">
|
||
<div class="toolbar-search">
|
||
<input type="text" id="diagramSearch" placeholder="Поиск по узлам (нажмите Enter)" onkeypress="handleSearchKeyPress(event)" />
|
||
<button class="search-clear" type="button">✕</button>
|
||
</div>
|
||
<div class="toolbar-actions">
|
||
<button class="toolbar-button" type="button" onclick="exportPng()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
|
||
<path d="M21 15V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<path d="M7 10L12 15L17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<path d="M12 15V3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
Экспорт PNG
|
||
</button>
|
||
<button class="toolbar-button secondary" id="resetViewBtn">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
|
||
<path d="M3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z" stroke="currentColor" stroke-width="2"/>
|
||
<path d="M9 12L12 9L15 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<path d="M12 15V9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
Сбросить вид
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="diagramContainer">
|
||
<div id="diagramSvgContainer"></div>
|
||
<div id="minimap"></div>
|
||
</div>
|
||
|
||
<div id="mermaidSource" style="display:none">
|
||
{EscapeHtml(MermaidRenderer.ToMermaidContent(diagram!))}
|
||
</div>
|
||
</div>
|
||
""");
|
||
}
|
||
|
||
// --- Табы ---
|
||
if (violations.Count > 0 || hasDiagram)
|
||
{
|
||
sb.AppendLine("""
|
||
<div class="tabs-container">
|
||
<div class="tabs">
|
||
""");
|
||
|
||
// Табы для файлов
|
||
for (int i = 0; i < groupedByFile.Count; i++)
|
||
{
|
||
var fileGroup = groupedByFile[i];
|
||
string fileName = Path.GetFileName(fileGroup.Key);
|
||
string activeClass = i == 0 ? " active" : "";
|
||
string tabBadge = fileGroup.Count().ToString();
|
||
|
||
sb.AppendLine($"""
|
||
<div class="tab{activeClass}" data-target="file_{i}">
|
||
{fileName}
|
||
<span class="tab-badge">{tabBadge}</span>
|
||
</div>
|
||
""");
|
||
}
|
||
|
||
// Таб для диаграммы
|
||
if (hasDiagram)
|
||
{
|
||
sb.AppendLine("""
|
||
<div class="tab" data-target="mermaid">
|
||
Диаграмма
|
||
</div>
|
||
""");
|
||
}
|
||
|
||
sb.AppendLine("""
|
||
</div>
|
||
</div>
|
||
""");
|
||
}
|
||
|
||
GenerateEndingHtml(sb, hasDiagram);
|
||
return sb.ToString();
|
||
}
|
||
|
||
private void GenerateBeginningHtml(StringBuilder sb)
|
||
{
|
||
sb.AppendLine("""
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Отчёт по SQL‑проверкам</title>
|
||
<style>
|
||
""");
|
||
|
||
// Загружаем CSS из ресурсов и добавляем стили для заголовка файла
|
||
sb.AppendLine(LoadResource("HtmlFormatter.css"));
|
||
|
||
sb.AppendLine("""
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main id="main-content">
|
||
""");
|
||
}
|
||
|
||
private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram)
|
||
{
|
||
sb.AppendLine("""
|
||
</main>
|
||
""");
|
||
|
||
sb.AppendLine("""
|
||
<script type="module">
|
||
""");
|
||
|
||
// Загружаем JS из ресурсов
|
||
sb.AppendLine(LoadResource("HtmlFormatter.js"));
|
||
|
||
sb.AppendLine("""
|
||
</script>
|
||
</body>
|
||
</html>
|
||
""");
|
||
}
|
||
|
||
private static string LoadResource(string endsWith)
|
||
{
|
||
var assembly = Assembly.GetExecutingAssembly();
|
||
|
||
var name = assembly.GetManifestResourceNames()
|
||
.First(n => n.EndsWith(endsWith, StringComparison.OrdinalIgnoreCase));
|
||
|
||
using var stream = assembly.GetManifestResourceStream(name);
|
||
using var reader = new StreamReader(stream);
|
||
return reader.ReadToEnd();
|
||
}
|
||
|
||
private static string EscapeHtml(string text)
|
||
{
|
||
return System.Net.WebUtility.HtmlEncode(text);
|
||
}
|
||
} |