Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
153 changes: 153 additions & 0 deletions docs/scripts/check-links.mjs
Original file line number Diff line number Diff line change
@@ -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(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}

function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function formatPath(path) {
return relative(docsRoot, path).split(sep).join('/');
}
16 changes: 16 additions & 0 deletions docs/scripts/create-404-alias.mjs
Original file line number Diff line number Diff line change
@@ -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.');
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Original file line number Diff line number Diff line change
Expand Up @@ -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/)。
Loading