@@ -355,6 +355,77 @@ function applySettings(): void {
355355// Word wrap setting
356356let 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, '&' )
403+ . replace ( / < / g, '<' )
404+ . replace ( / > / g, '>' ) ;
405+
406+ // Apply syntax highlighting
407+ return json . replace (
408+ / ( " ( \\ u [ a - f A - F 0 - 9 ] { 4 } | \\ [ ^ u ] | [ ^ \\ " ] ) * " ( \s * : ) ? | \b ( t r u e | f a l s e | n u l l ) \b | - ? \d + (?: \. \d * ) ? (?: [ e E ] [ + \- ] ? \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 ( / t r u e | f a l s e / . test ( match ) ) {
420+ cls = 'json-boolean' ;
421+ } else if ( / n u l l / . test ( match ) ) {
422+ cls = 'json-null' ;
423+ }
424+ return `<span class="${ cls } ">${ match } </span>` ;
425+ }
426+ ) ;
427+ }
428+
358429// Markdown preview
359430let isMarkdownFile = false ;
360431let 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 ) ;
0 commit comments