start doc work

This commit is contained in:
Your Name
2026-06-14 03:26:18 -04:00
parent 759a1a4089
commit 261c99f46e
35 changed files with 1604 additions and 7 deletions
+492
View File
@@ -0,0 +1,492 @@
(() => {
"use strict";
const SERVER_INFO = {
us: "108.175.11.140",
eu: "65.21.79.231",
};
const GUIDE_TREE = {
connection: {
label: "Connection Guide",
children: {
overview: {
label: "Overview",
doc: "docs/guide/connection/overview.md",
},
dreamcast: {
label: "Dreamcast",
children: {
hardware: {
label: "Hardware",
doc: "docs/guide/connection/dreamcast/hardware.md",
},
flycastDialup: {
label: "Flycast (Dialup)",
doc: "docs/guide/connection/dreamcast/flycast-dialup.md",
},
flycastBba: {
label: "Flycast (BBA)",
doc: "docs/guide/connection/dreamcast/flycast-bba.md",
},
},
},
pc: {
label: "PC",
children: {
pc: {
label: "PC",
children: {
windows: {
label: "Windows",
doc: "docs/guide/connection/pc/pc-windows.md",
},
linux: {
label: "Linux",
doc: "docs/guide/connection/pc/pc-linux.md",
},
},
},
blueBurst: {
label: "Blue Burst",
children: {
windows: {
label: "Windows",
doc: "docs/guide/connection/pc/blue-burst-windows.md",
},
linux: {
label: "Linux",
doc: "docs/guide/connection/pc/blue-burst-linux.md",
},
},
},
},
},
gamecube: {
label: "GameCube",
children: {
hardwareBba: {
label: "Hardware (BBA)",
doc: "docs/guide/connection/gamecube/hardware-bba.md",
},
dolphin: {
label: "Dolphin",
doc: "docs/guide/connection/gamecube/dolphin.md",
},
nintendont: {
label: "Nintendont",
doc: "docs/guide/connection/gamecube/nintendont.md",
},
},
},
xbox: {
label: "Xbox",
doc: "docs/guide/connection/xbox.md",
},
psp: {
label: "Phantasy Star Portable",
doc: "docs/guide/connection/phantasy-star-portable.md",
},
serverSideSaves: {
label: "Server-side saves",
doc: "docs/guide/connection/server-side-saves.md",
},
commonProblems: {
label: "Common problems",
doc: "docs/guide/connection/common-problems.md",
},
quickReference: {
label: "Quick reference",
doc: "docs/guide/connection/quick-reference.md",
},
},
},
peeps: {
label: "Peeps Guide",
children: {
gettingStarted: {
label: "Getting Started",
doc: "docs/guide/peeps/getting-started.md",
},
crossplayRooms: {
label: "Crossplay Rooms",
doc: "docs/guide/peeps/crossplay-rooms.md",
},
xpPeriods: {
label: "XP Periods",
doc: "docs/guide/peeps/xp-periods.md",
},
brutalPeeps: {
label: "Brutal Peeps",
doc: "docs/guide/peeps/brutal-peeps.md",
},
},
},
hardcore: {
label: "Hardcore Peeps Guide",
children: {
gettingStarted: {
label: "Getting Started",
doc: "docs/guide/hardcore/getting-started.md",
},
progression: {
label: "Progression",
doc: "docs/guide/hardcore/progression.md",
},
mesetaAndBankLimits: {
label: "Meseta and Bank Limits",
doc: "docs/guide/hardcore/meseta-and-bank-limits.md",
},
pointsSystem: {
label: "Points System",
doc: "docs/guide/hardcore/points-system.md",
},
brutalPeeps: {
label: "Brutal Peeps",
doc: "docs/guide/hardcore/brutal-peeps.md",
},
},
},
};
const qs = (sel) => document.querySelector(sel);
const selects = [
qs("#guide-level-0"),
qs("#guide-level-1"),
qs("#guide-level-2"),
qs("#guide-level-3"),
];
const wrappers = [
null,
qs("#guide-level-1-wrap"),
qs("#guide-level-2-wrap"),
qs("#guide-level-3-wrap"),
];
const labels = [
null,
qs('label[for="guide-level-1"]'),
qs('label[for="guide-level-2"]'),
qs('label[for="guide-level-3"]'),
];
const box = qs("#guide-content");
let pendingPath = null;
let loadingPath = "";
const mdCache = new Map();
function esc(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function nodeChildren(node) {
return node && node.children ? Object.entries(node.children) : [];
}
function fillSelect(select, children, preferredValue) {
if (!select) return "";
const entries = Object.entries(children || {});
const previous = preferredValue || select.value;
select.innerHTML = "";
for (const [key, child] of entries) {
const opt = document.createElement("option");
opt.value = key;
opt.textContent = child.label;
select.appendChild(opt);
}
if (entries.some(([key]) => key === previous)) {
select.value = previous;
} else if (entries.length) {
select.value = entries[0][0];
}
return select.value;
}
function setStatus(message, kind = "") {
if (!box) return;
box.innerHTML = `<div class="guide-status ${kind ? `guide-status--${esc(kind)}` : ""}">${esc(message)}</div>`;
}
function pathLabels(path) {
let node = { children: GUIDE_TREE };
const out = [];
for (const key of path) {
const next = node.children?.[key];
if (!next) break;
out.push(next.label);
node = next;
}
return out;
}
function safeHref(raw) {
const href = String(raw || "").trim();
if (/^(https?:|mailto:|#|\/|\.\/|\.\.\/)/i.test(href)) return href;
if (!/^[a-z][a-z0-9+.-]*:/i.test(href)) return href;
return "#";
}
function inlineMarkdown(raw) {
const codeSpans = [];
let text = String(raw ?? "").replace(/`([^`]+)`/g, (_, code) => {
const token = `@@CODE${codeSpans.length}@@`;
codeSpans.push(`<code>${esc(code)}</code>`);
return token;
});
let html = esc(text);
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => {
return `<a href="${esc(safeHref(href))}">${label}</a>`;
});
html = html
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/\*([^*]+)\*/g, "<em>$1</em>");
codeSpans.forEach((span, index) => {
html = html.replaceAll(`@@CODE${index}@@`, span);
});
return html;
}
function renderMarkdown(markdown) {
const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n");
const out = [];
let paragraph = [];
let list = [];
let orderedList = [];
let code = [];
let inFence = false;
const fence = String.fromCharCode(96, 96, 96);
function flushParagraph() {
if (!paragraph.length) return;
out.push(`<p>${inlineMarkdown(paragraph.join(" "))}</p>`);
paragraph = [];
}
function flushList() {
if (!list.length) return;
out.push(`<ul>${list.map((item) => `<li>${inlineMarkdown(item)}</li>`).join("")}</ul>`);
list = [];
}
function flushOrderedList() {
if (!orderedList.length) return;
out.push(`<ol>${orderedList.map((item) => `<li>${inlineMarkdown(item)}</li>`).join("")}</ol>`);
orderedList = [];
}
function flushCode() {
if (!code.length) return;
out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
code = [];
}
function flushTextBlocks() {
flushParagraph();
flushList();
flushOrderedList();
}
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith(fence)) {
flushTextBlocks();
if (inFence) {
flushCode();
inFence = false;
} else {
inFence = true;
code = [];
}
continue;
}
if (inFence) {
code.push(line);
continue;
}
if (/^ {4,}/.test(line)) {
flushTextBlocks();
code.push(line.replace(/^ {4}/, ""));
continue;
}
if (code.length && trimmed === "") {
flushCode();
continue;
}
if (code.length) {
flushCode();
}
if (!trimmed) {
flushTextBlocks();
continue;
}
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
flushTextBlocks();
out.push("<hr>");
continue;
}
const heading = trimmed.match(/^(#{1,5})\s+(.+)$/);
if (heading) {
flushTextBlocks();
const level = Math.min(heading[1].length + 1, 6);
out.push(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`);
continue;
}
const bullet = line.match(/^\s*[-*]\s+(.+)$/);
if (bullet) {
flushParagraph();
flushOrderedList();
list.push(bullet[1]);
continue;
}
const ordered = line.match(/^\s*\d+\.\s+(.+)$/);
if (ordered) {
flushParagraph();
flushList();
orderedList.push(ordered[1]);
continue;
}
paragraph.push(trimmed);
}
flushTextBlocks();
flushCode();
return `<div class="guide-doc">${out.join("\n")}</div>`;
}
async function loadDoc(node, path) {
if (!node.doc) {
setStatus("Choose a guide page from the menus above.");
return;
}
const pathKey = path.join("/");
loadingPath = pathKey;
setStatus(`Loading ${pathLabels(path).join(" ")}...`);
try {
let markdown = mdCache.get(node.doc);
if (!markdown) {
const response = await fetch(node.doc, { cache: "no-cache" });
if (!response.ok) {
throw new Error(`Unable to load ${node.doc} (${response.status})`);
}
markdown = await response.text();
mdCache.set(node.doc, markdown);
}
if (loadingPath !== pathKey) return;
const breadcrumb = pathLabels(path).join(" ");
box.innerHTML = `
<div class="guide-breadcrumb">${esc(breadcrumb)}</div>
${renderMarkdown(markdown)}
`;
} catch (err) {
if (loadingPath !== pathKey) return;
setStatus(err?.message || "Unable to load guide page.", "error");
}
}
function updateHash(path) {
const next = `#${path.join("/")}`;
if (window.location.hash !== next) {
history.replaceState(null, "", next);
}
}
function readHashPath() {
const raw = window.location.hash.replace(/^#/, "").trim();
if (!raw) return null;
return raw.split("/").filter(Boolean);
}
function syncGuide(changedLevel = -1) {
if (!box || selects.some((select) => !select)) return;
if (changedLevel >= 0) {
for (let i = changedLevel + 1; i < selects.length; i += 1) {
selects[i].value = "";
}
}
let node = { children: GUIDE_TREE };
const path = [];
for (let level = 0; level < selects.length; level += 1) {
const select = selects[level];
if (!node.children) {
for (let hideLevel = level; hideLevel < wrappers.length; hideLevel += 1) {
if (wrappers[hideLevel]) wrappers[hideLevel].hidden = true;
}
break;
}
const preferred = pendingPath?.[level] || select.value;
const selected = fillSelect(select, node.children, preferred);
if (wrappers[level]) wrappers[level].hidden = false;
const current = node.children[selected];
path.push(selected);
node = current;
if (labels[level]) {
labels[level].textContent =
level === 1 && path[0] === "connection" ? "Platform" :
level === 1 ? "Topic" :
level === 2 && path[0] === "connection" && path[1] === "pc" ? "Client" :
level === 2 && path[0] === "connection" ? "Setup" :
level === 3 ? "System" :
"Option";
}
}
pendingPath = null;
for (let level = 1; level < wrappers.length; level += 1) {
const parent = path.slice(0, level).reduce((cur, key) => cur?.children?.[key], { children: GUIDE_TREE });
if (!parent?.children && wrappers[level]) wrappers[level].hidden = true;
}
updateHash(path);
loadDoc(node, path);
}
document.addEventListener("DOMContentLoaded", () => {
pendingPath = readHashPath();
selects.forEach((select, index) => {
select?.addEventListener("change", () => syncGuide(index));
});
syncGuide();
});
})();