fluent ui + mermaid
This commit is contained in:
@@ -1,51 +0,0 @@
|
||||
using System.Text;
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Diagram;
|
||||
|
||||
public static class FragmentDiagramBuilder
|
||||
{
|
||||
public static string RenderMermaid(TSqlFragment fragment)
|
||||
{
|
||||
if (fragment == null) return string.Empty;
|
||||
var diagram = BpmnBuilder.Build(fragment);
|
||||
return MermaidRenderer.RenderMarkdown(diagram);
|
||||
}
|
||||
|
||||
public static string RenderHtmlSvg(TSqlFragment fragment)
|
||||
{
|
||||
if (fragment == null) return string.Empty;
|
||||
var diagram = BpmnBuilder.Build(fragment);
|
||||
// Use mermaid HTML instead of SVG fallback
|
||||
return MermaidRenderer.RenderHtml(diagram);
|
||||
}
|
||||
|
||||
// keep helpers used by earlier code if needed
|
||||
private static string Escape(string s)
|
||||
{
|
||||
if (s == null) return string.Empty;
|
||||
return System.Net.WebUtility.HtmlEncode(s).Replace("\n", " ").Replace("\r", " ");
|
||||
}
|
||||
|
||||
private static string Truncate(string s, int len)
|
||||
{
|
||||
if (s == null) return string.Empty;
|
||||
if (s.Length <= len) return s;
|
||||
return s.Substring(0, len - 3) + "...";
|
||||
}
|
||||
|
||||
private static string SanitizeId(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return "id";
|
||||
var sb = new StringBuilder();
|
||||
foreach (var ch in s)
|
||||
{
|
||||
if (char.IsLetterOrDigit(ch)) sb.Append(ch);
|
||||
else sb.Append('_');
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public static class MermaidRenderer
|
||||
{
|
||||
var procId = SanitizeId(proc.Id);
|
||||
sb.AppendLine($" subgraph {procId} [\"{Escape(proc.Name)}\"]");
|
||||
sb.AppendLine($" direction TB");
|
||||
|
||||
var startNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Start).ToList();
|
||||
var taskNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Task || n.Type == BpmnNodeType.Gateway).ToList();
|
||||
@@ -116,17 +117,6 @@ public static class MermaidRenderer
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string RenderHtml(BpmnDiagram diagram)
|
||||
{
|
||||
var content = ToMermaidContent(diagram);
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Put raw mermaid text inside .mermaid container so mermaid.js can render it
|
||||
sb.AppendLine("<div class=\"mermaid\">\n" + content + "\n</div>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string Escape(string s)
|
||||
{
|
||||
if (s == null) return string.Empty;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Infrastructure.Diagram;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Reporters;
|
||||
@@ -7,31 +8,64 @@ 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)
|
||||
{
|
||||
if (violations.Count == 0)
|
||||
var sb = new StringBuilder();
|
||||
|
||||
GenerateBeginningHtml(sb);
|
||||
|
||||
if (violations.Count == 0 && diagram == null)
|
||||
{
|
||||
return "<p><em>Нет нарушений</em></p>";
|
||||
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);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
GenerateBeginningHtml(sb);
|
||||
.OrderBy(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
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>");
|
||||
string fileName = Path.GetFileName(fileGroup.Key);
|
||||
|
||||
var groupedBySeverity = fileGroup
|
||||
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);
|
||||
.OrderByDescending(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var severityGroup in groupedBySeverity)
|
||||
foreach (var severityGroup in severityGroups)
|
||||
{
|
||||
string severityClass = severityGroup.Key switch
|
||||
{
|
||||
@@ -41,202 +75,210 @@ public class HtmlReportFormatter : IReportFormatter
|
||||
_ => ""
|
||||
};
|
||||
|
||||
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))
|
||||
string severityTitle = severityGroup.Key switch
|
||||
{
|
||||
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",
|
||||
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>");
|
||||
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 rowIndex = 1;
|
||||
int row = 1;
|
||||
foreach (var v in severityGroup
|
||||
.OrderBy(x => x.Line)
|
||||
.ThenBy(x => x.Column))
|
||||
.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(
|
||||
$"""
|
||||
<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>");
|
||||
sb.AppendLine("</table>");
|
||||
sb.AppendLine("</div>");
|
||||
sb.AppendLine($"""
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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)
|
||||
// --- Вкладка с диаграммой ---
|
||||
bool hasDiagram = diagram != null;
|
||||
if (hasDiagram)
|
||||
{
|
||||
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('file_{fileIndex}', this)\">{fileGroup.Key}</div>");
|
||||
fileIndex++;
|
||||
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>
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('mermaid', this)\">Диграмма</div>");
|
||||
sb.AppendLine("</div>");
|
||||
// --- Табы ---
|
||||
if (violations.Count > 0 || hasDiagram)
|
||||
{
|
||||
sb.AppendLine("""
|
||||
<div class="tabs-container">
|
||||
<div class="tabs">
|
||||
""");
|
||||
|
||||
GenerateEndingHtml(sb);
|
||||
// Табы для файлов
|
||||
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>");
|
||||
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("""
|
||||
<!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("@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>");
|
||||
// Загружаем CSS из ресурсов и добавляем стили для заголовка файла
|
||||
sb.AppendLine(LoadResource("HtmlFormatter.css"));
|
||||
|
||||
sb.AppendLine("""
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="main-content">
|
||||
""");
|
||||
}
|
||||
|
||||
private void GenerateEndingHtml(StringBuilder sb)
|
||||
private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram)
|
||||
{
|
||||
sb.AppendLine("""
|
||||
</main>
|
||||
""");
|
||||
|
||||
// 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("""
|
||||
<script type="module">
|
||||
""");
|
||||
|
||||
sb.AppendLine("</body>");
|
||||
sb.AppendLine("</html>");
|
||||
// Загружаем 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);
|
||||
}
|
||||
}
|
||||
1210
SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css
Normal file
1210
SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css
Normal file
File diff suppressed because it is too large
Load Diff
1317
SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js
Normal file
1317
SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user