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 || data.f || [];
this.rules = data.rules || data.r || {};
this.diagram = data.diagram || data.d || {};
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() {
const tabsList = document.getElementById('tabs-list');
if (!tabsList) return;
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;
const total = (file.v.c?.length || 0) + (file.v.w?.length || 0) + (file.v.i?.length || 0);
tab.innerHTML = `
${this.escapeHtml(file.n)}
${total}
`;
this.tabsList.appendChild(tab);
});
// Таб для диаграммы (если есть)
if (this.diagram.h || this.diagram.hasDiagram) {
const diagramTab = document.createElement('button');
diagramTab.className = 'tab';
diagramTab.dataset.target = 'mermaid';
diagramTab.textContent = 'Диаграмма';
tabsList.appendChild(diagramTab);
}
}
renderFileReports() {
const container = document.getElementById('reports-container');
if (!container) return;
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';
const fileName = file.n || file.fileName || '';
const violations = file.v || file.violations || {};
// Заголовок файла
reportDiv.innerHTML = `
${this.escapeHtml(fileName)}
`;
// Добавляем секции нарушений
if (file.v.c?.length > 0) {
reportDiv.appendChild(this.createViolationSection('critical', file.v.c));
}
if (file.v.w?.length > 0) {
reportDiv.appendChild(this.createViolationSection('warning', file.v.w));
}
if (file.v.i?.length > 0) {
reportDiv.appendChild(this.createViolationSection('info', file.v.i));
}
this.reportsContainer.appendChild(reportDiv);
});
// Контейнер для диаграммы (если есть)
if (this.diagram.h || this.diagram.hasDiagram) {
const diagramContent = this.diagram.c || this.diagram.Content || '';
const diagramDiv = document.createElement('div');
diagramDiv.id = 'mermaid';
diagramDiv.className = 'file-report';
diagramDiv.style.display = 'none';
diagramDiv.innerHTML = `
${this.escapeHtml(diagramContent)}
`;
this.reportsContainer.appendChild(diagramDiv);
}
}
createViolationSection(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(violation => {
// Получаем правило по ID
const ruleId = violation.r || violation.ruleId;
const rule = this.rules[ruleId] || { n: 'Unknown', t: 'Описание отсутствует' };
// Формируем текст с подстановкой параметров
let text = rule.t || '';
const args = violation.a || violation.args || [];
if (args.length > 0 && text.includes('{')) {
args.forEach((arg, index) => {
text = text.replace(new RegExp(`\\{${index}\\}`, 'g'), arg);
});
}
return `
| ${violation.i || violation.index} |
${violation.l || violation.line} |
${violation.c || violation.column} |
${this.escapeHtml(rule.n || rule.name)} |
${this.escapeHtml(text)} |
`;
}).join('');
section.innerHTML = `
| # |
Строка |
Колонка |
Правило |
Описание |
${tableRows}
`;
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
--------------------------------------------------------- */
function initTabs(onTabActivated) {
const tabs = document.querySelectorAll(".tab");
const reports = document.querySelectorAll(".file-report");
tabs.forEach(tab => {
tab.addEventListener("click", () => {
const id = tab.dataset.target;
// Add ripple effect
const ripple = document.createElement('span');
const rect = tab.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
ripple.style.cssText = `
position: absolute;
border-radius: 50%;
background: rgba(0, 120, 212, 0.2);
transform: scale(0);
animation: ripple 0.6s linear;
width: ${size}px;
height: ${size}px;
left: ${x - size / 2}px;
top: ${y - size / 2}px;
`;
tab.style.position = 'relative';
tab.style.overflow = 'hidden';
tab.appendChild(ripple);
setTimeout(() => {
if (ripple.parentElement === tab) {
ripple.remove();
}
}, 600);
tabs.forEach(t => t.classList.remove("active"));
reports.forEach(r => r.classList.remove("active"));
tab.classList.add("active");
const report = document.getElementById(id);
if (report) {
report.classList.add("active");
// Scroll to top when switching tabs
window.scrollTo({ top: 0, behavior: 'smooth' });
}
if (onTabActivated) onTabActivated(id);
});
});
}
// Исправление для табов на странице диаграммы
document.addEventListener('DOMContentLoaded', function () {
const tabsContainer = document.querySelector('.tabs-container');
if (tabsContainer) {
tabsContainer.style.zIndex = '1001';
tabsContainer.style.position = 'fixed';
tabsContainer.style.bottom = '0';
tabsContainer.style.left = '0';
tabsContainer.style.right = '0';
}
// Гарантируем, что табы всегда поверх всего
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.style.zIndex = '1002';
});
});
/* ---------------------------------------------------------
DIAGRAM VIEWER CLASS
--------------------------------------------------------- */
class DiagramViewer {
constructor() {
this.container = document.getElementById("diagramSvgContainer");
this.minimap = document.getElementById("minimap");
this.searchInput = document.getElementById("diagramSearch");
this.resetBtn = document.getElementById("resetViewBtn");
this.svg = null;
this.viewportGroup = null;
this.nodes = [];
this.scale = 1;
this.tx = 0;
this.ty = 0;
this.minScale = 0.3;
this.maxScale = 12;
this.originalViewBox = null;
this.minimapSvg = null;
this.minimapViewport = null;
this.needsRender = false;
this.isPanning = false;
this.lastSearchTerm = '';
this.searchResults = [];
this.currentSearchIndex = -1;
this.init();
}
init() {
// Add CSS for ripple animation
if (!document.querySelector('#ripple-styles')) {
const style = document.createElement('style');
style.id = 'ripple-styles';
style.textContent = `
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
@keyframes fadeIn {
from {opacity: 0; transform: translateY(-10px); }
to {opacity: 1; transform: translateY(0); }
}
`;
document.head.appendChild(style);
}
}
async render() {
this.container.classList.add('loading');
try {
const mermaidSource = document.getElementById("mermaidSource").textContent.trim();
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
mermaid.initialize({
startOnLoad: false,
theme: isDark ? "dark" : "default",
themeVariables: isDark ? {
fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif",
primaryColor: "#2d2d2d",
primaryBorderColor: "#2899f5",
primaryTextColor: "#f3f2f1",
lineColor: "#797775",
lineWidth: 2.5,
secondaryColor: "#3c3c3c",
tertiaryColor: "#484644",
background: "#1e1e1e",
clusterBkg: "#201f1e",
clusterBorder: "#605e5c",
edgeLabelBackground: "#252423",
nodeBkg: "#2d2d2d",
nodeBorder: "#2899f5",
nodeTextColor: "#f3f2f1",
mainBkg: "#1e1e1e",
textColor: "#f3f2f1",
arrowheadColor: "#797775",
fontSize: "14px"
} : {
fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif",
primaryColor: "#f0f8ff",
primaryBorderColor: "#0078d4",
primaryTextColor: "#323130",
lineColor: "#8a8886",
lineWidth: 2.5,
secondaryColor: "#deecf9",
tertiaryColor: "#c7e0f4",
background: "#ffffff",
clusterBkg: "#f3f2f1",
clusterBorder: "#a19f9d",
edgeLabelBackground: "#ffffff",
nodeBkg: "#f0f8ff",
nodeBorder: "#0078d4",
nodeTextColor: "#323130",
mainBkg: "#ffffff",
textColor: "#323130",
arrowheadColor: "#8a8886",
fontSize: "14px"
},
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: "basis"
}
});
try {
const { svg } = await mermaid.render("diagram", mermaidSource);
// Убираем только встроенные стили Mermaid, оставляя структуру
const cleanedSvg = svg.replace(/