import { readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const knowledgePath = resolve(repoRoot, "content", "knowledge.json"); const args = process.argv.slice(2); const inputPath = args.find((value) => !value.startsWith("--")); const dryRun = args.includes("--dry-run"); if (!inputPath) { throw new Error("Укажите путь к NODEDC_Knowledge_SEO_replacement_pack.md"); } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function cleanMetaValue(value) { const trimmed = String(value || "").trim(); return trimmed.startsWith("`") && trimmed.endsWith("`") ? trimmed.slice(1, -1).trim() : trimmed; } function metadataValue(section, label) { const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const match = section.match(new RegExp(`^\\*\\*${escapedLabel}:\\*\\*\\s*(.+?)\\s*$`, "m")); return cleanMetaValue(match?.[1] || ""); } function inlineMarkdown(value) { return escapeHtml(String(value || "").trim()) .replace(/`([^`]+)`/g, "$1") .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1'); } function buttonShortcode(label, href) { const name = String(label || "Открыть").replaceAll('"', """); const link = String(href || "/").replaceAll('"', """); return `

[button name="${name}" link="${link}"]

`; } function markdownToHtml(markdown) { const lines = String(markdown || "").replaceAll("\r\n", "\n").split("\n"); const blocks = []; let index = 0; while (index < lines.length) { const line = lines[index].trim(); if (!line || line === "---") { index += 1; continue; } if (/^#\s+/.test(line)) { index += 1; continue; } const heading = /^(#{2,3})\s+(.+)$/.exec(line); if (heading) { const level = heading[1].length; blocks.push(`${inlineMarkdown(heading[2])}`); index += 1; continue; } const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(line); if (link) { blocks.push(buttonShortcode(link[1], link[2])); index += 1; continue; } const bullet = /^[-*]\s+(.+)$/.exec(line); if (bullet) { const items = []; while (index < lines.length) { const item = /^[-*]\s+(.+)$/.exec(lines[index].trim()); if (!item) break; items.push(`
  • ${inlineMarkdown(item[1])}
  • `); index += 1; } blocks.push(``); continue; } const ordered = /^\d+\.\s+(.+)$/.exec(line); if (ordered) { const items = []; while (index < lines.length) { const item = /^\d+\.\s+(.+)$/.exec(lines[index].trim()); if (!item) break; items.push(`
  • ${inlineMarkdown(item[1])}
  • `); index += 1; } blocks.push(`
      ${items.join("")}
    `); continue; } const paragraph = []; while (index < lines.length) { const next = lines[index].trim(); if (!next || next === "---" || /^#{1,3}\s+/.test(next) || /^[-*]\s+/.test(next) || /^\d+\.\s+/.test(next) || /^\[[^\]]+\]\([^)\s]+\)$/.test(next)) { break; } paragraph.push(next); index += 1; } if (paragraph.length) { blocks.push(`

    ${inlineMarkdown(paragraph.join(" "))}

    `); } else { index += 1; } } return `${blocks.join("\n")}\n`; } function parsePack(markdown) { const matches = [...markdown.matchAll(/^## (\d+)\. (.+)$/gm)]; const pages = new Map(); for (let index = 0; index < matches.length; index += 1) { const match = matches[index]; const number = Number(match[1]); if (!Number.isInteger(number) || number < 1 || number > 23) continue; const section = markdown.slice(match.index + match[0].length, matches[index + 1]?.index || markdown.length); const h1 = /^# (.+)$/m.exec(section); if (!h1) throw new Error(`В SEO-паке не найден H1 для раздела ${number}`); const bodyStart = (h1.index || 0) + h1[0].length; const page = { number, title: h1[1].trim(), id: metadataValue(section, "ID"), slug: metadataValue(section, "Slug"), seoTitle: metadataValue(section, "SEO title"), seoDescription: metadataValue(section, "SEO description"), bodyHtml: markdownToHtml(section.slice(bodyStart)), }; if (number > 1 && (!page.id || !page.slug || !page.seoTitle || !page.seoDescription)) { throw new Error(`В SEO-паке неполные метаданные для «${page.title}»`); } pages.set(number, page); } if (pages.size !== 23 || !pages.has(1)) { throw new Error(`Ожидалось 23 страницы SEO-пака, найдено ${pages.size}`); } return pages; } function walk(nodes, callback) { for (const node of nodes || []) { callback(node); walk(node.children, callback); } } function leadingMediaParagraph(html) { return String(html || "").match(/^\s*(

    \s*<(?:img|video)\b[\s\S]*?<\/p>)/i)?.[1] || ""; } function materialFromPage(page, existing) { const media = leadingMediaParagraph(existing?.bodyHtml); return { ...(existing || {}), id: page.id, title: page.title, slug: page.slug, enabled: true, useContent: true, seo: { title: page.seoTitle, description: page.seoDescription, }, bodyHtml: `${media ? `${media}\n` : ""}${page.bodyHtml}`, children: [], }; } const treeOrder = [ { id: "platform-overview", children: ["hub", "engine", "ops", "integration-layer", "artifact-event-layer"], }, { id: "agent-context", children: ["document-agent", "agent-catalog"], }, { id: "applied-modules", children: [ "tender-agent", "1c-integration", "drone-management", "city-digital-twins", "industrial-contour", "enterprise-management", "construction-contour", "seo-module", "procurement-contour", "project-office", "video-analytics", "engineering-contour", ], }, ]; const [markdown, rawKnowledge] = await Promise.all([ readFile(resolve(inputPath), "utf8"), readFile(knowledgePath, "utf8"), ]); const pages = parsePack(markdown); const knowledge = JSON.parse(rawKnowledge); const existingNodes = new Map(); walk(knowledge.tree, (node) => existingNodes.set(node.id, node)); const rootPage = pages.get(1); const pageById = new Map([...pages.values()].filter((page) => page.number > 1).map((page) => [page.id, page])); const materials = new Map([...pageById].map(([id, page]) => [id, materialFromPage(page, existingNodes.get(id))])); for (const { id, children } of treeOrder) { const node = materials.get(id); if (!node) throw new Error(`Не найден раздел «${id}» в SEO-паке`); node.children = children.map((childId) => { const child = materials.get(childId); if (!child) throw new Error(`Не найден дочерний раздел «${childId}» в SEO-паке`); return child; }); } knowledge.title = rootPage.title; knowledge.seo = { ...(knowledge.seo || {}), title: rootPage.seoTitle, description: rootPage.seoDescription, }; knowledge.settings ||= {}; knowledge.settings.description = rootPage.seoDescription; knowledge.introHtml = rootPage.bodyHtml; knowledge.tree = treeOrder.map(({ id }) => materials.get(id)); const summary = { pages: pages.size, updated: [...pageById.keys()].filter((id) => existingNodes.has(id)).length, added: [...pageById.keys()].filter((id) => !existingNodes.has(id)).length, tree: knowledge.tree.map((node) => ({ id: node.id, children: node.children.map((child) => child.id) })), }; if (!dryRun) { await writeFile(knowledgePath, `${JSON.stringify(knowledge, null, 2)}\n`); } console.log(`${dryRun ? "Checked" : "Imported"} Knowledge SEO pack: ${JSON.stringify(summary)}`);