UNPKG

@kaifronsdal/transcript-viewer

Version:

A web-based viewer for AI conversation transcripts with rollback support

95 lines (82 loc) 3.18 kB
// Helpers for path normalization and URL building used across components /** Convert any Windows-style path separators to POSIX forward slashes */ export function toPosixPath(p: string): string { return p.replace(/\\/g, '/'); } /** Build a transcript URL from a relative POSIX path, encoding each segment */ export function buildTranscriptUrl(relativePath: string): string { const safe = toPosixPath(relativePath || ''); const segments = safe.split('/').filter(Boolean).map(encodeURIComponent); return `/transcript/${segments.join('/')}`; } /** Ensure a client-provided path is safe for splitting/URL usage */ export function normalizeClientFilePath(p: string | undefined | null): string | null { if (!p) return null; return toPosixPath(p); } /** * Client-side URL utilities - no Node.js path module dependency */ /** * Parse URL path segments into a clean file path (client-side) */ export function parseUrlPath(pathSegments: string[]): string { if (!pathSegments || pathSegments.length === 0) { throw new Error('Invalid path: empty path segments'); } // Decode each segment const decodedSegments = pathSegments.map(segment => { try { return decodeURIComponent(segment); } catch (error) { throw new Error(`Invalid URL encoding in path segment: ${segment}`); } }); // Join segments into a file path const filePath = decodedSegments.join('/'); // Basic client-side validation aligned with server rules (server will still validate) if (!filePath || !filePath.endsWith('.json')) { throw new Error(`Invalid file path: ${filePath}`); } // Reject absolute paths if (filePath.startsWith('/')) { throw new Error('Invalid file path: must be relative, not absolute'); } // Reject directory traversal attempts if (filePath.includes('../')) { throw new Error('Invalid file path: directory traversal is not allowed'); } // Require transcript_ prefix on filename (matches server) const lastSlash = filePath.lastIndexOf('/') const filename = lastSlash >= 0 ? filePath.slice(lastSlash + 1) : filePath; if (!filename.startsWith('transcript_')) { throw new Error('Invalid file path: filename must start with "transcript_"'); } return filePath; } /** * Generate a URL path for SvelteKit routing from a file path */ export function generateTranscriptUrl(filePath: string): string { if (!filePath) { throw new Error('Invalid file path: empty string'); } // Split path into segments and encode each one const segments = filePath.split('/').map(segment => encodeURIComponent(segment)); return `/transcript/${segments.join('/')}`; } /** * Extract directory name from file path (client-side version) */ export function extractDirectoryName(filePath: string): string { const lastSlashIndex = filePath.lastIndexOf('/'); if (lastSlashIndex === -1) return 'root'; return filePath.substring(0, lastSlashIndex); } /** * Extract filename without extension (client-side version) */ export function extractFilenameWithoutExtension(filePath: string): string { const filename = filePath.split('/').pop() || ''; return filename.replace(/\.json$/, ''); }