Files
SQLLint/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter.cs
2025-12-26 02:16:51 +03:00

284 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}