Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159// 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<string, string> = { // 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<string>() while (cursor && !seen.has(cursor)) { seen.add(cursor) cursor = CONFORMS_TO[cursor] if (cursor === ancestor) return true } return false}
const EXTENSION_TO_CT: Record<string, string> = { 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<string, string> = { '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<string, string> = 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<string, string> = 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}