| 1 | import { fileBasename } from "@t3tools/shared/path"; |
| 2 | import type { ThreadId } from "@t3tools/contracts"; |
| 3 | import { resolveMarkdownLinkPresentation } from "../../lib/markdownLinks"; |
| 4 | import type { MarkdownFileContextMenu } from "../../native/SelectableMarkdownText.types"; |
| 5 | import { hostPreviewMimeTypeFromExtension } from "@t3tools/shared/filePreview"; |
| 6 | |
| 7 | import { |
| 8 | isAbsolutePath, |
| 9 | resolveWorkspaceFilePath, |
| 10 | resolveWorkspaceRelativeFilePath, |
| 11 | } from "../files/filePath"; |
| 12 | |
| 13 | export type FileChipAction = "copy-full-path" | "copy-relative-path" | "open-file" | "save"; |
| 14 | |
| 15 | export interface FileChipTarget { |
| 16 | /** The host path, when the link is absolute or the workspace root is known. */ |
| 17 | readonly fullPath?: string; |
| 18 | /** The path inside the workspace, when the link resolves there. */ |
| 19 | readonly relativePath?: string; |
| 20 | } |
| 21 | |
| 22 | /** Null when the link is not a file or resolves nowhere the feed can open, such as `~/x` or `../x`. */ |
| 23 | export function resolveFileChipTarget( |
| 24 | href: string, |
| 25 | workspaceRoot: string | null | undefined, |
| 26 | ): FileChipTarget | null { |
| 27 | const presentation = resolveMarkdownLinkPresentation(href); |
| 28 | if (presentation.kind !== "file") return null; |
| 29 | const relativePath = resolveWorkspaceRelativeFilePath(workspaceRoot, presentation.path); |
| 30 | const fullPath = isAbsolutePath(presentation.path) |
| 31 | ? presentation.path |
| 32 | : workspaceRoot && relativePath |
| 33 | ? resolveWorkspaceFilePath(workspaceRoot, relativePath) |
| 34 | : undefined; |
| 35 | if (!fullPath && !relativePath) return null; |
| 36 | return { |
| 37 | ...(fullPath ? { fullPath } : {}), |
| 38 | ...(relativePath ? { relativePath } : {}), |
| 39 | }; |
| 40 | } |
| 41 | |
| 42 | function fileChipMetadata(target: FileChipTarget) { |
| 43 | const path = target.fullPath ?? target.relativePath; |
| 44 | if (!path) return null; |
| 45 | const name = fileBasename(path); |
| 46 | const dot = name.lastIndexOf("."); |
| 47 | const mimeType = dot < 0 ? null : hostPreviewMimeTypeFromExtension(name.slice(dot)); |
| 48 | return mimeType ? { path, name, mimeType } : null; |
| 49 | } |
| 50 | |
| 51 | /** Use literal resolved paths so encoded filename characters are not decoded twice. */ |
| 52 | export function fileChipShareSource(target: FileChipTarget, threadId: ThreadId) { |
| 53 | const metadata = fileChipMetadata(target); |
| 54 | return metadata |
| 55 | ? { |
| 56 | name: metadata.name, |
| 57 | mimeType: metadata.mimeType, |
| 58 | resource: { _tag: "media-file" as const, threadId, path: metadata.path }, |
| 59 | } |
| 60 | : null; |
| 61 | } |
| 62 | |
| 63 | /** Saving is available for the media and documents the host asset endpoint can serve. */ |
| 64 | export function fileChipMenu(target: FileChipTarget): MarkdownFileContextMenu { |
| 65 | return { |
| 66 | title: target.fullPath ?? target.relativePath ?? "", |
| 67 | actions: [ |
| 68 | ...(target.fullPath ? [{ id: "copy-full-path", title: "Copy full path" }] : []), |
| 69 | ...(target.relativePath ? [{ id: "copy-relative-path", title: "Copy relative path" }] : []), |
| 70 | { id: "open-file", title: "Open in file viewer" }, |
| 71 | ...(fileChipMetadata(target) |
| 72 | ? [ |
| 73 | { |
| 74 | id: "save", |
| 75 | title: "Save or share", |
| 76 | }, |
| 77 | ] |
| 78 | : []), |
| 79 | ], |
| 80 | }; |
| 81 | } |