// ===================================================== // Render — markdown loading and lesson rendering // ===================================================== const noteCache = new Map(); async function loadNote(file) { if (noteCache.has(file)) return noteCache.get(file); const res = await fetch(`notes/${file}`); if (!res.ok) throw new Error(`Klarte ikke laste ${file}`); const text = await res.text(); noteCache.set(file, text); return text; } // Split markdown by week sections — looking for headers like "## Uke X — ..." or "# UKE X" function extractWeekSection(markdown, weekId) { const lines = markdown.split('\n'); // Match #, ##, ### at start; "Uke" or "UKE"; the week id as separate token const headerRe = new RegExp(`^#{1,3}\\s+UKE\\s+${weekId}(?!\\d)`, 'i'); let start = -1; let startLevel = 0; for (let i = 0; i < lines.length; i++) { const m = lines[i].match(headerRe); if (m) { start = i; // Count leading hashes const hashMatch = lines[i].match(/^(#+)/); startLevel = hashMatch ? hashMatch[1].length : 2; break; } } if (start === -1) return null; // Find next header at same or higher level that starts a NEW week const nextHeaderRe = new RegExp(`^#{1,${startLevel}}\\s+UKE\\s+\\d+`, 'i'); let end = lines.length; for (let i = start + 1; i < lines.length; i++) { if (nextHeaderRe.test(lines[i])) { end = i; break; } } let section = lines.slice(start, end).join('\n'); // Strip the first heading entirely (we show the title in the lesson-header) section = section.replace(/^#{1,3}\s+[^\n]+\n+/, ''); // Normalize remaining heading levels: bring everything to H2/H3/H4 starting fresh. // Original starting level (after stripped header) was startLevel + 1 typically. // We want the deepest visible header in the section to start at H2. const subLines = section.split('\n'); // Find the minimum heading level inside section let minLevel = Infinity; for (const l of subLines) { const m = l.match(/^(#{1,6})\s/); if (m) minLevel = Math.min(minLevel, m[1].length); } if (minLevel === Infinity) return section; const shift = 2 - minLevel; // bring minLevel up to 2 if (shift !== 0) { section = subLines.map(l => { const m = l.match(/^(#{1,6})\s/); if (!m) return l; const newLevel = Math.min(6, Math.max(1, m[1].length + shift)); return '#'.repeat(newLevel) + l.slice(m[1].length); }).join('\n'); } return section; } // Custom marked renderer for tighter HTML function renderMarkdown(md) { marked.setOptions({ gfm: true, breaks: false, headerIds: true, mangle: false }); return marked.parse(md); } // ============= Home view ============= function renderHome() { const tpl = document.getElementById('t-home').content.cloneNode(true); return tpl; } // ============= Lesson view ============= async function renderLesson(weekId) { const tpl = document.getElementById('t-lesson').content.cloneNode(true); const week = SMF.WEEKS.find(w => w.id === weekId); if (!week) { tpl.getElementById('lessonNum').textContent = 'Ikke funnet'; tpl.getElementById('lessonTitle').textContent = 'Ukjent uke'; return tpl; } const themeColors = SMF.THEME_COLORS[week.theme] || SMF.THEME_COLORS.all; // Header const num = tpl.getElementById('lessonNum'); num.textContent = `Uke ${String(weekId).padStart(2, '0')} · ${themeLabel(week.theme)}`; num.style.color = themeColors.color; const title = tpl.getElementById('lessonTitle'); title.innerHTML = formatTitleWithEm(week.title); // Meta const meta = tpl.getElementById('lessonMeta'); const tag = document.createElement('span'); tag.className = 'tag'; tag.textContent = themeLabel(week.theme); tag.style.setProperty('--theme-color', themeColors.color); tag.style.setProperty('--theme-color-bg', themeColors.bg); meta.appendChild(tag); // Body — load and render const body = tpl.getElementById('lessonBody'); try { const md = await loadNote(week.file); let weekMd = extractWeekSection(md, weekId); if (!weekMd) { // Fallback: entire file (Uke 17 case) weekMd = md.replace(/^#\s+[^\n]+\n+/, ''); } body.innerHTML = renderMarkdown(weekMd); } catch (e) { body.innerHTML = `

Klarte ikke laste ukens innhold: ${e.message}

`; } // Pager const idx = SMF.WEEKS.findIndex(w => w.id === weekId); const prev = idx > 0 ? SMF.WEEKS[idx - 1] : null; const next = idx < SMF.WEEKS.length - 1 ? SMF.WEEKS[idx + 1] : null; const pager = tpl.getElementById('lessonPager'); if (prev) { const a = document.createElement('a'); a.href = `#/uke/${prev.id}`; a.className = 'pager-link pager-link--prev'; a.innerHTML = ``; pager.appendChild(a); } else { pager.appendChild(spacer()); } if (next) { const a = document.createElement('a'); a.href = `#/uke/${next.id}`; a.className = 'pager-link pager-link--next'; a.innerHTML = ``; pager.appendChild(a); } else { pager.appendChild(spacer()); } return tpl; } function spacer() { const d = document.createElement('div'); d.style.flex = '1'; return d; } function themeLabel(theme) { return { etikk: 'Etikk', baerekraft: 'Bærekraft', samfunn: 'Samfunnsansvar', verktoy: 'Verktøy & implementering', all: 'Oversikt' }[theme] || theme; } function formatTitleWithEm(title) { // Wrap "Etikk", "bærekraft" osv. in for visual rhythm const parts = title.split(/\s+/); if (parts.length === 1) return `${title}`; // pick the last word for em treatment return parts.slice(0, -1).join(' ') + ' ' + parts.at(-1) + ''; } // ============= Tema view ============= async function renderTema(temaId) { const tpl = document.getElementById('t-tema').content.cloneNode(true); const tema = SMF.getTheme(temaId); if (!tema) return tpl; const themeColors = SMF.THEME_COLORS[temaId]; const header = tpl.getElementById('temaHeader'); header.innerHTML = `
${tema.eyebrow}

${tema.title}

Uker: ${tema.weeks.join(' · ')}
`; const body = tpl.getElementById('temaBody'); // Intro blockquote const intro = document.createElement('blockquote'); intro.textContent = tema.intro; body.appendChild(intro); // Concepts const conceptsH = document.createElement('h2'); conceptsH.textContent = 'Kjernebegreper'; body.appendChild(conceptsH); tema.keyConcepts.forEach(c => { const wrap = document.createElement('div'); wrap.className = 'concept'; wrap.innerHTML = `
${c.term}
${c.def}
`; body.appendChild(wrap); }); // Weeks const weeksH = document.createElement('h2'); weeksH.textContent = 'Uker i dette temaet'; body.appendChild(weeksH); const weekList = document.createElement('div'); weekList.className = 'lesson-pager'; weekList.style.flexWrap = 'wrap'; weekList.style.borderTop = '0'; weekList.style.paddingTop = '0'; weekList.style.marginTop = '0'; tema.weeks.forEach(wid => { const w = SMF.WEEKS.find(x => x.id === wid); if (!w) return; const a = document.createElement('a'); a.href = `#/uke/${w.id}`; a.className = 'pager-link'; a.style.flexBasis = '260px'; a.style.flexGrow = '0'; a.innerHTML = ``; weekList.appendChild(a); }); body.appendChild(weekList); return tpl; } // ============= Fast-track ============= const FT_PROGRESS_KEY = 'smf-fasttrack-done'; function ftGetDone() { try { return new Set(JSON.parse(localStorage.getItem(FT_PROGRESS_KEY) || '[]')); } catch { return new Set(); } } function ftSetDone(set) { localStorage.setItem(FT_PROGRESS_KEY, JSON.stringify([...set])); } // Extract a single module from fast-track.md by its sentinel function extractFastTrackModule(markdown, moduleId) { const startRe = new RegExp(``); const lines = markdown.split('\n'); let start = lines.findIndex(l => startRe.test(l)); if (start === -1) return null; let end = lines.length; for (let i = start + 1; i < lines.length; i++) { if (//.test(lines[i])) { end = i; break; } } let section = lines.slice(start + 1, end).join('\n').trim(); // Strip the leading "## ..." heading — it's shown in the module header section = section.replace(/^##\s+[^\n]+\n+/, ''); return section; } async function renderFastTrackHome() { const tpl = document.getElementById('t-fasttrack-home').content.cloneNode(true); const grid = tpl.getElementById('ftGrid'); const done = ftGetDone(); const total = SMF.FASTTRACK.length; const completed = SMF.FASTTRACK.filter(m => done.has(m.id)).length; // Progress bar const bar = tpl.getElementById('ftProgressBar'); if (bar) bar.style.width = `${Math.round((completed / total) * 100)}%`; const count = tpl.getElementById('ftProgressCount'); if (count) count.textContent = `${completed} / ${total} moduler`; SMF.FASTTRACK.forEach(m => { const colors = SMF.THEME_COLORS[m.theme] || SMF.THEME_COLORS.all; const isDone = done.has(m.id); const a = document.createElement('a'); a.href = `#/fast-track/${m.id}`; a.className = 'ft-card reveal' + (isDone ? ' ft-card--done' : ''); a.style.setProperty('--ft-color', colors.color); a.style.setProperty('--ft-bg', colors.bg); a.innerHTML = `
${String(m.id).padStart(2, '0')}
${m.tag}

${m.title}

${m.desc}

~${m.mins} min ${isDone ? 'Repetér' : 'Start'} →
`; grid.appendChild(a); }); return tpl; } async function renderFastTrackModule(moduleId) { const tpl = document.getElementById('t-fasttrack-module').content.cloneNode(true); const module = SMF.getFastTrack(moduleId); if (!module) { tpl.getElementById('ftModNum').textContent = 'Ikke funnet'; tpl.getElementById('ftModTitle').textContent = 'Ukjent modul'; return tpl; } const colors = SMF.THEME_COLORS[module.theme] || SMF.THEME_COLORS.all; const num = tpl.getElementById('ftModNum'); num.textContent = `Fast-track · Modul ${String(moduleId).padStart(2, '0')} · ${module.tag}`; num.style.color = colors.color; tpl.getElementById('ftModTitle').innerHTML = formatTitleWithEm(module.title); const body = tpl.getElementById('ftModBody'); try { const md = await loadNote('fast-track.md'); const section = extractFastTrackModule(md, moduleId); body.innerHTML = section ? renderMarkdown(section) : '

Fant ikke modulinnholdet.

'; } catch (e) { body.innerHTML = `

Klarte ikke laste modulen: ${e.message}

`; } // "Mark as read" toggle const doneBtn = tpl.getElementById('ftDoneBtn'); function syncDoneBtn() { const done = ftGetDone(); const isDone = done.has(moduleId); doneBtn.classList.toggle('ft-done-btn--active', isDone); doneBtn.textContent = isDone ? '✓ Markert som lest' : 'Marker som lest'; } doneBtn.addEventListener('click', () => { const done = ftGetDone(); if (done.has(moduleId)) done.delete(moduleId); else done.add(moduleId); ftSetDone(done); syncDoneBtn(); }); syncDoneBtn(); // Pager const idx = SMF.FASTTRACK.findIndex(m => m.id === moduleId); const prev = idx > 0 ? SMF.FASTTRACK[idx - 1] : null; const next = idx < SMF.FASTTRACK.length - 1 ? SMF.FASTTRACK[idx + 1] : null; const pager = tpl.getElementById('ftModPager'); if (prev) { const a = document.createElement('a'); a.href = `#/fast-track/${prev.id}`; a.className = 'pager-link pager-link--prev'; a.innerHTML = ``; pager.appendChild(a); } else { const a = document.createElement('a'); a.href = '#/fast-track'; a.className = 'pager-link pager-link--prev'; a.innerHTML = ``; pager.appendChild(a); } if (next) { const a = document.createElement('a'); a.href = `#/fast-track/${next.id}`; a.className = 'pager-link pager-link--next'; a.innerHTML = ``; pager.appendChild(a); } else { const a = document.createElement('a'); a.href = '#/tldr'; a.className = 'pager-link pager-link--next'; a.innerHTML = ``; pager.appendChild(a); } return tpl; } // ============= TL;DR view ============= async function renderTldr() { const tpl = document.getElementById('t-tldr').content.cloneNode(true); const body = tpl.getElementById('tldrBody'); try { const md = await loadNote('tldr.md'); // Strip the first H1 since we have it in the hero const stripped = md.replace(/^#\s+[^\n]+\n+/, ''); body.innerHTML = renderMarkdown(stripped); } catch (e) { body.innerHTML = `

Klarte ikke laste tl;dr: ${e.message}

`; } return tpl; } SMF.renderHome = renderHome; SMF.renderLesson = renderLesson; SMF.renderTema = renderTema; SMF.renderTldr = renderTldr; SMF.renderFastTrackHome = renderFastTrackHome; SMF.renderFastTrackModule = renderFastTrackModule; SMF.loadNote = loadNote; SMF.renderMarkdown = renderMarkdown; SMF.extractWeekSection = extractWeekSection; SMF.themeLabel = themeLabel;