mirror of
https://github.com/zadam/trilium.git
synced 2025-10-30 01:36:24 +01:00
Compare commits
1 Commits
feat/ui-im
...
fix/mkdocs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66074ddbc9 |
7
.github/workflows/deploy-docs.yml
vendored
7
.github/workflows/deploy-docs.yml
vendored
@@ -16,6 +16,7 @@ on:
|
|||||||
- 'requirements-docs.txt'
|
- 'requirements-docs.txt'
|
||||||
- '.github/workflows/deploy-docs.yml'
|
- '.github/workflows/deploy-docs.yml'
|
||||||
- 'scripts/fix-mkdocs-structure.ts'
|
- 'scripts/fix-mkdocs-structure.ts'
|
||||||
|
- 'validate-docs-links.ts'
|
||||||
|
|
||||||
# Allow manual triggering from Actions tab
|
# Allow manual triggering from Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -32,6 +33,7 @@ on:
|
|||||||
- 'requirements-docs.txt'
|
- 'requirements-docs.txt'
|
||||||
- '.github/workflows/deploy-docs.yml'
|
- '.github/workflows/deploy-docs.yml'
|
||||||
- 'scripts/fix-mkdocs-structure.ts'
|
- 'scripts/fix-mkdocs-structure.ts'
|
||||||
|
- 'validate-docs-links.ts'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
@@ -111,6 +113,11 @@ jobs:
|
|||||||
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
|
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
|
||||||
echo "✅ Site validation passed"
|
echo "✅ Site validation passed"
|
||||||
|
|
||||||
|
- name: Validate Documentation Links
|
||||||
|
run: |
|
||||||
|
# Run the TypeScript link validation script
|
||||||
|
pnpm tsx validate-docs-links.ts
|
||||||
|
|
||||||
# Install wrangler globally to avoid workspace issues
|
# Install wrangler globally to avoid workspace issues
|
||||||
- name: Install Wrangler
|
- name: Install Wrangler
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"chore:update-build-info": "tsx ./scripts/update-build-info.ts",
|
"chore:update-build-info": "tsx ./scripts/update-build-info.ts",
|
||||||
"chore:update-version": "tsx ./scripts/update-version.ts",
|
"chore:update-version": "tsx ./scripts/update-version.ts",
|
||||||
"chore:fix-mkdocs-structure": "tsx ./scripts/fix-mkdocs-structure.ts",
|
"chore:fix-mkdocs-structure": "tsx ./scripts/fix-mkdocs-structure.ts",
|
||||||
|
"docs:validate-links": "tsx ./validate-docs-links.ts",
|
||||||
"edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs",
|
"edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs",
|
||||||
"edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo",
|
"edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo",
|
||||||
"test:all": "pnpm test:parallel && pnpm test:sequential",
|
"test:all": "pnpm test:parallel && pnpm test:sequential",
|
||||||
|
|||||||
@@ -109,8 +109,25 @@ function updateReferences(docsDir: string): FixResult[] {
|
|||||||
const updatesMade: FixResult[] = [];
|
const updatesMade: FixResult[] = [];
|
||||||
|
|
||||||
function fixLink(match: string, text: string, link: string, currentDir: string, isIndex: boolean): string {
|
function fixLink(match: string, text: string, link: string, currentDir: string, isIndex: boolean): string {
|
||||||
// Skip external links
|
// Skip external links, mailto, and special protocols
|
||||||
if (link.startsWith('http')) {
|
if (link.startsWith('http') || link.startsWith('mailto:') || link.startsWith('xmpp:')) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip anchor-only links
|
||||||
|
if (link.startsWith('#')) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle malformed links with nested brackets (e.g., [developers]([url](https://...))
|
||||||
|
if (link.includes('[') || link.includes(']')) {
|
||||||
|
// This is a malformed link, skip it
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle links wrapped in angle brackets (e.g., <https://...>)
|
||||||
|
if (link.startsWith('<') && link.endsWith('>')) {
|
||||||
|
// This is likely a literal URL that shouldn't be processed
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +140,14 @@ function updateReferences(docsDir: string): FixResult[] {
|
|||||||
decodedLink = link;
|
decodedLink = link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract anchor if present
|
||||||
|
let anchorPart = '';
|
||||||
|
if (decodedLink.includes('#')) {
|
||||||
|
const parts = decodedLink.split('#');
|
||||||
|
decodedLink = parts[0];
|
||||||
|
anchorPart = '#' + parts.slice(1).join('#');
|
||||||
|
}
|
||||||
|
|
||||||
// Special case: if we're in index.md and the link starts with the parent directory name
|
// Special case: if we're in index.md and the link starts with the parent directory name
|
||||||
// This happens when a file was converted to index.md and had links to siblings
|
// This happens when a file was converted to index.md and had links to siblings
|
||||||
if (isIndex && decodedLink.includes('/')) {
|
if (isIndex && decodedLink.includes('/')) {
|
||||||
@@ -136,7 +161,7 @@ function updateReferences(docsDir: string): FixResult[] {
|
|||||||
// Re-encode spaces for URL compatibility before recursing
|
// Re-encode spaces for URL compatibility before recursing
|
||||||
const fixedLinkEncoded = fixedLink.replace(/ /g, '%20');
|
const fixedLinkEncoded = fixedLink.replace(/ /g, '%20');
|
||||||
// Recursively process the fixed link
|
// Recursively process the fixed link
|
||||||
return fixLink(`[${text}](${fixedLinkEncoded})`, text, fixedLinkEncoded, currentDir, isIndex);
|
return fixLink(`[${text}](${fixedLinkEncoded}${anchorPart})`, text, fixedLinkEncoded + anchorPart, currentDir, isIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,26 +186,43 @@ function updateReferences(docsDir: string): FixResult[] {
|
|||||||
if (path.dirname(potentialDir) === path.dirname(currentDir)) {
|
if (path.dirname(potentialDir) === path.dirname(currentDir)) {
|
||||||
// It's a sibling - just use directory name
|
// It's a sibling - just use directory name
|
||||||
const dirName = path.basename(potentialDir).replace(/ /g, '%20');
|
const dirName = path.basename(potentialDir).replace(/ /g, '%20');
|
||||||
return `[${text}](${dirName}/)`;
|
return `[${text}](${dirName}/${anchorPart})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate relative path from current file to the directory
|
// Calculate relative path from current file to the directory
|
||||||
const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20');
|
const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20');
|
||||||
return `[${text}](${newPath}/)`;
|
return `[${text}](${newPath}/${anchorPart})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the target file exists
|
||||||
|
if (!fs.existsSync(resolvedPath)) {
|
||||||
|
// Try to find a similar file by removing special characters from the filename
|
||||||
|
const dirPath = path.dirname(resolvedPath);
|
||||||
|
const fileName = path.basename(resolvedPath);
|
||||||
|
|
||||||
|
// Remove problematic characters and try to find the file
|
||||||
|
const cleanFileName = fileName.replace(/[\(\)\\]/g, '');
|
||||||
|
const cleanPath = path.join(dirPath, cleanFileName);
|
||||||
|
|
||||||
|
if (fs.existsSync(cleanPath)) {
|
||||||
|
// Calculate relative path from current file to the cleaned file
|
||||||
|
const newPath = path.relative(currentDir, cleanPath).replace(/\\/g, '/').replace(/ /g, '%20');
|
||||||
|
return `[${text}](${newPath}${anchorPart})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also handle local references (same directory)
|
// Also handle local references (same directory)
|
||||||
if (!decodedLink.includes('/')) {
|
if (!decodedLink.includes('/')) {
|
||||||
const basename = decodedLink.slice(0, -3); // Remove .md extension
|
const basename = decodedLink.endsWith('.md') ? decodedLink.slice(0, -3) : decodedLink;
|
||||||
const possibleDir = path.join(currentDir, basename);
|
const possibleDir = path.join(currentDir, basename);
|
||||||
|
|
||||||
if (fs.existsSync(possibleDir) && fs.statSync(possibleDir).isDirectory()) {
|
if (fs.existsSync(possibleDir) && fs.statSync(possibleDir).isDirectory()) {
|
||||||
// Re-encode spaces for URL compatibility
|
// Re-encode spaces for URL compatibility
|
||||||
const encodedBasename = basename.replace(/ /g, '%20');
|
const encodedBasename = basename.replace(/ /g, '%20');
|
||||||
return `[${text}](${encodedBasename}/)`;
|
return `[${text}](${encodedBasename}/${anchorPart})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +308,20 @@ function syncReadmeToIndex(projectRoot: string, docsDir: string): FixResult[] {
|
|||||||
|
|
||||||
// Fix internal documentation links (./docs/User%20Guide -> ./User%20Guide)
|
// Fix internal documentation links (./docs/User%20Guide -> ./User%20Guide)
|
||||||
content = content.replace(/\.\/docs\/User%20Guide/g, './User%20Guide');
|
content = content.replace(/\.\/docs\/User%20Guide/g, './User%20Guide');
|
||||||
|
content = content.replace(/\.\/docs\/Script%20API/g, './Script%20API');
|
||||||
|
content = content.replace(/\.\/docs\/Developer%20Guide/g, './Developer%20Guide');
|
||||||
|
|
||||||
|
// Fix specific broken links found in index.md
|
||||||
|
// These links point to non-existent files, so we need to fix them
|
||||||
|
content = content.replace(/User%20Guide\/quick-start\.md/g, 'User%20Guide/User%20Guide/');
|
||||||
|
content = content.replace(/User%20Guide\/installation\.md/g, 'User%20Guide/User%20Guide/Installation%20&%20Setup/');
|
||||||
|
content = content.replace(/User%20Guide\/docker\.md/g, 'User%20Guide/User%20Guide/Installation%20&%20Setup/Docker%20installation/');
|
||||||
|
content = content.replace(/User%20Guide\/index\.md/g, 'User%20Guide/User%20Guide/');
|
||||||
|
content = content.replace(/Script%20API\/index\.md/g, 'Script%20API/');
|
||||||
|
content = content.replace(/Developer%20Guide\/index\.md/g, 'Developer%20Guide/Developer%20Guide/');
|
||||||
|
content = content.replace(/Developer%20Guide\/contributing\.md/g, 'Developer%20Guide/Developer%20Guide/');
|
||||||
|
content = content.replace(/support\/faq\.md/g, 'User%20Guide/User%20Guide/FAQ/');
|
||||||
|
content = content.replace(/support\/troubleshooting\.md/g, 'User%20Guide/User%20Guide/');
|
||||||
|
|
||||||
// Write the adjusted content to docs/index.md
|
// Write the adjusted content to docs/index.md
|
||||||
fs.writeFileSync(indexPath, content, 'utf-8');
|
fs.writeFileSync(indexPath, content, 'utf-8');
|
||||||
|
|||||||
366
validate-docs-links.ts
Normal file
366
validate-docs-links.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
||||||
|
import { join, relative, dirname, resolve, extname } from 'path';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
|
|
||||||
|
interface LinkValidationResult {
|
||||||
|
file: string;
|
||||||
|
line: number;
|
||||||
|
link: string;
|
||||||
|
type: 'relative' | 'anchor' | 'absolute' | 'external';
|
||||||
|
valid: boolean;
|
||||||
|
reason?: string;
|
||||||
|
targetFile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DocumentationLinkValidator {
|
||||||
|
private siteDir: string;
|
||||||
|
private sourceDir: string;
|
||||||
|
private results: LinkValidationResult[] = [];
|
||||||
|
private fileCache: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
constructor(siteDir: string = './site', sourceDir: string = './docs') {
|
||||||
|
this.siteDir = resolve(siteDir);
|
||||||
|
this.sourceDir = resolve(sourceDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all HTML files in the site directory
|
||||||
|
*/
|
||||||
|
private getAllHtmlFiles(dir: string): string[] {
|
||||||
|
const files: string[] = [];
|
||||||
|
const items = readdirSync(dir);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = join(dir, item);
|
||||||
|
const stat = statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
files.push(...this.getAllHtmlFiles(fullPath));
|
||||||
|
} else if (item.endsWith('.html')) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all markdown files in the docs directory
|
||||||
|
*/
|
||||||
|
private getAllMarkdownFiles(dir: string): string[] {
|
||||||
|
const files: string[] = [];
|
||||||
|
const items = readdirSync(dir);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = join(dir, item);
|
||||||
|
const stat = statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
files.push(...this.getAllMarkdownFiles(fullPath));
|
||||||
|
} else if (item.endsWith('.md')) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract links from HTML content
|
||||||
|
*/
|
||||||
|
private extractHtmlLinks(content: string, filePath: string): Array<{link: string, line: number}> {
|
||||||
|
const links: Array<{link: string, line: number}> = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Regex patterns for different types of links
|
||||||
|
const patterns = [
|
||||||
|
/href=["']([^"']+)["']/g, // href attributes
|
||||||
|
/src=["']([^"']+)["']/g, // src attributes (for images, scripts, etc.)
|
||||||
|
];
|
||||||
|
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
patterns.forEach(pattern => {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(line)) !== null) {
|
||||||
|
const link = match[1];
|
||||||
|
// Skip external links, mailto, javascript, and data URLs
|
||||||
|
if (!link.startsWith('http://') &&
|
||||||
|
!link.startsWith('https://') &&
|
||||||
|
!link.startsWith('mailto:') &&
|
||||||
|
!link.startsWith('javascript:') &&
|
||||||
|
!link.startsWith('data:') &&
|
||||||
|
!link.startsWith('//')) {
|
||||||
|
links.push({ link, line: index + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract links from Markdown content
|
||||||
|
*/
|
||||||
|
private extractMarkdownLinks(content: string, filePath: string): Array<{link: string, line: number}> {
|
||||||
|
const links: Array<{link: string, line: number}> = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Regex patterns for markdown links
|
||||||
|
const patterns = [
|
||||||
|
/\[([^\]]*)\]\(([^)]+)\)/g, // [text](link)
|
||||||
|
/!\[([^\]]*)\]\(([^)]+)\)/g, // 
|
||||||
|
];
|
||||||
|
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
patterns.forEach(pattern => {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(line)) !== null) {
|
||||||
|
const link = match[2];
|
||||||
|
// Skip external links
|
||||||
|
if (!link.startsWith('http://') &&
|
||||||
|
!link.startsWith('https://') &&
|
||||||
|
!link.startsWith('mailto:') &&
|
||||||
|
!link.startsWith('//')) {
|
||||||
|
links.push({ link, line: index + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a single link
|
||||||
|
*/
|
||||||
|
private validateLink(link: string, sourceFile: string, isHtml: boolean = true): LinkValidationResult {
|
||||||
|
const baseResult = {
|
||||||
|
file: relative(process.cwd(), sourceFile),
|
||||||
|
link,
|
||||||
|
line: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle anchor links
|
||||||
|
if (link.startsWith('#')) {
|
||||||
|
return {
|
||||||
|
...baseResult,
|
||||||
|
type: 'anchor',
|
||||||
|
valid: true, // We'll assume anchors are valid for now
|
||||||
|
reason: 'Anchor link (not validated)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle absolute paths
|
||||||
|
if (link.startsWith('/')) {
|
||||||
|
return {
|
||||||
|
...baseResult,
|
||||||
|
type: 'absolute',
|
||||||
|
valid: false,
|
||||||
|
reason: 'Absolute paths are not recommended for relative documentation'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relative links
|
||||||
|
const sourceDir = dirname(sourceFile);
|
||||||
|
let targetPath: string;
|
||||||
|
let anchorPart = '';
|
||||||
|
|
||||||
|
// Split off anchor if present
|
||||||
|
const anchorIndex = link.indexOf('#');
|
||||||
|
let linkPath = link;
|
||||||
|
if (anchorIndex > 0) {
|
||||||
|
linkPath = link.substring(0, anchorIndex);
|
||||||
|
anchorPart = link.substring(anchorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode URL-encoded characters
|
||||||
|
linkPath = decodeURIComponent(linkPath);
|
||||||
|
|
||||||
|
if (isHtml) {
|
||||||
|
// For HTML files in site directory
|
||||||
|
targetPath = resolve(sourceDir, linkPath);
|
||||||
|
|
||||||
|
// Check if it's a directory link (should have index.html)
|
||||||
|
if (!linkPath.endsWith('.html') && !linkPath.endsWith('/')) {
|
||||||
|
// Try with .html extension
|
||||||
|
const htmlPath = targetPath + '.html';
|
||||||
|
if (existsSync(htmlPath)) {
|
||||||
|
targetPath = htmlPath;
|
||||||
|
} else {
|
||||||
|
// Try as directory with index.html
|
||||||
|
const indexPath = join(targetPath, 'index.html');
|
||||||
|
if (existsSync(indexPath)) {
|
||||||
|
targetPath = indexPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (linkPath.endsWith('/')) {
|
||||||
|
targetPath = join(targetPath, 'index.html');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For markdown files in docs directory
|
||||||
|
targetPath = resolve(sourceDir, linkPath);
|
||||||
|
|
||||||
|
// MkDocs converts .md to .html, so we need to check both
|
||||||
|
if (linkPath.endsWith('.md')) {
|
||||||
|
// Check if the .md file exists
|
||||||
|
if (!existsSync(targetPath)) {
|
||||||
|
// Try without .md and with various extensions
|
||||||
|
const basePath = targetPath.slice(0, -3);
|
||||||
|
if (existsSync(basePath + '.md')) {
|
||||||
|
targetPath = basePath + '.md';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!extname(linkPath)) {
|
||||||
|
// No extension, could be a directory or file
|
||||||
|
if (existsSync(targetPath + '.md')) {
|
||||||
|
targetPath = targetPath + '.md';
|
||||||
|
} else if (existsSync(join(targetPath, 'index.md'))) {
|
||||||
|
targetPath = join(targetPath, 'index.md');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = existsSync(targetPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseResult,
|
||||||
|
type: 'relative',
|
||||||
|
valid: exists,
|
||||||
|
reason: exists ? undefined : `Target file not found: ${relative(process.cwd(), targetPath)}`,
|
||||||
|
targetFile: exists ? relative(process.cwd(), targetPath) : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all links in HTML files
|
||||||
|
*/
|
||||||
|
public validateHtmlFiles(): void {
|
||||||
|
console.log(`\n🔍 Validating HTML files in ${this.siteDir}...\n`);
|
||||||
|
|
||||||
|
if (!existsSync(this.siteDir)) {
|
||||||
|
console.error(`❌ Site directory not found: ${this.siteDir}`);
|
||||||
|
console.log('Please run "mkdocs build" first to generate the site.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlFiles = this.getAllHtmlFiles(this.siteDir);
|
||||||
|
console.log(`Found ${htmlFiles.length} HTML files to validate.\n`);
|
||||||
|
|
||||||
|
for (const file of htmlFiles) {
|
||||||
|
const content = readFileSync(file, 'utf-8');
|
||||||
|
const links = this.extractHtmlLinks(content, file);
|
||||||
|
|
||||||
|
for (const { link, line } of links) {
|
||||||
|
const result = this.validateLink(link, file, true);
|
||||||
|
result.line = line;
|
||||||
|
if (!result.valid) {
|
||||||
|
this.results.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all links in Markdown files
|
||||||
|
*/
|
||||||
|
public validateMarkdownFiles(): void {
|
||||||
|
console.log(`\n🔍 Validating Markdown files in ${this.sourceDir}...\n`);
|
||||||
|
|
||||||
|
if (!existsSync(this.sourceDir)) {
|
||||||
|
console.error(`❌ Docs directory not found: ${this.sourceDir}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mdFiles = this.getAllMarkdownFiles(this.sourceDir);
|
||||||
|
console.log(`Found ${mdFiles.length} Markdown files to validate.\n`);
|
||||||
|
|
||||||
|
for (const file of mdFiles) {
|
||||||
|
const content = readFileSync(file, 'utf-8');
|
||||||
|
const links = this.extractMarkdownLinks(content, file);
|
||||||
|
|
||||||
|
for (const { link, line } of links) {
|
||||||
|
const result = this.validateLink(link, file, false);
|
||||||
|
result.line = line;
|
||||||
|
if (!result.valid) {
|
||||||
|
this.results.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print validation results
|
||||||
|
*/
|
||||||
|
public printResults(): void {
|
||||||
|
if (this.results.length === 0) {
|
||||||
|
console.log('✅ All links are valid!\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n❌ Found ${this.results.length} broken links:\n`);
|
||||||
|
console.log('=' .repeat(80));
|
||||||
|
|
||||||
|
// Group results by file
|
||||||
|
const resultsByFile = new Map<string, LinkValidationResult[]>();
|
||||||
|
for (const result of this.results) {
|
||||||
|
if (!resultsByFile.has(result.file)) {
|
||||||
|
resultsByFile.set(result.file, []);
|
||||||
|
}
|
||||||
|
resultsByFile.get(result.file)!.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print results grouped by file
|
||||||
|
for (const [file, fileResults] of resultsByFile) {
|
||||||
|
console.log(`\n📄 ${file}`);
|
||||||
|
console.log('-'.repeat(80));
|
||||||
|
|
||||||
|
for (const result of fileResults) {
|
||||||
|
console.log(` Line ${result.line}: ${result.link}`);
|
||||||
|
if (result.reason) {
|
||||||
|
console.log(` ⚠️ ${result.reason}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(80));
|
||||||
|
console.log(`\nTotal: ${this.results.length} broken links found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation results for programmatic use
|
||||||
|
*/
|
||||||
|
public getResults(): LinkValidationResult[] {
|
||||||
|
return this.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run full validation
|
||||||
|
*/
|
||||||
|
public validate(): boolean {
|
||||||
|
this.results = [];
|
||||||
|
|
||||||
|
// Validate built HTML files
|
||||||
|
this.validateHtmlFiles();
|
||||||
|
|
||||||
|
// Also validate source markdown files for better debugging
|
||||||
|
this.validateMarkdownFiles();
|
||||||
|
|
||||||
|
this.printResults();
|
||||||
|
|
||||||
|
return this.results.length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||||
|
const validator = new DocumentationLinkValidator();
|
||||||
|
const isValid = validator.validate();
|
||||||
|
|
||||||
|
// Exit with error code if links are broken
|
||||||
|
process.exit(isValid ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DocumentationLinkValidator };
|
||||||
Reference in New Issue
Block a user