770 lines
34 KiB
JavaScript
770 lines
34 KiB
JavaScript
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||
import { dirname, join } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||
const knowledgePath = join(repoRoot, "content", "knowledge.json");
|
||
const homePath = join(repoRoot, "content", "pages", "home.json");
|
||
const robotsPath = join(repoRoot, "robots.txt");
|
||
const sitemapPath = join(repoRoot, "sitemap.xml");
|
||
|
||
const knowledge = JSON.parse(await readFile(knowledgePath, "utf8"));
|
||
const home = JSON.parse(await readFile(homePath, "utf8").catch(() => "{}"));
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">");
|
||
}
|
||
|
||
function escapeAttr(value) {
|
||
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'");
|
||
}
|
||
|
||
function escapeXml(value) {
|
||
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'");
|
||
}
|
||
|
||
function escapeJson(value) {
|
||
return JSON.stringify(value).replaceAll("</script", "<\\/script");
|
||
}
|
||
|
||
function stripHtml(value) {
|
||
return String(value ?? "")
|
||
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
||
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
||
.replace(/<[^>]*>/g, " ")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
|
||
function slugify(value, fallback = "section") {
|
||
const map = {
|
||
а: "a",
|
||
б: "b",
|
||
в: "v",
|
||
г: "g",
|
||
д: "d",
|
||
е: "e",
|
||
ё: "e",
|
||
ж: "zh",
|
||
з: "z",
|
||
и: "i",
|
||
й: "y",
|
||
к: "k",
|
||
л: "l",
|
||
м: "m",
|
||
н: "n",
|
||
о: "o",
|
||
п: "p",
|
||
р: "r",
|
||
с: "s",
|
||
т: "t",
|
||
у: "u",
|
||
ф: "f",
|
||
х: "h",
|
||
ц: "c",
|
||
ч: "ch",
|
||
ш: "sh",
|
||
щ: "sch",
|
||
ъ: "",
|
||
ы: "y",
|
||
ь: "",
|
||
э: "e",
|
||
ю: "yu",
|
||
я: "ya",
|
||
};
|
||
const transliterated = String(value ?? "")
|
||
.toLowerCase()
|
||
.replace(/[а-яё]/g, (char) => map[char] ?? "")
|
||
.replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/^-+|-+$/g, "");
|
||
|
||
return transliterated || fallback;
|
||
}
|
||
|
||
function normalizePathSegment(node, index) {
|
||
return slugify(node.slug || node.title || node.id, `page-${index + 1}`);
|
||
}
|
||
|
||
function publicAssetUrl(value) {
|
||
const url = String(value || "").trim();
|
||
if (!url) return "";
|
||
if (/^(https?:|data:|blob:|\/)/i.test(url)) return url;
|
||
if (url.startsWith("./")) return `/${url.slice(2)}`;
|
||
return `/${url.replace(/^\/+/, "")}`;
|
||
}
|
||
|
||
function normalizeContentAssets(html) {
|
||
return String(html || "")
|
||
.replace(/\s(src|href)="\.\/assets\//g, ' $1="/assets/')
|
||
.replace(/\s(src|href)="assets\//g, ' $1="/assets/');
|
||
}
|
||
|
||
function decodeShortcodeAttr(value) {
|
||
return String(value ?? "")
|
||
.replaceAll(""", '"')
|
||
.replaceAll(""", '"')
|
||
.replaceAll("'", "'")
|
||
.replaceAll("'", "'")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll("&", "&");
|
||
}
|
||
|
||
function parseShortcodeAttrs(value) {
|
||
const attrs = {};
|
||
String(value || "").replace(
|
||
/([a-zA-Z][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/g,
|
||
(match, key, doubleQuoted, singleQuoted, bare) => {
|
||
attrs[key.toLowerCase()] = decodeShortcodeAttr(doubleQuoted ?? singleQuoted ?? bare ?? "");
|
||
return match;
|
||
},
|
||
);
|
||
return attrs;
|
||
}
|
||
|
||
function normalizeKnowledgeButtonTarget(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.replace(/^#+/, "")
|
||
.replace(/^index\.html#/i, "")
|
||
.replace(/^\/?#/, "");
|
||
}
|
||
|
||
function normalizeKnowledgeButtonLink(value) {
|
||
return (String(value ?? "").trim() || "/").replace(/^\/+(https?:\/\/)/i, "$1").replace(/^\/+((?:mailto|tel):)/i, "$1");
|
||
}
|
||
|
||
function allHomeBlocks() {
|
||
return (home.regions || []).flatMap((region) => region.blocks || []);
|
||
}
|
||
|
||
function landingBlockById(id) {
|
||
const blockId = String(id || "").trim();
|
||
if (!blockId) return null;
|
||
return allHomeBlocks().find((block) => block.id === blockId && block.enabled !== false) || null;
|
||
}
|
||
|
||
function landingTargetForAttrs(attrs) {
|
||
const blockId = attrs.block || attrs["block-id"] || attrs.blockid || attrs.landingblock || attrs["landing-block"];
|
||
const blockTarget = normalizeKnowledgeButtonTarget(landingBlockById(blockId)?.anchor || "");
|
||
return blockTarget || normalizeKnowledgeButtonTarget(attrs.target || attrs.anchor || "");
|
||
}
|
||
|
||
function knowledgeButtonHref(link, attrs) {
|
||
const anchor = landingTargetForAttrs(attrs);
|
||
const rawLink = normalizeKnowledgeButtonLink(link);
|
||
if (!anchor) return rawLink;
|
||
const base = rawLink.split("#")[0] || "/";
|
||
if (/^index\.html$/i.test(base)) return `/#${anchor}`;
|
||
return `${base}#${anchor}`;
|
||
}
|
||
|
||
function renderKnowledgeShortcodes(html) {
|
||
return String(html || "").replace(/\[button\b([^\]]*)\]/gi, (match, rawAttrs) => {
|
||
const attrs = parseShortcodeAttrs(rawAttrs);
|
||
const label = attrs.name || attrs.label || attrs.text || "Посмотреть";
|
||
const href = knowledgeButtonHref(attrs.link || attrs.href || "/", attrs);
|
||
return `<a class="knowledge-button" href="${escapeAttr(href)}">${escapeHtml(label)}</a>`;
|
||
});
|
||
}
|
||
|
||
function canonicalHome() {
|
||
const fallback = "https://nodedc.dctouch.ru/";
|
||
const value = String(home.seo?.canonicalUrl || fallback).trim() || fallback;
|
||
|
||
try {
|
||
return new URL(value).href;
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
}
|
||
|
||
function canonicalBase() {
|
||
const url = new URL(canonicalHome());
|
||
return `${url.origin}/`;
|
||
}
|
||
|
||
function knowledgeBasePath() {
|
||
return String(knowledge.basePath || "knowledge").replace(/^\/+|\/+$/g, "") || "knowledge";
|
||
}
|
||
|
||
function nodeChildren(node) {
|
||
return Array.isArray(node.children) ? node.children : [];
|
||
}
|
||
|
||
function nodeHasPage(node) {
|
||
return node?.enabled !== false && node.useContent !== false && stripHtml(renderKnowledgeShortcodes(node.bodyHtml || "")).length > 0;
|
||
}
|
||
|
||
function walkNodes(nodes = knowledge.tree || [], parentSegments = [], parents = [], result = []) {
|
||
nodes.forEach((node, index) => {
|
||
if (node.enabled === false) return;
|
||
const segment = normalizePathSegment(node, index);
|
||
const segments = [...parentSegments, segment];
|
||
const record = {
|
||
node,
|
||
parents,
|
||
segment,
|
||
segments,
|
||
urlPath: `/${knowledgeBasePath()}/${segments.join("/")}/`,
|
||
};
|
||
result.push(record);
|
||
walkNodes(nodeChildren(node), segments, [...parents, record], result);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
function pageRecords() {
|
||
return walkNodes().filter((record) => nodeHasPage(record.node));
|
||
}
|
||
|
||
function currentAncestorIds(record) {
|
||
return new Set([record?.node?.id, ...(record?.parents || []).map((parent) => parent.node.id)].filter(Boolean));
|
||
}
|
||
|
||
function caretIcon(className = "caret astro-j63poqv6 astro-fr7meb3z", size = "1.25rem") {
|
||
return `<svg aria-hidden="true" class="${className}" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: ${size};"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg>`;
|
||
}
|
||
|
||
function searchIcon() {
|
||
return '<svg aria-hidden="true" class="astro-2f6du2pg astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.71 20.29 18 16.61A9 9 0 1 0 16.61 18l3.68 3.68a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.39ZM11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z"/></svg>';
|
||
}
|
||
|
||
function anchorIcon(id, title) {
|
||
return `<a class="sl-anchor-link" href="#${escapeAttr(id)}"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Раздел «${escapeHtml(title)}»</span></a>`;
|
||
}
|
||
|
||
function headingId(rawTitle, usedIds) {
|
||
const base = slugify(stripHtml(rawTitle), "section");
|
||
let id = base;
|
||
let index = 2;
|
||
while (usedIds.has(id)) {
|
||
id = `${base}-${index}`;
|
||
index += 1;
|
||
}
|
||
usedIds.add(id);
|
||
return id;
|
||
}
|
||
|
||
function prepareArticleHtml(html) {
|
||
const usedIds = new Set();
|
||
const toc = [];
|
||
const normalized = renderKnowledgeShortcodes(normalizeContentAssets(html)).replace(/<h([23])([^>]*)>([\s\S]*?)<\/h\1>/gi, (match, level, attrs, inner) => {
|
||
const existingId = /\sid=(["'])(.*?)\1/i.exec(attrs);
|
||
const id = existingId?.[2] || headingId(inner, usedIds);
|
||
const title = stripHtml(inner);
|
||
if (existingId) usedIds.add(id);
|
||
toc.push({ level: Number(level), id, title });
|
||
const nextAttrs = existingId ? attrs : `${attrs} id="${escapeAttr(id)}"`;
|
||
return `<div class="sl-heading-wrapper level-h${level}"><h${level}${nextAttrs}>${inner}</h${level}>${anchorIcon(id, title)}</div>`;
|
||
});
|
||
|
||
return { html: normalized, toc };
|
||
}
|
||
|
||
function renderNavRecord(record, currentIds, currentId, restoreCounter) {
|
||
const children = nodeChildren(record.node)
|
||
.map((child, index) => {
|
||
if (child.enabled === false) return "";
|
||
const childSegment = normalizePathSegment(child, index);
|
||
return renderNavRecord(
|
||
{
|
||
node: child,
|
||
parents: [...record.parents, record],
|
||
segment: childSegment,
|
||
segments: [...record.segments, childSegment],
|
||
urlPath: `/${knowledgeBasePath()}/${[...record.segments, childSegment].join("/")}/`,
|
||
},
|
||
currentIds,
|
||
currentId,
|
||
restoreCounter,
|
||
);
|
||
})
|
||
.filter(Boolean)
|
||
.join("");
|
||
const title = escapeHtml(record.node.title || record.node.id || "Материал");
|
||
const isCurrent = record.node.id === currentId;
|
||
const currentAttr = isCurrent ? ' aria-current="page"' : "";
|
||
|
||
if (!children) {
|
||
const largeClass = record.parents.length === 0 ? "large " : "";
|
||
const href = nodeHasPage(record.node) ? record.urlPath : "#";
|
||
return `<li class="astro-j63poqv6"><a href="${escapeAttr(href)}"${currentAttr} class="${largeClass}astro-j63poqv6"><span class="astro-j63poqv6">${title}</span></a></li>`;
|
||
}
|
||
|
||
const restoreIndex = restoreCounter.value;
|
||
restoreCounter.value += 1;
|
||
const open = currentIds.has(record.node.id) || record.parents.length === 0;
|
||
const label = nodeHasPage(record.node)
|
||
? `<a href="${escapeAttr(record.urlPath)}" class="knowledge-summary-link astro-j63poqv6"><span class="large astro-j63poqv6">${title}</span></a>`
|
||
: `<span class="large astro-j63poqv6">${title}</span>`;
|
||
|
||
return `<li class="astro-j63poqv6"><details ${open ? "open " : ""}class="astro-j63poqv6"><summary${currentAttr} class="astro-j63poqv6"><span class="group-label astro-j63poqv6">${label}</span>${caretIcon()}</summary><sl-sidebar-restore data-index="${restoreIndex}"></sl-sidebar-restore><ul class="astro-j63poqv6">${children}</ul></details></li>`;
|
||
}
|
||
|
||
function renderSidebar(currentRecord) {
|
||
const currentIds = currentAncestorIds(currentRecord);
|
||
const currentId = currentRecord?.node?.id || "";
|
||
const restoreCounter = { value: 0 };
|
||
const baseActive = currentRecord ? "" : ' aria-current="page"';
|
||
const items = (knowledge.tree || [])
|
||
.map((node, index) => {
|
||
if (node.enabled === false) return "";
|
||
const segment = normalizePathSegment(node, index);
|
||
return renderNavRecord(
|
||
{
|
||
node,
|
||
parents: [],
|
||
segment,
|
||
segments: [segment],
|
||
urlPath: `/${knowledgeBasePath()}/${segment}/`,
|
||
},
|
||
currentIds,
|
||
currentId,
|
||
restoreCounter,
|
||
);
|
||
})
|
||
.filter(Boolean)
|
||
.join("");
|
||
|
||
return `<nav class="sidebar print:hidden astro-iyrf6vv4" aria-label="Основная навигация">
|
||
<starlight-menu-button class="print:hidden astro-yrbfzory">
|
||
<button aria-expanded="false" aria-label="Меню" aria-controls="starlight__sidebar" class="sl-flex md:sl-hidden astro-yrbfzory">${menuIcons()}</button>
|
||
</starlight-menu-button>
|
||
<div id="starlight__sidebar" class="sidebar-pane astro-iyrf6vv4">
|
||
<div class="sidebar-content sl-flex astro-iyrf6vv4">
|
||
<sl-sidebar-state-persist data-hash="nodedc-knowledge" class="astro-fouifdpt">
|
||
<ul class="top-level astro-j63poqv6">
|
||
<li class="astro-j63poqv6"><a href="/${knowledgeBasePath()}/"${baseActive} class="large astro-j63poqv6"><span class="astro-j63poqv6">${escapeHtml(knowledge.title || "База знаний")}</span></a></li>${items}
|
||
</ul>
|
||
</sl-sidebar-state-persist>
|
||
<div class="md:sl-hidden">
|
||
<div class="mobile-preferences sl-flex astro-22fp5ct5">
|
||
<div class="social-icons astro-22fp5ct5"></div>${renderThemeSelect()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>`;
|
||
}
|
||
|
||
function menuIcons() {
|
||
return `<svg aria-hidden="true" class="open-menu astro-yrbfzory astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M3 8h18a1 1 0 1 0 0-2H3a1 1 0 0 0 0 2Zm18 8H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Zm0-5H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Z"/></svg><svg aria-hidden="true" class="close-menu astro-yrbfzory astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="m13.41 12 6.3-6.29a1.004 1.004 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1.004 1.004 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 0 1.42.998.998 0 0 0 1.42 0l6.29-6.3 6.29 6.3a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.42L13.41 12Z"/></svg>`;
|
||
}
|
||
|
||
function renderTocItems(toc, mobile = false) {
|
||
const items = [{ level: 2, id: "_top", title: "Обзор" }, ...toc];
|
||
return items
|
||
.map((item) => {
|
||
const depth = Math.max(0, item.level - 2);
|
||
return `<li class="astro-uehw6aht" style="--depth: ${depth};"><a href="#${escapeAttr(item.id)}" class="astro-uehw6aht" style="--depth: ${depth};"><span class="astro-uehw6aht" style="--depth: ${depth};">${escapeHtml(item.title)}</span></a></li>`;
|
||
})
|
||
.join("");
|
||
}
|
||
|
||
function renderRightSidebar(toc) {
|
||
const items = renderTocItems(toc);
|
||
const mobileItems = renderTocItems(toc, true);
|
||
return `<aside class="right-sidebar-container print:hidden astro-6ptv7odt">
|
||
<div class="right-sidebar astro-6ptv7odt">
|
||
<script type="module" src="/assets/vendor/ssscript-docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.hwBsy0Mo.js"></script>
|
||
<script type="module" src="/assets/vendor/ssscript-docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js"></script>
|
||
<div class="lg:sl-hidden astro-auw6kndz">
|
||
<mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-2zbszx3p">
|
||
<nav aria-labelledby="starlight__on-this-page--mobile" class="astro-2zbszx3p">
|
||
<details id="starlight__mobile-toc" class="astro-2zbszx3p">
|
||
<summary id="starlight__on-this-page--mobile" class="sl-flex astro-2zbszx3p"><span class="toggle sl-flex astro-2zbszx3p">На этой странице${caretIcon("caret astro-2zbszx3p astro-fr7meb3z", "1rem")}</span><span class="display-current astro-2zbszx3p"></span></summary>
|
||
<div class="dropdown astro-2zbszx3p"><ul class="isMobile astro-uehw6aht" style="--depth: 0;">${mobileItems}</ul></div>
|
||
</details>
|
||
</nav>
|
||
</mobile-starlight-toc>
|
||
</div>
|
||
<div class="right-sidebar-panel sl-hidden lg:sl-block astro-auw6kndz">
|
||
<div class="sl-container astro-auw6kndz">
|
||
<starlight-toc data-min-h="2" data-max-h="3">
|
||
<nav aria-labelledby="starlight__on-this-page">
|
||
<h2 id="starlight__on-this-page">На этой странице</h2>
|
||
<ul class="astro-uehw6aht" style="--depth: 0;">${items}</ul>
|
||
</nav>
|
||
</starlight-toc>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>`;
|
||
}
|
||
|
||
function renderThemeIconsTemplate() {
|
||
return `<template id="theme-icons"><svg aria-hidden="true" class="light astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M5 12a1 1 0 0 0-1-1H3a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Zm.64 5-.71.71a1 1 0 0 0 0 1.41 1 1 0 0 0 1.41 0l.71-.71A1 1 0 0 0 5.64 17ZM12 5a1 1 0 0 0 1-1V3a1 1 0 0 0-2 0v1a1 1 0 0 0 1 1Zm5.66 2.34a1 1 0 0 0 .7-.29l.71-.71a1 1 0 1 0-1.41-1.41l-.66.71a1 1 0 0 0 0 1.41 1 1 0 0 0 .66.29Zm-12-.29a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.71-.71a1.004 1.004 0 1 0-1.43 1.41l.73.71ZM21 11h-1a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2Zm-2.64 6A1 1 0 0 0 17 18.36l.71.71a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.76-.66ZM12 6.5a5.5 5.5 0 1 0 5.5 5.5A5.51 5.51 0 0 0 12 6.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0 3.5a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1a1 1 0 0 0-1-1Z"/></svg><svg aria-hidden="true" class="dark astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.64 13a1 1 0 0 0-1.05-.14 8.049 8.049 0 0 1-3.37.73 8.15 8.15 0 0 1-8.14-8.1 8.59 8.59 0 0 1 .25-2A1 1 0 0 0 8 2.36a10.14 10.14 0 1 0 14 11.69 1 1 0 0 0-.36-1.05Zm-9.5 6.69A8.14 8.14 0 0 1 7.08 5.22v.27a10.15 10.15 0 0 0 10.14 10.14 9.784 9.784 0 0 0 2.1-.22 8.11 8.11 0 0 1-7.18 4.32v-.04Z"/></svg><svg aria-hidden="true" class="auto astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg></template>`;
|
||
}
|
||
|
||
function renderThemeSelect() {
|
||
return `<starlight-theme-select><label style="--sl-select-width: 6.25em" class="astro-47nlhtql"><span class="sr-only astro-47nlhtql">Выбор темы</span><svg aria-hidden="true" class="icon label-icon astro-47nlhtql astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg><select autocomplete="off" class="astro-47nlhtql"><option value="dark" class="astro-47nlhtql">Dark</option><option value="light" class="astro-47nlhtql">Light</option><option value="auto" class="astro-47nlhtql">Auto</option></select><svg aria-hidden="true" class="icon caret astro-47nlhtql astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg></label></starlight-theme-select>`;
|
||
}
|
||
|
||
function themeProviderScript() {
|
||
const defaultTheme = escapeAttr(knowledge.settings?.themeDefault || "dark");
|
||
return `<script>
|
||
window.StarlightThemeProvider = (() => {
|
||
const storedTheme = typeof localStorage !== "undefined" && localStorage.getItem("starlight-theme");
|
||
const theme = storedTheme || "${defaultTheme}";
|
||
document.documentElement.dataset.theme = theme === "auto" ? (window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : theme;
|
||
return {
|
||
updatePickers(theme = storedTheme || "${defaultTheme}") {
|
||
document.querySelectorAll("starlight-theme-select").forEach((picker) => {
|
||
const select = picker.querySelector("select");
|
||
if (select) select.value = theme;
|
||
const tmpl = document.querySelector("#theme-icons");
|
||
const newIcon = tmpl && tmpl.content.querySelector("." + theme);
|
||
if (newIcon) {
|
||
const oldIcon = picker.querySelector("svg.label-icon");
|
||
if (oldIcon) oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes);
|
||
}
|
||
});
|
||
},
|
||
};
|
||
})();
|
||
</script>`;
|
||
}
|
||
|
||
function interactionScript() {
|
||
return `<script type="module">
|
||
const themeKey = "starlight-theme";
|
||
const defaultTheme = "${escapeAttr(knowledge.settings?.themeDefault || "dark")}";
|
||
const normalizeTheme = (value) => value === "auto" || value === "dark" || value === "light" ? value : defaultTheme;
|
||
const readTheme = () => normalizeTheme(typeof localStorage !== "undefined" && localStorage.getItem(themeKey));
|
||
const systemTheme = () => matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
||
function storeTheme(theme) {
|
||
if (typeof localStorage !== "undefined") localStorage.setItem(themeKey, theme);
|
||
}
|
||
function applyTheme(theme) {
|
||
StarlightThemeProvider.updatePickers(theme);
|
||
document.documentElement.dataset.theme = theme === "auto" ? systemTheme() : theme;
|
||
storeTheme(theme);
|
||
}
|
||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
|
||
if (readTheme() === "auto") applyTheme("auto");
|
||
});
|
||
class StarlightThemeSelect extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
applyTheme(readTheme());
|
||
this.querySelector("select")?.addEventListener("change", (event) => {
|
||
if (event.currentTarget instanceof HTMLSelectElement) applyTheme(normalizeTheme(event.currentTarget.value));
|
||
});
|
||
}
|
||
}
|
||
if (!customElements.get("starlight-theme-select")) customElements.define("starlight-theme-select", StarlightThemeSelect);
|
||
|
||
class StarlightMenuButton extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
this.btn = this.querySelector("button");
|
||
this.btn?.addEventListener("click", () => this.toggleExpanded());
|
||
const nav = this.closest("nav");
|
||
nav?.addEventListener("keyup", (event) => this.closeOnEscape(event));
|
||
}
|
||
setExpanded(expanded) {
|
||
this.setAttribute("aria-expanded", String(expanded));
|
||
this.btn?.setAttribute("aria-expanded", String(expanded));
|
||
document.body.toggleAttribute("data-mobile-menu-expanded", expanded);
|
||
}
|
||
toggleExpanded() {
|
||
this.setExpanded(this.getAttribute("aria-expanded") !== "true");
|
||
}
|
||
closeOnEscape(event) {
|
||
if (event.code === "Escape") {
|
||
this.setExpanded(false);
|
||
this.btn?.focus();
|
||
}
|
||
}
|
||
}
|
||
if (!customElements.get("starlight-menu-button")) customElements.define("starlight-menu-button", StarlightMenuButton);
|
||
|
||
const sidebar = document.getElementById("starlight__sidebar");
|
||
const persist = sidebar?.querySelector("sl-sidebar-state-persist");
|
||
const storageKey = "sl-sidebar-state:nodedc-knowledge";
|
||
const readState = () => {
|
||
try {
|
||
return JSON.parse(sessionStorage.getItem(storageKey) || "{}");
|
||
} catch {
|
||
return {};
|
||
}
|
||
};
|
||
const writeState = () => {
|
||
if (!sidebar || !persist) return;
|
||
const details = [...persist.querySelectorAll("details")];
|
||
try {
|
||
sessionStorage.setItem(storageKey, JSON.stringify({ open: details.map((item) => item.open), scroll: sidebar.scrollTop }));
|
||
} catch {}
|
||
};
|
||
if (sidebar && persist && matchMedia("(min-width: 50em)").matches) {
|
||
const state = readState();
|
||
if (Array.isArray(state.open)) {
|
||
[...persist.querySelectorAll("details")].forEach((details, index) => {
|
||
if (typeof state.open[index] === "boolean") details.open = state.open[index];
|
||
});
|
||
}
|
||
if (typeof state.scroll === "number") sidebar.scrollTop = state.scroll;
|
||
persist.addEventListener("toggle", writeState, true);
|
||
addEventListener("pagehide", writeState);
|
||
}
|
||
</script>`;
|
||
}
|
||
|
||
function renderHeader(searchPlaceholder) {
|
||
const logoSrc = publicAssetUrl(home.staticElements?.content?.navigation?.logoSrc || home.staticElements?.content?.assets?.appIconSrc || "");
|
||
const logoAlt = home.staticElements?.content?.navigation?.logoAlt || "NODE.DC";
|
||
const brand = knowledge.settings?.navTitle || knowledge.title || "База знаний";
|
||
const headerHref = knowledge.settings?.homeHref || "/";
|
||
|
||
return `<header class="header astro-iyrf6vv4">
|
||
<div class="max-md:flex gap-(--sl-nav-gap) justify-between items-center h-full header-grid astro-3ef6ksr2">
|
||
<div class="overflow-clip p-1 -m-1 min-w-0 sl-flex title-wrapper astro-3ef6ksr2">
|
||
<a href="${escapeAttr(headerHref)}" class="flex items-center gap-(--sl-nav-gap) font-semibold no-underline whitespace-nowrap min-w-0 site-title astro-idrpryed">
|
||
<span class="site-title-logo inline-flex items-center shrink-0 astro-idrpryed" aria-hidden="true">${logoSrc ? `<img src="${escapeAttr(logoSrc)}" alt="${escapeAttr(logoAlt)}">` : ""}</span>
|
||
<span class="overflow-hidden astro-idrpryed" translate="no">${escapeHtml(brand)}</span>
|
||
</a>
|
||
</div>
|
||
<div class="sl-flex print:hidden search-wrapper justify-center astro-3ef6ksr2" data-search>
|
||
<site-search class="astro-3ef6ksr2 astro-2f6du2pg" data-translations="{"placeholder":"${escapeAttr(searchPlaceholder)}"}">
|
||
<button data-open-modal aria-label="${escapeAttr(searchPlaceholder)}" aria-keyshortcuts="Meta+K" class="astro-2f6du2pg" type="button">${searchIcon()}<span class="sl-hidden md:sl-block astro-2f6du2pg" aria-hidden="true">${escapeHtml(searchPlaceholder)}</span><kbd class="sl-hidden md:sl-flex astro-2f6du2pg"><kbd class="astro-2f6du2pg">⌘</kbd><kbd class="astro-2f6du2pg">K</kbd></kbd></button>
|
||
</site-search>
|
||
<div class="knowledge-search-popover" data-search-popover hidden>
|
||
<input class="knowledge-search-input" type="search" placeholder="${escapeAttr(searchPlaceholder)}" autocomplete="off" data-search-input>
|
||
<div class="knowledge-search-results" data-search-results></div>
|
||
</div>
|
||
</div>
|
||
<div class="sl-hidden md:sl-flex print:hidden flex gap-4 items-center right-group astro-3ef6ksr2">
|
||
<div class="sl-flex gap-4 items-center social-icons astro-3ef6ksr2"></div>
|
||
<a href="#" class="inline-flex items-center gap-2 font-medium no-underline whitespace-nowrap transition-[color,filter] duration-150 download-cta knowledge-download-placeholder py-[0.3rem] px-[0.5rem] pl-[0.4rem] text-sm rounded-[8px] astro-sle7k2hz" aria-hidden="true" tabindex="-1"><span class="inline-flex items-center justify-center flex-shrink-0 w-6 h-6 astro-sle7k2hz"></span><span class="astro-sle7k2hz">Download</span></a>
|
||
${renderThemeSelect()}
|
||
</div>
|
||
</div>
|
||
</header>`;
|
||
}
|
||
|
||
function pageTitle(record = null) {
|
||
if (!record) return knowledge.seo?.title || knowledge.title || "База знаний";
|
||
return record.node.seo?.title || record.node.title || "Материал";
|
||
}
|
||
|
||
function visibleTitle(record = null) {
|
||
if (!record) return knowledge.title || "База знаний";
|
||
return record.node.title || "Материал";
|
||
}
|
||
|
||
function pageDescription(record = null) {
|
||
if (!record) return knowledge.seo?.description || knowledge.settings?.description || "";
|
||
return record.node.seo?.description || stripHtml(renderKnowledgeShortcodes(record.node.bodyHtml)).slice(0, 180);
|
||
}
|
||
|
||
function canonicalForPath(path) {
|
||
return new URL(path.replace(/^\//, ""), canonicalBase()).href;
|
||
}
|
||
|
||
function renderIndexCards(records) {
|
||
const topPages = records.filter((record) => record.parents.length <= 1).slice(0, 12);
|
||
if (!topPages.length) return "";
|
||
return `<h2>Материалы</h2><ul class="knowledge-card-list">${topPages
|
||
.map(
|
||
(record) =>
|
||
`<li><a href="${escapeAttr(record.urlPath)}">${escapeHtml(record.node.title)}</a> — ${escapeHtml(pageDescription(record))}</li>`,
|
||
)
|
||
.join("")}</ul>`;
|
||
}
|
||
|
||
function renderContent(record, bodyHtml) {
|
||
const title = visibleTitle(record);
|
||
return `<div class="main-pane astro-6ptv7odt">
|
||
<main data-pagefind-body class="astro-ept3ui5y" lang="ru" dir="ltr">
|
||
<div class="content-panel astro-hxjpthjh">
|
||
<div class="sl-container astro-hxjpthjh">
|
||
<h1 id="_top" data-page-title class="astro-gldynaio">${escapeHtml(title)}</h1>
|
||
<div class="sl-markdown-content">${bodyHtml}</div>
|
||
<footer class="sl-flex astro-vcuyfr7s"><div class="meta sl-flex astro-vcuyfr7s"></div></footer>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>`;
|
||
}
|
||
|
||
function renderPage({ record = null, bodyHtml, toc = [], searchRecords }) {
|
||
const title = pageTitle(record);
|
||
const description = pageDescription(record);
|
||
const path = record?.urlPath || `/${knowledgeBasePath()}/`;
|
||
const canonical = canonicalForPath(path);
|
||
const searchPlaceholder = knowledge.settings?.searchPlaceholder || "Поиск";
|
||
|
||
return `<!doctype html>
|
||
<html lang="ru" dir="ltr" data-theme="${escapeAttr(knowledge.settings?.themeDefault || "dark")}" data-has-toc data-has-sidebar class="astro-ept3ui5y">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>${escapeHtml(title)}</title>
|
||
<link rel="shortcut icon" href="/assets/vendor/ssscript-docs/favicon.svg" type="image/svg+xml">
|
||
<meta name="generator" content="Astro v5.18.0">
|
||
<meta name="generator" content="Starlight v0.37.7">
|
||
<meta name="description" content="${escapeAttr(description)}">
|
||
<meta name="robots" content="index,follow,max-image-preview:large">
|
||
<meta property="og:title" content="${escapeAttr(title)}">
|
||
<meta property="og:type" content="article">
|
||
<meta property="og:locale" content="ru">
|
||
<meta property="og:description" content="${escapeAttr(description)}">
|
||
<meta property="og:site_name" content="NODE.DC">
|
||
<meta property="og:url" content="${escapeAttr(canonical)}">
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<link rel="canonical" href="${escapeAttr(canonical)}">
|
||
${themeProviderScript()}
|
||
${renderThemeIconsTemplate()}
|
||
<link rel="stylesheet" href="/assets/vendor/ssscript-docs/_astro/print.DNXP8c50.css" media="print">
|
||
<link rel="stylesheet" href="/assets/vendor/ssscript-docs/_astro/index.Bh6uiXgK.css">
|
||
<link rel="stylesheet" href="/assets/custom/css/knowledge.css">
|
||
<script type="module" src="/assets/vendor/ssscript-docs/_astro/page.B1D-nYk3.js"></script>
|
||
<script type="application/ld+json">${escapeJson({
|
||
"@context": "https://schema.org",
|
||
"@type": record ? "Article" : "CollectionPage",
|
||
headline: title,
|
||
description,
|
||
url: canonical,
|
||
author: {
|
||
"@type": "Organization",
|
||
name: home.seo?.organizationName || "DCTOUCH",
|
||
},
|
||
publisher: {
|
||
"@type": "Organization",
|
||
name: home.seo?.organizationName || "DCTOUCH",
|
||
},
|
||
})}</script>
|
||
</head>
|
||
<body class="astro-ept3ui5y">
|
||
<a href="#_top" class="astro-yr4ykvqi">К содержанию</a>
|
||
<div class="page sl-flex astro-iyrf6vv4">
|
||
${renderHeader(searchPlaceholder)}
|
||
${renderSidebar(record)}
|
||
<div class="main-frame astro-iyrf6vv4">
|
||
<div class="lg:sl-flex astro-6ptv7odt">
|
||
${renderRightSidebar(toc)}
|
||
${renderContent(record, bodyHtml)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script type="application/json" id="knowledge-search-index">${escapeJson(searchRecords)}</script>
|
||
${interactionScript()}
|
||
${searchScript()}
|
||
</body>
|
||
</html>
|
||
`;
|
||
}
|
||
|
||
function searchScript() {
|
||
return `<script>
|
||
(() => {
|
||
const root = document.querySelector("[data-search]");
|
||
const button = root?.querySelector("[data-open-modal]");
|
||
const popover = root?.querySelector("[data-search-popover]");
|
||
const input = root?.querySelector("[data-search-input]");
|
||
const results = root?.querySelector("[data-search-results]");
|
||
const index = JSON.parse(document.getElementById("knowledge-search-index")?.textContent || "[]");
|
||
const escapeText = (value) => String(value ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
const render = () => {
|
||
const query = input.value.trim().toLowerCase();
|
||
const matches = query
|
||
? index.filter((item) => [item.title, item.description, item.text].join(" ").toLowerCase().includes(query)).slice(0, 8)
|
||
: [];
|
||
results.innerHTML = !query
|
||
? '<div class="knowledge-search-result"><strong>Введите запрос</strong><span>Поиск идет по опубликованным материалам базы знаний.</span></div>'
|
||
: matches.length
|
||
? matches.map((item) => '<a class="knowledge-search-result" href="' + escapeText(item.url) + '"><strong>' + escapeText(item.title) + '</strong><span>' + escapeText(item.description) + '</span></a>').join("")
|
||
: '<div class="knowledge-search-result"><strong>Ничего не найдено</strong><span>Попробуйте другой запрос.</span></div>';
|
||
};
|
||
const open = () => {
|
||
if (!popover || !input) return;
|
||
popover.hidden = false;
|
||
render();
|
||
window.setTimeout(() => input.focus(), 0);
|
||
};
|
||
const close = () => {
|
||
if (popover) popover.hidden = true;
|
||
};
|
||
button?.addEventListener("click", () => {
|
||
if (popover?.hidden) open();
|
||
else close();
|
||
});
|
||
input?.addEventListener("input", render);
|
||
document.addEventListener("keydown", (event) => {
|
||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
||
event.preventDefault();
|
||
open();
|
||
}
|
||
if (event.key === "Escape") close();
|
||
});
|
||
document.addEventListener("click", (event) => {
|
||
if (root && !root.contains(event.target)) close();
|
||
});
|
||
})();
|
||
</script>`;
|
||
}
|
||
|
||
async function writePage(urlPath, html) {
|
||
const outputDir = join(repoRoot, urlPath.replace(/^\/+|\/+$/g, ""));
|
||
await mkdir(outputDir, { recursive: true });
|
||
await writeFile(join(outputDir, "index.html"), html);
|
||
}
|
||
|
||
async function writeSeoFiles(records) {
|
||
const sitemapUrls = [
|
||
{ loc: canonicalHome(), priority: "1.0" },
|
||
{ loc: canonicalForPath(`/${knowledgeBasePath()}/`), priority: "0.7" },
|
||
...records.map((record) => ({ loc: canonicalForPath(record.urlPath), priority: "0.7" })),
|
||
];
|
||
const sitemapUrl = new URL("sitemap.xml", canonicalHome()).href;
|
||
const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n");
|
||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||
${sitemapUrls
|
||
.map(
|
||
(item) => ` <url>
|
||
<loc>${escapeXml(item.loc)}</loc>
|
||
<changefreq>weekly</changefreq>
|
||
<priority>${item.priority}</priority>
|
||
</url>`,
|
||
)
|
||
.join("\n")}
|
||
</urlset>
|
||
`;
|
||
await writeFile(robotsPath, robots);
|
||
await writeFile(sitemapPath, sitemap);
|
||
}
|
||
|
||
const records = pageRecords();
|
||
const searchRecords = records.map((record) => ({
|
||
title: record.node.title || "",
|
||
description: pageDescription(record),
|
||
url: record.urlPath,
|
||
text: stripHtml(renderKnowledgeShortcodes(record.node.bodyHtml || "")),
|
||
}));
|
||
|
||
const indexIntro = normalizeContentAssets(
|
||
knowledge.introHtml ||
|
||
`<p>${escapeHtml(knowledge.settings?.description || "Материалы NODE.DC для пользователей и поисковых систем.")}</p>`,
|
||
);
|
||
const indexPrepared = prepareArticleHtml(`${indexIntro}${renderIndexCards(records)}`);
|
||
await writePage(`/${knowledgeBasePath()}/`, renderPage({ bodyHtml: indexPrepared.html, toc: indexPrepared.toc, searchRecords }));
|
||
|
||
for (const record of records) {
|
||
const prepared = prepareArticleHtml(record.node.bodyHtml || "");
|
||
await writePage(record.urlPath, renderPage({ record, bodyHtml: prepared.html, toc: prepared.toc, searchRecords }));
|
||
}
|
||
|
||
await writeSeoFiles(records);
|
||
console.log(`Rendered ${records.length + 1} knowledge pages -> /${knowledgeBasePath()}/`);
|