Files
KindClustersDashboard/app/static/js/documentation.js
T
Sergey Antropoff d1a01cca9e Документация и kubectl из контейнера; Kind Clusters Dashboard
- Цель make docker|podman kubectl CLUSTER=… (KUBECTL_ARGS) — exec kubectl в kind-k8s-web
- README: без kubectl на хосте; раздел про проверку API из контейнера
- create_cluster/cluster_status: подсказки для UI, make kubectl и exec в контейнере
- app/docs: api_routes.md и README.md про kubectl и API workloads
- Прочее: переименование проекта, документация, UI документации (ранее в рабочем дереве)
2026-04-04 06:27:18 +03:00

257 lines
8.1 KiB
JavaScript

/**
* Страница /documentation: README и файлы app/docs/*.md через API, рендер Markdown.
* Зависимости из репозитория: /static/js/vendor/marked.min.js, purify.min.js
*
* Автор: Сергей Антропов
* Сайт: https://devops.org.ru
*/
(function () {
"use strict";
var body = document.body;
var API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
var DOC_BASE = "/documentation";
function showErr(el, msg) {
if (!el) return;
el.textContent = msg;
el.classList.remove("hidden");
}
function hideErr(el) {
if (!el) return;
el.textContent = "";
el.classList.add("hidden");
}
function getPathFromLocation() {
var q = new URLSearchParams(window.location.search || "");
var p = (q.get("path") || "").trim();
return p || null;
}
function docUrlForPath(relPath) {
if (!relPath) return DOC_BASE;
return DOC_BASE + "?path=" + encodeURIComponent(relPath);
}
/**
* Внутренняя ссылка на .md под app/docs/: полный путь или относительно текущего файла.
*/
function resolveInternalMdHref(href, currentDocPath) {
if (!href) return null;
var h = href.trim();
if (h.startsWith("#")) return null;
if (/^https?:\/\//i.test(h)) return null;
if (/^mailto:/i.test(h)) return null;
var abs = h.match(/^(\.\/)?(app\/docs\/[^?#]+\.md)([\?#].*)?$/i);
if (abs) {
return { path: abs[2].replace(/\\/g, "/"), tail: abs[3] || "" };
}
if (!currentDocPath || currentDocPath.indexOf("app/docs/") !== 0) return null;
if (h.indexOf("..") >= 0) return null;
var baseDir = currentDocPath.replace(/[^/]+\.md$/i, "");
if (!baseDir) baseDir = "app/docs/";
try {
var u = new URL(h, "http://doc.local/" + baseDir);
var pathname = decodeURIComponent(u.pathname.replace(/^\/+/, ""));
if (pathname.indexOf("..") >= 0) return null;
if (!pathname.startsWith("app/docs/") || !pathname.endsWith(".md")) return null;
var tail = (u.hash || "") + (u.search || "");
return { path: pathname, tail: tail };
} catch (e) {
return null;
}
}
function rewriteMdLinks(container, currentDocPath) {
container.querySelectorAll("a[href]").forEach(function (a) {
var href = a.getAttribute("href");
var resolved = resolveInternalMdHref(href, currentDocPath);
if (!resolved) return;
a.setAttribute("href", docUrlForPath(resolved.path) + resolved.tail);
a.classList.add("doc-md-link");
});
}
/**
* До первого H2 — одна карточка; для каждого H2 — карточка заголовка и карточка тела до следующего H2.
*/
function sectionizeFromNodes(sourceRoot) {
var page = document.createElement("div");
page.className = "readme-doc-page-inner";
var root = document.createElement("div");
root.className = "readme-doc-root";
page.appendChild(root);
var nodes = Array.prototype.slice.call(sourceRoot.childNodes);
var i = 0;
function appendCard(classExtra, chunk) {
if (!chunk.length) return;
var card = document.createElement("section");
card.className = "card readme-section-card markdown-body " + classExtra;
chunk.forEach(function (n) {
card.appendChild(n);
});
root.appendChild(card);
}
function flushIntro() {
var chunk = [];
while (i < nodes.length) {
var n = nodes[i];
if (n.nodeType === 1 && n.tagName === "H2") break;
chunk.push(n);
i++;
}
appendCard("readme-section-card--intro", chunk);
}
function flushH2Pair() {
if (i >= nodes.length) return;
var h2 = nodes[i];
if (h2.nodeType !== 1 || h2.tagName !== "H2") return;
i++;
appendCard("readme-section-card--head", [h2]);
var bodyChunk = [];
while (i < nodes.length) {
var n = nodes[i];
if (n.nodeType === 1 && n.tagName === "H2") break;
bodyChunk.push(n);
i++;
}
appendCard("readme-section-card--body", bodyChunk);
}
flushIntro();
while (i < nodes.length) flushH2Pair();
return page;
}
function renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl) {
if (typeof marked === "undefined" || typeof DOMPurify === "undefined") {
showErr(errEl, "Не загружены скрипты marked или DOMPurify из /static/js/vendor/.");
if (loadEl) loadEl.classList.add("hidden");
return;
}
try {
try {
if (marked.defaults && typeof marked.defaults === "object") {
marked.defaults.breaks = true;
marked.defaults.gfm = true;
}
} catch (e1) {
/* ignore */
}
var rawHtml =
typeof marked.parse === "function" ? marked.parse(text, { async: false }) : marked(text);
var temp = document.createElement("div");
temp.innerHTML = DOMPurify.sanitize(rawHtml);
rewriteMdLinks(temp, docPath);
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
var pageInner = sectionizeFromNodes(temp);
rootEl.appendChild(pageInner);
rootEl.removeAttribute("hidden");
} catch (e2) {
showErr(errEl, "Ошибка разбора: " + (e2.message || String(e2)));
}
}
function fetchUrlForPath(docPath) {
if (!docPath) return API + "/docs/readme";
return API + "/docs/file?path=" + encodeURIComponent(docPath);
}
async function loadDocumentation(docPath, opts) {
opts = opts || {};
var push = opts.pushState === true;
var replace = opts.replaceState === true;
var rootEl = document.getElementById("readme-doc-root");
var errEl = document.getElementById("readme-error");
var loadEl = document.getElementById("readme-loading");
if (!rootEl || !loadEl) return;
hideErr(errEl);
rootEl.setAttribute("hidden", "hidden");
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
loadEl.classList.remove("hidden");
var url = fetchUrlForPath(docPath);
try {
var r = await fetch(url, {
headers: { Accept: "text/markdown, text/plain, */*" },
});
var text = await r.text();
if (!r.ok) {
var detail = text;
try {
var j = JSON.parse(text);
if (j.detail) detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
} catch (e0) {
/* raw */
}
showErr(errEl, "Не удалось загрузить документ: " + (detail || r.statusText));
loadEl.classList.add("hidden");
return;
}
renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl);
loadEl.classList.add("hidden");
var newUrl = docUrlForPath(docPath);
if (push) {
window.history.pushState({ docPath: docPath }, "", newUrl);
} else if (replace) {
window.history.replaceState({ docPath: docPath }, "", newUrl);
}
} catch (e) {
showErr(errEl, "Ошибка: " + (e.message || String(e)));
loadEl.classList.add("hidden");
}
}
function onPopState() {
loadDocumentation(getPathFromLocation(), {});
}
function onRootClick(e) {
var a = e.target && e.target.closest ? e.target.closest("a.doc-md-link") : null;
if (!a || !a.getAttribute("href")) return;
var href = a.getAttribute("href");
if (href.indexOf(DOC_BASE) !== 0) return;
e.preventDefault();
try {
var u = new URL(href, window.location.origin);
var p = (u.searchParams.get("path") || "").trim() || null;
loadDocumentation(p, { pushState: true });
} catch (e1) {
loadDocumentation(getPathFromLocation(), { pushState: true });
}
}
function run() {
var rootEl = document.getElementById("readme-doc-root");
if (!rootEl) return;
window.addEventListener("popstate", onPopState);
rootEl.addEventListener("click", onRootClick);
var initial = getPathFromLocation();
loadDocumentation(initial, { replaceState: true });
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", run);
} else {
run();
}
})();