/** * Word-processor formats routed to the TipTap rich-text editor. * Cell/slide/diagram formats stay on OnlyOffice. */ export const RICHTEXT_EXTENSIONS = new Set([ "doc", "docm", "docx", "dot", "dotm", "dotx", "epub", "fb2", "fodt", "htm", "html", "hwp", "hwpx", "md", "mht", "mhtml", "odt", "ott", "rtf", "stw", "sxw", "txt", "wps", "wpt", "xml", "ultidoc", ] as const) export const ULTIDOC_EXTENSION = "ultidoc.json" const RICHTEXT_MIME_HINTS = [ "wordprocessingml", "msword", "opendocument.text", "text/html", "text/plain", "text/markdown", "application/rtf", "application/epub", ] as const export function fileExtension(name: string): string { const base = name.split("/").pop() ?? name const lower = base.toLowerCase() if (lower.endsWith(`.${ULTIDOC_EXTENSION}`)) return ULTIDOC_EXTENSION const i = base.lastIndexOf(".") if (i <= 0) return "" return base.slice(i + 1).toLowerCase() } export function isRichTextExtension(ext: string): boolean { const normalized = ext.toLowerCase() if (normalized === ULTIDOC_EXTENSION) return true return RICHTEXT_EXTENSIONS.has(normalized as (typeof RICHTEXT_EXTENSIONS extends Set ? T : never)) } export function isRichTextMime(mime: string): boolean { const m = mime.toLowerCase() return RICHTEXT_MIME_HINTS.some((hint) => m.includes(hint)) } export function isRichTextFile(file: { name: string; mime_type?: string }): boolean { const ext = fileExtension(file.name) if (ext && isRichTextExtension(ext)) return true const mime = (file.mime_type ?? "").toLowerCase() if (mime && isRichTextMime(mime)) return true return false } export function isUltidocPath(path: string): boolean { return path.toLowerCase().endsWith(`.${ULTIDOC_EXTENSION}`) } /** Sidecar path for a source document, e.g. /docs/report.docx → /docs/report.ultidoc.json */ export function sidecarPathForSource(sourcePath: string): string { const normalized = sourcePath.replace(/\/+/g, "/") const slash = normalized.lastIndexOf("/") const dir = slash >= 0 ? normalized.slice(0, slash) : "" const fileName = slash >= 0 ? normalized.slice(slash + 1) : normalized const dot = fileName.lastIndexOf(".") const base = dot > 0 ? fileName.slice(0, dot) : fileName const sidecarName = `${base}.${ULTIDOC_EXTENSION}` if (!dir) return `/${sidecarName}` return `${dir}/${sidecarName}`.replace(/\/+/g, "/") } /** Source path from an ultidoc sidecar name (best-effort). */ export function guessSourcePathFromSidecar(sidecarPath: string, knownExtensions: string[]): string | null { if (!isUltidocPath(sidecarPath)) return null const normalized = sidecarPath.replace(/\/+/g, "/") const slash = normalized.lastIndexOf("/") const dir = slash >= 0 ? normalized.slice(0, slash) : "" const fileName = slash >= 0 ? normalized.slice(slash + 1) : normalized const base = fileName.slice(0, -(ULTIDOC_EXTENSION.length + 1)) for (const ext of knownExtensions) { const candidate = `${base}.${ext}` return dir ? `${dir}/${candidate}` : `/${candidate}` } return null }