From 89ce9e9050076d3baeda9c51e10632adc0712d78 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 30 May 2026 16:05:38 +0800 Subject: [PATCH] docs: fix project configuration links --- docs/package.json | 4 +- docs/scripts/check-links.mjs | 153 ++++++++++++++++++ docs/scripts/create-404-alias.mjs | 16 ++ .../project-configuration-effects.mdx | 18 +-- .../project-configuration-effects.mdx | 18 +-- 5 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 docs/scripts/check-links.mjs create mode 100644 docs/scripts/create-404-alias.mjs diff --git a/docs/package.json b/docs/package.json index 604de1b4..b02538aa 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "npm run prepare:playground && astro dev", "start": "npm run dev", - "build": "npm run prepare:playground && astro build", + "build": "npm run prepare:playground && astro build && npm run postbuild:404 && npm run check:links", + "check:links": "node scripts/check-links.mjs", + "postbuild:404": "node scripts/create-404-alias.mjs", "prepare:playground": "npm --prefix ../playground run build:embed && node ../playground/scripts/copy-docs-assets.mjs", "preview": "astro preview" }, diff --git a/docs/scripts/check-links.mjs b/docs/scripts/check-links.mjs new file mode 100644 index 00000000..5dd922a3 --- /dev/null +++ b/docs/scripts/check-links.mjs @@ -0,0 +1,153 @@ +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { dirname, extname, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const docsRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const distRoot = resolve(docsRoot, 'dist'); +const site = new URL(process.env.ASTRO_SITE ?? 'https://vide.pascal-lab.net'); +const basePath = normalizeBasePath(process.env.ASTRO_BASE ?? '/'); +const ignoredProtocols = new Set(['mailto:', 'tel:', 'javascript:', 'data:', 'blob:']); + +if (!existsSync(distRoot)) { + console.error(`Docs dist directory not found: ${distRoot}`); + process.exit(1); +} + +const htmlFiles = listFiles(distRoot).filter((file) => extname(file) === '.html'); +const failures = []; + +for (const htmlFile of htmlFiles) { + const html = readFileSync(htmlFile, 'utf8'); + const pagePath = toPagePath(htmlFile); + + for (const { attribute, value } of extractLinks(html)) { + const target = value.trim(); + + if (!target || target.startsWith('#') || target.startsWith('{{')) { + continue; + } + + let url; + try { + url = new URL(target, new URL(pagePath, site)); + } catch { + failures.push(`${formatPath(htmlFile)}: invalid ${attribute}="${target}"`); + continue; + } + + if (ignoredProtocols.has(url.protocol)) { + continue; + } + + if (url.origin !== site.origin) { + continue; + } + + const localPath = stripBasePath(url.pathname); + const resolved = resolveLocalPath(localPath); + + if (!resolved) { + failures.push(`${formatPath(htmlFile)}: missing ${attribute}="${target}" -> ${url.pathname}`); + continue; + } + + if (url.hash && extname(resolved) === '.html' && !hasAnchor(resolved, decodeURIComponent(url.hash.slice(1)))) { + failures.push(`${formatPath(htmlFile)}: missing anchor ${attribute}="${target}" -> ${url.pathname}${url.hash}`); + } + } +} + +if (failures.length > 0) { + console.error(`Found ${failures.length} broken docs link${failures.length === 1 ? '' : 's'}:`); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); +} + +console.log(`Checked links in ${htmlFiles.length} generated docs page${htmlFiles.length === 1 ? '' : 's'}.`); + +function listFiles(directory) { + const entries = readdirSync(directory, { withFileTypes: true }); + return entries.flatMap((entry) => { + const entryPath = join(directory, entry.name); + return entry.isDirectory() ? listFiles(entryPath) : [entryPath]; + }); +} + +function extractLinks(html) { + const links = []; + const pattern = /\b(href|src)\s*=\s*(["'])(.*?)\2/gi; + let match; + + while ((match = pattern.exec(html)) !== null) { + links.push({ attribute: match[1].toLowerCase(), value: decodeHtml(match[3]) }); + } + + return links; +} + +function resolveLocalPath(pathname) { + const normalizedPath = pathname.replace(/^\/+/, ''); + const candidates = []; + + if (pathname.endsWith('/')) { + candidates.push(join(distRoot, normalizedPath, 'index.html')); + } else { + candidates.push(join(distRoot, normalizedPath)); + candidates.push(join(distRoot, normalizedPath, 'index.html')); + candidates.push(join(distRoot, `${normalizedPath}.html`)); + } + + return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile()); +} + +function hasAnchor(htmlFile, anchor) { + if (!anchor) { + return true; + } + + const html = readFileSync(htmlFile, 'utf8'); + const escapedAnchor = escapeRegExp(anchor); + return new RegExp(`\\b(?:id|name)=["']${escapedAnchor}["']`, 'i').test(html); +} + +function toPagePath(htmlFile) { + const path = relative(distRoot, htmlFile).split(sep).join('/'); + + if (path === 'index.html') { + return basePath; + } + + return `${basePath}${path.replace(/(?:^|\/)index\.html$/, '/')}`; +} + +function stripBasePath(pathname) { + if (basePath === '/') { + return pathname; + } + + return pathname.startsWith(basePath) ? `/${pathname.slice(basePath.length)}` : pathname; +} + +function normalizeBasePath(pathname) { + const withSlashes = `/${pathname}/`.replace(/\/+/g, '/'); + return withSlashes === '//' ? '/' : withSlashes; +} + +function decodeHtml(value) { + return value + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>'); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function formatPath(path) { + return relative(docsRoot, path).split(sep).join('/'); +} diff --git a/docs/scripts/create-404-alias.mjs b/docs/scripts/create-404-alias.mjs new file mode 100644 index 00000000..696ab904 --- /dev/null +++ b/docs/scripts/create-404-alias.mjs @@ -0,0 +1,16 @@ +import { copyFileSync, existsSync, mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const docsRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const source = resolve(docsRoot, 'dist', '404.html'); +const target = resolve(docsRoot, 'dist', '404', 'index.html'); + +if (!existsSync(source)) { + console.error(`Root 404 page not found: ${source}`); + process.exit(1); +} + +mkdirSync(dirname(target), { recursive: true }); +copyFileSync(source, target); +console.log('Created /404/ alias for root 404 page.'); diff --git a/docs/src/content/docs/en/user-guide/project-configuration-effects.mdx b/docs/src/content/docs/en/user-guide/project-configuration-effects.mdx index c11851e0..b7d23b97 100644 --- a/docs/src/content/docs/en/user-guide/project-configuration-effects.mdx +++ b/docs/src/content/docs/en/user-guide/project-configuration-effects.mdx @@ -15,13 +15,13 @@ This page summarizes how the same feature behaves under different project config | Area | Without a project configuration file | After `vide.toml` is configured | | --- | --- | --- | -| [Go to Definition](./features/navigation/), [Find References](./features/references/), and [Hover](./features/hover/) | Scans the workspace on a best-effort basis and provides jumps, references, and symbol information for code reading | Resolves actual targets through `sources`, `libraries`, include directories, and macro branches | -| [Completion](./features/completion/) and [Signature Help](./features/signature-help/) | Uses the current file and scanned files for candidates; shows port/parameter help once the target module is resolved | Uses target modules, include directories, and macro branches for candidates and position-aware hints | -| [Rename](./features/rename/) and [Automatic Refactoring](./features/quick-fixes/) | Local code actions are available; project-wide rename is disabled | Fills or converts ports and parameters across the project, and updates declarations, references, and connections | -| [Syntax Highlighting](./features/syntax-highlighting/) and [Semantic Highlighting](./features/semantic-highlighting/) | Syntax highlighting is available, and the current file gets basic semantic styling | Uses project analysis to style port directions, read/write locations, and macro-branch-related semantics | -| [Annotations](./features/annotations/) | Ports, parameters, and structure endings that can be resolved in the current file can show annotations; instance counts can be incomplete | Shows port, parameter, structure-name, and instance-count annotations according to the project structure | -| [Document Symbols](./features/document-symbols/), [Folding](./features/folding/), and [Selection Range](./features/selection-range/) | The current file gets outline, folding, and selection ranges | Include files and macro branches are parsed from project configuration, so structure ranges better match the actual project | -| [Diagnostics](./features/diagnostics/) | Open files get syntax and parse diagnostics; project-level semantic diagnostics are not created | Uses `sources`, `include_dirs`, and `defines` for cross-file semantic diagnostics | -| [Qihe Integration](./features/qihe/) | Uses the currently open file as the analysis input | Uses input files from the project compile plan and can derive compile arguments from `top_modules`, `include_dirs`, and `defines` | +| [Go to Definition](../features/navigation/), [Find References](../features/references/), and [Hover](../features/hover/) | Scans the workspace on a best-effort basis and provides jumps, references, and symbol information for code reading | Resolves actual targets through `sources`, `libraries`, include directories, and macro branches | +| [Completion](../features/completion/) and [Signature Help](../features/signature-help/) | Uses the current file and scanned files for candidates; shows port/parameter help once the target module is resolved | Uses target modules, include directories, and macro branches for candidates and position-aware hints | +| [Rename](../features/rename/) and [Automatic Refactoring](../features/quick-fixes/) | Local code actions are available; project-wide rename is disabled | Fills or converts ports and parameters across the project, and updates declarations, references, and connections | +| [Syntax Highlighting](../features/syntax-highlighting/) and [Semantic Highlighting](../features/semantic-highlighting/) | Syntax highlighting is available, and the current file gets basic semantic styling | Uses project analysis to style port directions, read/write locations, and macro-branch-related semantics | +| [Annotations](../features/annotations/) | Ports, parameters, and structure endings that can be resolved in the current file can show annotations; instance counts can be incomplete | Shows port, parameter, structure-name, and instance-count annotations according to the project structure | +| [Document Symbols](../features/document-symbols/), [Folding](../features/folding/), and [Selection Range](../features/selection-range/) | The current file gets outline, folding, and selection ranges | Include files and macro branches are parsed from project configuration, so structure ranges better match the actual project | +| [Diagnostics](../features/diagnostics/) | Open files get syntax and parse diagnostics; project-level semantic diagnostics are not created | Uses `sources`, `include_dirs`, and `defines` for cross-file semantic diagnostics | +| [Qihe Integration](../features/qihe/) | Uses the currently open file as the analysis input | Uses input files from the project compile plan and can derive compile arguments from `top_modules`, `include_dirs`, and `defines` | -For field syntax, see [Project Configuration Reference](./project-configuration/). +For field syntax, see [Project Configuration Reference](../project-configuration/). diff --git a/docs/src/content/docs/user-guide/project-configuration-effects.mdx b/docs/src/content/docs/user-guide/project-configuration-effects.mdx index 0a15e27b..024ee23f 100644 --- a/docs/src/content/docs/user-guide/project-configuration-effects.mdx +++ b/docs/src/content/docs/user-guide/project-configuration-effects.mdx @@ -15,13 +15,13 @@ description: 对比没有 vide.toml、默认空模板和写好 vide.toml 后各 | 能力 | 没有项目配置文件 | 写好 `vide.toml` 后 | | --- | --- | --- | -| [定义跳转](./features/navigation/)、[引用搜索](./features/references/) 与 [悬停信息](./features/hover/) | 尽量扫描工作区,提供读代码所需的跳转、引用和符号信息 | 按 `sources`、`libraries`、include 目录和宏分支解析真实目标 | -| [代码补全](./features/completion/) 与 [签名提示](./features/signature-help/) | 使用当前文件和已扫描文件给候选;目标模块解析后显示端口/参数提示 | 结合目标模块、include 目录和宏分支给出候选和当前位置提示 | -| [重命名](./features/rename/) 与 [自动重构](./features/quick-fixes/) | 局部代码操作可用;工程范围重命名不启用 | 按工程范围补齐/转换端口参数,并更新声明、引用和连接 | -| [语法高亮](./features/syntax-highlighting/) 与 [语义高亮](./features/semantic-highlighting/) | 语法高亮可用,当前文件有基础语义样式 | 结合项目解析显示端口方向、读写位置和宏分支相关语义样式 | -| [代码注解](./features/annotations/) | 当前文件内可解析到的端口、参数和结构结尾可以显示注解;实例数量统计可能不完整 | 按工程结构显示端口、参数、结构名称和实例数量注解 | -| [符号大纲](./features/document-symbols/)、[折叠](./features/folding/) 与 [语义选区](./features/selection-range/) | 当前文件的大纲、折叠和选区可用 | include 文件和宏分支按工程配置解析,结构范围更接近真实工程 | -| [实时诊断](./features/diagnostics/) | 打开的文件有语法和解析诊断;没有项目级语义诊断 | 使用 `sources`、`include_dirs`、`defines` 做跨文件语义诊断 | -| [Qihe 集成](./features/qihe/) | 使用当前打开的文件作为分析输入 | 输入文件来自项目编译计划,并可从 `top_modules`、`include_dirs`、`defines` 推导编译参数 | +| [定义跳转](../features/navigation/)、[引用搜索](../features/references/) 与 [悬停信息](../features/hover/) | 尽量扫描工作区,提供读代码所需的跳转、引用和符号信息 | 按 `sources`、`libraries`、include 目录和宏分支解析真实目标 | +| [代码补全](../features/completion/) 与 [签名提示](../features/signature-help/) | 使用当前文件和已扫描文件给候选;目标模块解析后显示端口/参数提示 | 结合目标模块、include 目录和宏分支给出候选和当前位置提示 | +| [重命名](../features/rename/) 与 [自动重构](../features/quick-fixes/) | 局部代码操作可用;工程范围重命名不启用 | 按工程范围补齐/转换端口参数,并更新声明、引用和连接 | +| [语法高亮](../features/syntax-highlighting/) 与 [语义高亮](../features/semantic-highlighting/) | 语法高亮可用,当前文件有基础语义样式 | 结合项目解析显示端口方向、读写位置和宏分支相关语义样式 | +| [代码注解](../features/annotations/) | 当前文件内可解析到的端口、参数和结构结尾可以显示注解;实例数量统计可能不完整 | 按工程结构显示端口、参数、结构名称和实例数量注解 | +| [符号大纲](../features/document-symbols/)、[折叠](../features/folding/) 与 [语义选区](../features/selection-range/) | 当前文件的大纲、折叠和选区可用 | include 文件和宏分支按工程配置解析,结构范围更接近真实工程 | +| [实时诊断](../features/diagnostics/) | 打开的文件有语法和解析诊断;没有项目级语义诊断 | 使用 `sources`、`include_dirs`、`defines` 做跨文件语义诊断 | +| [Qihe 集成](../features/qihe/) | 使用当前打开的文件作为分析输入 | 输入文件来自项目编译计划,并可从 `top_modules`、`include_dirs`、`defines` 推导编译参数 | -字段写法见 [项目配置参考](./project-configuration/)。 +字段写法见 [项目配置参考](../project-configuration/)。