UNPKG

markmv

Version:

TypeScript CLI for markdown file operations with intelligent link refactoring

206 lines 8.23 kB
import { existsSync, statSync } from 'node:fs'; import { homedir } from 'node:os'; import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'; /** * Utility class for path manipulation and resolution operations. * * Provides comprehensive path handling for markdown file operations including relative path * updates, home directory resolution, and cross-platform compatibility. * * @category Utilities * * @example * Path resolution * ```typescript * // Resolve various path formats * PathUtils.resolvePath('~/docs/file.md'); // Home directory * PathUtils.resolvePath('../guide.md', '/current/dir'); // Relative * PathUtils.resolvePath('/absolute/path.md'); // Absolute * ``` * * @example * Relative path updates for moved files * ```typescript * // When moving a file, update its relative links * const originalLink = '../images/diagram.png'; * const updatedLink = PathUtils.updateRelativePath( * originalLink, * 'docs/guide.md', // old file location * 'tutorials/guide.md' // new file location * ); * // Result: '../../docs/images/diagram.png' * ``` */ export class PathUtils { /** * Resolve a path that may be relative, absolute, or use home directory notation. * * @example * ```typescript * PathUtils.resolvePath('~/docs/file.md'); * // Returns: '/Users/username/docs/file.md' * * PathUtils.resolvePath('../file.md', '/current/working/dir'); * // Returns: '/current/working/file.md' * ```; * * @param path - The path to resolve (supports ~/, relative, and absolute paths) * @param basePath - Optional base directory for relative path resolution * * @returns Resolved absolute path */ static resolvePath(path, basePath) { if (path.startsWith('~/')) { return resolve(join(homedir(), path.slice(2))); } if (isAbsolute(path)) { return resolve(path); } if (basePath) { return resolve(join(basePath, path)); } return resolve(path); } /** Convert an absolute path back to a relative path from a base directory */ static makeRelative(absolutePath, fromDir) { return relative(fromDir, absolutePath); } /** Update a relative path when a file is moved */ static updateRelativePath(originalLinkPath, sourceFilePath, newSourceFilePath) { // If it's not a relative path, return as-is if (isAbsolute(originalLinkPath) || originalLinkPath.startsWith('~/')) { return originalLinkPath; } // Resolve the original target const sourceDir = dirname(sourceFilePath); const targetPath = PathUtils.resolvePath(originalLinkPath, sourceDir); // Create new relative path from new location const newSourceDir = dirname(newSourceFilePath); return PathUtils.makeRelative(targetPath, newSourceDir); } /** Update a Claude import path when a file is moved */ static updateClaudeImportPath(originalImportPath, sourceFilePath, newSourceFilePath) { // Handle absolute paths and home directory paths - they don't need updating if (isAbsolute(originalImportPath) || originalImportPath.startsWith('~/')) { return originalImportPath; } // For relative imports, update the path const sourceDir = dirname(sourceFilePath); const targetPath = PathUtils.resolvePath(originalImportPath, sourceDir); const newSourceDir = dirname(newSourceFilePath); return PathUtils.makeRelative(targetPath, newSourceDir); } /** Normalize path separators for cross-platform compatibility */ static normalizePath(path) { return path.split(/[/\\]/).join(sep); } /** Check if a path is within a given directory */ static isWithinDirectory(filePath, directoryPath) { const relativePath = relative(directoryPath, filePath); return !relativePath.startsWith('..') && !isAbsolute(relativePath); } /** Generate a unique filename if a file already exists */ static generateUniqueFilename(desiredPath) { const dir = dirname(desiredPath); const name = basename(desiredPath, extname(desiredPath)); const ext = extname(desiredPath); let counter = 1; let uniquePath = desiredPath; const fs = require('node:fs'); while (fs.existsSync(uniquePath)) { uniquePath = join(dir, `${name}-${counter}${ext}`); counter++; } return uniquePath; } /** Validate that a path is safe for file operations */ static validatePath(path) { if (!path || path.trim() === '') { return { valid: false, reason: 'Path cannot be empty' }; } if (path.includes('\0')) { return { valid: false, reason: 'Path cannot contain null bytes' }; } // Check for dangerous path traversal patterns const normalized = resolve(path); if (path.includes('..') && !PathUtils.isWithinDirectory(normalized, process.cwd())) { return { valid: false, reason: 'Path traversal outside working directory is not allowed' }; } return { valid: true }; } /** Extract directory depth from a path */ static getDirectoryDepth(path) { const normalized = resolve(path); return normalized.split(sep).filter((part) => part !== '').length; } /** Find common base directory for multiple paths */ static findCommonBase(paths) { if (paths.length === 0) return ''; if (paths.length === 1) return dirname(paths[0]); const resolvedPaths = paths.map((p) => resolve(p)); const splitPaths = resolvedPaths.map((p) => p.split(sep)); const commonParts = []; const minLength = Math.min(...splitPaths.map((p) => p.length)); for (let i = 0; i < minLength; i++) { const part = splitPaths[0][i]; if (splitPaths.every((splitPath) => splitPath[i] === part)) { commonParts.push(part); } else { break; } } return commonParts.join(sep) || sep; } /** Convert Windows paths to Unix-style for markdown links */ static toUnixPath(path) { return path.replace(/\\/g, '/'); } /** Get file extension with fallback handling */ static getExtension(path) { const ext = extname(path); return ext || ''; } /** Check if path represents a markdown file */ static isMarkdownFile(path) { const ext = PathUtils.getExtension(path).toLowerCase(); return ['.md', '.markdown', '.mdown', '.mkd', '.mdx'].includes(ext); } /** Safely join paths, handling edge cases */ static safejoin(...parts) { const filteredParts = parts.filter((part) => part && part.trim() !== ''); if (filteredParts.length === 0) return ''; return resolve(join(...filteredParts)); } /** Check if a path is a directory */ static isDirectory(path) { try { const resolvedPath = PathUtils.resolvePath(path); return existsSync(resolvedPath) && statSync(resolvedPath).isDirectory(); } catch { return false; } } /** Check if a path looks like a directory (ends with / or ) */ static looksLikeDirectory(path) { return path.endsWith('/') || path.endsWith('\\'); } /** * Resolve destination path when target might be a directory If destination is a directory, * preserves the source filename */ static resolveDestination(sourcePath, destinationPath) { const resolvedDest = PathUtils.resolvePath(destinationPath); // If destination looks like a directory or exists as a directory if (PathUtils.looksLikeDirectory(destinationPath) || PathUtils.isDirectory(resolvedDest)) { const sourceFileName = basename(sourcePath); return join(resolvedDest, sourceFileName); } return resolvedDest; } } //# sourceMappingURL=path-utils.js.map