// ===================================================== // 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; } // ============= 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.loadNote = loadNote; SMF.renderMarkdown = renderMarkdown; SMF.extractWeekSection = extractWeekSection; SMF.themeLabel = themeLabel;