PageSourceSearch

https://www.atr.jp/js/news-events.js

js atr.jp collected 2026-09-24 09:19:26 UTC 9,719 bytes, 338 lines download raw bytes

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('&', '&amp;')
334    .replaceAll('<', '&lt;')
335    .replaceAll('>', '&gt;')
336    .replaceAll('"', '&quot;')
337    .replaceAll("'", '&#039;');
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.