Добавлена минификация html
This commit is contained in:
325
SQLLinter/Infrastructure/Reporters/HtmlMinifier.cs
Normal file
325
SQLLinter/Infrastructure/Reporters/HtmlMinifier.cs
Normal file
@@ -0,0 +1,325 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
public static class HtmlMinifier
|
||||
{
|
||||
public static string MinifyHtml(string html)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return html;
|
||||
|
||||
// Вырезаем чувствительные теги
|
||||
var placeholders = new Dictionary<string, string>();
|
||||
html = ExtractTag(html, "pre", placeholders);
|
||||
html = ExtractTag(html, "code", placeholders);
|
||||
html = ExtractTag(html, "textarea", placeholders);
|
||||
|
||||
// Минификация CSS и JS
|
||||
html = MinifyCssInHtml(html);
|
||||
html = MinifyJavaScriptInHtml(html);
|
||||
|
||||
// Удаление HTML комментариев
|
||||
html = Regex.Replace(html,
|
||||
@"<!--(?!\[if|\s*\[endif).*?-->",
|
||||
"",
|
||||
RegexOptions.Singleline | RegexOptions.Compiled);
|
||||
|
||||
// Удаление лишних пробелов между тегами
|
||||
html = Regex.Replace(html, @">\s+<", "><");
|
||||
|
||||
// Collapse whitespace
|
||||
html = Regex.Replace(html, @"\s{2,}", " ");
|
||||
|
||||
// Возвращаем чувствительные блоки
|
||||
foreach (var kv in placeholders)
|
||||
html = html.Replace(kv.Key, kv.Value);
|
||||
|
||||
return html.Trim();
|
||||
}
|
||||
private static string ExtractTag(string html, string tag, Dictionary<string, string> dict)
|
||||
{
|
||||
return Regex.Replace(html,
|
||||
$@"<{tag}[^>]*>[\s\S]*?<\/{tag}>",
|
||||
m =>
|
||||
{
|
||||
var key = $"__PLACEHOLDER_{dict.Count}__";
|
||||
dict[key] = m.Value;
|
||||
return key;
|
||||
},
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
private static string MinifyCssInHtml(string html)
|
||||
{
|
||||
// Находим все теги <style>
|
||||
var styleRegex = new Regex(@"<style[^>]*>([\s\S]*?)</style>",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
return styleRegex.Replace(html, match =>
|
||||
{
|
||||
var css = match.Groups[1].Value;
|
||||
var minifiedCss = MinifyCss(css);
|
||||
return $"<style>{minifiedCss}</style>";
|
||||
});
|
||||
}
|
||||
|
||||
public static string MinifyCss(string css)
|
||||
{
|
||||
if (string.IsNullOrEmpty(css))
|
||||
return css;
|
||||
|
||||
// 1. Удаление комментариев
|
||||
css = Regex.Replace(css, @"/\*[\s\S]*?\*/", "",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// 2. Удаление лишних пробелов и переносов
|
||||
css = Regex.Replace(css, @"\s+", " ",
|
||||
RegexOptions.Compiled);
|
||||
css = Regex.Replace(css, @"\s*{\s*", "{",
|
||||
RegexOptions.Compiled);
|
||||
css = Regex.Replace(css, @"\s*}\s*", "}",
|
||||
RegexOptions.Compiled);
|
||||
css = Regex.Replace(css, @"\s*:\s*", ":",
|
||||
RegexOptions.Compiled);
|
||||
css = Regex.Replace(css, @"\s*;\s*", ";",
|
||||
RegexOptions.Compiled);
|
||||
css = Regex.Replace(css, @"\s*,\s*", ",",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// 3. Удаление последней точки с запятой перед }
|
||||
css = Regex.Replace(css, @";}", "}",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// 4. Удаление пробелов вокруг селекторов
|
||||
css = Regex.Replace(css, @"\s*>\s*", ">",
|
||||
RegexOptions.Compiled);
|
||||
css = Regex.Replace(css, @"\s*\+\s*", "+",
|
||||
RegexOptions.Compiled);
|
||||
css = Regex.Replace(css, @"\s*~\s*", "~",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// 5. Удаление пробелов в значениях
|
||||
css = Regex.Replace(css, @"(\d)\s+(px|em|rem|%|pt|pc|in|cm|mm|ex|ch|vw|vh|vmin|vmax)",
|
||||
"$1$2", RegexOptions.Compiled);
|
||||
|
||||
// 6. Удаление ведущих нулей
|
||||
css = Regex.Replace(css, @"(?<=[ :\(,])0?\.(\d+)", ".$1",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
css = Regex.Replace(css, @"url\(\s*(.*?)\s*\)", "url($1)");
|
||||
|
||||
return css.Trim();
|
||||
}
|
||||
|
||||
private static string MinifyJavaScriptInHtml(string html)
|
||||
{
|
||||
// Находим все теги <script>
|
||||
var scriptRegex = new Regex(@"<script[^>]*>([\s\S]*?)</script>",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
return scriptRegex.Replace(html, match =>
|
||||
{
|
||||
var scriptContent = match.Groups[1].Value;
|
||||
|
||||
// Пропускаем script с type="application/json" или src
|
||||
var tag = match.Value;
|
||||
if (tag.Contains("type=\"application/json\"") ||
|
||||
tag.Contains("src=") ||
|
||||
scriptContent.Trim().Length == 0)
|
||||
return match.Value;
|
||||
|
||||
var minifiedJs = MinifyJavaScript(scriptContent);
|
||||
|
||||
if (tag.Contains("type=\"module\""))
|
||||
return $"<script type=\"module\">{minifiedJs}</script>";
|
||||
else
|
||||
return $"<script>{minifiedJs}</script>";
|
||||
});
|
||||
}
|
||||
|
||||
public static string MinifyJavaScript(string js)
|
||||
{
|
||||
if (string.IsNullOrEmpty(js))
|
||||
return js;
|
||||
|
||||
var sb = new StringBuilder(js.Length);
|
||||
int i = 0;
|
||||
int len = js.Length;
|
||||
|
||||
bool inSingle = false;
|
||||
bool inDouble = false;
|
||||
bool inTemplate = false;
|
||||
bool inLineComment = false;
|
||||
bool inBlockComment = false;
|
||||
|
||||
while (i < len)
|
||||
{
|
||||
char c = js[i];
|
||||
char next = i + 1 < len ? js[i + 1] : '\0';
|
||||
|
||||
// -----------------------------
|
||||
// LINE COMMENT //
|
||||
// -----------------------------
|
||||
if (inLineComment)
|
||||
{
|
||||
if (c == '\n' || c == '\r')
|
||||
{
|
||||
inLineComment = false;
|
||||
sb.Append(' ');
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// BLOCK COMMENT /* ... */ //
|
||||
// -----------------------------
|
||||
if (inBlockComment)
|
||||
{
|
||||
if (c == '*' && next == '/')
|
||||
{
|
||||
inBlockComment = false;
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// STRING: '...' //
|
||||
// -----------------------------
|
||||
if (inSingle)
|
||||
{
|
||||
sb.Append(c);
|
||||
if (c == '\\')
|
||||
{
|
||||
if (i + 1 < len) sb.Append(js[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c == '\'') inSingle = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// STRING: "..." //
|
||||
// -----------------------------
|
||||
if (inDouble)
|
||||
{
|
||||
sb.Append(c);
|
||||
if (c == '\\')
|
||||
{
|
||||
if (i + 1 < len) sb.Append(js[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') inDouble = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// TEMPLATE: `...` //
|
||||
// -----------------------------
|
||||
if (inTemplate)
|
||||
{
|
||||
sb.Append(c);
|
||||
if (c == '\\')
|
||||
{
|
||||
if (i + 1 < len) sb.Append(js[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c == '`') inTemplate = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// NORMAL CODE //
|
||||
// -----------------------------
|
||||
|
||||
// Start of line comment
|
||||
if (c == '/' && next == '/')
|
||||
{
|
||||
inLineComment = true;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start of block comment
|
||||
if (c == '/' && next == '*')
|
||||
{
|
||||
inBlockComment = true;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start of strings
|
||||
if (c == '\'')
|
||||
{
|
||||
inSingle = true;
|
||||
sb.Append(c);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"')
|
||||
{
|
||||
inDouble = true;
|
||||
sb.Append(c);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '`')
|
||||
{
|
||||
inTemplate = true;
|
||||
sb.Append(c);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collapse whitespace in code
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
sb.Append(' ');
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(c);
|
||||
i++;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// FINAL WHITESPACE MINIFICATION
|
||||
// -----------------------------
|
||||
var result = sb.ToString();
|
||||
|
||||
// Удаляем повторяющиеся пробелы
|
||||
result = Regex.Replace(result, @"\s+", " ");
|
||||
|
||||
// Убираем пробелы вокруг операторов
|
||||
result = Regex.Replace(result, @"\s*([=+\-*/%&|^<>!?:,;{}()\[\]])\s*", "$1");
|
||||
|
||||
return result.Trim();
|
||||
}
|
||||
|
||||
public static string CompressJson(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return json;
|
||||
|
||||
// Удаление пробелов и переносов из JSON
|
||||
json = Regex.Replace(json, @"(""[^""\\]*(?:\\.[^""\\]*)*"")|\s+",
|
||||
match => match.Groups[1].Success ? match.Groups[1].Value : "",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
using SQLLinter.Infrastructure.Diagram;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Reporters;
|
||||
|
||||
@@ -13,11 +15,19 @@ public class HtmlReportFormatter : IReportFormatter
|
||||
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>");
|
||||
@@ -26,40 +36,42 @@ public class HtmlReportFormatter : IReportFormatter
|
||||
sb.AppendLine("</div>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
GenerateEndingHtml(sb, false);
|
||||
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();
|
||||
|
||||
int fileIndex = 0;
|
||||
|
||||
// --- Отчёты по файлам ---
|
||||
foreach (var fileGroup in groupedByFile)
|
||||
{
|
||||
string fileName = Path.GetFileName(fileGroup.Key);
|
||||
var fileData = new FileReportData
|
||||
{
|
||||
FileName = 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 и выводим таблицы
|
||||
// Группировка по severity
|
||||
var severityGroups = fileGroup
|
||||
.GroupBy(v => v.Severity)
|
||||
.OrderByDescending(g => g.Key)
|
||||
@@ -67,159 +79,41 @@ public class HtmlReportFormatter : IReportFormatter
|
||||
|
||||
foreach (var severityGroup in severityGroups)
|
||||
{
|
||||
string severityClass = severityGroup.Key switch
|
||||
var violationsList = severityGroup
|
||||
.OrderBy(v => v.Line)
|
||||
.ThenBy(v => v.Column)
|
||||
.Select(v => new ViolationData
|
||||
{
|
||||
RuleViolationSeverity.Critical => "critical",
|
||||
RuleViolationSeverity.Warning => "warning",
|
||||
RuleViolationSeverity.Info => "info",
|
||||
_ => ""
|
||||
Line = v.Line,
|
||||
Column = v.Column,
|
||||
RuleName = v.RuleName,
|
||||
Text = EscapeHtml(v.Text),
|
||||
Index = severityGroup.ToList().IndexOf(v) + 1
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (severityGroup.Key == RuleViolationSeverity.Critical)
|
||||
fileData.CriticalViolations = violationsList;
|
||||
else if (severityGroup.Key == RuleViolationSeverity.Warning)
|
||||
fileData.WarningViolations = violationsList;
|
||||
else if (severityGroup.Key == RuleViolationSeverity.Info)
|
||||
fileData.InfoViolations = violationsList;
|
||||
}
|
||||
|
||||
reportData.Files.Add(fileData);
|
||||
}
|
||||
|
||||
// Добавление диаграммы, если есть
|
||||
if (diagram != null)
|
||||
{
|
||||
reportData.Diagram = new DiagramData
|
||||
{
|
||||
MermaidContent = MermaidRenderer.ToMermaidContent(diagram),
|
||||
HasDiagram = true
|
||||
};
|
||||
|
||||
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();
|
||||
return reportData;
|
||||
}
|
||||
|
||||
private void GenerateBeginningHtml(StringBuilder sb)
|
||||
@@ -234,28 +128,24 @@ public class HtmlReportFormatter : IReportFormatter
|
||||
<style>
|
||||
""");
|
||||
|
||||
// Загружаем CSS из ресурсов и добавляем стили для заголовка файла
|
||||
sb.AppendLine(LoadResource("HtmlFormatter.css"));
|
||||
|
||||
sb.AppendLine("""
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="main-content">
|
||||
""");
|
||||
sb.AppendLine("</style></head><body><main id=\"main-content\">");
|
||||
}
|
||||
|
||||
private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram)
|
||||
private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram, string jsonData)
|
||||
{
|
||||
sb.AppendLine("""
|
||||
</main>
|
||||
// Вставка JSON данных
|
||||
sb.AppendLine($"""
|
||||
<script id="report-data" type="application/json">
|
||||
{jsonData}
|
||||
</script>
|
||||
""");
|
||||
|
||||
sb.AppendLine("""
|
||||
<script type="module">
|
||||
""");
|
||||
|
||||
// Загружаем JS из ресурсов
|
||||
// Загружаем основной JS
|
||||
sb.AppendLine(LoadResource("HtmlFormatter.js"));
|
||||
|
||||
sb.AppendLine("""
|
||||
@@ -268,10 +158,8 @@ public class HtmlReportFormatter : IReportFormatter
|
||||
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();
|
||||
@@ -281,4 +169,40 @@ public class HtmlReportFormatter : IReportFormatter
|
||||
{
|
||||
return System.Net.WebUtility.HtmlEncode(text);
|
||||
}
|
||||
|
||||
// Классы для сериализации
|
||||
private class ReportData
|
||||
{
|
||||
public List<FileReportData> Files { get; set; } = new();
|
||||
public DiagramData? Diagram { get; set; }
|
||||
}
|
||||
|
||||
private class FileReportData
|
||||
{
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public List<ViolationData>? CriticalViolations { get; set; }
|
||||
public List<ViolationData>? WarningViolations { get; set; }
|
||||
public List<ViolationData>? InfoViolations { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int TotalViolations =>
|
||||
(CriticalViolations?.Count ?? 0) +
|
||||
(WarningViolations?.Count ?? 0) +
|
||||
(InfoViolations?.Count ?? 0);
|
||||
}
|
||||
|
||||
private class ViolationData
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public int Line { get; set; }
|
||||
public int Column { get; set; }
|
||||
public string RuleName { get; set; } = string.Empty;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class DiagramData
|
||||
{
|
||||
public string MermaidContent { get; set; } = string.Empty;
|
||||
public bool HasDiagram { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,267 @@
|
||||
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
REPORT RENDERER - динамическое создание таблиц и табов
|
||||
--------------------------------------------------------- */
|
||||
class ReportRenderer {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.files = data.files || [];
|
||||
this.diagram = data.diagram;
|
||||
this.currentFileIndex = 0;
|
||||
|
||||
this.reportsContainer = document.getElementById('reports-container');
|
||||
this.tabsList = document.getElementById('tabs-list');
|
||||
|
||||
if (!this.reportsContainer || !this.tabsList) {
|
||||
console.error('Не найдены контейнеры для отчета');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.renderTabs();
|
||||
this.renderFileReports();
|
||||
this.setupTabNavigation();
|
||||
|
||||
// Активируем первый таб
|
||||
this.activateTab(0);
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
this.tabsList.innerHTML = '';
|
||||
|
||||
// Табы для файлов
|
||||
this.files.forEach((file, index) => {
|
||||
const tab = document.createElement('button');
|
||||
tab.className = `tab ${index === 0 ? 'active' : ''}`;
|
||||
tab.dataset.target = `file_${index}`;
|
||||
tab.dataset.index = index;
|
||||
|
||||
tab.innerHTML = `
|
||||
${this.escapeHtml(file.fileName)}
|
||||
<span class="tab-badge">${file.totalViolations || this.calculateTotalViolations(file)}</span>
|
||||
`;
|
||||
|
||||
this.tabsList.appendChild(tab);
|
||||
});
|
||||
|
||||
// Таб для диаграммы (если есть)
|
||||
if (this.diagram?.hasDiagram) {
|
||||
const diagramTab = document.createElement('button');
|
||||
diagramTab.className = 'tab';
|
||||
diagramTab.dataset.target = 'mermaid';
|
||||
diagramTab.textContent = 'Диаграмма';
|
||||
this.tabsList.appendChild(diagramTab);
|
||||
}
|
||||
}
|
||||
|
||||
renderFileReports() {
|
||||
this.reportsContainer.innerHTML = '';
|
||||
|
||||
// Рендеринг отчетов по файлам
|
||||
this.files.forEach((file, index) => {
|
||||
const reportDiv = document.createElement('div');
|
||||
reportDiv.id = `file_${index}`;
|
||||
reportDiv.className = 'file-report';
|
||||
reportDiv.style.display = 'none';
|
||||
|
||||
// Заголовок файла
|
||||
reportDiv.innerHTML = `
|
||||
<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">${this.escapeHtml(file.fileName)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Секции по severity
|
||||
if (file.criticalViolations?.length > 0) {
|
||||
reportDiv.appendChild(this.createSeveritySection('critical', file.criticalViolations));
|
||||
}
|
||||
if (file.warningViolations?.length > 0) {
|
||||
reportDiv.appendChild(this.createSeveritySection('warning', file.warningViolations));
|
||||
}
|
||||
if (file.infoViolations?.length > 0) {
|
||||
reportDiv.appendChild(this.createSeveritySection('info', file.infoViolations));
|
||||
}
|
||||
|
||||
this.reportsContainer.appendChild(reportDiv);
|
||||
});
|
||||
|
||||
// Контейнер для диаграммы (если есть)
|
||||
if (this.diagram?.hasDiagram) {
|
||||
const diagramDiv = document.createElement('div');
|
||||
diagramDiv.id = 'mermaid';
|
||||
diagramDiv.className = 'file-report';
|
||||
diagramDiv.style.display = 'none';
|
||||
|
||||
diagramDiv.innerHTML = `
|
||||
<div class="diagram-toolbar">
|
||||
<div class="toolbar-search">
|
||||
<input type="text" id="diagramSearch" placeholder="Поиск по узлам (нажмите Enter)" />
|
||||
<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">
|
||||
${this.escapeHtml(this.diagram.mermaidContent)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.reportsContainer.appendChild(diagramDiv);
|
||||
}
|
||||
}
|
||||
|
||||
createSeveritySection(severity, violations) {
|
||||
const severityTitle = {
|
||||
critical: 'Critical',
|
||||
warning: 'Warning',
|
||||
info: 'Info'
|
||||
}[severity] || 'Unknown';
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = `severity-section ${severity}`;
|
||||
|
||||
const tableRows = violations.map(v => `
|
||||
<tr>
|
||||
<td class="index">${v.index}</td>
|
||||
<td class="line">${v.line}</td>
|
||||
<td class="column">${v.column}</td>
|
||||
<td class="rule">${this.escapeHtml(v.ruleName)}</td>
|
||||
<td class="description">${this.escapeHtml(v.text)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
section.innerHTML = `
|
||||
<div class="severity-header">
|
||||
<div class="severity-title">
|
||||
<h3>${severityTitle}</h3>
|
||||
<span class="severity-count">${violations.length}</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>
|
||||
${tableRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
setupTabNavigation() {
|
||||
const tabs = this.tabsList.querySelectorAll('.tab');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = tab.dataset.target;
|
||||
const index = tab.dataset.index;
|
||||
|
||||
// Обновляем активный таб
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
// Скрываем все отчеты
|
||||
document.querySelectorAll('.file-report').forEach(report => {
|
||||
report.style.display = 'none';
|
||||
});
|
||||
|
||||
// Показываем выбранный отчет
|
||||
const targetReport = document.getElementById(targetId);
|
||||
if (targetReport) {
|
||||
targetReport.style.display = 'block';
|
||||
|
||||
// Если это диаграмма, инициализируем ее
|
||||
if (targetId === 'mermaid' && window.viewer) {
|
||||
window.viewer.render().catch(console.error);
|
||||
}
|
||||
|
||||
// Прокрутка к верху
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
activateTab(index) {
|
||||
const tab = this.tabsList.querySelector(`.tab[data-index="${index}"]`);
|
||||
if (tab) {
|
||||
tab.click();
|
||||
}
|
||||
}
|
||||
|
||||
calculateTotalViolations(file) {
|
||||
return (file.criticalViolations?.length || 0) +
|
||||
(file.warningViolations?.length || 0) +
|
||||
(file.infoViolations?.length || 0);
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
ГЛОБАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ
|
||||
--------------------------------------------------------- */
|
||||
function initReport() {
|
||||
if (!window.reportData) {
|
||||
console.error('Данные отчета не найдены');
|
||||
return;
|
||||
}
|
||||
|
||||
const renderer = new ReportRenderer(window.reportData);
|
||||
renderer.init();
|
||||
|
||||
// Сохраняем рендерер в глобальной области видимости
|
||||
window.reportRenderer = renderer;
|
||||
}
|
||||
|
||||
// Экспортируем функции для глобального использования
|
||||
window.initReport = initReport;
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
TABS MANAGEMENT
|
||||
--------------------------------------------------------- */
|
||||
@@ -1293,6 +1554,15 @@ function setupThemeChangeHandler() {
|
||||
let viewer = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
window.reportData = JSON.parse(document.getElementById('report-data').textContent);
|
||||
window.hasDiagram = true;
|
||||
|
||||
// Инициализация отчета
|
||||
if (window.initReport) {
|
||||
window.initReport();
|
||||
}
|
||||
|
||||
|
||||
initTabs((id) => {
|
||||
if (id === "mermaid") {
|
||||
if (!viewer) {
|
||||
|
||||
Reference in New Issue
Block a user