Skip to content

Commit 578cdd2

Browse files
committed
feat: add resizable columns and syntax highlighting to DataTables report view
1 parent 638b360 commit 578cdd2

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

src/report/utility/dataTables/genDataTablesPage.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Database from "better-sqlite3";
2+
import hljs from "highlight.js";
23

34
interface AnalysisFinding {
45
ruleId: string;
@@ -51,6 +52,18 @@ const severityRank = (sev: string): number => {
5152
}
5253
};
5354

55+
// Render JS code with highlight.js inside a pre/code block
56+
const renderJsCode = (code: string | null | undefined): string => {
57+
const src = code ?? "";
58+
try {
59+
const highlighted = hljs.highlight(src, { language: "javascript", ignoreIllegals: true }).value;
60+
return `<pre class="code-cell"><code class="hljs language-javascript">${highlighted}</code></pre>`;
61+
} catch {
62+
// Fallback to escaped plain text
63+
return `<pre class="code-cell">${escapeHtml(src)}</pre>`;
64+
}
65+
};
66+
5467
const genDataTablesPage = (db: Database.Database): string => {
5568
const findings = db.prepare(`SELECT * FROM analysis_findings`).all() as AnalysisFinding[];
5669
const mapped = db.prepare(`SELECT * FROM mapped`).all() as MappedData[];
@@ -67,7 +80,7 @@ const genDataTablesPage = (db: Database.Database): string => {
6780
<td>${escapeHtml(f.ruleTech)}</td>
6881
<td data-order="${severityRank(f.severity)}">${escapeHtml(f.severity)}</td>
6982
<td>${escapeHtml(f.message)}</td>
70-
<td><pre class="code-cell">${escapeHtml(f.findingLocation)}</pre></td>
83+
<td>${renderJsCode(f.findingLocation)}</td>
7184
</tr>`
7285
)
7386
.join("\n");

src/report/utility/genHtml.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ const html = async (
113113
font-size: 0.85rem;
114114
box-sizing: border-box;
115115
}
116+
/* Column resize styles */
117+
table.display.data-table { table-layout: fixed; }
118+
table.display.data-table thead th { position: relative; }
119+
.th-resizer {
120+
position: absolute;
121+
right: 0;
122+
top: 0;
123+
width: 6px;
124+
height: 100%;
125+
cursor: col-resize;
126+
user-select: none;
127+
opacity: 0;
128+
transition: opacity 0.15s ease-in-out;
129+
}
130+
table.display.data-table thead th:hover .th-resizer { opacity: 1; }
116131
</style>
117132
</head>
118133
<body>
@@ -269,6 +284,50 @@ const html = async (
269284
});
270285
};
271286
287+
const setColWidth = (tableEl, colIndex, widthPx) => {
288+
const nth = colIndex + 1;
289+
const w = Math.max(50, widthPx) + 'px';
290+
const th = tableEl.querySelector('thead tr:first-child th:nth-child(' + nth + ')');
291+
if (th) th.style.width = w;
292+
const filterTh = tableEl.querySelector('thead tr.filter-row th:nth-child(' + nth + ')');
293+
if (filterTh) filterTh.style.width = w;
294+
const tds = tableEl.querySelectorAll('tbody tr td:nth-child(' + nth + ')');
295+
tds.forEach(td => { td.style.width = w; });
296+
};
297+
298+
const addColumnResizers = (tableEl) => {
299+
if (!tableEl || tableEl.dataset.resizers === '1') return;
300+
const ths = tableEl.querySelectorAll('thead tr:first-child th');
301+
ths.forEach((th, idx) => {
302+
if (th.querySelector('.th-resizer')) return;
303+
const handle = document.createElement('div');
304+
handle.className = 'th-resizer';
305+
th.appendChild(handle);
306+
let startX = 0;
307+
let startWidth = 0;
308+
const onMouseMove = (e) => {
309+
const dx = e.pageX - startX;
310+
setColWidth(tableEl, idx, startWidth + dx);
311+
};
312+
const onMouseUp = () => {
313+
document.removeEventListener('mousemove', onMouseMove);
314+
document.removeEventListener('mouseup', onMouseUp);
315+
document.body.style.cursor = '';
316+
};
317+
handle.addEventListener('mousedown', (e) => {
318+
e.preventDefault();
319+
startX = e.pageX;
320+
startWidth = th.offsetWidth;
321+
document.addEventListener('mousemove', onMouseMove);
322+
document.addEventListener('mouseup', onMouseUp);
323+
document.body.style.cursor = 'col-resize';
324+
});
325+
});
326+
tableEl.dataset.resizers = '1';
327+
// Ensure table layout
328+
tableEl.style.tableLayout = 'fixed';
329+
};
330+
272331
const initializeDataTablesIfPresent = async () => {
273332
try {
274333
// Ensure DataTables (ESM or UMD) is present
@@ -289,6 +348,7 @@ const html = async (
289348
ordering: true,
290349
orderMulti: true,
291350
pageLength: 25,
351+
autoWidth: false,
292352
// v1 fallback
293353
dom: 'lfrtip',
294354
// v2 layout API
@@ -351,6 +411,10 @@ const html = async (
351411
thead.appendChild(filterRow);
352412
}
353413
table.dataset.dtInit = '1';
414+
// Attach column resizers after DataTables and filters are in place
415+
addColumnResizers(table);
416+
// Let DataTables recalc
417+
try { if (dt && dt.columns) dt.columns().adjust(); } catch (e) {}
354418
});
355419
} catch (e) {
356420
console.error('DataTables init error', e);

0 commit comments

Comments
 (0)