@@ -304,9 +304,9 @@ <h1 id="title"></h1>
304304 let monthHtml = '<div class="heatmap-months">' ; monthSpans . forEach ( ms => { monthHtml += `<span style="flex:${ ms . count } ${ ms . count } 0;text-align:left;">${ ms . label } </span>` ; } ) ; monthHtml += '</div>' ;
305305 let dayLabelsHtml = '<div class="heatmap-day-labels">' ; [ 'Mon' , '' , 'Wed' , '' , 'Fri' , '' , '' ] . forEach ( l => { dayLabelsHtml += `<span>${ l } </span>` ; } ) ; dayLabelsHtml += '</div>' ;
306306 const baseUrl = D . githubBaseUrl ;
307- const branch = D . defaultBranch || 'main' ;
307+ const branchPath = ( D . defaultBranch || 'main' ) . split ( '/' ) . map ( encodeURIComponent ) . join ( '/' ) ;
308308 let gridHtml = '<div class="heatmap">' ;
309- weeksArr . forEach ( ( week , wi ) => { gridHtml += '<div class="heatmap-week">' ; if ( wi === 0 && week [ 0 ] . dow > 0 ) for ( let i = 0 ; i < week [ 0 ] . dow ; i ++ ) gridHtml += '<div class="heatmap-cell" style="visibility:hidden;"></div>' ; week . forEach ( day => { const bg = cellColor ( day . count ) ; const href = baseUrl ? `${ baseUrl } /commits/${ branch } ?after=&since=${ day . key } &until=${ day . key } ` : '#' ; gridHtml += `<a href="${ href } " target="_blank" class="heatmap-cell" style="background:${ bg } ;" data-date="${ day . key } " data-count="${ day . count } " data-color="${ bg } "></a>` ; } ) ; if ( wi === weeksArr . length - 1 && week [ week . length - 1 ] . dow < 6 ) for ( let i = week [ week . length - 1 ] . dow + 1 ; i <= 6 ; i ++ ) gridHtml += '<div class="heatmap-cell" style="visibility:hidden;"></div>' ; gridHtml += '</div>' ; } ) ;
309+ weeksArr . forEach ( ( week , wi ) => { gridHtml += '<div class="heatmap-week">' ; if ( wi === 0 && week [ 0 ] . dow > 0 ) for ( let i = 0 ; i < week [ 0 ] . dow ; i ++ ) gridHtml += '<div class="heatmap-cell" style="visibility:hidden;"></div>' ; week . forEach ( day => { const bg = cellColor ( day . count ) ; const href = baseUrl ? `${ baseUrl } /commits/${ branchPath } ?after=&since=${ day . key } &until=${ day . key } ` : '#' ; gridHtml += `<a href="${ href } " target="_blank" class="heatmap-cell" style="background:${ bg } ;" data-date="${ day . key } " data-count="${ day . count } " data-color="${ bg } "></a>` ; } ) ; if ( wi === weeksArr . length - 1 && week [ week . length - 1 ] . dow < 6 ) for ( let i = week [ week . length - 1 ] . dow + 1 ; i <= 6 ; i ++ ) gridHtml += '<div class="heatmap-cell" style="visibility:hidden;"></div>' ; gridHtml += '</div>' ; } ) ;
310310 gridHtml += '</div>' ;
311311 const legendHtml = `<div class="heatmap-legend"><span>Less</span><div class="heatmap-cell" style="background:var(--bg-empty-cell);"></div><div class="heatmap-cell" style="background:${ shade ( 0.25 ) } ;"></div><div class="heatmap-cell" style="background:${ shade ( 0.5 ) } ;"></div><div class="heatmap-cell" style="background:${ shade ( 0.75 ) } ;"></div><div class="heatmap-cell" style="background:${ color } ;"></div><span>More</span></div>` ;
312312 container . innerHTML = `<div class="heatmap-wrap">${ monthHtml } <div class="heatmap-grid">${ dayLabelsHtml } ${ gridHtml } </div>${ legendHtml } </div>` ;
@@ -359,10 +359,12 @@ <h1 id="title"></h1>
359359// === COMMIT TIMELINE ===
360360function escapeHtml ( s ) { return String ( s ) . replace ( / [ & < > " ' ] / g, m => ( { '&' :'&' , '<' :'<' , '>' :'>' , '"' :'"' , "'" :''' } [ m ] ) ) ; }
361361
362+ // GitHub branch paths preserve slashes (`feature/foo`), so encode segment-wise.
363+ function encodeBranch ( b ) { return ( b || 'main' ) . split ( '/' ) . map ( encodeURIComponent ) . join ( '/' ) ; }
364+
362365function authorUrl ( c ) {
363366 if ( ! D . githubBaseUrl ) return '#' ;
364- const branch = D . defaultBranch || 'main' ;
365- return `${ D . githubBaseUrl } /commits/${ branch } ?author=${ encodeURIComponent ( c . email ) } ` ;
367+ return `${ D . githubBaseUrl } /commits/${ encodeBranch ( D . defaultBranch ) } ?author=${ encodeURIComponent ( c . email ) } ` ;
366368}
367369
368370const authorPopover = document . createElement ( 'div' ) ;
@@ -375,14 +377,29 @@ <h1 id="title"></h1>
375377 return m ? '@' + m [ 1 ] : '' ;
376378}
377379
380+ // Reads data-bg/data-initial from the failed <img> and swaps in a styled fallback.
381+ // Doing this from a real event listener avoids the inline-onerror double-decoding
382+ // trap where a name starting with a single quote would break the JS string literal.
383+ function installAvatarFallback ( img ) {
384+ if ( ! img ) return ;
385+ img . addEventListener ( 'error' , ( ) => {
386+ const tag = img . dataset . fallbackTag || 'div' ;
387+ const fb = document . createElement ( tag ) ;
388+ fb . className = img . dataset . fallbackClass || '' ;
389+ if ( img . dataset . bg ) fb . style . background = img . dataset . bg ;
390+ if ( img . dataset . initial ) fb . textContent = img . dataset . initial ;
391+ img . replaceWith ( fb ) ;
392+ } ) ;
393+ }
394+
378395function showAuthorPopover ( idx , el ) {
379396 const c = contributors [ idx ] ;
380397 if ( ! c ) return ;
381398 const av = c . avatarUrl ;
382399 const handle = githubHandleFromAvatar ( av ) ;
383400 const initial = ( c . name || '?' ) . trim ( ) . charAt ( 0 ) . toUpperCase ( ) ;
384401 const avatarHtml = av
385- ? `<img class="lp-avatar" src="${ escapeHtml ( av ) } " alt="" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:' lp-avatar-fallback',style:'background: ${ clr ( idx ) } ',textContent:' ${ escapeHtml ( initial ) } '})) ">`
402+ ? `<img class="lp-avatar" src="${ escapeHtml ( av ) } " alt="" data-fallback-class=" lp-avatar-fallback" data-bg=" ${ escapeHtml ( clr ( idx ) ) } " data-initial=" ${ escapeHtml ( initial ) } ">`
386403 : `<div class="lp-avatar-fallback" style="background:${ clr ( idx ) } ">${ escapeHtml ( initial ) } </div>` ;
387404 const net = c . added - c . deleted ;
388405 const netHtml = net > 0
@@ -396,6 +413,7 @@ <h1 id="title"></h1>
396413 `<div class="lp-stats">${ fmt ( c . commits ) } commits · ${ c . activeDays } active day${ c . activeDays === 1 ? '' : 's' } </div>` +
397414 `<div class="lp-stats"><span class="add">+${ fmt ( c . added ) } </span> <span class="del">-${ fmt ( c . deleted ) } </span> (net ${ netHtml } )</div>` +
398415 `<div class="lp-period">${ c . first } — ${ c . last } </div>` ;
416+ installAvatarFallback ( authorPopover . querySelector ( '.lp-avatar' ) ) ;
399417
400418 const rect = el . getBoundingClientRect ( ) ;
401419 authorPopover . style . left = ( rect . right + 12 ) + 'px' ;
@@ -459,7 +477,8 @@ <h1 id="title"></h1>
459477 } ) ;
460478
461479 // One-time precomputation. r/y depend on zoom so they're computed in drawCanvas.
462- const sorted = commits . slice ( ) . sort ( ( a , b ) => a . d . localeCompare ( b . d ) ) ;
480+ // Sort by parsed UTC instant — lex-sort on ISO mis-orders across timezone offsets.
481+ const sorted = commits . slice ( ) . sort ( ( a , b ) => new Date ( a . d ) - new Date ( b . d ) ) ;
463482 const dayStackTmp = new Map ( ) ;
464483 const drawables = [ ] ;
465484 sorted . forEach ( c => {
@@ -884,12 +903,13 @@ <h1 id="title"></h1>
884903 const timeStr = dt . toLocaleTimeString ( 'en-GB' , { hour : '2-digit' , minute : '2-digit' } ) ;
885904 const av = author . avatarUrl ;
886905 const avatarHtml = av
887- ? `<img class="tt-avatar" src="${ escapeHtml ( av ) } " alt="" onerror="this.replaceWith(Object.assign(document.createElement(' span'),{className:' tt-dot',style:'background: ${ hit . color } '})) ">`
906+ ? `<img class="tt-avatar" src="${ escapeHtml ( av ) } " alt="" data-fallback-tag=" span" data-fallback-class=" tt-dot" data-bg=" ${ escapeHtml ( hit . color ) } ">`
888907 : `<span class="tt-dot" style="background:${ hit . color } "></span>` ;
889908 tooltip . innerHTML =
890909 `<div class="tt-author-row">${ avatarHtml } <span class="tt-author">${ escapeHtml ( author . name ) } </span></div>` +
891910 `<div class="tt-subject">${ escapeHtml ( c . s || '' ) } </div>` +
892911 `<div class="tt-meta">${ dateStr } ${ timeStr } · <span style="color:${ colorAdded } ">+${ fmt ( c . a || 0 ) } </span> <span style="color:${ colorDeleted } ">-${ fmt ( c . l || 0 ) } </span> · <span class="tt-hash">${ c . h } </span></div>` ;
912+ installAvatarFallback ( tooltip . querySelector ( '.tt-avatar' ) ) ;
893913 tooltip . style . left = clientX + 'px' ;
894914 tooltip . style . top = clientY + 'px' ;
895915 tooltip . classList . add ( 'visible' ) ;
0 commit comments