/** * Страница /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); } /** * Хлебные крошки: Документация → файл (для app/docs/* кроме api_routes — ссылка «app/docs» на api_routes.md). */ function updateBreadcrumbs(docPath) { var ol = document.getElementById("doc-breadcrumbs-list"); if (!ol) return; ol.replaceChildren(); function addLiLink(href, text) { var li = document.createElement("li"); li.className = "doc-breadcrumbs__item"; var a = document.createElement("a"); a.href = href; a.className = "doc-breadcrumbs__link"; a.textContent = text; li.appendChild(a); ol.appendChild(li); } function addLiCurrent(text) { var li = document.createElement("li"); li.className = "doc-breadcrumbs__item doc-breadcrumbs__item--current"; li.setAttribute("aria-current", "page"); li.textContent = text; ol.appendChild(li); } addLiLink(DOC_BASE, "Документация"); if (!docPath) { addLiCurrent("README.md (корень репозитория)"); return; } if (docPath.indexOf("app/docs/") === 0) { if (docPath === "app/docs/api_routes.md") { addLiCurrent("api_routes.md"); } else { addLiLink(docUrlForPath("app/docs/api_routes.md"), "app/docs"); addLiCurrent(docPath.replace(/^.*\//, "")); } } else { addLiCurrent(docPath); } } /** * Внутренняя ссылка на .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"); }); } /** Текст первого H1 в разобранном HTML (для заголовка вкладки). */ function extractFirstH1Text(root) { var h = root.querySelector("h1"); if (!h) return ""; return (h.textContent || "").replace(/\s+/g, " ").trim(); } /** * Заголовок вкладки: «Документация — <первый H1> — » (как в api_routes: заголовок документа). */ function applyDocumentationTabTitle(h1Text) { var appTitle = (body.dataset.appTitle || "").trim() || "kind"; if (h1Text) { document.title = "Документация — " + h1Text + " — " + appTitle; } else { document.title = "Документация — " + appTitle; } } /** * До первого H2 — одна карточка; для каждого 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 flushH2Section() { if (i >= nodes.length) return; var h2 = nodes[i]; if (h2.nodeType !== 1 || h2.tagName !== "H2") return; i++; var chunk = [h2]; while (i < nodes.length) { var n = nodes[i]; if (n.nodeType === 1 && n.tagName === "H2") break; chunk.push(n); i++; } appendCard("readme-section-card--block", chunk); } flushIntro(); while (i < nodes.length) flushH2Section(); 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"); applyDocumentationTabTitle(""); 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); var h1ForTitle = extractFirstH1Text(temp); applyDocumentationTabTitle(h1ForTitle); 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))); applyDocumentationTabTitle(""); } } 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"); updateBreadcrumbs(docPath); 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"); applyDocumentationTabTitle(""); 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"); applyDocumentationTabTitle(""); } } 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(); } })();