Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206import type { AutomergeUrl } from '../storage/document-runtime-contract'
/** Reams-owned folder/file marker; it does not change Pushwork's directory entry shape. */export const REAMS_STATE_MARKER = '@reamsState'
/** * Entry in a directory document's docs array. * * `type` is intentionally `string` rather than `'file' | 'folder'` — * different producers populate it with different conventions: * * - Pushwork 1.x: `'file'` or `'folder'`. * - Pushwork 2.x (`patchwork-folder` shape): `'folder'` for * subdirectories, but the file's extension (e.g. `'md'`, `'png'`) * for files. The producer treats `type` as a content-type hint, * not a file-vs-folder discriminator. * - Reams (when it creates entries via `DocumentManager`): always * literally `'file'` or `'folder'` (Reams predates the 2.x * content-type convention). * * To stay compatible with all three, Reams treats `type === 'folder'` * as the only thing meaning "this entry points at a directory doc"; * anything else is treated as a file leaf. Use {@link isFolderEntry} * everywhere the choice matters; never compare `type === 'file'` * directly (it would mis-classify pushwork-2 files as folders). */export interface DirectoryEntry { name: string type: string url: AutomergeUrl}
/** * True if a `DirectoryEntry` points at a sub-directory document. * False for any file leaf, regardless of how the producer encoded the * file's `type` (`'file'`, `'md'`, `'png'`, etc.). See the * {@link DirectoryEntry} docstring for the multi-producer history. */export function isFolderEntry(entry: { type: unknown }): boolean { return entry.type === 'folder'}
/** * Convenience predicate: a file leaf. Requires `type` to actually be * a non-empty string — `undefined`, `null`, and `''` are NOT files * (they're malformed entries we don't classify either way, and the * callers that branch off this predicate go into the file-leaf code * path with no recovery if we let them through). The empty-string * filter also catches the case where a buggy producer wrote * `type: ''` (Automerge round-trips through a default-construct path * on schema gaps). * * The asymmetry with `isFolderEntry` (which returns true only for * the exact string `'folder'`) is intentional. Folder is the * narrow case across every producer we ship to; everything else * NAMED is a file leaf; everything UNNAMED is broken and should not * silently masquerade as a file. */export function isFileEntry(entry: { type: unknown }): boolean { return typeof entry.type === 'string' && entry.type.length > 0 && entry.type !== 'folder'}
/** * Pushwork Directory Document. * Represents a folder in the document hierarchy. * * `name` / `title` were added in pushwork 1.0.20 and carry the local * directory's basename across the wire. Earlier pushwork versions (and * Reams-created folders that don't go through pushwork init) omit these * fields — they are optional for that reason. Callers that need a * user-facing name (e.g. adopting a folder by URL) should read `name`, * fall back to `title`, and only synthesize a default when both are * absent. * * `lastSyncAt` and `with` are pushwork-specific metadata that Reams * does not read but preserves on the wire (Automerge is structural — * unknown fields round-trip). */export interface PushworkDirectoryDocument { /** Forward-compatible fields authored by other Pushwork producers. */ [key: string]: unknown '@patchwork': { type: 'folder' } docs: DirectoryEntry[] name?: string title?: string lastSyncAt?: number with?: string}
/** * Pushwork File Document. * Represents a file with content and metadata. * * `contentType` is an optional Reams UTI-style identifier (e.g. * `public.markdown`, `public.png`) that drives content-type → view-type * routing. Reams authors it on file creation; external Pushwork documents may * omit it, in which case routing infers it from the name and MIME type without * writing back to the document. */export interface PushworkFileDocument { /** Forward-compatible fields authored by other Pushwork producers. */ [key: string]: unknown '@patchwork': { type: 'file' } name: string extension: string mimeType: string contentType?: string content: string | Uint8Array metadata: { permissions: number }}
/** * Union type for any Pushwork document. */export type PushworkDocument = PushworkDirectoryDocument | PushworkFileDocument
/** * Type guard: is this a Pushwork directory document? */export function isPushworkDirectory(doc: unknown): doc is PushworkDirectoryDocument { return ( typeof doc === 'object' && doc !== null && '@patchwork' in doc && typeof (doc as any)['@patchwork'] === 'object' && (doc as any)['@patchwork'] !== null && 'type' in (doc as any)['@patchwork'] && (doc as any)['@patchwork'].type === 'folder' && 'docs' in doc && Array.isArray((doc as any).docs) )}
/** * Type guard: is this a Pushwork file document? */export function isPushworkFile(doc: unknown): doc is PushworkFileDocument { return ( typeof doc === 'object' && doc !== null && '@patchwork' in doc && typeof (doc as any)['@patchwork'] === 'object' && (doc as any)['@patchwork'] !== null && 'type' in (doc as any)['@patchwork'] && (doc as any)['@patchwork'].type === 'file' && 'name' in doc && 'content' in doc )}
/** * Type guard: is file content text (string) vs binary (Uint8Array)? */export function isTextContent(content: string | Uint8Array): content is string { return typeof content === 'string'}
/** * Extract file extension from a file name. * Returns empty string if no extension is present. * * Examples: * - "note.md" -> "md" * - "archive.tar.gz" -> "gz" * - "Makefile" -> "" * - ".gitignore" -> "gitignore" * - "" -> "" */export function extensionFromFileName(fileName: string): string { if (!fileName || !fileName.includes('.')) return '' return fileName.split('.').pop()!}
const MIME_TYPES: Record<string, string> = { md: 'text/markdown', txt: 'text/plain', json: 'application/json', base: 'text/yaml', js: 'application/javascript', ts: 'application/typescript', jsx: 'application/javascript', tsx: 'application/typescript', css: 'text/css', html: 'text/html', htm: 'text/html', // Image types png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp', bmp: 'image/bmp', ico: 'image/x-icon',}
/** * Infer MIME type from a file extension. * Returns "application/octet-stream" for unknown extensions. */export function mimeTypeFromExtension(ext: string): string { return MIME_TYPES[ext] || 'application/octet-stream'}