mirror of
https://github.com/Dimillian/Skills.git
synced 2026-03-25 08:55:54 +00:00
Replaces hardcoded skills in docs/app.js with dynamic loading from a generated docs/skills.json. Adds scripts/build_docs_index.py to generate the index from SKILL.md files, and a pre-commit hook to keep the index in sync. Updates README with setup instructions for the pre-commit hook.
311 lines
8.8 KiB
JavaScript
311 lines
8.8 KiB
JavaScript
const githubLink = document.getElementById("githubLink");
|
|
const themeToggle = document.getElementById("themeToggle");
|
|
const skillsList = document.getElementById("skillsList");
|
|
const skillTitle = document.getElementById("skillTitle");
|
|
const skillDescription = document.getElementById("skillDescription");
|
|
const skillUsage = document.getElementById("skillUsage");
|
|
const referenceBar = document.getElementById("referenceBar");
|
|
const markdownContent = document.getElementById("markdownContent");
|
|
|
|
const repoInfo = getRepoInfo();
|
|
githubLink.href = repoInfo
|
|
? `https://github.com/${repoInfo.owner}/${repoInfo.repo}`
|
|
: "#";
|
|
|
|
initTheme();
|
|
loadSkills();
|
|
|
|
function loadSkills() {
|
|
fetch("skills.json")
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error("Failed to load skills index");
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((skills) => {
|
|
if (!Array.isArray(skills) || skills.length === 0) {
|
|
throw new Error("No skills found");
|
|
}
|
|
const normalized = skills.map((skill) => normalizeSkill(skill));
|
|
renderSkillList(normalized);
|
|
selectSkill(normalized[0], normalized);
|
|
})
|
|
.catch(() => {
|
|
markdownContent.textContent =
|
|
"Unable to load the skills index. Run scripts/build_docs_index.py to regenerate docs/skills.json.";
|
|
});
|
|
}
|
|
|
|
function normalizeSkill(skill) {
|
|
const displayName = prettifyName(skill.name || skill.folder || "Skill");
|
|
return {
|
|
name: displayName,
|
|
rawName: skill.name || displayName,
|
|
folder: skill.folder,
|
|
description: skill.description || "",
|
|
references: skill.references || [],
|
|
};
|
|
}
|
|
|
|
function renderSkillList(skills) {
|
|
skillsList.innerHTML = "";
|
|
skills.forEach((skill) => {
|
|
const item = document.createElement("button");
|
|
item.className = "skill-item";
|
|
item.type = "button";
|
|
item.dataset.folder = skill.folder;
|
|
|
|
const title = document.createElement("div");
|
|
title.className = "skill-item__title";
|
|
title.textContent = skill.name;
|
|
|
|
const meta = document.createElement("div");
|
|
meta.className = "skill-item__meta";
|
|
|
|
const badge = document.createElement("span");
|
|
badge.className = "skill-item__badge";
|
|
badge.textContent = `${skill.references.length} ref${skill.references.length === 1 ? "" : "s"}`;
|
|
|
|
const folder = document.createElement("span");
|
|
folder.textContent = skill.folder;
|
|
|
|
meta.append(badge, folder);
|
|
|
|
const preview = document.createElement("div");
|
|
preview.className = "skill-item__preview";
|
|
preview.textContent = truncateText(skill.description, 110);
|
|
|
|
item.append(title, meta, preview);
|
|
item.addEventListener("click", () => selectSkill(skill, skills));
|
|
skillsList.append(item);
|
|
});
|
|
}
|
|
|
|
function selectSkill(skill, skills) {
|
|
setActiveSkill(skill);
|
|
skillTitle.textContent = skill.name;
|
|
skillDescription.textContent = skill.description;
|
|
skillUsage.textContent = "";
|
|
renderReferenceBar(skill, skills);
|
|
loadMarkdown(skill, "SKILL.md");
|
|
}
|
|
|
|
function setActiveSkill(skill) {
|
|
Array.from(skillsList.children).forEach((node) => {
|
|
node.classList.toggle("active", node.dataset.folder === skill.folder);
|
|
});
|
|
}
|
|
|
|
function renderReferenceBar(skill) {
|
|
referenceBar.innerHTML = "";
|
|
|
|
const mainButton = document.createElement("button");
|
|
mainButton.className = "reference-pill active";
|
|
mainButton.type = "button";
|
|
mainButton.textContent = "SKILL.md";
|
|
mainButton.addEventListener("click", () => {
|
|
setActiveReference(mainButton);
|
|
loadMarkdown(skill, "SKILL.md");
|
|
});
|
|
referenceBar.append(mainButton);
|
|
|
|
if (!skill.references.length) {
|
|
const empty = document.createElement("span");
|
|
empty.className = "muted";
|
|
empty.textContent = "No references";
|
|
referenceBar.append(empty);
|
|
return;
|
|
}
|
|
|
|
skill.references.forEach((ref) => {
|
|
const refButton = document.createElement("button");
|
|
refButton.className = "reference-pill";
|
|
refButton.type = "button";
|
|
refButton.textContent = ref.title || prettifyName(ref.file);
|
|
refButton.addEventListener("click", () => {
|
|
setActiveReference(refButton);
|
|
loadMarkdown(skill, ref.file);
|
|
});
|
|
referenceBar.append(refButton);
|
|
});
|
|
}
|
|
|
|
function setActiveReference(activeButton) {
|
|
Array.from(referenceBar.querySelectorAll(".reference-pill")).forEach((node) => {
|
|
node.classList.toggle("active", node === activeButton);
|
|
});
|
|
}
|
|
|
|
function loadMarkdown(skill, filePath) {
|
|
const contentPath = buildContentPath(`${skill.folder}/${filePath}`);
|
|
fetch(contentPath)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error("Failed to load content");
|
|
}
|
|
return response.text();
|
|
})
|
|
.then((text) => {
|
|
const parsed = parseFrontmatter(text);
|
|
const content = stripH1(parsed.content, parsed.frontmatter.name || skill.name);
|
|
const overview = extractOverview(content);
|
|
updateHeader(parsed.frontmatter, overview, skill);
|
|
|
|
if (window.marked) {
|
|
markdownContent.innerHTML = window.marked.parse(content);
|
|
} else {
|
|
markdownContent.textContent = content;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
markdownContent.textContent =
|
|
"Unable to load this document. Make sure the file exists and the server can reach it.";
|
|
});
|
|
}
|
|
|
|
function buildContentPath(path) {
|
|
if (!repoInfo) {
|
|
return `../${path}`;
|
|
}
|
|
return `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/main/${path}`;
|
|
}
|
|
|
|
function updateHeader(frontmatter, overview, skill) {
|
|
if (frontmatter.name) {
|
|
skillTitle.textContent = prettifyName(frontmatter.name);
|
|
}
|
|
if (frontmatter.description) {
|
|
skillDescription.textContent = frontmatter.description;
|
|
}
|
|
if (overview) {
|
|
skillUsage.textContent = `Usage: ${overview}`;
|
|
} else if (skill.description) {
|
|
skillUsage.textContent = `Usage: ${skill.description}`;
|
|
}
|
|
}
|
|
|
|
function parseFrontmatter(text) {
|
|
if (!text.startsWith("---")) {
|
|
return { frontmatter: {}, content: text };
|
|
}
|
|
|
|
const lines = text.split("\n");
|
|
let endIndex = -1;
|
|
for (let i = 1; i < lines.length; i += 1) {
|
|
if (lines[i].trim() === "---") {
|
|
endIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (endIndex === -1) {
|
|
return { frontmatter: {}, content: text };
|
|
}
|
|
|
|
const yamlLines = lines.slice(1, endIndex);
|
|
const content = lines.slice(endIndex + 1).join("\n");
|
|
const frontmatter = {};
|
|
|
|
yamlLines.forEach((line) => {
|
|
const match = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
if (!match) return;
|
|
const key = match[1];
|
|
let value = match[2].trim();
|
|
if (value.startsWith("\"") && value.endsWith("\"")) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
frontmatter[key] = value;
|
|
});
|
|
|
|
return { frontmatter, content };
|
|
}
|
|
|
|
function stripH1(content, title) {
|
|
const lines = content.split("\n");
|
|
if (!lines.length) return content;
|
|
|
|
const firstLine = lines[0].trim();
|
|
const normalizedTitle = (title || "").trim().toLowerCase();
|
|
if (firstLine.startsWith("#")) {
|
|
const heading = firstLine.replace(/^#+\s*/, "").trim().toLowerCase();
|
|
if (!normalizedTitle || heading === normalizedTitle) {
|
|
return lines.slice(1).join("\n").trimStart();
|
|
}
|
|
}
|
|
return content;
|
|
}
|
|
|
|
function extractOverview(content) {
|
|
const match = content.match(/##\s+Overview\s+([\s\S]*?)(\n##\s|$)/i);
|
|
if (!match) return "";
|
|
const body = match[1].trim();
|
|
const paragraph = body.split("\n\n")[0];
|
|
return paragraph.replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function truncateText(text, maxLength) {
|
|
if (!text) return "";
|
|
if (text.length <= maxLength) return text;
|
|
return `${text.slice(0, maxLength - 1).trim()}…`;
|
|
}
|
|
|
|
function prettifyName(name) {
|
|
if (!name) return "";
|
|
if (/[A-Z]/.test(name) && !name.includes("-")) {
|
|
return name;
|
|
}
|
|
const parts = name.replace(/-/g, " ").split(" ");
|
|
const map = {
|
|
ios: "iOS",
|
|
swiftui: "SwiftUI",
|
|
swift: "Swift",
|
|
app: "App",
|
|
gh: "GitHub",
|
|
};
|
|
return parts
|
|
.map((part) => {
|
|
const lower = part.toLowerCase();
|
|
if (map[lower]) return map[lower];
|
|
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
function getRepoInfo() {
|
|
const host = window.location.hostname;
|
|
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
|
|
|
if (!host || host.indexOf(".github.io") === -1) {
|
|
return null;
|
|
}
|
|
|
|
const owner = host.split(".")[0];
|
|
const repo = pathParts[0];
|
|
|
|
if (!owner || !repo) {
|
|
return null;
|
|
}
|
|
|
|
return { owner, repo };
|
|
}
|
|
|
|
function initTheme() {
|
|
const saved = localStorage.getItem("codex-theme");
|
|
if (saved) {
|
|
setTheme(saved);
|
|
} else {
|
|
const prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
|
|
setTheme(prefersLight ? "light" : "dark");
|
|
}
|
|
|
|
themeToggle.addEventListener("click", () => {
|
|
const next = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
|
|
setTheme(next);
|
|
localStorage.setItem("codex-theme", next);
|
|
});
|
|
}
|
|
|
|
function setTheme(theme) {
|
|
document.documentElement.setAttribute("data-theme", theme);
|
|
}
|