1(function () { 2 function articleHref(slug) { 3 var path = window.location.pathname.replace(/\\/g, '/'); 4 if (/\/pages(\/|$)/i.test(path)) return '../news/' + encodeURIComponent(slug) + '.html'; 5 if (/\/news(\/|$)/i.test(path)) return encodeURIComponent(slug) + '.html'; 6 return 'news/' + encodeURIComponent(slug) + '.html'; 7 } 8 9 function formatDate(iso) { 10 if (!iso) return ''; 11 var d = new Date(iso + 'T12:00:00'); 12 if (isNaN(d.getTime())) return iso; 13 return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); 14 } 15 16 function escapeHtml(s) { 17 return String(s || '') 18 .replace(/&/g, '&') 19 .replace(/</g, '<') 20 .replace(/>/g, '>') 21 .replace(/"/g, '"'); 22 } 23 24 function published(list) { 25 return (list || []).filter(function (a) { 26 return a && a.published !== false && a.slug && a.title; 27 }); 28 } 29 30 function mediaUrl(src) { 31 if (!src) return ''; 32 if (/^https?:\/\//i.test(src) || src.indexOf('data:') === 0) return src; 33 var clean = String(src).replace(/^\.\.\//, ''); 34 var path = window.location.pathname.replace(/\\/g, '/'); 35 if (/\/pages(\/|$)/i.test(path) || /\/news(\/|$)/i.test(path)) return '../' + clean; 36 return clean; 37 } 38 39 function truncate(text, max) { 40 var t = String(text || '').replace(/\s+/g, ' ').trim(); 41 if (t.length <= max) return t; 42 return t.slice(0, max - 1).replace(/\s+\S*$/, '') + 'â¦'; 43 } 44 45 function cardHtml(a, opts) { 46 opts = opts || {}; 47 var excerpt = opts.short ? truncate(a.excerpt || '', 140) : (a.excerpt || ''); 48 var href = articleHref(a.slug); 49 return ( 50 '<article class="news-card reveal is-visible">' + 51 '<a class="news-card-media" href="' + href + '">' + 52 '<img src="' + escapeHtml(mediaUrl(a.image)) + '" alt="" width="640" height="400" loading="lazy">' + 53 '</a>' + 54 '<div class="news-card-body">' + 55 '<h3><a href="' + href + '">' + escapeHtml(a.title) + '</a></h3>' + 56 '<p class="news-date">' + escapeHtml(formatDate(a.date)) + '</p>' + 57 '<p class="news-excerpt">' + escapeHtml(excerpt) + '</p>' + 58 '<a class="read-more" href="' + href + '">read more</a>' + 59 '</div>' + 60 '</article>' 61 ); 62 } 63 64 function renderHome(list) { 65 var el = document.getElementById('home-news-grid'); 66 if (!el) return; 67 var items = published(list).slice(0, 3); 68 if (!items.length) { 69 el.innerHTML = '<p class="lede">News coming soon.</p>'; 70 return; 71 } 72 el.innerHTML = items.map(function (a) { return cardHtml(a, { short: true }); }).join(''); 73 } 74 75 function renderNewsPage(list) { 76 var el = document.getElementById('news-grid'); 77 if (!el) return; 78 var items = published(list); 79 var q = (document.getElementById('news-search') || {}).value || ''; 80 q = String(q).trim().toLowerCase(); 81 if (q) { 82 items = items.filter(function (a) { 83 return ( 84 (a.title || '').toLowerCase().indexOf(q) !== -1 || 85 (a.excerpt || '').toLowerCase().indexOf(q) !== -1 86 ); 87 }); 88 } 89 var count = document.getElementById('news-count'); 90 if (count) count.textContent = items.length + ' article' + (items.length === 1 ? '' : 's'); 91 if (!items.length) { 92 el.innerHTML = '<p class="lede">No articles match your search.</p>'; 93 return; 94 } 95 el.innerHTML = items.map(function (a) { return cardHtml(a); }).join(''); 96 } 97 98 function applyNews(list) { 99 window.__OPS_NEWS__ = list; 100 renderHome(list); 101 renderNewsPage(list); 102 var search = document.getElementById('news-search'); 103 if (search && !search._opsBound) { 104 search._opsBound = true; 105 search.addEventListener('input', function () { 106 renderNewsPage(window.__OPS_NEWS__ || []); 107 }); 108 } 109 } 110 111 function newsJsonUrl() { 112 var path = window.location.pathname.replace(/\\/g, '/'); 113 if (/\/pages(\/|$)/i.test(path) || /\/news(\/|$)/i.test(path)) return '../data/news.json';
114 return 'data/news.json'; 115 } 116 117 function showError() { 118 var home = document.getElementById('home-news-grid'); 119 var grid = document.getElementById('news-grid'); 120 var msg = 'Unable to load news. Upload <code>data/news-data.js</code> (and <code>data/news.json</code>) with the site.'; 121 if (home && /Loading/i.test(home.textContent || '')) home.innerHTML = '<p class="lede">' + msg + '</p>'; 122 if (grid) grid.innerHTML = '<p class="lede">' + msg + '</p>'; 123 } 124 125 // Prefer embedded data (works on file:// and HTTP). Fall back to fetch. 126 if (Array.isArray(window.__OPS_NEWS__) && window.__OPS_NEWS__.length) { 127 applyNews(window.__OPS_NEWS__); 128 return; 129 } 130 131 fetch(newsJsonUrl(), { cache: 'no-store' }) 132 .then(function (r) { 133 if (!r.ok) throw new Error('news load failed'); 134 return r.json(); 135 }) 136 .then(applyNews) 137 .catch(showError); 138})();
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.