VEGA-6128 Генерация ссылок для index старниц
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.4-content",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "docs",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.4-content",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@beeline/design-tokens": "^1.31.0",
|
||||
|
||||
+84
-80
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
|
||||
import { overrideComponents } from './override-components'
|
||||
import { autoSectionLinksPlugin } from './plugins/auto-section-links'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const gitlab = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
@@ -39,81 +41,7 @@ const gitlab = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
|
||||
const new_version = process.env?.VITE_NEW_VERSION;
|
||||
console.log({ base: typeof new_version !== 'undefined' ? '/' : '/docs/' })
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
srcDir: ".",
|
||||
title: " ",
|
||||
description: "Документация Beeline Cloud",
|
||||
head: [['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/bee-favicon.png' }]],
|
||||
base: typeof new_version !== 'undefined' ? '/' : '/docs/',
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(tabsMarkdownPlugin)
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: overrideComponents(),
|
||||
}
|
||||
},
|
||||
locales: {
|
||||
root: {
|
||||
label: 'Русский',
|
||||
lang: 'ru',
|
||||
}
|
||||
},
|
||||
themeConfig: {
|
||||
logo: {
|
||||
light: '/img/logo-cloud.svg',
|
||||
dark: '/img/logo-cloud.svg',
|
||||
alt: '',
|
||||
},
|
||||
search: {
|
||||
provider: 'local',
|
||||
options: {
|
||||
locales: {
|
||||
root: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: 'Поиск',
|
||||
buttonAriaLabel: 'Поиск'
|
||||
},
|
||||
modal: {
|
||||
noResultsText: 'По вашему запросу ничего не найдено',
|
||||
resetButtonTitle: 'Сбросить',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
// nav: [
|
||||
// {
|
||||
// text: 'Документация',
|
||||
// link: '/guide/',
|
||||
// },
|
||||
// {
|
||||
// text: 'API',
|
||||
// link: '',
|
||||
// },
|
||||
// {
|
||||
// text: 'Terraform',
|
||||
// // link: '/terraform/',
|
||||
// link: '',
|
||||
// },
|
||||
// ],
|
||||
|
||||
docFooter: {
|
||||
next: 'Вперед',
|
||||
prev: 'Назад'
|
||||
},
|
||||
|
||||
outline: {
|
||||
label: 'Содержание'
|
||||
},
|
||||
sidebar: {
|
||||
const sidebarConfig = {
|
||||
'/platform/': [
|
||||
{
|
||||
text: 'Платформа Beeline Cloud', link: '/platform/index.md',
|
||||
@@ -140,11 +68,10 @@ export default defineConfig({
|
||||
text: 'Резервное копирование', link: '/backups/index.md',
|
||||
},
|
||||
{
|
||||
text: 'Обзор сервиса', link: '/backups/backups-overview.md',
|
||||
text: 'Обзор сервиса', link: '/backups/about.md',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{text: 'О сервисе', link: '/backups/about.md'},
|
||||
{text: 'Квоты и лимиты', link: '/backups/backup-quatos.md'},
|
||||
{text: 'Квоты и лимиты', link: '/backups/backup-quatos.md'},
|
||||
]
|
||||
},
|
||||
{text: 'Резервное копирование виртуальных машин Beeline Cloud', link: '/backups/backup-internal-infra.md'},
|
||||
@@ -216,7 +143,6 @@ export default defineConfig({
|
||||
{ text: 'Настройка site-to-site подключения с помощью IPSec', link: '/vdc/vdc-how-to/networks/how-to-setup-ipsec-vpn.md',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{text: 'Настройка IPSec VPN', link: '/vdc/vdc-how-to/networks/ipsec/setup-ipsec-vpn.md'},
|
||||
{text: 'Настройка ASAv', link: '/vdc/vdc-how-to/networks/ipsec/asav.md'},
|
||||
{text: 'Настройка CSR 1000v', link: '/vdc/vdc-how-to/networks/ipsec/csr1000v.md'},
|
||||
{text: 'Настройка Fortigate', link: '/vdc/vdc-how-to/networks/ipsec/fortigate.md'},
|
||||
@@ -321,7 +247,85 @@ export default defineConfig({
|
||||
text: 'Подключение к виртуальному рабочему месту', link: '/vdi/vdi-how-to/vdi-connect.md'
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
srcDir: ".",
|
||||
title: " ",
|
||||
description: "Документация Beeline Cloud",
|
||||
head: [['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/bee-favicon.png' }]],
|
||||
base: typeof new_version !== 'undefined' ? '/' : '/docs/',
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(tabsMarkdownPlugin)
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: overrideComponents(),
|
||||
},
|
||||
plugins: [
|
||||
autoSectionLinksPlugin(resolve(__dirname, '..'), sidebarConfig) as any,
|
||||
]
|
||||
},
|
||||
locales: {
|
||||
root: {
|
||||
label: 'Русский',
|
||||
lang: 'ru',
|
||||
}
|
||||
},
|
||||
themeConfig: {
|
||||
logo: {
|
||||
light: '/img/logo-cloud.svg',
|
||||
dark: '/img/logo-cloud.svg',
|
||||
alt: '',
|
||||
},
|
||||
search: {
|
||||
provider: 'local',
|
||||
options: {
|
||||
locales: {
|
||||
root: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: 'Поиск',
|
||||
buttonAriaLabel: 'Поиск'
|
||||
},
|
||||
modal: {
|
||||
noResultsText: 'По вашему запросу ничего не найдено',
|
||||
resetButtonTitle: 'Сбросить',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
// nav: [
|
||||
// {
|
||||
// text: 'Документация',
|
||||
// link: '/guide/',
|
||||
// },
|
||||
// {
|
||||
// text: 'API',
|
||||
// link: '',
|
||||
// },
|
||||
// {
|
||||
// text: 'Terraform',
|
||||
// // link: '/terraform/',
|
||||
// link: '',
|
||||
// },
|
||||
// ],
|
||||
|
||||
docFooter: {
|
||||
next: 'Вперед',
|
||||
prev: 'Назад'
|
||||
},
|
||||
|
||||
outline: {
|
||||
label: 'Содержание'
|
||||
},
|
||||
sidebar: sidebarConfig,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import type { SidebarItem } from './utils/types'
|
||||
import { processAllFiles } from './hook/process-files'
|
||||
|
||||
export const autoSectionLinksPlugin = (
|
||||
srcDir: string,
|
||||
sidebarConfig: Record<string, SidebarItem[]>
|
||||
): Plugin => ({
|
||||
name: 'auto-section-links',
|
||||
buildStart: () => {
|
||||
processAllFiles(srcDir, sidebarConfig)
|
||||
},
|
||||
configureServer: () => {
|
||||
processAllFiles(srcDir, sidebarConfig)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
export const INDEX_FILE = 'index.md'
|
||||
export const INDEX_FILE_PATTERN = /-index\.md$|index\.md$/
|
||||
export const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---/
|
||||
export const SECTION_LINK_KEYS = ['title', 'link', 'description'] as const
|
||||
@@ -0,0 +1,36 @@
|
||||
import { existsSync } from 'fs'
|
||||
import { relative, resolve } from 'path'
|
||||
import type { SidebarItem } from '../utils/types'
|
||||
import { findMarkdownFiles } from '../utils/file-finder'
|
||||
import { processIndexFile, processPageWithItems } from '../utils/file-processor'
|
||||
import { INDEX_FILE, INDEX_FILE_PATTERN } from '../constants'
|
||||
|
||||
export const processAllFiles = (
|
||||
srcDir: string,
|
||||
sidebarConfig: Record<string, SidebarItem[]>
|
||||
) => {
|
||||
const srcPath = resolve(srcDir)
|
||||
|
||||
for (const [folderPath, items] of Object.entries(sidebarConfig)) {
|
||||
const normalizedFolder = folderPath.replace(/^\/+|\/+$/g, '')
|
||||
const folderFullPath = resolve(srcPath, normalizedFolder)
|
||||
|
||||
if (!existsSync(folderFullPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const mdFiles = findMarkdownFiles(folderFullPath)
|
||||
|
||||
for (const filePath of mdFiles) {
|
||||
const fileName = filePath.split(/[/\\]/).pop() || ''
|
||||
const relativePath = relative(srcPath, filePath).replace(/\\/g, '/')
|
||||
const normalizedPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`
|
||||
|
||||
if (fileName === INDEX_FILE) {
|
||||
processIndexFile(filePath, items, srcPath)
|
||||
} else if (INDEX_FILE_PATTERN.test(fileName) && fileName !== INDEX_FILE) {
|
||||
processPageWithItems(filePath, items, srcPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { readFileSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { parseFrontmatter } from './frontmatter'
|
||||
|
||||
export const getPageDescription = (filePath: string, srcDir: string) => {
|
||||
try {
|
||||
const fullPath = resolve(srcDir, filePath.replace(/^\//, ''))
|
||||
if (!existsSync(fullPath)) return undefined
|
||||
|
||||
const fileContent = readFileSync(fullPath, 'utf-8')
|
||||
const { frontmatter } = parseFrontmatter(fileContent)
|
||||
|
||||
return frontmatter.description as string | undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { readdirSync, statSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
export const findMarkdownFiles = (dir: string) => {
|
||||
const files: string[] = []
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dir)
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry)
|
||||
const stat = statSync(fullPath)
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...findMarkdownFiles(fullPath))
|
||||
} else if (entry.endsWith('.md')) {
|
||||
files.push(fullPath)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
||||
import { relative, resolve } from 'path'
|
||||
import type { SidebarItem } from './types'
|
||||
import { parseFrontmatter, stringifyFrontmatter } from './frontmatter'
|
||||
import { mergeSectionLinks, extractTopLevelLinks, extractItemsForPage } from './links'
|
||||
import { normalizeLink } from './path-utils'
|
||||
import { SectionLinkListItem } from '../../theme/components/SectionLinkList/SectionLinkList.types'
|
||||
|
||||
const removeDuplicates = (links: SectionLinkListItem[]) => {
|
||||
const result: SectionLinkListItem[] = []
|
||||
const seenLinks = new Set<string>()
|
||||
|
||||
for (const link of links) {
|
||||
if (link.link) {
|
||||
const normalized = normalizeLink(link.link)
|
||||
if (!seenLinks.has(normalized)) {
|
||||
seenLinks.add(normalized)
|
||||
result.push(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const hasChanges = (
|
||||
existingLinks: SectionLinkListItem[],
|
||||
mergedLinks: SectionLinkListItem[],
|
||||
newLinks: SectionLinkListItem[]
|
||||
) => {
|
||||
const existingLinksSet = new Set(
|
||||
existingLinks
|
||||
.map(link => link.link)
|
||||
.filter((link): link is string => Boolean(link))
|
||||
.map(normalizeLink)
|
||||
)
|
||||
|
||||
const hasNewLinks = newLinks.some(
|
||||
link => link.link && !existingLinksSet.has(normalizeLink(link.link))
|
||||
)
|
||||
|
||||
if (hasNewLinks) return true
|
||||
|
||||
const sidebarLinksSet = new Set(
|
||||
newLinks
|
||||
.map(link => link.link)
|
||||
.filter((link): link is string => Boolean(link))
|
||||
.map(normalizeLink)
|
||||
)
|
||||
|
||||
const existingSidebarLinks = existingLinks
|
||||
.filter(link => link.link && sidebarLinksSet.has(normalizeLink(link.link)))
|
||||
.map(link => normalizeLink(link.link!))
|
||||
.join('|')
|
||||
|
||||
const mergedSidebarLinks = mergedLinks
|
||||
.slice(0, newLinks.length)
|
||||
.filter(link => link.link && sidebarLinksSet.has(normalizeLink(link.link)))
|
||||
.map(link => normalizeLink(link.link!))
|
||||
.join('|')
|
||||
|
||||
return existingSidebarLinks !== mergedSidebarLinks && mergedLinks.length === existingLinks.length
|
||||
}
|
||||
|
||||
export const processIndexFile = (
|
||||
filePath: string,
|
||||
sidebarItems: SidebarItem[],
|
||||
srcDir: string
|
||||
) => {
|
||||
if (!existsSync(filePath)) return false
|
||||
|
||||
try {
|
||||
const fileContent = readFileSync(filePath, 'utf-8')
|
||||
const { frontmatter, content } = parseFrontmatter(fileContent)
|
||||
|
||||
const relativePath = relative(srcDir, filePath).replace(/\\/g, '/')
|
||||
const normalizedPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`
|
||||
|
||||
const rawExistingLinks: SectionLinkListItem[] = Array.isArray(frontmatter.section_links)
|
||||
? [...frontmatter.section_links]
|
||||
: []
|
||||
|
||||
const existingLinks = removeDuplicates(rawExistingLinks)
|
||||
const newLinks = extractTopLevelLinks(sidebarItems, normalizedPath, srcDir)
|
||||
|
||||
if (!newLinks.length) return false
|
||||
|
||||
const mergedLinks = removeDuplicates(mergeSectionLinks(existingLinks, newLinks))
|
||||
|
||||
if (!hasChanges(existingLinks, mergedLinks, newLinks)) return false
|
||||
|
||||
frontmatter.section_links = mergedLinks
|
||||
const updatedContent = stringifyFrontmatter(frontmatter, content)
|
||||
writeFileSync(filePath, updatedContent, 'utf-8')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const processPageWithItems = (
|
||||
filePath: string,
|
||||
sidebarItems: SidebarItem[],
|
||||
srcDir: string
|
||||
) => {
|
||||
if (!existsSync(filePath)) return false
|
||||
|
||||
try {
|
||||
const fileContent = readFileSync(filePath, 'utf-8')
|
||||
const { frontmatter, content } = parseFrontmatter(fileContent)
|
||||
|
||||
const relativePath = relative(srcDir, filePath).replace(/\\/g, '/')
|
||||
const normalizedPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`
|
||||
|
||||
const rawExistingLinks: SectionLinkListItem[] = Array.isArray(frontmatter.section_links)
|
||||
? [...frontmatter.section_links]
|
||||
: []
|
||||
|
||||
const existingLinks = removeDuplicates(rawExistingLinks)
|
||||
const newLinks = extractItemsForPage(sidebarItems, normalizedPath, srcDir)
|
||||
|
||||
if (!newLinks.length) return false
|
||||
|
||||
const mergedLinks = removeDuplicates(mergeSectionLinks(existingLinks, newLinks))
|
||||
|
||||
if (!hasChanges(existingLinks, mergedLinks, newLinks)) return false
|
||||
|
||||
frontmatter.section_links = mergedLinks
|
||||
const updatedContent = stringifyFrontmatter(frontmatter, content)
|
||||
writeFileSync(filePath, updatedContent, 'utf-8')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { Frontmatter } from './types'
|
||||
import { SECTION_LINK_KEYS, FRONTMATTER_REGEX } from '../constants'
|
||||
import { SectionLinkListItem } from '../../theme/components/SectionLinkList/SectionLinkList.types'
|
||||
|
||||
export const parseFrontmatter = (content: string) => {
|
||||
const frontmatterMatch = content.match(FRONTMATTER_REGEX)
|
||||
if (!frontmatterMatch) {
|
||||
const cleanContent = content.replace(/^---[\s\S]*?---\s*\n*/g, '').trim()
|
||||
return { frontmatter: {}, content: cleanContent || content }
|
||||
}
|
||||
|
||||
const [, frontmatterText] = frontmatterMatch
|
||||
const frontmatterEnd = frontmatterMatch[0].length
|
||||
const cleanContent = content.slice(frontmatterEnd).replace(/^---[\s\S]*?---\s*\n*/g, '').trim()
|
||||
|
||||
const frontmatter: Frontmatter = {}
|
||||
const lines = frontmatterText.split('\n')
|
||||
let currentKey: string | undefined
|
||||
let currentValue: SectionLinkListItem[] = []
|
||||
let currentItem: Partial<SectionLinkListItem> | undefined
|
||||
let inArray = false
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const lineIndent = line.match(/^(\s*)/)?.[1]?.length ?? 0
|
||||
|
||||
if (lineIndent === 0 && trimmed.includes(':')) {
|
||||
if (inArray && currentKey) {
|
||||
frontmatter[currentKey] = currentValue
|
||||
currentValue = []
|
||||
inArray = false
|
||||
}
|
||||
|
||||
const colonMatch = trimmed.match(/^([^:]+):\s*(.*)$/)
|
||||
if (colonMatch) {
|
||||
const [, key, value] = colonMatch
|
||||
currentKey = key.trim()
|
||||
const trimmedValue = value.trim()
|
||||
|
||||
if (trimmedValue === '') {
|
||||
inArray = true
|
||||
currentValue = []
|
||||
} else {
|
||||
frontmatter[currentKey] = trimmedValue.replace(/^["']|["']$/g, '')
|
||||
}
|
||||
}
|
||||
} else if (lineIndent === 2 && trimmed.startsWith('- ')) {
|
||||
if (!inArray && currentKey) {
|
||||
inArray = true
|
||||
currentValue = []
|
||||
}
|
||||
|
||||
if (currentItem && !currentValue.includes(currentItem as SectionLinkListItem)) {
|
||||
currentValue.push(currentItem as SectionLinkListItem)
|
||||
}
|
||||
|
||||
const itemText = trimmed.slice(2).trim()
|
||||
if (itemText.includes(':')) {
|
||||
currentItem = {}
|
||||
const parts = itemText.split(',').map(p => p.trim())
|
||||
for (const part of parts) {
|
||||
const colonMatch = part.match(/^(\w+):\s*(.+)$/)
|
||||
if (colonMatch) {
|
||||
const [, key, value] = colonMatch
|
||||
const sectionKey = key as keyof SectionLinkListItem
|
||||
if (SECTION_LINK_KEYS.includes(sectionKey)) {
|
||||
currentItem[sectionKey] = value.replace(/^["']|["']$/g, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentValue.push({ title: itemText, link: '' })
|
||||
}
|
||||
} else if (lineIndent >= 4 && currentItem && trimmed.includes(':')) {
|
||||
const colonIndex = trimmed.indexOf(':')
|
||||
if (colonIndex !== -1) {
|
||||
const key = trimmed.substring(0, colonIndex).trim()
|
||||
const value = trimmed.substring(colonIndex + 1).trim()
|
||||
const sectionKey = key as keyof SectionLinkListItem
|
||||
if (SECTION_LINK_KEYS.includes(sectionKey)) {
|
||||
currentItem[sectionKey] = value === '' ? '' : value.replace(/^["']|["']$/g, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentItem && inArray && !currentValue.includes(currentItem as SectionLinkListItem)) {
|
||||
currentValue.push(currentItem as SectionLinkListItem)
|
||||
}
|
||||
if (inArray && currentKey) {
|
||||
frontmatter[currentKey] = currentValue
|
||||
}
|
||||
|
||||
return { frontmatter, content: cleanContent || content }
|
||||
}
|
||||
|
||||
export const stringifyFrontmatter = (frontmatter: Frontmatter, content: string) => {
|
||||
const lines: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(frontmatter)) {
|
||||
if (Array.isArray(value)) {
|
||||
lines.push(`${key}:`)
|
||||
for (const item of value) {
|
||||
if (typeof item === 'object' && item) {
|
||||
const link = item as SectionLinkListItem
|
||||
lines.push(` - title: ${link.title ?? ''}`)
|
||||
if (link.link) lines.push(` link: ${link.link}`)
|
||||
if ('description' in link && link.description !== undefined && link.description !== null && String(link.description).trim() !== '') {
|
||||
lines.push(` description: ${String(link.description)}`)
|
||||
}
|
||||
} else {
|
||||
lines.push(` - ${item}`)
|
||||
}
|
||||
}
|
||||
} else if (value) {
|
||||
const strValue = String(value)
|
||||
const needsQuotes = strValue.includes(':') || (strValue.includes(' ') && !strValue.startsWith('"')) || strValue === ''
|
||||
const escaped = strValue.replace(/"/g, '\\"')
|
||||
lines.push(`${key}: ${needsQuotes ? `"${escaped}"` : strValue}`)
|
||||
}
|
||||
}
|
||||
|
||||
return `---\n${lines.join('\n')}\n---\n\n${content}`
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { SidebarItem } from './types'
|
||||
import { getPageDescription } from './descriptions'
|
||||
import { normalizeLink } from './path-utils'
|
||||
import { SectionLinkListItem } from '../../theme/components/SectionLinkList/SectionLinkList.types'
|
||||
|
||||
export const mergeSectionLinks = (
|
||||
existingLinks: SectionLinkListItem[],
|
||||
newLinks: SectionLinkListItem[]
|
||||
) => {
|
||||
const existingLinksMap = new Map<string, SectionLinkListItem>()
|
||||
const result: SectionLinkListItem[] = []
|
||||
const processedLinks = new Set<string>()
|
||||
|
||||
for (const existingLink of existingLinks) {
|
||||
if (existingLink.link) {
|
||||
const normalizedLink = normalizeLink(existingLink.link)
|
||||
if (!existingLinksMap.has(normalizedLink)) {
|
||||
existingLinksMap.set(normalizedLink, existingLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const newLink of newLinks) {
|
||||
if (newLink.link) {
|
||||
const normalizedLink = normalizeLink(newLink.link)
|
||||
|
||||
if (!processedLinks.has(normalizedLink)) {
|
||||
processedLinks.add(normalizedLink)
|
||||
|
||||
if (existingLinksMap.has(normalizedLink)) {
|
||||
const existingLink = existingLinksMap.get(normalizedLink)!
|
||||
result.push({ ...existingLink, title: newLink.title })
|
||||
} else {
|
||||
result.push(newLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingLink of existingLinks) {
|
||||
if (existingLink.link) {
|
||||
const normalizedLink = normalizeLink(existingLink.link)
|
||||
if (!processedLinks.has(normalizedLink)) {
|
||||
processedLinks.add(normalizedLink)
|
||||
result.push({ ...existingLink })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const extractTopLevelLinks = (
|
||||
items: SidebarItem[],
|
||||
currentIndexPath: string,
|
||||
srcDir: string
|
||||
) => {
|
||||
const links: SectionLinkListItem[] = []
|
||||
const normalizedCurrentPath = normalizeLink(currentIndexPath)
|
||||
|
||||
for (const item of items) {
|
||||
if (item.link) {
|
||||
const normalizedItemPath = normalizeLink(item.link)
|
||||
if (normalizedItemPath === normalizedCurrentPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
links.push({
|
||||
title: item.text || '',
|
||||
link: item.link,
|
||||
description: getPageDescription(item.link, srcDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
const findItemsRecursive = (
|
||||
itemsList: SidebarItem[],
|
||||
pagePath: string,
|
||||
srcDir: string
|
||||
): SectionLinkListItem[] => {
|
||||
const normalizedPagePath = normalizeLink(pagePath)
|
||||
const links: SectionLinkListItem[] = []
|
||||
|
||||
for (const item of itemsList) {
|
||||
if (item.link) {
|
||||
const normalizedItemPath = normalizeLink(item.link)
|
||||
|
||||
if (normalizedItemPath === normalizedPagePath && item.items && Array.isArray(item.items) && item.items.length > 0) {
|
||||
for (const subItem of item.items) {
|
||||
if (subItem.link) {
|
||||
links.push({
|
||||
title: subItem.text || '',
|
||||
link: subItem.link,
|
||||
description: getPageDescription(subItem.link, srcDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
return links
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
const found = findItemsRecursive(item.items, pagePath, srcDir)
|
||||
if (found.length > 0) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
export const extractItemsForPage = (
|
||||
items: SidebarItem[],
|
||||
pagePath: string,
|
||||
srcDir: string
|
||||
) => findItemsRecursive(items, pagePath, srcDir)
|
||||
@@ -0,0 +1,6 @@
|
||||
export const normalizeLink = (link: string): string =>
|
||||
!link ? '' : link
|
||||
.replace(/^\/+/, '')
|
||||
.replace(/\.md$/, '')
|
||||
.replace(/\\/g, '/')
|
||||
.trim()
|
||||
@@ -0,0 +1,12 @@
|
||||
import { SectionLinkListItem } from "../../theme/components/SectionLinkList/SectionLinkList.types"
|
||||
|
||||
export type SidebarItem = {
|
||||
text: string
|
||||
link?: string
|
||||
items?: SidebarItem[]
|
||||
}
|
||||
|
||||
export type Frontmatter = {
|
||||
section_links?: SectionLinkListItem[]
|
||||
[key: string]: any
|
||||
}
|
||||
@@ -7,8 +7,7 @@ section_links:
|
||||
link: /compute/compute-how-to/compute-connect-inside.md
|
||||
description: Подключиться к виртуальной машине по SSH с помощью ключевой пары по внутреннему IP-адресу через джамп-хост
|
||||
- title: Подключение по SSH по логину и паролю
|
||||
link: /compute/compute-how-to/compute-connect-inside.md
|
||||
description: Подключиться к виртуальной машине по SSH с помощью логина и пароля
|
||||
---
|
||||
link: /compute/compute-how-to/compute-connect-pwd.md
|
||||
---
|
||||
|
||||
# Подключение к ВМ
|
||||
# Подключение к ВМ
|
||||
@@ -3,6 +3,8 @@ section_links:
|
||||
- title: Обзор сервиса
|
||||
link: /compute/compute-overview-index.md
|
||||
description: Обзор сервиса, решаемые задачи, характеристики оборудования
|
||||
- title: Быстрый старт
|
||||
link: /compute/compute-getting-started.md
|
||||
- title: Виртуальные машины
|
||||
link: /compute/compute-how-to/compute-index.md
|
||||
description: Создание виртуальной машины и подключение к ней, управление виртуальной машиной
|
||||
@@ -14,9 +16,11 @@ section_links:
|
||||
description: Резервирование, назначение IP-адреса виртуальной машине, удаление IP-адресов
|
||||
- title: Группы размещения
|
||||
link: /compute/compute-how-to/compute-affinity.md
|
||||
description: Создание правил размещения виртуальных машин на физических хостах, управление группами размещения
|
||||
---
|
||||
description: Создание правил размещения виртуальных машин на физических хостах, управление группами размещения
|
||||
- title: Сети
|
||||
link: /compute/compute-how-to/compute-network/compute-network-index.md
|
||||
---
|
||||
|
||||
# Виртуальные машины
|
||||
|
||||
Сервис **Виртуальные машины** предоставляет пользователям виртуальные машины.
|
||||
Сервис **Виртуальные машины** предоставляет пользователям виртуальные машины.
|
||||
Reference in New Issue
Block a user