// soleprint docs — sidebar + markdown renderer // Logic adapted from mcrn.ar/design, reskinned for minimal-neon const state = { lang: "en", topics: [], contentCache: {}, activeTopic: null, expandedGroup: null, }; const dom = { sidebar: () => document.getElementById("sidebar"), content: () => document.getElementById("content"), }; // --- Topic grouping --- function buildGroups(topics) { const groups = []; for (const t of topics) { if (!t.sub) { groups.push({ parent: t, subs: [] }); } else if (groups.length > 0) { groups[groups.length - 1].subs.push(t); } } return groups; } function findParentGroup(topicId) { for (const g of buildGroups(state.topics)) { if (g.parent.id === topicId || g.subs.some((s) => s.id === topicId)) { return g.parent.id; } } return null; } // --- Sidebar --- function renderSidebar() { const groups = buildGroups(state.topics); const items = []; items.push(`
`); for (const g of groups) { const isExpanded = state.expandedGroup === g.parent.id; const hasSubs = g.subs.length > 0; items.push(parentItem(g.parent, hasSubs, isExpanded)); if (isExpanded && hasSubs) { for (const s of g.subs) { items.push(subItem(s)); } } } dom.sidebar().innerHTML = items.join(""); bindParentClicks(); } function parentItem(topic, hasSubs, isExpanded) { const active = state.activeTopic === topic.id ? " active" : ""; const chevron = hasSubs ? `` : ""; const label = topic.title[state.lang] || topic.title.en; return `${chevron}${label}`; } function subItem(topic) { const active = state.activeTopic === topic.id ? " active" : ""; const label = topic.title[state.lang] || topic.title.en; return `${label}`; } function bindParentClicks() { dom.sidebar().querySelectorAll(".sidebar-item:not(.sidebar-sub)").forEach((el) => { el.addEventListener("click", (e) => { const groupId = el.dataset.group; const group = buildGroups(state.topics).find((g) => g.parent.id === groupId); if (group?.subs.length && state.expandedGroup === groupId && state.activeTopic === groupId) { e.preventDefault(); state.expandedGroup = null; renderSidebar(); } else { state.expandedGroup = groupId; } }); }); } // --- Content loading --- async function loadTopic(id) { const el = dom.content(); const cacheKey = `${state.lang}:${id}`; if (state.contentCache[cacheKey]) { el.innerHTML = state.contentCache[cacheKey]; } else { el.innerHTML = 'Loading...
'; try { const md = await fetchMarkdown(id); const html = markdown.render(md); state.contentCache[cacheKey] = html; el.innerHTML = html; } catch (e) { el.innerHTML = `Failed to load: ${e.message}
`; } } window.scrollTo(0, 0); el.scrollTop = 0; } async function fetchMarkdown(id) { let resp = await fetch(`data/${state.lang}/${id}.md`); if (!resp.ok && state.lang !== "en") { resp = await fetch(`data/en/${id}.md`); } if (!resp.ok) throw new Error("Not found"); return resp.text(); } // --- Markdown renderer --- const markdown = { render(md) { const lines = md.split("\n"); const parts = []; let i = 0; while (i < lines.length) { const trimmed = lines[i].trim(); if (!trimmed) { i++; continue; } const [html, next] = this.parseLine(lines, i, trimmed); if (html) parts.push(html); i = next; } return parts.join(""); }, parseLine(lines, i, trimmed) { for (const parser of this.parsers) { if (parser.match(trimmed)) { return parser.parse(lines, i, trimmed); } } return [`${this.inline(trimmed)}
`, i + 1]; }, parsers: [ { name: "fenced-code", match: (t) => t.startsWith("```"), parse(lines, i, trimmed) { const lang = trimmed.slice(3).trim(); const codeLines = []; i++; while (i < lines.length && !lines[i].trim().startsWith("```")) { codeLines.push(markdown.escape(lines[i])); i++; } return [`${codeLines.join("\n")}`, i + 1];
},
},
{
name: "hr",
match: (t) => /^---+$/.test(t),
parse(_, i) { return ["${markdown.inline(l)}
`).join(""); return [`${inner}`, i]; }, }, { name: "image", match: (t) => /^!\[.*\]\(.*\)$/.test(t), parse(_, i, t) { const m = t.match(/^!\[(.*?)\]\((.*?)\)$/); if (m) { const alt = m[1]; const src = m[2]; return [`
${t}
`, i + 1]; }, }, { name: "list", match: (t) => t.startsWith("- "), parse(lines, i) { const items = []; while (i < lines.length && lines[i].trim().startsWith("- ")) { items.push(lines[i].trim().slice(2)); i++; } const inner = items.map((item) => `Failed to load topics: ${e.message}
`; } }); window.addEventListener("hashchange", () => { const hash = location.hash.slice(1); if (state.topics.find((t) => t.id === hash) && hash !== state.activeTopic) { navigate(hash); } });