updated docs

This commit is contained in:
2026-04-14 10:32:05 -03:00
parent 2e5a304181
commit a80b72a9b1
67 changed files with 3260 additions and 5005 deletions

318
docs/docs.js Normal file
View 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
},
};
// --- 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);
}
});