updated docs
This commit is contained in:
318
docs/docs.js
Normal file
318
docs/docs.js
Normal file
@@ -0,0 +1,318 @@
|
||||
// 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(`<div class="sidebar-header"><a href="#intro"><h1>soleprint</h1><span>docs</span></a></div>`);
|
||||
|
||||
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
|
||||
? `<span class="sidebar-chevron${isExpanded ? " expanded" : ""}">›</span>`
|
||||
: "";
|
||||
const label = topic.title[state.lang] || topic.title.en;
|
||||
return `<a class="sidebar-item${active}" href="#${topic.id}" data-group="${topic.id}">${chevron}${label}</a>`;
|
||||
}
|
||||
|
||||
function subItem(topic) {
|
||||
const active = state.activeTopic === topic.id ? " active" : "";
|
||||
const label = topic.title[state.lang] || topic.title.en;
|
||||
return `<a class="sidebar-item sidebar-sub${active}" href="#${topic.id}">${label}</a>`;
|
||||
}
|
||||
|
||||
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 = '<p class="loading">Loading...</p>';
|
||||
try {
|
||||
const md = await fetchMarkdown(id);
|
||||
const html = markdown.render(md);
|
||||
state.contentCache[cacheKey] = html;
|
||||
el.innerHTML = html;
|
||||
} catch (e) {
|
||||
el.innerHTML = `<p style="color:var(--artery-text);">Failed to load: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 [`<p>${this.inline(trimmed)}</p>`, 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 [`<pre><code>${codeLines.join("\n")}</code></pre>`, i + 1];
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hr",
|
||||
match: (t) => /^---+$/.test(t),
|
||||
parse(_, i) { return ["<hr>", 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 [`<h3>${markdown.inline(t.slice(4))}</h3>`, i + 1]; },
|
||||
},
|
||||
{
|
||||
name: "h2",
|
||||
match: (t) => t.startsWith("## "),
|
||||
parse(_, i, t) { return [`<h2>${markdown.inline(t.slice(3))}</h2>`, i + 1]; },
|
||||
},
|
||||
{
|
||||
name: "h1",
|
||||
match: (t) => t.startsWith("# "),
|
||||
parse(_, i, t) { return [`<h1>${markdown.inline(t.slice(2))}</h1>`, 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) => `<p>${markdown.inline(l)}</p>`).join("");
|
||||
return [`<blockquote>${inner}</blockquote>`, 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 [`<a href="viewer.html?src=${encodeURIComponent(src)}"><img src="${src}" alt="${alt}" title="Click to expand"></a>`, i + 1];
|
||||
}
|
||||
return [`<p>${t}</p>`, 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) => `<li>${markdown.inline(item)}</li>`).join("");
|
||||
return [`<ul>${inner}</ul>`, 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)
|
||||
? `<thead><tr>${headers.map((h) => `<th>${this.inline(h)}</th>`).join("")}</tr></thead>`
|
||||
: "";
|
||||
const tbody = body
|
||||
.map((row) => `<tr>${row.map((c) => `<td>${this.inline(c)}</td>`).join("")}</tr>`)
|
||||
.join("");
|
||||
|
||||
return `<table>${thead}<tbody>${tbody}</tbody></table>`;
|
||||
},
|
||||
|
||||
inlineRules: [
|
||||
{ pattern: /\*\*(.+?)\*\*/g, tag: "strong" },
|
||||
{ pattern: /\*(.+?)\*/g, tag: "em" },
|
||||
{ pattern: /`(.+?)`/g, tag: "code" },
|
||||
{ pattern: /\[(.+?)\]\((.+?)\)/g, render: (_, text, href) => `<a href="${href}">${text}</a>` },
|
||||
],
|
||||
|
||||
inline(text) {
|
||||
if (!text) return "";
|
||||
return this.inlineRules.reduce((result, rule) => {
|
||||
const replacer = rule.render || ((_, content) => `<${rule.tag}>${content}</${rule.tag}>`);
|
||||
return result.replace(rule.pattern, replacer);
|
||||
}, text);
|
||||
},
|
||||
|
||||
escape(str) {
|
||||
return str.replace(/&/g, "&").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 = `<p style="color:var(--artery-text);">Failed to load topics: ${e.message}</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("hashchange", () => {
|
||||
const hash = location.hash.slice(1);
|
||||
if (state.topics.find((t) => t.id === hash) && hash !== state.activeTopic) {
|
||||
navigate(hash);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user