Исправления ссылок
This commit is contained in:
+4
-1
@@ -4,9 +4,12 @@
|
|||||||
"description": "Beeline Cloud docs",
|
"description": "Beeline Cloud docs",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"predev": "node scripts/check-links.mjs",
|
||||||
"dev": "vitepress dev src",
|
"dev": "vitepress dev src",
|
||||||
|
"prebuild": "node scripts/check-links.mjs",
|
||||||
"build": "vitepress build src",
|
"build": "vitepress build src",
|
||||||
"preview": "vitepress preview src"
|
"preview": "vitepress preview src",
|
||||||
|
"check-links": "node scripts/check-links.mjs"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"authors": {
|
"authors": {
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { readFileSync, readdirSync, statSync, existsSync, realpathSync } from 'node:fs';
|
||||||
|
import { join, dirname, resolve, relative, extname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = resolve(fileURLToPath(import.meta.url), '../..');
|
||||||
|
const SRC = join(ROOT, 'src');
|
||||||
|
const PUBLIC_DIR = join(SRC, 'public');
|
||||||
|
const CONFIG = join(SRC, '.vitepress/config.mts');
|
||||||
|
const HOME = join(SRC, 'index.md');
|
||||||
|
|
||||||
|
function walk(dir, out = [], skipDirs = new Set()) {
|
||||||
|
if (!existsSync(dir)) return out;
|
||||||
|
for (const name of readdirSync(dir)) {
|
||||||
|
if (skipDirs.has(name)) continue;
|
||||||
|
const p = join(dir, name);
|
||||||
|
const s = statSync(p);
|
||||||
|
if (s.isDirectory()) walk(p, out, skipDirs);
|
||||||
|
else out.push(p);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docFiles = walk(SRC, [], new Set(['node_modules', '.vitepress', 'public', 'assets']));
|
||||||
|
const mdFiles = docFiles.filter(f => f.endsWith('.md'));
|
||||||
|
|
||||||
|
function checkTarget(target, fromFile) {
|
||||||
|
const url = target.trim();
|
||||||
|
if (!url) return { ok: false, reason: 'empty' };
|
||||||
|
if (/^(https?:|mailto:|tel:|ftp:|data:|javascript:|#)/i.test(url)) return { ok: true, skip: true };
|
||||||
|
const [pathPart] = url.split('#');
|
||||||
|
const [pathOnly] = pathPart.split('?');
|
||||||
|
if (!pathOnly) return { ok: true, skip: true };
|
||||||
|
|
||||||
|
let abs;
|
||||||
|
if (pathOnly.startsWith('/')) {
|
||||||
|
abs = join(SRC, pathOnly);
|
||||||
|
if (!existsSync(abs)) {
|
||||||
|
const inPublic = join(PUBLIC_DIR, pathOnly);
|
||||||
|
if (existsSync(inPublic)) return { ok: true };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
abs = resolve(dirname(fromFile), pathOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(abs)) return { ok: true };
|
||||||
|
if (extname(abs) === '') {
|
||||||
|
if (existsSync(abs + '.md')) return { ok: true };
|
||||||
|
if (existsSync(join(abs, 'index.md'))) return { ok: true };
|
||||||
|
}
|
||||||
|
if (extname(abs) === '.html') {
|
||||||
|
if (existsSync(abs.replace(/\.html$/, '.md'))) return { ok: true };
|
||||||
|
}
|
||||||
|
return { ok: false, reason: 'not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveToMd(target, fromFile) {
|
||||||
|
const url = target.trim();
|
||||||
|
if (!url) return null;
|
||||||
|
if (/^(https?:|mailto:|tel:|ftp:|data:|javascript:|#)/i.test(url)) return null;
|
||||||
|
const [pathPart] = url.split('#');
|
||||||
|
const [pathOnly] = pathPart.split('?');
|
||||||
|
if (!pathOnly) return null;
|
||||||
|
|
||||||
|
const abs = pathOnly.startsWith('/')
|
||||||
|
? join(SRC, pathOnly)
|
||||||
|
: resolve(dirname(fromFile), pathOnly);
|
||||||
|
|
||||||
|
if (extname(abs) === '.md' && existsSync(abs)) return abs;
|
||||||
|
if (extname(abs) === '') {
|
||||||
|
if (existsSync(abs + '.md')) return abs + '.md';
|
||||||
|
if (existsSync(join(abs, 'index.md'))) return join(abs, 'index.md');
|
||||||
|
}
|
||||||
|
if (extname(abs) === '.html') {
|
||||||
|
const asMd = abs.replace(/\.html$/, '.md');
|
||||||
|
if (existsSync(asMd)) return asMd;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const broken = [];
|
||||||
|
const usedMd = new Set();
|
||||||
|
const mdLinkRe = /(!?)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
||||||
|
const htmlLinkRe = /<(?:img[^>]+src|a[^>]+href|source[^>]+src|video[^>]+src|link[^>]+href)\s*=\s*["']([^"']+)["']/gi;
|
||||||
|
const frontmatterLinkRe = /^\s*-?\s*link:\s*['"]?([^\s'"#]+)['"]?\s*$/;
|
||||||
|
const includeRe = /<!--\s*@include:\s*([^\s>-]+(?:\.md)?)[^>]*-->/g;
|
||||||
|
|
||||||
|
function recordUse(target, fromFile) {
|
||||||
|
const md = resolveToMd(target, fromFile);
|
||||||
|
if (md) usedMd.add(md);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of mdFiles) {
|
||||||
|
const content = readFileSync(file, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let fmStart = -1, fmEnd = -1;
|
||||||
|
if (lines[0]?.trim() === '---') {
|
||||||
|
fmStart = 0;
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
if (lines[i].trim() === '---') { fmEnd = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const inFrontmatter = fmStart !== -1 && i > fmStart && i < fmEnd;
|
||||||
|
if (inFrontmatter) {
|
||||||
|
const m = line.match(frontmatterLinkRe);
|
||||||
|
if (m) {
|
||||||
|
const res = checkTarget(m[1], file);
|
||||||
|
if (!res.ok && !res.skip) {
|
||||||
|
broken.push({ file: relative(ROOT, file), line: i + 1, target: m[1], type: 'frontmatter' });
|
||||||
|
} else {
|
||||||
|
recordUse(m[1], file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const m of line.matchAll(mdLinkRe)) {
|
||||||
|
const res = checkTarget(m[3], file);
|
||||||
|
if (!res.ok && !res.skip) {
|
||||||
|
broken.push({ file: relative(ROOT, file), line: i + 1, target: m[3], type: m[1] === '!' ? 'image' : 'link' });
|
||||||
|
} else {
|
||||||
|
recordUse(m[3], file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const m of line.matchAll(htmlLinkRe)) {
|
||||||
|
const res = checkTarget(m[1], file);
|
||||||
|
if (!res.ok && !res.skip) {
|
||||||
|
broken.push({ file: relative(ROOT, file), line: i + 1, target: m[1], type: 'html' });
|
||||||
|
} else {
|
||||||
|
recordUse(m[1], file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const m of line.matchAll(includeRe)) {
|
||||||
|
recordUse(m[1], file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfgLines = readFileSync(CONFIG, 'utf8').split('\n');
|
||||||
|
const sidebarLinkRe = /link:\s*['"]([^'"]+)['"]/g;
|
||||||
|
for (let i = 0; i < cfgLines.length; i++) {
|
||||||
|
const line = cfgLines[i];
|
||||||
|
if (/^\s*\/\//.test(line)) continue;
|
||||||
|
for (const m of line.matchAll(sidebarLinkRe)) {
|
||||||
|
const target = m[1];
|
||||||
|
if (!target) continue;
|
||||||
|
const res = checkTarget(target, CONFIG);
|
||||||
|
if (!res.ok && !res.skip) {
|
||||||
|
broken.push({ file: relative(ROOT, CONFIG), line: i + 1, target, type: 'sidebar' });
|
||||||
|
} else {
|
||||||
|
recordUse(target, CONFIG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const orphans = mdFiles
|
||||||
|
.filter(f => f !== HOME && !usedMd.has(f))
|
||||||
|
.map(f => relative(ROOT, f))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
broken.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
||||||
|
|
||||||
|
let hasBroken = false;
|
||||||
|
|
||||||
|
if (broken.length === 0) {
|
||||||
|
console.log('OK: битых ссылок не найдено');
|
||||||
|
} else {
|
||||||
|
hasBroken = true;
|
||||||
|
console.log(`Найдено битых ссылок: ${broken.length}\n`);
|
||||||
|
let lastFile = '';
|
||||||
|
for (const b of broken) {
|
||||||
|
if (b.file !== lastFile) { console.log(`\n${b.file}`); lastFile = b.file; }
|
||||||
|
console.log(` L${b.line} [${b.type}] -> ${b.target}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (orphans.length === 0) {
|
||||||
|
console.log('OK: неиспользуемых страниц не найдено');
|
||||||
|
} else {
|
||||||
|
console.log(`WARN: найдено неиспользуемых страниц (не упомянуты ни в sidebar, ни в других md): ${orphans.length}\n`);
|
||||||
|
for (const f of orphans) console.log(` ${f}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(hasBroken ? 1 : 0);
|
||||||
+19
-19
@@ -164,7 +164,7 @@ export default defineConfig({
|
|||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
text: 'Состав сервиса SA', link: '/security/Cloud-SA/compond-index.md',
|
text: 'Состав сервиса SA', link: '/security/Cloud-SA/compond-SA/compond-index.md',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Обзор сервиса', link: '/security/Cloud-SA/compond-SA/about.md' },
|
{ text: 'Обзор сервиса', link: '/security/Cloud-SA/compond-SA/about.md' },
|
||||||
@@ -206,7 +206,7 @@ export default defineConfig({
|
|||||||
items: [
|
items: [
|
||||||
{ text: 'Обзор сервиса', link: '/security/Cloud-MDM/about.md' },
|
{ text: 'Обзор сервиса', link: '/security/Cloud-MDM/about.md' },
|
||||||
{
|
{
|
||||||
text: 'Описание сервиса MDM', link: '/security/Cloud-MDM/discription-index.md',
|
text: 'Описание сервиса MDM', link: '/security/Cloud-MDM/description/description-index.md',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Состав сервиса', link: '/security/Cloud-MDM/description/compound.md' },
|
{ text: 'Состав сервиса', link: '/security/Cloud-MDM/description/compound.md' },
|
||||||
@@ -220,7 +220,7 @@ export default defineConfig({
|
|||||||
{ text: 'Сроки и условия предоставления сервиса', link: '/security/Cloud-MDM/provision.md' },
|
{ text: 'Сроки и условия предоставления сервиса', link: '/security/Cloud-MDM/provision.md' },
|
||||||
{ text: 'Порядок платежей', link: '/security/Cloud-MDM/payments.md' },
|
{ text: 'Порядок платежей', link: '/security/Cloud-MDM/payments.md' },
|
||||||
{
|
{
|
||||||
text: 'Инструкиця', link: '/security/Cloud-MDM/instructions-index.md',
|
text: 'Инструкиця', link: '/security/Cloud-MDM/instructions/index.md',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Инструкция', link: '/security/Cloud-MDM/instructions/instructions.md' },
|
{ text: 'Инструкция', link: '/security/Cloud-MDM/instructions/instructions.md' },
|
||||||
@@ -231,15 +231,15 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Cloud NGFW', link: '/security/NGFW-index.md',
|
text: 'Cloud NGFW', link: '/security/Cloud-NGFW/NGFW-index.md',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Обзор сервиса', link: '/security/Cloud-NGFW/about.md'},
|
{ text: 'Обзор сервиса', link: '/security/Cloud-NGFW/about.md'},
|
||||||
{ text: 'Основные возможности', link: '/security/Cloud-NGFW/provision.md' },
|
{ text: 'Основные возможности', link: '/security/Cloud-NGFW/possibilities.md' },
|
||||||
{ text: 'Спецификация сервиса', link: '/security/Cloud-NGFW/specification.md' },
|
{ text: 'Спецификация сервиса', link: '/security/Cloud-NGFW/specification.md' },
|
||||||
{ text: 'Состав сервиса', link: '/security/Cloud-NGFW/compound.md' },
|
{ text: 'Состав сервиса', link: '/security/Cloud-NGFW/compound.md' },
|
||||||
{ text: 'Сроки и условия предоставления сервиса. Зоны ответственности', link: '/security/Cloud-NGFW/provision.md' },
|
{ text: 'Сроки и условия предоставления сервиса. Зоны ответственности', link: '/security/Cloud-NGFW/provision.md' },
|
||||||
{ text: 'Структура платежей', link: '/security/Cloud-NGFW/payment-structure.md' },
|
{ text: 'Структура платежей', link: '/security/Cloud-NGFW/payment.md' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,19 +276,19 @@ export default defineConfig({
|
|||||||
// ],
|
// ],
|
||||||
|
|
||||||
|
|
||||||
'/backups/': [
|
// '/backups/': [
|
||||||
{
|
// {
|
||||||
text: 'Резервное копирование', link: '/backups/index.md',
|
// text: 'Резервное копирование', link: '/backups/index.md',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
text: 'Обзор сервиса', link: '/backups/backups-overview.md',
|
// text: 'Обзор сервиса', link: '/backups/backups-overview.md',
|
||||||
collapsed: true,
|
// collapsed: true,
|
||||||
items: [
|
// items: [
|
||||||
{text: 'О сервисе', link: '/backups/about.md'},
|
// {text: 'О сервисе', link: '/backups/about.md'},
|
||||||
{text: 'Квоты и лимиты', link: '/backups/backup-quatos.md'},
|
// {text: 'Квоты и лимиты', link: '/backups/backup-quatos.md'},
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
|
|
||||||
|
|
||||||
'/PostgreSQL/': [
|
'/PostgreSQL/': [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
section_links:
|
section_links:
|
||||||
- title: Cloud PostgreSQL
|
- title: Cloud PostgreSQL
|
||||||
link: /PostgreSQL/service-index.md
|
link: /PostgreSQL/service/service-index.md
|
||||||
description: Обзор сервиса PostgreSQL
|
description: Обзор сервиса PostgreSQL
|
||||||
- title: IPSEC
|
- title: IPSEC
|
||||||
link: /PostgreSQL/IPSEC.md
|
link: /PostgreSQL/IPSEC.md
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ section_links:
|
|||||||
- title: Подключиться к виртуальному дата-центру
|
- title: Подключиться к виртуальному дата-центру
|
||||||
link: /vdc/vdc-getting-started.md
|
link: /vdc/vdc-getting-started.md
|
||||||
description: Подключение 2FA, подключение к дата-центру и создание виртуальной машины
|
description: Подключение 2FA, подключение к дата-центру и создание виртуальной машины
|
||||||
- title: Виртуальные дата-центры
|
|
||||||
link: /vdc/vdc-how-to/vdc-index.md
|
|
||||||
description: Создание виртуального дата-цента, вход с двухфакторной аутентификацией
|
|
||||||
- title: Виртуальные машины
|
- title: Виртуальные машины
|
||||||
link: /vdc/vdc-how-to/vm/vm-index.md
|
link: /vdc/vdc-how-to/vm/vm-index.md
|
||||||
description: Управление виртуальными машинами в дата-центре с помощью Cloud Director
|
description: Управление виртуальными машинами в дата-центре с помощью Cloud Director
|
||||||
|
|||||||
Reference in New Issue
Block a user