// ===================================================== // Search — index across all notes and concepts // ===================================================== let searchIndex = null; async function buildSearchIndex() { if (searchIndex) return searchIndex; const idx = []; // Index concepts from THEMES Object.values(SMF.THEMES).forEach(theme => { theme.keyConcepts.forEach(c => { idx.push({ type: 'concept', category: theme.label, title: c.term, snippet: c.def, href: `#/tema/${theme.id}`, searchText: `${c.term} ${c.def}`.toLowerCase() }); }); }); // Index weeks SMF.WEEKS.forEach(w => { idx.push({ type: 'uke', category: SMF.themeLabel(w.theme), title: `Uke ${w.id}: ${w.title}`, snippet: '', href: `#/uke/${w.id}`, searchText: `uke ${w.id} ${w.title}`.toLowerCase() }); }); // Load all notes and extract headings + paragraphs const noteFiles = ['uke02-03-04.md', 'uke05-06.md', 'uke07-08.md', 'uke10-11.md', 'uke12-16.md', 'uke17-eksamen.md']; for (const file of noteFiles) { try { const md = await SMF.loadNote(file); const lines = md.split('\n'); let currentWeek = null; let currentH = null; let buffer = []; const flush = () => { if (buffer.length && currentH) { const txt = buffer.join(' ').trim(); if (txt.length > 20) { idx.push({ type: 'avsnitt', category: currentWeek ? `Uke ${currentWeek}` : 'Pensum', title: currentH, snippet: txt.slice(0, 220) + (txt.length > 220 ? '…' : ''), href: currentWeek ? `#/uke/${currentWeek}` : `#/uke/17`, searchText: (currentH + ' ' + txt).toLowerCase() }); } } buffer = []; }; for (const line of lines) { const ukeMatch = line.match(/^##\s+Uke\s+(\d+)/i); if (ukeMatch) { flush(); currentWeek = parseInt(ukeMatch[1], 10); currentH = line.replace(/^#+\s+/, ''); continue; } const hMatch = line.match(/^#{3,4}\s+(.+)/); if (hMatch) { flush(); currentH = hMatch[1].trim(); continue; } if (line.trim() && !line.startsWith('|') && !line.startsWith('---')) { buffer.push(line.replace(/[*_`]/g, '')); } } flush(); } catch (e) { console.warn('Could not load note for indexing:', file, e); } } searchIndex = idx; return idx; } function searchQuery(q) { if (!searchIndex || !q || q.length < 2) return []; const qLow = q.toLowerCase(); const terms = qLow.split(/\s+/).filter(t => t.length > 1); if (!terms.length) return []; const scored = []; for (const item of searchIndex) { let score = 0; let titleHits = 0; const titleLow = item.title.toLowerCase(); terms.forEach(t => { if (titleLow.includes(t)) { score += 10; titleHits++; } if (item.searchText.includes(t)) { score += 2; } }); // Bonus: type-priority — concepts > weeks > paragraphs if (item.type === 'concept') score += 3; else if (item.type === 'uke') score += 2; // Bonus: title contains whole query if (titleLow.includes(qLow)) score += 8; if (score > 0) scored.push({ item, score, titleHits }); } scored.sort((a, b) => b.score - a.score); return scored.slice(0, 12).map(s => s.item); } function highlight(text, q) { if (!q) return text; const terms = q.toLowerCase().split(/\s+/).filter(t => t.length > 1); let out = text; terms.forEach(t => { const re = new RegExp(`(${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); out = out.replace(re, '$1'); }); return out; } function attachSearch() { const input = document.getElementById('searchInput'); const results = document.getElementById('searchResults'); if (!input || !results) return; let timer = null; input.addEventListener('input', async (e) => { const q = e.target.value.trim(); if (!q || q.length < 2) { results.classList.remove('search-results--open'); results.innerHTML = ''; return; } await buildSearchIndex(); if (timer) clearTimeout(timer); timer = setTimeout(() => { const matches = searchQuery(q); if (!matches.length) { results.innerHTML = `
Ingen treff på "${q}"
`; } else { results.innerHTML = matches.map(m => `
${m.type} · ${m.category}
${highlight(m.title, q)}
${m.snippet ? `
${highlight(m.snippet, q)}
` : ''}
`).join(''); } results.classList.add('search-results--open'); }, 80); }); input.addEventListener('focus', () => { if (input.value.length >= 2) { results.classList.add('search-results--open'); } }); // Hide on outside click document.addEventListener('click', (e) => { if (!input.contains(e.target) && !results.contains(e.target)) { results.classList.remove('search-results--open'); } }); // Click result -> close results.addEventListener('click', (e) => { const a = e.target.closest('.search-result'); if (a) { results.classList.remove('search-results--open'); input.value = ''; input.blur(); } }); // Keyboard: '/' to focus document.addEventListener('keydown', (e) => { if (e.key === '/' && document.activeElement !== input && !e.metaKey && !e.ctrlKey) { const tag = document.activeElement?.tagName; if (tag !== 'INPUT' && tag !== 'TEXTAREA') { e.preventDefault(); input.focus(); input.select(); } } if (e.key === 'Escape') { results.classList.remove('search-results--open'); input.blur(); } }); } SMF.attachSearch = attachSearch; SMF.buildSearchIndex = buildSearchIndex;