@kaifronsdal/transcript-viewer
Version:
A web-based viewer for AI conversation transcripts with rollback support
95 lines (82 loc) • 3.18 kB
text/typescript
// 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$/, '');
}