1import { fileBasename } from "@t3tools/shared/path";
2import type { ThreadId } from "@t3tools/contracts";
3import { resolveMarkdownLinkPresentation } from "../../lib/markdownLinks";
4import type { MarkdownFileContextMenu } from "../../native/SelectableMarkdownText.types";
5import { hostPreviewMimeTypeFromExtension } from "@t3tools/shared/filePreview";
6
7import {
8 isAbsolutePath,
9 resolveWorkspaceFilePath,
10 resolveWorkspaceRelativeFilePath,
11} from "../files/filePath";
12
13export type FileChipAction = "copy-full-path" | "copy-relative-path" | "open-file" | "save";
14
15export 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`. */
23export 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
42function 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. */
52export 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. */
64export 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}