diff --git a/SQLLinter/Infrastructure/Diagram/BpmnArrowType.cs b/SQLLinter/Infrastructure/Diagram/Models/BpmnArrowType.cs similarity index 100% rename from SQLLinter/Infrastructure/Diagram/BpmnArrowType.cs rename to SQLLinter/Infrastructure/Diagram/Models/BpmnArrowType.cs diff --git a/SQLLinter/Infrastructure/Diagram/BpmnDiagram.cs b/SQLLinter/Infrastructure/Diagram/Models/BpmnDiagram.cs similarity index 100% rename from SQLLinter/Infrastructure/Diagram/BpmnDiagram.cs rename to SQLLinter/Infrastructure/Diagram/Models/BpmnDiagram.cs diff --git a/SQLLinter/Infrastructure/Diagram/BpmnDiagramExtensions.cs b/SQLLinter/Infrastructure/Diagram/Models/BpmnDiagramExtensions.cs similarity index 100% rename from SQLLinter/Infrastructure/Diagram/BpmnDiagramExtensions.cs rename to SQLLinter/Infrastructure/Diagram/Models/BpmnDiagramExtensions.cs diff --git a/SQLLinter/Infrastructure/Diagram/BpmnEdge.cs b/SQLLinter/Infrastructure/Diagram/Models/BpmnEdge.cs similarity index 100% rename from SQLLinter/Infrastructure/Diagram/BpmnEdge.cs rename to SQLLinter/Infrastructure/Diagram/Models/BpmnEdge.cs diff --git a/SQLLinter/Infrastructure/Diagram/BpmnNode.cs b/SQLLinter/Infrastructure/Diagram/Models/BpmnNode.cs similarity index 100% rename from SQLLinter/Infrastructure/Diagram/BpmnNode.cs rename to SQLLinter/Infrastructure/Diagram/Models/BpmnNode.cs diff --git a/SQLLinter/Infrastructure/Diagram/BpmnNodeType.cs b/SQLLinter/Infrastructure/Diagram/Models/BpmnNodeType.cs similarity index 100% rename from SQLLinter/Infrastructure/Diagram/BpmnNodeType.cs rename to SQLLinter/Infrastructure/Diagram/Models/BpmnNodeType.cs diff --git a/SQLLinter/Infrastructure/Diagram/BpmnProcess.cs b/SQLLinter/Infrastructure/Diagram/Models/BpmnProcess.cs similarity index 100% rename from SQLLinter/Infrastructure/Diagram/BpmnProcess.cs rename to SQLLinter/Infrastructure/Diagram/Models/BpmnProcess.cs diff --git a/SQLLinter/Infrastructure/Reporters/HtmlMinifier.cs b/SQLLinter/Infrastructure/Reporters/HtmlMinifier.cs new file mode 100644 index 0000000..c601224 --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/HtmlMinifier.cs @@ -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(); + 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, + @"", + "", + 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 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) + { + // Находим все теги ", + RegexOptions.Compiled); + + return styleRegex.Replace(html, match => + { + var css = match.Groups[1].Value; + var minifiedCss = MinifyCss(css); + return $""; + }); + } + + 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) + { + // Находим все теги ", + 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 $""; + else + return $""; + }); + } + + 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; + } +} \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter.cs b/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter.cs index 1347be9..6ee9c69 100644 --- a/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter.cs +++ b/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter.cs @@ -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 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("
"); sb.AppendLine("
"); sb.AppendLine("
"); @@ -26,40 +36,42 @@ public class HtmlReportFormatter : IReportFormatter sb.AppendLine("
"); sb.AppendLine("
"); - GenerateEndingHtml(sb, false); + GenerateEndingHtml(sb, false, HtmlMinifier.CompressJson(jsonData)); return sb.ToString(); } + // Основной контейнер для отчета + sb.AppendLine(""" +
+
+
+
+ """); + + GenerateEndingHtml(sb, diagram != null, jsonData); + + var html = HtmlMinifier.MinifyHtml(sb.ToString()); + return html; + } + + private ReportData PrepareReportData(List 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($""" -
- """); - - // Заголовок файла - добавляем ПЕРЕД секциями с ошибками - sb.AppendLine($""" -
-
- - - - - {fileName} -
-
- """); - - // Группируем по 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 - { - RuleViolationSeverity.Critical => "critical", - RuleViolationSeverity.Warning => "warning", - RuleViolationSeverity.Info => "info", - _ => "" - }; + var violationsList = severityGroup + .OrderBy(v => v.Line) + .ThenBy(v => v.Column) + .Select(v => new ViolationData + { + Line = v.Line, + Column = v.Column, + RuleName = v.RuleName, + Text = EscapeHtml(v.Text), + Index = severityGroup.ToList().IndexOf(v) + 1 + }) + .ToList(); - string severityTitle = severityGroup.Key switch - { - RuleViolationSeverity.Critical => "Critical", - RuleViolationSeverity.Warning => "Warning", - RuleViolationSeverity.Info => "Info", - _ => "" - }; - - sb.AppendLine($""" -
-
-
-

{severityTitle}

- {severityGroup.Count()} -
-
-
- - - - - - - - - - - - """); - - int row = 1; - foreach (var v in severityGroup - .OrderBy(x => x.Line) - .ThenBy(x => x.Column)) - { - sb.AppendLine( - $""" - - - - - - - - """); - row++; - } - - sb.AppendLine($""" - -
#СтрокаКолонкаПравилоОписание
{row}{v.Line}{v.Column}{v.RuleName}{EscapeHtml(v.Text)}
-
-
- """); + 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; } - sb.AppendLine("
"); - fileIndex++; + reportData.Files.Add(fileData); } - // --- Вкладка с диаграммой --- - bool hasDiagram = diagram != null; - if (hasDiagram) + // Добавление диаграммы, если есть + if (diagram != null) { - sb.AppendLine($""" -
-
- -
- - -
-
- -
-
-
-
- - -
- """); + reportData.Diagram = new DiagramData + { + MermaidContent = MermaidRenderer.ToMermaidContent(diagram), + HasDiagram = true + }; } - // --- Табы --- - if (violations.Count > 0 || hasDiagram) - { - sb.AppendLine(""" -
-
- """); - - // Табы для файлов - 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($""" -
- {fileName} - {tabBadge} -
- """); - } - - // Таб для диаграммы - if (hasDiagram) - { - sb.AppendLine(""" -
- Диаграмма -
- """); - } - - sb.AppendLine(""" -
-
- """); - } - - GenerateEndingHtml(sb, hasDiagram); - return sb.ToString(); + return reportData; } private void GenerateBeginningHtml(StringBuilder sb) @@ -234,28 +128,24 @@ public class HtmlReportFormatter : IReportFormatter - - -
- """); + sb.AppendLine("
"); } - private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram) + private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram, string jsonData) { - sb.AppendLine(""" -
+ // Вставка JSON данных + sb.AppendLine($""" + """); sb.AppendLine("""