// 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 ["
", i + 1]; }, }, { name: "table", match: (t) => t.startsWith("|"), parse(lines, i) { const rows = []; while (i < lines.length && lines[i].trim().startsWith("|")) { rows.push(lines[i].trim()); i++; } return [markdown.table(rows), i]; }, }, { name: "h3", match: (t) => t.startsWith("### "), parse(_, i, t) { return [`

${markdown.inline(t.slice(4))}

`, i + 1]; }, }, { name: "h2", match: (t) => t.startsWith("## "), parse(_, i, t) { return [`

${markdown.inline(t.slice(3))}

`, i + 1]; }, }, { name: "h1", match: (t) => t.startsWith("# "), parse(_, i, t) { return [`

${markdown.inline(t.slice(2))}

`, i + 1]; }, }, { name: "blockquote", match: (t) => t.startsWith("> ") || t === ">", parse(lines, i) { const content = []; while (i < lines.length) { const ql = lines[i].trim(); if (ql.startsWith("> ")) content.push(ql.slice(2)); else if (ql === ">") content.push(""); else break; i++; } const inner = content.map((l) => `

${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 [`${alt}`, i + 1]; } 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) => `
  • ${markdown.inline(item)}
  • `).join(""); return [``, i]; }, }, ], table(rows) { if (rows.length < 2) return ""; const split = (line) => line.split("|").slice(1, -1).map((c) => c.trim()); const headers = split(rows[0]); const body = rows.slice(2).map(split); const thead = headers.some((h) => h) ? `${headers.map((h) => `${this.inline(h)}`).join("")}` : ""; const tbody = body .map((row) => `${row.map((c) => `${this.inline(c)}`).join("")}`) .join(""); return `${thead}${tbody}
    `; }, inlineRules: [ { pattern: /\*\*(.+?)\*\*/g, tag: "strong" }, { pattern: /\*(.+?)\*/g, tag: "em" }, { pattern: /`(.+?)`/g, tag: "code" }, { pattern: /\[(.+?)\]\((.+?)\)/g, render: (_, text, href) => `${text}` }, ], inline(text) { if (!text) return ""; return this.inlineRules.reduce((result, rule) => { const replacer = rule.render || ((_, content) => `<${rule.tag}>${content}`); return result.replace(rule.pattern, replacer); }, text); }, escape(str) { return str.replace(/&/g, "&").replace(//g, ">"); }, }; // --- Navigation --- function navigate(topicId) { state.activeTopic = topicId; state.expandedGroup = findParentGroup(topicId); renderSidebar(); loadTopic(topicId); } // --- Init --- document.addEventListener("DOMContentLoaded", async () => { try { const resp = await fetch("data/topics.json"); state.topics = await resp.json(); const hash = location.hash.slice(1); state.activeTopic = state.topics.find((t) => t.id === hash) ? hash : state.topics[0].id; state.expandedGroup = findParentGroup(state.activeTopic); renderSidebar(); await loadTopic(state.activeTopic); } catch (e) { dom.content().innerHTML = `

    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); } });