@@ -47,7 +47,20 @@ function scrollToHeight(top, smoothScrollLimit = 10000) {
window . scrollTo ( { left : 0 , top , behavior } ) ;
}
/* error collapse toggle */
/*
* Error collapse toggle.
*
* Smart fold: every entry within ±ERROR_WINDOW_SIZE of an error stays
* visible; runs of non-error entries with no nearby errors collapse into
* a draggable fold bar. Vertical drag on the bar progressively reveals or
* re-hides lines; a click without drag reveals the next DEFAULT_CLICK_REVEAL
* lines.
*/
const ERROR _WINDOW _SIZE = 25 ;
const PIXELS _PER _LINE _DRAG = 6 ;
const DEFAULT _CLICK _REVEAL = 25 ;
const DRAG _PIXEL _THRESHOLD = 3 ;
const toggleErrorsButton = document . getElementById ( "error-toggle" ) ;
if ( toggleErrorsButton ) {
toggleErrorsButton . addEventListener ( "click" , toggleErrors ) ;
@@ -64,76 +77,134 @@ function toggleErrors() {
}
function collapseAllErrors ( ) {
let firstNoErrorLine = false ;
let lines = document . querySelectorAll ( '.log-inner > .entry' ) ;
let totalLines = lines . length ;
for ( const [ i , line ] of lines . entries ( ) ) {
let lineNumber = line . querySelector ( ".line-number" ) . innerHTML ;
if ( line . classList . contains ( "entry-no-error" ) ) {
line . style . display = "none" ;
const lines = Array . from ( document . querySelectorAll ( '.log-inner > .entry' ) ) ;
const total = lines . length ;
if ( ! total ) return ;
if ( firstNoErrorLine === false ) {
firstNoErrorLine = lineNumber ;
}
if ( i + 1 === totalLines && firstNoErrorLine ) {
line . insertAdjacentElement ( "afterend" , generateCollapsedLines ( firstNoErrorLine , lineNumber ) ) ;
}
} else {
if ( firstNoErrorLine ) {
line . insertAdjacentElement ( "beforebegin" , generateCollapsedLines ( firstNoErrorLine , lineNumber - 1 ) ) ;
firstNoErrorLine = false ;
}
// Pass 1: mark entries within ±ERROR_WINDOW_SIZE of any error.
const mustShow = new Array ( total ) . fill ( false ) ;
for ( let i = 0 ; i < total ; i ++ ) {
if ( lines [ i ] . classList . contains ( "entry-error" ) ) {
const lo = Math . max ( 0 , i - ERROR _WINDOW _SIZE ) ;
const hi = Math . m in( total - 1 , i + ERROR _WINDOW _SIZE ) ;
for ( let j = lo ; j <= hi ; j ++ ) mustShow [ j ] = true ;
}
}
// Pass 2: hide unmarked entries; emit a fold bar at the START of each run.
let runEntries = [ ] ;
const flushRun = ( ) => {
if ( ! runEntries . length ) return ;
const bar = createFoldBar ( runEntries ) ;
runEntries [ 0 ] . insertAdjacentElement ( "beforebegin" , bar ) ;
runEntries = [ ] ;
} ;
for ( let i = 0 ; i < total ; i ++ ) {
if ( ! mustShow [ i ] ) {
lines [ i ] . style . display = "none" ;
runEntries . push ( lines [ i ] ) ;
} else {
flushRun ( ) ;
}
}
flushRun ( ) ;
}
function uncollapseAllErrors ( ) {
document . querySelectorAll ( '.entry-no-error' ) . forEach ( line => line . style . removeProperty ( "display" ) ) ;
document . querySelectorAll ( ' .collapsed-lines' ) . forEach ( collapsed => collapsed . remove ( ) ) ;
document . querySelectorAll ( ".log-inner > .entry" ) . forEach ( line => line . style . removeProperty ( "display" ) ) ;
document . querySelectorAll ( " .collapsed-lines" ) . forEach ( bar => bar . remove ( ) ) ;
}
function handleCollapsedClick ( e ) {
let collapsed = e . currentTarget ;
let positionElement = document . getElementById ( ` L ${ parseInt ( collapsed . dataset . end ) + 1 } ` ) ;
let position ;
if ( positionElement ) {
position = positionElement . getBoundingClientRect ( ) . top - window . scrollY ;
}
for ( let i = parseInt ( collapsed . dataset . start ) ; i <= parseInt ( collapsed . dataset . end ) ; i ++ ) {
document . getElementById ( ` L ${ i } ` ) . parentElement . parentElement . style . removeProperty ( "display" ) ;
}
if ( positionElement ) {
window . scrollTo ( {
left : 0 ,
top : positionElement . getBoundingClientRect ( ) . top - position - collapsed . offsetHeight ,
behavior : "instant"
} ) ;
}
collapsed . remove ( ) ;
function createFoldBar ( entries ) {
const bar = document . createElement ( "div" ) ;
bar . classList . add ( "collapsed-lines" , "collapsed-lines-foldable" ) ;
bar . appendChild ( document . createElement ( "div" ) ) ; // line-number column slot
const count = document . createElement ( "div" ) ;
count . classList . add ( "collapsed-lines-count" ) ;
bar . appendChild ( count ) ;
bar . _hiddenEntries = entries ;
bar . _revealedCount = 0 ;
renderFoldLabel ( bar ) ;
attachFoldDragHandler ( bar ) ;
return bar ;
}
function g enerateCollapsedLines ( start , end ) {
let count = end - start + 1 ;
let string = count === 1 ? "line" : "lines" ;
function r end erFoldLabel ( bar ) {
const entries = bar . _hiddenEntries ;
const total = entries . length ;
const revealed = bar . _revealedCount ;
const remaining = total - revealed ;
const count = bar . querySelector ( ".collapsed-lines-count" ) ;
count . replaceChildren ( ) ;
let collapsedRow = document . createElement ( "div" ) ;
collapsedRow . classList . add ( "collapsed-lines" ) ;
collapsedRow . dataset . start = start ;
collapsedRow . dataset . end = end ;
collapsedRow . appendChild ( document . createElement ( "div" ) ) ;
collapsedRow . addEventListener ( "click" , handleCollapsedClick ) ;
if ( remaining < = 0 ) return ;
let collapsedLinesCount = docume nt. createElement ( "div" ) ;
collapsedLinesCou nt. classList . add ( "collapsed-lines-count" ) ;
let icon = document . createElement ( "i" ) ;
icon . classList . add ( "fa-solid" , "fa-angle-up" ) ;
collapsedLinesCount . appendChild ( icon ) ;
collapsedLinesCount . append ( ` ${ count } ${ string } ` ) ;
collapsedLinesCount . append ( icon . cloneNode ( ) ) ;
collapsedRow . appendChild ( collapsedLinesCount ) ;
const firstHiddenLine = parseI nt( entries [ revealed ] . querySelector ( ".line-number" ) . innerHTML , 10 ) ;
const lastHiddenLine = parseI nt( entries [ total - 1 ] . querySelector ( ".line-number" ) . innerHTML , 10 ) ;
const word = remaining === 1 ? "line" : "lines" ;
return collapsedRow ;
const grip = document . createElement ( "i" ) ;
grip . className = "fa-solid fa-grip-lines" ;
count . appendChild ( grip ) ;
count . append ( ` ${ remaining } ${ word } hidden · ${ firstHiddenLine } – ${ lastHiddenLine } · drag to reveal ` ) ;
count . appendChild ( grip . cloneNode ( ) ) ;
}
function applyFoldReveal ( bar , n ) {
const entries = bar . _hiddenEntries ;
n = Math . max ( 0 , Math . min ( entries . length , n ) ) ;
if ( n === bar . _revealedCount ) return ;
for ( let i = 0 ; i < entries . length ; i ++ ) {
if ( i < n ) entries [ i ] . style . removeProperty ( "display" ) ;
else entries [ i ] . style . display = "none" ;
}
bar . _revealedCount = n ;
renderFoldLabel ( bar ) ;
}
function attachFoldDragHandler ( bar ) {
let dragging = false ;
let dragStartY = 0 ;
let dragStartReveal = 0 ;
let didDrag = false ;
bar . addEventListener ( "pointerdown" , ( e ) => {
if ( e . button !== 0 ) return ;
dragging = true ;
didDrag = false ;
dragStartY = e . clientY ;
dragStartReveal = bar . _revealedCount ;
bar . setPointerCapture ( e . pointerId ) ;
bar . classList . add ( "collapsed-lines-dragging" ) ;
e . preventDefault ( ) ;
} ) ;
bar . addEventListener ( "pointermove" , ( e ) => {
if ( ! dragging ) return ;
const dy = e . clientY - dragStartY ;
if ( Math . abs ( dy ) >= DRAG _PIXEL _THRESHOLD ) didDrag = true ;
const target = dragStartReveal + Math . round ( dy / PIXELS _PER _LINE _DRAG ) ;
applyFoldReveal ( bar , target ) ;
} ) ;
const finish = ( e ) => {
if ( ! dragging ) return ;
dragging = false ;
try { bar . releasePointerCapture ( e . pointerId ) ; } catch { }
bar . classList . remove ( "collapsed-lines-dragging" ) ;
if ( ! didDrag ) {
// Plain click: reveal the next chunk of lines.
applyFoldReveal ( bar , bar . _revealedCount + DEFAULT _CLICK _REVEAL ) ;
}
if ( bar . _revealedCount >= bar . _hiddenEntries . length ) {
bar . remove ( ) ;
}
} ;
bar . addEventListener ( "pointerup" , finish ) ;
bar . addEventListener ( "pointercancel" , finish ) ;
}
/* convert timestamps */