export function guessMimeTypeFromExtension( filename: string, ): string | undefined { const extension = filename.split(".").pop(); if (!extension || extension.includes(" ")) { return undefined; } switch (extension.toLowerCase()) { case "pdf": return "application/pdf"; case "txt": case "rtf": return "text/plain"; case "json": return "application/json"; case "xml": return "application/xml"; case "html": return "text/html"; case "css": return "text/css"; case "js": case "cjs": case "mjs": case "jsx": case "ts": case "tsx": return "text/javascript"; case "md": case "mdx": return "text/markdown"; case "csv": return "text/csv"; case "zip": return "application/zip"; case "apng": return "image/apng"; case "png": return "image/png"; case "avif": return "image/avif"; case "gif": return "image/gif"; case "svg": return "image/svg+xml"; case "webp": return "image/webp"; case "tiff": return "image/tiff"; case "ico": return "image/x-icon"; case "jpeg": case "jpg": return "image/jpeg"; case "mp1": case "mp2": case "mp3": return "audio/mpeg"; case "mp4": return "video/mp4"; default: return "application/octet-stream"; } } /** * Return a best-guess MIME type based on the magic-number signature * found at the start of an ArrayBuffer. * * @param buf – the source ArrayBuffer * @returns the detected MIME type, or `"application/octet-stream"` if unknown */ export function guessMimeTypeFromContents(buf: ArrayBuffer | string): string { if (typeof buf === "string") { if (buf.match(/^data:\w+\/\w+;base64/)) { return buf.split(";")[0].split(":")[1]!; } return "text/plain"; } if (buf.byteLength < 4) return "application/octet-stream"; // Read the first 12 bytes (enough for all signatures below) const bytes = new Uint8Array(buf.slice(0, 12)); const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join(""); // Helper so we can look at only the needed prefix const startsWith = (sig: string) => hex.startsWith(sig.toLowerCase()); // --- image formats --- if (startsWith("89504e47")) return "image/png"; // PNG - 89 50 4E 47 if ( startsWith("ffd8ffdb") || startsWith("ffd8ffe0") || startsWith("ffd8ffee") || startsWith("ffd8ffe1") ) return "image/jpeg"; // JPEG if (startsWith("47494638")) return "image/gif"; // GIF if (startsWith("424d")) return "image/bmp"; // BMP if (startsWith("52494646") && hex.substr(16, 8) === "57454250") return "image/webp"; // WEBP (RIFF....WEBP) if (startsWith("49492a00")) return "image/tiff"; // TIFF // b.toString(16).padStart(2, "0")) .join(""); } /** * Split a filename into a keyword-friendly string. Specifically adds sections * of camelCase and TitleCase into a space-separated strings. * e.g. "MyFile is soGreat.txt" -> "MyFile is soGreat.txt My File so Great" * Note: it doesn't split up titles that don't have a file extension. */ export function splitFilename(title: string | undefined): string | undefined { if (!title) { return undefined; } const parts = title.split("."); if (parts.pop()?.includes(" ")) { // There isn't an extension, so don't treat it as a filename return title; } // split up camelCase into "camel Case" return [ title, ...parts.flatMap((part) => { const words = part.split(" "); const camelCaseWords = words.flatMap((word) => { const pieces = word.split(/(?=[A-Z])/); if (pieces.length === 1) { // This will already be verbatim in the regular title parts return []; } return pieces; }); return camelCaseWords; }), ].join(" "); }