Enhance wiki link handling during file renaming

This commit is contained in:
tusharmagar 2026-02-24 20:20:52 +05:30
parent 9de0b4aea2
commit 34792e9c19
3 changed files with 170 additions and 3 deletions

View file

@ -0,0 +1,148 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const WIKI_LINK_REGEX = /\[\[([^[\]]+)\]\]/g;
const KNOWLEDGE_PREFIX = 'knowledge/';
const MARKDOWN_EXTENSION = '.md';
function normalizeRelPath(relPath: string): string {
return relPath.replace(/\\/g, '/');
}
function isKnowledgeMarkdownPath(relPath: string): boolean {
const normalized = normalizeRelPath(relPath).replace(/^\/+/, '');
const lower = normalized.toLowerCase();
return lower.startsWith(KNOWLEDGE_PREFIX) && lower.endsWith(MARKDOWN_EXTENSION);
}
function stripKnowledgePrefix(relPath: string): string {
const normalized = normalizeRelPath(relPath).replace(/^\/+/, '');
if (!normalized.toLowerCase().startsWith(KNOWLEDGE_PREFIX)) return normalized;
return normalized.slice(KNOWLEDGE_PREFIX.length);
}
function stripMarkdownExtension(wikiPath: string): string {
return wikiPath.toLowerCase().endsWith(MARKDOWN_EXTENSION)
? wikiPath.slice(0, -MARKDOWN_EXTENSION.length)
: wikiPath;
}
function toWikiPathCompareKey(wikiPath: string): string {
return stripMarkdownExtension(wikiPath).toLowerCase();
}
function splitWikiPathPrefix(rawPath: string): { pathWithoutPrefix: string; hadKnowledgePrefix: boolean } {
let normalized = rawPath.trim().replace(/^\/+/, '').replace(/^\.\//, '');
const hadKnowledgePrefix = /^knowledge\//i.test(normalized);
if (hadKnowledgePrefix) {
normalized = normalized.slice(KNOWLEDGE_PREFIX.length);
}
return { pathWithoutPrefix: normalized, hadKnowledgePrefix };
}
function rewriteWikiLinksInMarkdown(markdown: string, fromWikiPath: string, toWikiPath: string): string {
const fromCompareKey = toWikiPathCompareKey(fromWikiPath);
const toWikiPathWithoutExtension = stripMarkdownExtension(toWikiPath);
return markdown.replace(WIKI_LINK_REGEX, (fullMatch, innerRaw: string) => {
const pipeIndex = innerRaw.indexOf('|');
const pathAndAnchor = pipeIndex >= 0 ? innerRaw.slice(0, pipeIndex) : innerRaw;
const aliasSuffix = pipeIndex >= 0 ? innerRaw.slice(pipeIndex) : '';
const hashIndex = pathAndAnchor.indexOf('#');
const pathPart = hashIndex >= 0 ? pathAndAnchor.slice(0, hashIndex) : pathAndAnchor;
const anchorSuffix = hashIndex >= 0 ? pathAndAnchor.slice(hashIndex) : '';
const leadingWhitespace = pathPart.match(/^\s*/)?.[0] ?? '';
const trailingWhitespace = pathPart.match(/\s*$/)?.[0] ?? '';
const rawPath = pathPart.trim();
if (!rawPath) return fullMatch;
const { pathWithoutPrefix, hadKnowledgePrefix } = splitWikiPathPrefix(rawPath);
if (!pathWithoutPrefix) return fullMatch;
if (toWikiPathCompareKey(pathWithoutPrefix) !== fromCompareKey) {
return fullMatch;
}
const preserveMarkdownExtension = rawPath.toLowerCase().endsWith(MARKDOWN_EXTENSION);
const rewrittenPath = preserveMarkdownExtension ? toWikiPath : toWikiPathWithoutExtension;
const finalPath = hadKnowledgePrefix ? `${KNOWLEDGE_PREFIX}${rewrittenPath}` : rewrittenPath;
return `[[${leadingWhitespace}${finalPath}${trailingWhitespace}${anchorSuffix}${aliasSuffix}]]`;
});
}
async function collectKnowledgeMarkdownFiles(workspaceRoot: string): Promise<string[]> {
const knowledgeRoot = path.join(workspaceRoot, 'knowledge');
try {
const stat = await fs.lstat(knowledgeRoot);
if (!stat.isDirectory()) return [];
} catch {
return [];
}
const markdownFiles: string[] = [];
const pendingDirectories: string[] = [knowledgeRoot];
while (pendingDirectories.length > 0) {
const currentDirectory = pendingDirectories.pop();
if (!currentDirectory) continue;
const entries = await fs.readdir(currentDirectory, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
const absolutePath = path.join(currentDirectory, entry.name);
if (entry.isDirectory()) {
pendingDirectories.push(absolutePath);
continue;
}
if (!entry.isFile()) continue;
if (!entry.name.toLowerCase().endsWith(MARKDOWN_EXTENSION)) continue;
const relativePath = normalizeRelPath(path.relative(workspaceRoot, absolutePath));
markdownFiles.push(relativePath);
}
}
return markdownFiles;
}
export async function rewriteWikiLinksForRenamedKnowledgeFile(
workspaceRoot: string,
fromRelPath: string,
toRelPath: string
): Promise<number> {
const normalizedFrom = normalizeRelPath(fromRelPath);
const normalizedTo = normalizeRelPath(toRelPath);
if (!isKnowledgeMarkdownPath(normalizedFrom) || !isKnowledgeMarkdownPath(normalizedTo)) {
return 0;
}
const fromWikiPath = stripKnowledgePrefix(normalizedFrom);
const toWikiPath = stripKnowledgePrefix(normalizedTo);
if (toWikiPathCompareKey(fromWikiPath) === toWikiPathCompareKey(toWikiPath)) return 0;
const markdownFiles = await collectKnowledgeMarkdownFiles(workspaceRoot);
let rewrittenFiles = 0;
for (const relativePath of markdownFiles) {
const absolutePath = path.join(workspaceRoot, ...relativePath.split('/'));
try {
const markdown = await fs.readFile(absolutePath, 'utf8');
if (!markdown.includes('[[')) continue;
const rewritten = rewriteWikiLinksInMarkdown(markdown, fromWikiPath, toWikiPath);
if (rewritten === markdown) continue;
await fs.writeFile(absolutePath, rewritten, 'utf8');
rewrittenFiles += 1;
} catch (error) {
console.error('Failed to rewrite wiki links in file:', relativePath, error);
}
}
return rewrittenFiles;
}

View file

@ -5,6 +5,7 @@ import { workspace } from '@x/shared';
import { z } from 'zod';
import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js';
import { WorkDir } from '../config/config.js';
import { rewriteWikiLinksForRenamedKnowledgeFile } from './wiki-link-rewrite.js';
// ============================================================================
// Path Utilities
@ -58,6 +59,11 @@ export function absToRelPosix(absPath: string): string | null {
return relPath.split(path.sep).join('/');
}
function isKnowledgeMarkdownRelPath(relPath: string): boolean {
const normalized = relPath.replace(/\\/g, '/').replace(/^\/+/, '').toLowerCase();
return normalized.startsWith('knowledge/') && normalized.endsWith('.md');
}
// ============================================================================
// File System Utilities
// ============================================================================
@ -286,6 +292,7 @@ export async function rename(
// Check if source exists
await fs.access(fromPath);
const fromStats = await fs.lstat(fromPath);
// Check if destination exists (only if overwrite is false)
if (!overwrite) {
@ -309,6 +316,19 @@ export async function rename(
await fs.mkdir(path.dirname(toPath), { recursive: true });
await fs.rename(fromPath, toPath);
if (
fromStats.isFile()
&& isKnowledgeMarkdownRelPath(from)
&& isKnowledgeMarkdownRelPath(to)
) {
try {
await rewriteWikiLinksForRenamedKnowledgeFile(WorkDir, from, to);
} catch (error) {
console.error('Failed to rewrite wiki backlinks after file rename:', error);
}
}
return { ok: true as const };
}
@ -383,4 +403,4 @@ export async function remove(
}
return { ok: true as const };
}
}