1document.addEventListener('DOMContentLoaded', () => { 2 const repoGrid = document.getElementById('repo-grid'); 3 const pinnedSource = '/data/repos.json'; 4 const publicationsList = document.getElementById('pub-list'); 5 const publicationsUpdated = document.querySelector('.pub-updated'); 6 const publicationsSource = '/data/publications.json'; 7 8 function setRepoMessage(message) { 9 if (!repoGrid) return; 10 repoGrid.innerHTML = `<div class="repo-card is-loading">${message}</div>`; 11 } 12 13 function formatCount(value) { 14 const count = Number(value); 15 return Number.isFinite(count) ? count.toLocaleString('en-US') : '0'; 16 } 17 18 function renderRepos(repos) { 19 if (!repoGrid) return; 20 if (!Array.isArray(repos) || repos.length === 0) { 21 setRepoMessage('No pinned repositories found.'); 22 return; 23 } 24 25 repoGrid.innerHTML = repos.map((repo) => { 26 const url = escapeHtml(safeLink(repo.url)); 27 const name = escapeHtml(repo.name); 28 const description = escapeHtml(repo.description) || 'No description provided.'; 29 const language = escapeHtml(repo.language) || 'N/A'; 30 31 return ` 32 <article class="repo-card"> 33 <h3 class="repo-title"><a href="${url}" target="_blank" rel="noopener noreferrer">${name}</a></h3> 34 <p class="repo-desc">${description}</p> 35 <div class="repo-meta"> 36 <span>Stars: ${formatCount(repo.stars)}</span> 37 <span>Forks: ${formatCount(repo.forks)}</span> 38 <span>${language}</span> 39 </div> 40 </article> 41 `; 42 }).join(''); 43 } 44 45 async function loadPinnedRepos() { 46 try { 47 const response = await fetch(pinnedSource, { cache: 'no-store' }); 48 if (!response.ok) { 49 throw new Error(`Pinned repo fetch error: ${response.status}`); 50 } 51 const payload = await response.json(); 52 renderRepos(payload && payload.items); 53 } catch (error) { 54 console.error('Failed to load pinned repositories:', error); 55 setRepoMessage('Unable to load pinned repositories right now.'); 56 } 57 } 58 59 if (repoGrid) { 60 loadPinnedRepos(); 61 } 62 63 function escapeHtml(value) { 64 return String(value || '') 65 .replace(/&/g, '&') 66 .replace(/</g, '<') 67 .replace(/>/g, '>') 68 .replace(/"/g, '"') 69 .replace(/'/g, '''); 70 } 71 72 function safeLink(url) { 73 if (!url) return ''; 74 try { 75 const parsed = new URL(url, window.location.href); 76 if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { 77 return parsed.href; 78 } 79 } catch (error) { 80 return ''; 81 } 82 return ''; 83 } 84 85 function formatUpdatedDate(value) { 86 if (!value) return ''; 87 const parts = value.split('-').map((part) => Number(part)); 88 if (parts.length === 3 && parts.every((part) => !Number.isNaN(part))) { 89 const parsed = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2])); 90 return new Intl.DateTimeFormat('en-US', { 91 year: 'numeric', 92 month: 'short', 93 day: 'numeric', 94 timeZone: 'UTC', 95 }).format(parsed); 96 } 97 const parsed = new Date(value); 98 if (Number.isNaN(parsed.getTime())) return value; 99 return parsed.toLocaleDateString('en-US', { 100 year: 'numeric', 101 month: 'short', 102 day: 'numeric', 103 }); 104 } 105 106 function renderPubTags(item) { 107 const tags = []; 108 if (item.paper) { 109 tags.push(`<a href="${escapeHtml(item.paper)}" target="_blank" rel="noopener noreferrer"><span class="tag-paper"><img src="/arxiv-logo.png" alt="arXiv" class="arxiv-logo"/> Paper</span></a>`); 110 } 111 if (item.blog) { 112 tags.push(`<a href="${escapeHtml(item.blog)}" target="_blank" rel="noopener noreferrer"><span class="tag-link"><svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 4.5h-2a3 3 0 0 0 0 6h2"/><path d="M9.5 4.5h2a3 3 0 0 1 0 6h-2"/><path d="M5.5 8h5"/></svg> Blog</span></a>`); 113 } 114 if (item.code) { 115 tags.push(`<a href="${escapeHtml(item.code)}" target="_blank" rel="noopener noreferrer"><span class="tag-code"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
115 Code</span></a>`); 116 } 117 if (item.tweet) { 118 tags.push(`<a href="${escapeHtml(item.tweet)}" target="_blank" rel="noopener noreferrer"><span class="tag-tweet"><svg viewBox="0 0 16 16" width="12" height="12"><text x="3" y="12" font-family="serif" font-weight="bold" font-size="12" fill="currentColor">ð</text></svg> Tweet</span></a>`); 119 } 120 if (tags.length === 0) return ''; 121 return `<div class="pub-tags">${tags.join('')}</div>`; 122 } 123 124 function renderPublications(items) { 125 if (!publicationsList) return; 126 if (!Array.isArray(items) || items.length === 0) return; 127 128 publicationsList.innerHTML = items.map((item) => { 129 const title = escapeHtml(item.title); 130 const authors = escapeHtml(item.authors); 131 const venue = escapeHtml(item.venue); 132 const year = escapeHtml(item.year || '--'); 133 const link = escapeHtml(safeLink(item.link)); 134 const titleMarkup = link 135 ? `<a href="${link}" target="_blank" rel="noopener noreferrer">${title}</a>` 136 : title; 137 const authorsMarkup = authors ? `<p class="pub-authors">${authors}</p>` : ''; 138 const venueHtml = venue ? `<p class="pub-venue">${venue}</p>` : ''; 139 const tagsHtml = renderPubTags(item); 140 const bottomMarkup = (venueHtml || tagsHtml) 141 ? `<div class="pub-bottom">${venueHtml}${tagsHtml}</div>` 142 : ''; 143 144 return ` 145 <li class="pub-item"> 146 <span class="pub-year">${year}</span> 147 <div class="pub-info"> 148 <p class="pub-title">${titleMarkup}</p> 149 ${authorsMarkup} 150 ${bottomMarkup} 151 </div> 152 </li> 153 `; 154 }).join(''); 155 } 156 157 async function loadPublications() { 158 if (!publicationsList) return; 159 try { 160 const response = await fetch(publicationsSource, { cache: 'no-store' }); 161 if (!response.ok) { 162 throw new Error(`Publications fetch error: ${response.status}`); 163 } 164 const payload = await response.json(); 165 if (payload && payload.items) { 166 renderPublications(payload.items); 167 } 168 if (publicationsUpdated && payload && payload.updated) { 169 publicationsUpdated.textContent = `Updated ${formatUpdatedDate(payload.updated)}.`; 170 } 171 } catch (error) { 172 console.error('Failed to load publications:', error); 173 } 174 } 175 176 if (publicationsList) { 177 loadPublications(); 178 } 179});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.