Skip to content
Source
Sign inSign up

markdownMedia.ts

180 lines6.4 KB

1import {
2 classifyMarkdownImageSource,
3 markdownImageSourceFragment,
4} from "@t3tools/client-runtime/markdown-images";
5import {
6 audioMimeTypeFromExtension,
7 mediaMimeTypeFromExtension,
8} from "@t3tools/shared/mediaPreview";
9import { splitFilePathPosition } from "@t3tools/shared/fileLinks";
10import { parseMarkdownFileLink } from "@t3tools/shared/markdownLinks";
11import {
12 mediaFileReference,
13 mediaUrlReference,
14 mediaReferenceFileName,
15} from "@t3tools/client-runtime/media-reference";
16import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
17import { normalizeNativeMarkdownUrl, resolveMarkdownLinkPresentation } from "./markdownLinks";
18import { resolveMobileMarkdownMediaSource } from "./markdownMediaSource";
19​
20import type { FilePreviewSource } from "../components/ui/FilePreviewModal";
21import type { MediaVideoPreviewSource } from "./videoPreviewSource";
22import type { MediaActionsSource } from "./mediaActionsSource";
23​
24const DOCUMENT_MIME_TYPES: Readonly<Record<string, string>> = {
25 ".pdf": "application/pdf",
26 ".csv": "text/csv",
27 ".tsv": "text/tab-separated-values",
28 ".doc": "application/msword",
29 ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
30 ".xls": "application/vnd.ms-excel",
31 ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
32 ".ppt": "application/vnd.ms-powerpoint",
33 ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
34};
35​
36export function resolveMarkdownMediaPreview(
37 href: string,
38 input: {
39 readonly environmentId: EnvironmentId;
40 readonly threadId: ThreadId | undefined;
41 readonly workspaceRoot: string | null | undefined;
42 readonly captured?: boolean;
43 readonly imageEmbed?: boolean;
44 },
45):
46 | { readonly kind: "image"; readonly source: FilePreviewSource }
47 | { readonly kind: "pdf" | "audio" | "document" | "file"; readonly source: FilePreviewSource }
48 | { readonly kind: "video"; readonly source: MediaVideoPreviewSource }
49 | null {
50 if (parseMarkdownFileLink(href)?.line !== undefined) return null;
51 const classified = classifyMarkdownImageSource(href, input.workspaceRoot);
52 if (classified._tag === "Direct" && /^data:(?!image\/|video\/)/i.test(classified.uri))
53 return null;
54 if (classified._tag !== "Blocked") {
55 const path =
56 classified._tag === "Direct" ? classified.uri : splitFilePathPosition(classified.path).path;
57 const reference =
58 classified._tag === "Direct"
59 ? mediaUrlReference(path)
60 : mediaFileReference(path, input.workspaceRoot);
61 const name = reference && mediaReferenceFileName(reference);
62 const extension = name?.slice(name.lastIndexOf(".")).toLowerCase() ?? "";
63 const isFile = classified._tag === "WorkspaceFile" || /^\.[a-z0-9]+$/i.test(extension);
64 if (
65 name &&
66 isFile &&
67 mediaMimeTypeFromExtension(extension) === null &&
68 (classified._tag !== "Direct" ||
69 Object.hasOwn(DOCUMENT_MIME_TYPES, extension) ||
70 audioMimeTypeFromExtension(extension) !== null)
71 ) {
72 const audioMimeType = audioMimeTypeFromExtension(extension);
73 const kind = audioMimeType
74 ? "audio"
75 : extension === ".pdf"
76 ? "pdf"
77 : classified._tag === "WorkspaceFile"
78 ? "file"
79 : "document";
80 const mimeType =
81 audioMimeType ??
82 (kind === "file" ? "text/plain" : DOCUMENT_MIME_TYPES[extension]) ??
83 "application/octet-stream";
84 const common = {
85 kind: kind === "pdf" ? ("pdf" as const) : ("document" as const),
86 name,
87 mimeType,
88 };
89 if (classified._tag === "Direct") {
90 const uri = normalizeNativeMarkdownUrl(path);
91 return {
92 kind,
93 source: {
94 ...common,
95 uri,
96 actionsSource: {
97 ...(reference ? { reference } : {}),
98 uri,
99 name,
100 mimeType: common.mimeType,
101 },
102 },
103 };
104 }
105 if (input.captured) return null;
106 const resource = input.threadId
107 ? { _tag: "media-file" as const, threadId: input.threadId, path }
108 : input.workspaceRoot
109 ? { _tag: "draft-workspace-file" as const, cwd: input.workspaceRoot, path }
110 : null;
111 if (!resource) return null;
112 return {
113 kind,
114 source: {
115 ...common,
116 environmentId: input.environmentId,
117 resource,
118 srcFragment: markdownImageSourceFragment(href),
119 actionsSource: {
120 ...(reference ? { reference } : {}),
121 environmentId: input.environmentId,
122 threadId: input.threadId,
123 resource,
124 name,
125 mimeType: common.mimeType,
126 },
127 },
128 };
129 }
130 }
131 const media = resolveMobileMarkdownMediaSource(href, input);
132 if (media === null || media.access === "unavailable") return null;
133 if (input.captured && media.access !== "direct") return null;
134 const { kind, name, mimeType, reference, srcFragment } = media;
135​
136 const target =
137 media.access === "direct"
138 ? { uri: normalizeNativeMarkdownUrl(media.uri) }
139 : {
140 environmentId: input.environmentId,
141 resource: media.resource,
142 ...(srcFragment ? { srcFragment } : {}),
143 };
144 const actionsSource: MediaActionsSource =
145 media.access === "direct"
146 ? { reference, uri: media.uri, name, mimeType }
147 : {
148 reference,
149 environmentId: input.environmentId,
150 threadId: input.threadId,
151 resource: media.resource,
152 name,
153 mimeType,
154 };
155 return kind === "video"
156 ? { kind, source: { type: "media", name, mimeType, ...target, actionsSource } }
157 : { kind, source: { kind, name, ...target, actionsSource } };
159​
160export function markdownFileEmbedLabel(
161 href: string,
162 input: Parameters<typeof resolveMarkdownMediaPreview>[1],
163 standalone: boolean,
164): string | null {
165 const media = resolveMarkdownMediaPreview(href, { ...input, imageEmbed: true });
166 if (media) {
167 if (
168 media.kind === "image" ||
169 ((media.kind === "video" || media.kind === "audio") && standalone)
170 )
171 return null;
172 return media.source.name ?? null;
173 }
174 const file = resolveMarkdownLinkPresentation(href);
175 if (file.kind === "file" && file.line === undefined) {
176 if (mediaMimeTypeFromExtension(file.path.slice(file.path.lastIndexOf("."))) !== null)
177 return null;
178 }
179 return file.kind === "file" ? file.label : null;