282 lines
9.0 KiB
C#
282 lines
9.0 KiB
C#
using SQLLinter.Common;
|
||
using SQLLinter.Infrastructure.Diagram;
|
||
using SQLLinter.Infrastructure.Rules.RuleViolations;
|
||
using System.Data;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
|
||
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);
|
||
|
||
// Подготовка данных для передачи в JS
|
||
var reportData = PrepareReportData(violations, diagram);
|
||
var jsonData = JsonSerializer.Serialize(reportData, new JsonSerializerOptions
|
||
{
|
||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||
});
|
||
|
||
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, HtmlMinifier.CompressJson(jsonData));
|
||
return sb.ToString();
|
||
}
|
||
|
||
// Основной контейнер для отчета
|
||
sb.AppendLine("""
|
||
<div id="reports-container"></div>
|
||
<div id="tabs-container" class="tabs-container">
|
||
<div class="tabs" id="tabs-list"></div>
|
||
</div>
|
||
""");
|
||
|
||
GenerateEndingHtml(sb, diagram != null, jsonData);
|
||
|
||
var html = HtmlMinifier.MinifyHtml(sb.ToString());
|
||
return html;
|
||
}
|
||
|
||
private ReportData PrepareReportData(List<IRuleViolation> violations, BpmnDiagram? diagram)
|
||
{
|
||
var reportData = new ReportData();
|
||
|
||
// Группировка по файлам
|
||
var groupedByFile = violations
|
||
.GroupBy(v => v.FileName)
|
||
.OrderBy(g => g.Key)
|
||
.ToList();
|
||
|
||
foreach (var fileGroup in groupedByFile)
|
||
{
|
||
var fileData = new FileReport
|
||
{
|
||
Name = fileGroup.Key,
|
||
};
|
||
|
||
|
||
// Группировка по severity
|
||
var severityGroups = fileGroup
|
||
.GroupBy(v => v.Severity)
|
||
.OrderByDescending(g => g.Key)
|
||
.ToList();
|
||
|
||
foreach (var violation in fileGroup.Select(t => t).OrderBy(v => v.Line).ThenBy(v => v.Column))
|
||
{
|
||
int ruleId;
|
||
List<string> args = new();
|
||
|
||
if (violation is RuleTemplateViolation templateRule)
|
||
{
|
||
args = templateRule.Params.Select(p => EscapeHtml(p)).ToList();
|
||
|
||
if (reportData.Rules.Any(t => t.Value.Name == templateRule.RuleName))
|
||
{
|
||
ruleId = reportData.Rules.First(t => t.Value.Name == templateRule.RuleName).Key;
|
||
}
|
||
else
|
||
{
|
||
ruleId = reportData.Rules.Count + 1;
|
||
reportData.Rules.Add(ruleId, new Rule
|
||
{
|
||
Name = templateRule.RuleName,
|
||
Template = templateRule.RuleTemplate
|
||
});
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ruleId = reportData.Rules.Count + 1;
|
||
reportData.Rules.Add(ruleId, new Rule
|
||
{
|
||
Name = violation.RuleName,
|
||
Template = violation.Text,
|
||
});
|
||
}
|
||
|
||
var v = new Violation()
|
||
{
|
||
RuleId = ruleId,
|
||
Args = args,
|
||
Column = violation.Column,
|
||
Line = violation.Line,
|
||
};
|
||
|
||
if (violation.Severity == RuleViolationSeverity.Critical)
|
||
{
|
||
v.Index = fileData.Violations.Critical.Count + 1;
|
||
fileData.Violations.Critical.Add(v);
|
||
}
|
||
|
||
else if (violation.Severity == RuleViolationSeverity.Warning)
|
||
{
|
||
v.Index = fileData.Violations.Warning.Count + 1;
|
||
fileData.Violations.Warning.Add(v);
|
||
}
|
||
|
||
else if (violation.Severity == RuleViolationSeverity.Info)
|
||
{
|
||
v.Index = fileData.Violations.Info.Count + 1;
|
||
fileData.Violations.Info.Add(v);
|
||
}
|
||
}
|
||
|
||
reportData.Files.Add(fileData);
|
||
}
|
||
|
||
// Добавление диаграммы, если есть
|
||
if (diagram != null)
|
||
{
|
||
reportData.Diagram = new Diagram
|
||
{
|
||
Content = MermaidRenderer.ToMermaidContent(diagram),
|
||
HasDiagram = true
|
||
};
|
||
}
|
||
|
||
return reportData;
|
||
}
|
||
|
||
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>
|
||
""");
|
||
|
||
sb.AppendLine(LoadResource("HtmlFormatter.css"));
|
||
sb.AppendLine("</style></head><body><main id=\"main-content\">");
|
||
}
|
||
|
||
private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram, string jsonData)
|
||
{
|
||
// Вставка JSON данных
|
||
sb.AppendLine($"""
|
||
<script id="report-data" type="application/json">
|
||
{jsonData}
|
||
</script>
|
||
""");
|
||
|
||
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);
|
||
}
|
||
|
||
// Классы для сериализации
|
||
private class ReportData
|
||
{
|
||
[JsonPropertyName("f")] // files
|
||
public List<FileReport> Files { get; set; } = new();
|
||
|
||
[JsonPropertyName("r")] // rules
|
||
public Dictionary<int, Rule> Rules { get; set; } = new();
|
||
|
||
[JsonPropertyName("d")] // diagram
|
||
public Diagram Diagram { get; set; } = new();
|
||
}
|
||
|
||
private class FileReport
|
||
{
|
||
[JsonPropertyName("n")] // name
|
||
public string Name { get; set; } = string.Empty;
|
||
|
||
[JsonPropertyName("v")] // violations
|
||
public Violations Violations { get; set; } = new();
|
||
}
|
||
|
||
private class Violations
|
||
{
|
||
[JsonPropertyName("c")] // critical
|
||
public List<Violation> Critical { get; set; } = new();
|
||
|
||
[JsonPropertyName("w")] // warning
|
||
public List<Violation> Warning { get; set; } = new();
|
||
|
||
[JsonPropertyName("i")] // info
|
||
public List<Violation> Info { get; set; } = new();
|
||
}
|
||
|
||
private class Violation
|
||
{
|
||
[JsonPropertyName("i")] // index
|
||
public int Index { get; set; }
|
||
|
||
[JsonPropertyName("l")] // line
|
||
public int Line { get; set; }
|
||
|
||
[JsonPropertyName("c")] // column
|
||
public int Column { get; set; }
|
||
|
||
[JsonPropertyName("r")] // ruleId
|
||
public int RuleId { get; set; }
|
||
|
||
[JsonPropertyName("a")] // args (optional)
|
||
public List<string>? Args { get; set; }
|
||
}
|
||
|
||
private class Rule
|
||
{
|
||
[JsonPropertyName("n")] // name
|
||
public string Name { get; set; } = string.Empty;
|
||
|
||
[JsonPropertyName("t")] // template
|
||
public string Template { get; set; } = string.Empty;
|
||
}
|
||
|
||
private class Diagram
|
||
{
|
||
[JsonPropertyName("c")] // content
|
||
public string Content { get; set; } = string.Empty;
|
||
|
||
[JsonPropertyName("h")] // hasDiagram
|
||
public bool HasDiagram { get; set; }
|
||
}
|
||
} |