// Branded string for documentation/readability — Patchwork ContentType values // are bare strings on the wire. The brand has no runtime cost. export type ContentType = string & { readonly __contentType?: never } export const KnownContentTypes = { // Root data type — unknown bytes Data: 'public.data', // Text family PlainText: 'public.plain-text', Markdown: 'public.markdown', Json: 'public.json', Base: 'public.base', // Image family Image: 'public.image', Png: 'public.png', Jpeg: 'public.jpeg', Gif: 'public.gif', Svg: 'public.svg-image', Webp: 'public.webp', Bmp: 'public.bmp', Icon: 'public.icon', // Media (declared for the taxonomy; no built-in viewers ship in #406) Audio: 'public.audio', Video: 'public.video', // Document Pdf: 'public.pdf', } as const /** * Parent → ancestor edges. Walks form chains, e.g. public.png → public.image → public.data. * Exported (rather than module-private) so the workspace lookup can walk the chain without * duplicating the table. Treat as readonly — do not mutate at runtime. */ export const CONFORMS_TO: Record = { // text family [KnownContentTypes.Markdown]: KnownContentTypes.PlainText, [KnownContentTypes.Json]: KnownContentTypes.PlainText, [KnownContentTypes.Base]: KnownContentTypes.PlainText, [KnownContentTypes.PlainText]: KnownContentTypes.Data, // image family [KnownContentTypes.Png]: KnownContentTypes.Image, [KnownContentTypes.Jpeg]: KnownContentTypes.Image, [KnownContentTypes.Gif]: KnownContentTypes.Image, [KnownContentTypes.Svg]: KnownContentTypes.Image, [KnownContentTypes.Webp]: KnownContentTypes.Image, [KnownContentTypes.Bmp]: KnownContentTypes.Image, [KnownContentTypes.Icon]: KnownContentTypes.Image, [KnownContentTypes.Image]: KnownContentTypes.Data, // pdf / audio / video are siblings of public.data [KnownContentTypes.Pdf]: KnownContentTypes.Data, [KnownContentTypes.Audio]: KnownContentTypes.Data, [KnownContentTypes.Video]: KnownContentTypes.Data, } /** * Predicate: does `child` conform to `ancestor` via the CONFORMS_TO chain? * Reflexive: a type always conforms to itself. */ export function conformsTo(child: string, ancestor: string): boolean { if (child === ancestor) return true let cursor: string | undefined = child // Bound the walk: chains are at most a handful of edges. Guard against // self-loops in case the table is ever mis-edited. const seen = new Set() while (cursor && !seen.has(cursor)) { seen.add(cursor) cursor = CONFORMS_TO[cursor] if (cursor === ancestor) return true } return false } const EXTENSION_TO_CT: Record = { md: KnownContentTypes.Markdown, txt: KnownContentTypes.PlainText, json: KnownContentTypes.Json, base: KnownContentTypes.Base, png: KnownContentTypes.Png, jpg: KnownContentTypes.Jpeg, jpeg: KnownContentTypes.Jpeg, gif: KnownContentTypes.Gif, svg: KnownContentTypes.Svg, webp: KnownContentTypes.Webp, bmp: KnownContentTypes.Bmp, ico: KnownContentTypes.Icon, pdf: KnownContentTypes.Pdf, mp3: KnownContentTypes.Audio, wav: KnownContentTypes.Audio, ogg: KnownContentTypes.Audio, mp4: KnownContentTypes.Video, webm: KnownContentTypes.Video, } const MIME_TO_CT: Record = { 'text/markdown': KnownContentTypes.Markdown, 'text/plain': KnownContentTypes.PlainText, 'application/json': KnownContentTypes.Json, 'image/png': KnownContentTypes.Png, 'image/jpeg': KnownContentTypes.Jpeg, 'image/gif': KnownContentTypes.Gif, 'image/svg+xml': KnownContentTypes.Svg, 'image/webp': KnownContentTypes.Webp, 'image/bmp': KnownContentTypes.Bmp, 'image/x-icon': KnownContentTypes.Icon, 'application/pdf': KnownContentTypes.Pdf, } const CT_TO_EXTENSION: Record = Object.fromEntries( Object.entries(EXTENSION_TO_CT) .reverse() // first-write-wins for the inverse: prefer 'jpg' over 'jpeg' as canonical jpeg ext if both appeared .map(([ext, ct]) => [ct, ext]), ) const CT_TO_MIME: Record = Object.fromEntries( Object.entries(MIME_TO_CT).map(([mime, ct]) => [ct, mime]), ) export function contentTypeFromExtension(ext: string): string { if (!ext) return KnownContentTypes.Data return EXTENSION_TO_CT[ext.toLowerCase()] ?? KnownContentTypes.Data } export function contentTypeFromMimeType(mime: string): string { if (!mime) return KnownContentTypes.Data return MIME_TO_CT[mime.toLowerCase()] ?? KnownContentTypes.Data } export function extensionFromContentType(ct: string): string | undefined { return CT_TO_EXTENSION[ct] } export function mimeTypeFromContentType(ct: string): string | undefined { return CT_TO_MIME[ct] } /** * Resolve a content type for a new file from its name and optional MIME hint. * * Resolution order: extension → mime → `public.data`. The extension wins when * known because it carries more information than a generic mime (e.g. `.md` * resolves to `public.markdown` even if the mime is `text/plain`). */ export function inferContentType(name: string, mimeType?: string): string { const dot = name.lastIndexOf('.') if (dot > 0) { const ext = name.slice(dot + 1) const fromExt = EXTENSION_TO_CT[ext.toLowerCase()] if (fromExt) return fromExt } if (mimeType) { const fromMime = MIME_TO_CT[mimeType.toLowerCase()] if (fromMime) return fromMime } return KnownContentTypes.Data }