1/* ========================= 2 NEWS & EVENT CSV VIEWER 3 - Top page: latest news/event 4 - Archive page: fiscal-year grouping + filters 5========================= */ 6 7const newsEventsScript = 8 document.currentScript || document.querySelector('script[src$="news-events.js"]'); 9 10const NEWS_CSV_PATH = newsEventsScript?.dataset?.csv || '../data/news_events.csv'; 11const LATEST_COUNT = Number(newsEventsScript?.dataset?.latestCount || 3); 12 13let NEWS_EVENTS_ITEMS = []; 14 15document.addEventListener('DOMContentLoaded', async () => { 16 const latestNewsList = document.getElementById('latest-news-list'); 17 const latestEventList = document.getElementById('latest-event-list'); 18 const archiveList = document.getElementById('news-archive-list'); 19 20 if (!latestNewsList && !latestEventList && !archiveList) { 21 return; 22 } 23 24 try { 25 const csvText = await fetchCsv(NEWS_CSV_PATH); 26 27 NEWS_EVENTS_ITEMS = parseCsv(csvText) 28 .filter((item) => (item.status || 'published') !== 'draft') 29 .sort(compareByDateDesc); 30 31 if (latestNewsList) { 32 renderLatestItems( 33 latestNewsList, 34 NEWS_EVENTS_ITEMS.filter((item) => item.type === 'news').slice(0, LATEST_COUNT) 35 ); 36 } 37 38 if (latestEventList) { 39 renderLatestItems( 40 latestEventList, 41 NEWS_EVENTS_ITEMS.filter((item) => item.type === 'event').slice(0, LATEST_COUNT) 42 ); 43 } 44 45 if (archiveList) { 46 setupArchivePage(); 47 } 48 } catch (error) { 49 console.error('Failed to load news_events.csv:', error); 50 51 if (latestNewsList) { 52 latestNewsList.innerHTML = '<p class="news-error">Failed to load news.</p>'; 53 } 54 55 if (latestEventList) { 56 latestEventList.innerHTML = '<p class="news-error">Failed to load events.</p>'; 57 } 58 59 if (archiveList) { 60 archiveList.innerHTML = '<p class="news-error">Failed to load news and events.</p>'; 61 } 62 } 63}); 64 65async function fetchCsv(path) { 66 const response = await fetch(path); 67 68 if (!response.ok) { 69 throw new Error(`CSV fetch failed: ${response.status}`); 70 } 71 72 return await response.text(); 73} 74 75function parseCsv(csvText) { 76 const rows = []; 77 let row = []; 78 let value = ''; 79 let insideQuotes = false; 80 81 for (let i = 0; i < csvText.length; i++) { 82 const char = csvText[i]; 83 const nextChar = csvText[i + 1]; 84 85 if (char === '"' && insideQuotes && nextChar === '"') { 86 value += '"'; 87 i++; 88 continue; 89 } 90 91 if (char === '"') { 92 insideQuotes = !insideQuotes; 93 continue; 94 } 95 96 if (char === ',' && !insideQuotes) { 97 row.push(value); 98 value = ''; 99 continue; 100 } 101 102 if ((char === '\n' || char === '\r') && !insideQuotes) { 103 if (char === '\r' && nextChar === '\n') { 104 i++; 105 } 106 107 row.push(value); 108 109 if (row.some((cell) => cell.trim() !== '')) { 110 rows.push(row); 111 } 112 113 row = []; 114 value = ''; 115 continue; 116 } 117 118 value += char; 119 } 120 121 if (value || row.length > 0) { 122 row.push(value); 123 rows.push(row); 124 } 125 126 if (!rows.length) { 127 return []; 128 } 129 130 const headers = rows.shift().map((header) => header.trim().replace(/^\uFEFF/, '')); 131 132 return rows.map((rowData) => { 133 const item = {}; 134 135 headers.forEach((header, index) => { 136 item[header] = rowData[index] ? rowData[index].trim() : ''; 137 }); 138 139 return item; 140 }); 141} 142 143function renderLatestItems(container, items) { 144 if (!items.length) { 145 container.innerHTML = '<p class="news-empty">No information is available.</p>'; 146 return; 147 } 148 149 container.innerHTML = items 150 .map((item) => { 151 const url = getItemUrl(item); 152 153 return ` 154 <article class="news-item"> 155 <time datetime="${escapeHtml(item.date)}"> 156 ${escapeHtml(formatDate(item.date))} 157 </time> 158 <p class="news-category"> 159 ${escapeHtml(item.category || getTypeLabel(item.type))} 160 </p> 161 <a href="${escapeHtml(url)}" target="_blank" rel="noopener"> 162 ${escapeHtml(item.title || '')} 163 </a> 164 </article> 165 `; 166 }) 167 .join(''); 168} 169 170function setupArchivePage() { 171 const typeSelect = document.getElementById('news-type-filter'); 172 const yearSelect = document.getElementById('news-year-filter'); 173 const categorySelect = document.getElementById('news-category-filter'); 174 const searchInput = document.getElementById('news-keyword-input'); 175 176 setupTypeFromQuery(typeSelect); 177 setupSelectOptions(yearSelect, getUniqueValues(NEWS_EVENTS_ITEMS, 'fiscal_year'), 'All Fiscal Years'); 178 setupSelectOptions(categorySelect, getUniqueValues(NEWS_EVENTS_ITEMS, 'category'), 'All Categories'); 179 180 [typeSelect, yearSelect, categorySelect, searchInput].forEach((element) => { 181 if (element) { 182 element.addEventListener('change', renderArchiveItems); 183 element.addEventListener('input', renderArchiveItems); 184 } 185 }); 186 187 renderArchiveItems();
188} 189 190function setupTypeFromQuery(typeSelect) { 191 if (!typeSelect) return; 192 193 const params = new URLSearchParams(window.location.search); 194 const type = params.get('type'); 195 196 if (type === 'news' || type === 'event') { 197 typeSelect.value = type; 198 } 199} 200 201function setupSelectOptions(select, values, defaultLabel) { 202 if (!select) return; 203 204 const current = select.value; 205 select.innerHTML = `<option value="all">${escapeHtml(defaultLabel)}</option>`; 206 207 values.forEach((value) => { 208 if (!value) return; 209 210 const option = document.createElement('option'); 211 option.value = value; 212 option.textContent = value; 213 select.appendChild(option); 214 }); 215 216 if ([...select.options].some((option) => option.value === current)) { 217 select.value = current; 218 } 219} 220 221function renderArchiveItems() { 222 const archiveList = document.getElementById('news-archive-list'); 223 const resultCount = document.getElementById('news-archive-count'); 224 const typeValue = document.getElementById('news-type-filter')?.value || 'all'; 225 const yearValue = document.getElementById('news-year-filter')?.value || 'all'; 226 const categoryValue = document.getElementById('news-category-filter')?.value || 'all'; 227 const keyword = (document.getElementById('news-keyword-input')?.value || '').trim().toLowerCase(); 228 229 if (!archiveList) return; 230 231 const filtered = NEWS_EVENTS_ITEMS.filter((item) => { 232 const matchesType = typeValue === 'all' || item.type === typeValue; 233 const matchesYear = yearValue === 'all' || item.fiscal_year === yearValue; 234 const matchesCategory = categoryValue === 'all' || item.category === categoryValue; 235 236 const searchable = [ 237 item.title, 238 item.summary, 239 item.category, 240 item.fiscal_year, 241 item.date, 242 item.type, 243 item.notes 244 ] 245 .join(' ') 246 .toLowerCase(); 247 248 const matchesKeyword = !keyword || searchable.includes(keyword); 249 250 return matchesType && matchesYear && matchesCategory && matchesKeyword; 251 }); 252 253 if (resultCount) { 254 resultCount.textContent = `${filtered.length} items`; 255 } 256 257 if (!filtered.length) { 258 archiveList.innerHTML = '<p class="news-empty">No information matches the selected criteria.</p>'; 259 return; 260 } 261 262 const grouped = groupByFiscalYear(filtered); 263 264 archiveList.innerHTML = Object.keys(grouped) 265 .sort((a, b) => Number(b) - Number(a)) 266 .map((year) => ` 267 <section class="news-year-block"> 268 <h2 class="news-year-title">FY ${escapeHtml(year)}</h2> 269 <div class="news-archive-list"> 270 ${grouped[year].map(renderArchiveItem).join('')} 271 </div> 272 </section> 273 `) 274 .join(''); 275} 276 277function renderArchiveItem(item) { 278 const url = getItemUrl(item); 279 280 return ` 281 <article class="news-archive-item"> 282 <a href="${escapeHtml(url)}" target="_blank" rel="noopener" class="news-archive-link"> 283 <div class="news-archive-meta"> 284 <span class="news-archive-type">${escapeHtml(getTypeLabel(item.type))}</span> 285 <span class="news-archive-date">${escapeHtml(formatDate(item.date))}</span> 286 <span class="news-archive-category">${escapeHtml(item.category || '')}</span> 287 </div> 288 <h3>${escapeHtml(item.title || '')}</h3> 289 ${item.summary ? `<p class="news-archive-summary">${escapeHtml(item.summary)}</p>` : ''} 290 <span class="news-archive-source">View on Original Site</span> 291 </a> 292 </article> 293 `; 294} 295 296function groupByFiscalYear(items) { 297 return items.reduce((acc, item) => { 298 const year = item.fiscal_year || (item.date || '').slice(0, 4) || 'Other'; 299 acc[year] = acc[year] || []; 300 acc[year].push(item); 301 return acc; 302 }, {}); 303} 304 305function getUniqueValues(items, key) { 306 return [...new Set(items.map((item) => item[key]).filter(Boolean))] 307 .sort((a, b) => String(b).localeCompare(String(a), 'ja')); 308} 309 310function compareByDateDesc(a, b) { 311 const dateA = new Date(a.date || '1900-01-01').getTime(); 312 const dateB = new Date(b.date || '1900-01-01').getTime(); 313 return dateB - dateA; 314} 315 316function getItemUrl(item) { 317 return item.item_url || item.source_url || '#'; 318} 319 320function getTypeLabel(type) { 321 if (type === 'news') return 'NEWS'; 322 if (type === 'event') return 'EVENT'; 323 return 'INFO'; 324} 325 326function formatDate(dateText) { 327 if (!dateText) return ''; 328 return dateText.replaceAll('-', '/'); 329} 330 331function escapeHtml(value) { 332 return String(value ?? '') 333 .replaceAll('&', '&') 334 .replaceAll('<', '<') 335 .replaceAll('>', '>') 336 .replaceAll('"', '"') 337 .replaceAll("'", '''); 338}
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.