Skip to content

Commit a2b5cb2

Browse files
ozgesolidkeyclaude
andcommitted
Add JSON formatting and syntax highlighting
Features: - JSON toggle button in toolbar to enable/disable formatting - Automatic detection of JSON objects in log lines - Pretty-print JSON with indentation - Syntax highlighting colors: - Keys: blue (#9cdcfe) - Strings: orange (#ce9178) - Numbers: green (#b5cea8) - Booleans: blue (#569cd6) - Null: blue italic Also adds vitest.config.ts to exclude dist folder from tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c5e8408 commit a2b5cb2

4 files changed

Lines changed: 144 additions & 7 deletions

File tree

src/renderer/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<button id="btn-analyze" class="toolbar-btn" title="Analyze file" disabled>Analyze</button>
4343
<button id="btn-columns" class="toolbar-btn" title="Show/hide columns" disabled>Columns</button>
4444
<button id="btn-word-wrap" class="toolbar-btn" title="Toggle word wrap (⌥Z)">Wrap</button>
45+
<button id="btn-json-format" class="toolbar-btn" title="Format & highlight JSON in logs">JSON</button>
4546
</div>
4647
<div class="toolbar-right">
4748
<button id="btn-settings" class="toolbar-btn small" title="Settings">&#9881;</button>

src/renderer/renderer.ts

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,77 @@ function applySettings(): void {
355355
// Word wrap setting
356356
let wordWrapEnabled = false;
357357

358+
// JSON formatting setting
359+
let jsonFormattingEnabled = false;
360+
361+
// Check if text contains JSON
362+
function containsJson(text: string): boolean {
363+
// Quick check for JSON-like content
364+
return text.includes('{') && text.includes('}');
365+
}
366+
367+
// Format JSON with syntax highlighting
368+
function formatJsonContent(text: string): string {
369+
// Try to find JSON objects or arrays in the line
370+
const jsonPattern = /(\{[\s\S]*\}|\[[\s\S]*\])/g;
371+
let result = '';
372+
let lastIndex = 0;
373+
let match;
374+
375+
while ((match = jsonPattern.exec(text)) !== null) {
376+
// Add text before the JSON
377+
result += escapeHtml(text.slice(lastIndex, match.index));
378+
379+
try {
380+
// Try to parse and format the JSON
381+
const jsonStr = match[1];
382+
const parsed = JSON.parse(jsonStr);
383+
const formatted = syntaxHighlightJson(JSON.stringify(parsed, null, 2));
384+
result += `<span class="json-block">${formatted}</span>`;
385+
} catch {
386+
// Not valid JSON, just escape it
387+
result += escapeHtml(match[1]);
388+
}
389+
390+
lastIndex = match.index + match[0].length;
391+
}
392+
393+
// Add remaining text
394+
result += escapeHtml(text.slice(lastIndex));
395+
return result;
396+
}
397+
398+
// Apply syntax highlighting to JSON string
399+
function syntaxHighlightJson(json: string): string {
400+
// Escape HTML first
401+
json = json
402+
.replace(/&/g, '&amp;')
403+
.replace(/</g, '&lt;')
404+
.replace(/>/g, '&gt;');
405+
406+
// Apply syntax highlighting
407+
return json.replace(
408+
/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
409+
(match) => {
410+
let cls = 'json-number';
411+
if (/^"/.test(match)) {
412+
if (/:$/.test(match)) {
413+
cls = 'json-key';
414+
// Remove the colon from the match for cleaner display
415+
return `<span class="${cls}">${match.slice(0, -1)}</span>:`;
416+
} else {
417+
cls = 'json-string';
418+
}
419+
} else if (/true|false/.test(match)) {
420+
cls = 'json-boolean';
421+
} else if (/null/.test(match)) {
422+
cls = 'json-null';
423+
}
424+
return `<span class="${cls}">${match}</span>`;
425+
}
426+
);
427+
}
428+
358429
// Markdown preview
359430
let isMarkdownFile = false;
360431
let markdownPreviewMode = true; // Start in preview mode for md files
@@ -438,6 +509,7 @@ const elements = {
438509
// Columns
439510
btnColumns: document.getElementById('btn-columns') as HTMLButtonElement,
440511
btnWordWrap: document.getElementById('btn-word-wrap') as HTMLButtonElement,
512+
btnJsonFormat: document.getElementById('btn-json-format') as HTMLButtonElement,
441513
columnsModal: document.getElementById('columns-modal') as HTMLDivElement,
442514
columnsLoading: document.getElementById('columns-loading') as HTMLDivElement,
443515
columnsContent: document.getElementById('columns-content') as HTMLDivElement,
@@ -1115,9 +1187,15 @@ function createLineElementPooled(line: LogLine): HTMLDivElement {
11151187
// Create content using innerHTML for speed (single parse)
11161188
const lineNumHtml = `<span class="line-number">${line.lineNumber + 1}</span>`;
11171189
const displayText = applyColumnFilter(line.text);
1118-
// Apply search highlights and manual highlights together
1119-
const searchResult = applySearchHighlightsRaw(displayText, line.lineNumber);
1120-
const contentHtml = `<span class="line-content">${applyHighlightsWithSearch(displayText, searchResult.searchRanges)}</span>`;
1190+
1191+
let formattedContent: string;
1192+
if (jsonFormattingEnabled && containsJson(displayText)) {
1193+
formattedContent = formatJsonContent(displayText);
1194+
} else {
1195+
const searchResult = applySearchHighlightsRaw(displayText, line.lineNumber);
1196+
formattedContent = applyHighlightsWithSearch(displayText, searchResult.searchRanges);
1197+
}
1198+
const contentHtml = `<span class="line-content">${formattedContent}</span>`;
11211199
div.innerHTML = lineNumHtml + contentHtml;
11221200

11231201
return div;
@@ -1157,10 +1235,16 @@ function createLineElement(line: LogLine): HTMLDivElement {
11571235

11581236
// Apply column filter, then search highlights, then manual highlights
11591237
const displayText = applyColumnFilter(line.text);
1160-
// Apply search highlights first (on raw text), returns { html, hasSearchMatch }
1161-
const searchResult = applySearchHighlightsRaw(displayText, line.lineNumber);
1162-
// Then apply manual highlights (escapes HTML and adds highlight spans)
1163-
const finalHtml = applyHighlightsWithSearch(displayText, searchResult.searchRanges);
1238+
1239+
let finalHtml: string;
1240+
if (jsonFormattingEnabled && containsJson(displayText)) {
1241+
// JSON formatting takes precedence - apply JSON syntax highlighting
1242+
finalHtml = formatJsonContent(displayText);
1243+
} else {
1244+
// Normal highlighting pipeline
1245+
const searchResult = applySearchHighlightsRaw(displayText, line.lineNumber);
1246+
finalHtml = applyHighlightsWithSearch(displayText, searchResult.searchRanges);
1247+
}
11641248
contentSpan.innerHTML = finalHtml;
11651249

11661250
div.appendChild(lineNumSpan);
@@ -5169,6 +5253,15 @@ function init(): void {
51695253
// Word wrap
51705254
elements.btnWordWrap.addEventListener('click', toggleMarkdownPreview);
51715255

5256+
// JSON formatting toggle
5257+
elements.btnJsonFormat.addEventListener('click', () => {
5258+
jsonFormattingEnabled = !jsonFormattingEnabled;
5259+
elements.btnJsonFormat.classList.toggle('active', jsonFormattingEnabled);
5260+
// Re-render visible lines with new formatting
5261+
cachedLines.clear();
5262+
loadVisibleLines();
5263+
});
5264+
51725265
// Split mode and value change handlers
51735266
document.querySelectorAll('input[name="split-mode"]').forEach((radio) => {
51745267
radio.addEventListener('change', updateSplitPreview);

src/renderer/styles.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,41 @@ body {
14011401
border-color: rgba(255, 165, 0, 0.8);
14021402
}
14031403

1404+
/* JSON Syntax Highlighting */
1405+
.json-block {
1406+
display: inline;
1407+
white-space: pre-wrap;
1408+
}
1409+
1410+
.json-key {
1411+
color: #9cdcfe;
1412+
font-weight: 500;
1413+
}
1414+
1415+
.json-string {
1416+
color: #ce9178;
1417+
}
1418+
1419+
.json-number {
1420+
color: #b5cea8;
1421+
}
1422+
1423+
.json-boolean {
1424+
color: #569cd6;
1425+
font-weight: 500;
1426+
}
1427+
1428+
.json-null {
1429+
color: #569cd6;
1430+
font-style: italic;
1431+
}
1432+
1433+
/* JSON toggle button active state */
1434+
#btn-json-format.active {
1435+
background-color: var(--accent-color);
1436+
color: white;
1437+
}
1438+
14041439
/* Status Bar */
14051440
.status-bar {
14061441
display: flex;

vitest.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
include: ['src/tests/**/*.test.ts'],
6+
exclude: ['node_modules', 'dist'],
7+
},
8+
});

0 commit comments

Comments
 (0)