mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
Enhance wiki link handling during file renaming
This commit is contained in:
parent
9de0b4aea2
commit
34792e9c19
3 changed files with 170 additions and 3 deletions
|
|
@ -9,8 +9,7 @@ export const normalizeWikiPath = (input: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ensureMarkdownExtension = (path: string) => {
|
export const ensureMarkdownExtension = (path: string) => {
|
||||||
const lastSegment = path.split('/').pop() ?? path
|
if (path.toLowerCase().endsWith('.md')) return path
|
||||||
if (lastSegment.includes('.')) return path
|
|
||||||
return `${path}.md`
|
return `${path}.md`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
148
apps/x/packages/core/src/workspace/wiki-link-rewrite.ts
Normal file
148
apps/x/packages/core/src/workspace/wiki-link-rewrite.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { workspace } from '@x/shared';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js';
|
import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
|
import { rewriteWikiLinksForRenamedKnowledgeFile } from './wiki-link-rewrite.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Path Utilities
|
// Path Utilities
|
||||||
|
|
@ -58,6 +59,11 @@ export function absToRelPosix(absPath: string): string | null {
|
||||||
return relPath.split(path.sep).join('/');
|
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
|
// File System Utilities
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -286,6 +292,7 @@ export async function rename(
|
||||||
|
|
||||||
// Check if source exists
|
// Check if source exists
|
||||||
await fs.access(fromPath);
|
await fs.access(fromPath);
|
||||||
|
const fromStats = await fs.lstat(fromPath);
|
||||||
|
|
||||||
// Check if destination exists (only if overwrite is false)
|
// Check if destination exists (only if overwrite is false)
|
||||||
if (!overwrite) {
|
if (!overwrite) {
|
||||||
|
|
@ -309,6 +316,19 @@ export async function rename(
|
||||||
await fs.mkdir(path.dirname(toPath), { recursive: true });
|
await fs.mkdir(path.dirname(toPath), { recursive: true });
|
||||||
|
|
||||||
await fs.rename(fromPath, toPath);
|
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 };
|
return { ok: true as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,4 +403,4 @@ export async function remove(
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue