1import { isWindowsAbsolutePath, stripSlashPrefixedWindowsDrive } from "@t3tools/shared/path";
2
3import {
4 normalizeMarkdownLinkDestination,
5 safeDecodeURIComponent,
6 splitMarkdownLinkSearchAndHash,
7} from "@t3tools/shared/markdownLinks";
8import { parseFileUrlHref } from "@t3tools/shared/fileLinks";
9
10const DIRECT_IMAGE_SOURCE_PATTERN = /^(?:https?:|data:|blob:|\/\/)/i;
11const URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;
12
13export type MarkdownImageSource =
14 | { readonly _tag: "Direct"; readonly uri: string }
15 | { readonly _tag: "WorkspaceFile"; readonly path: string }
16 | { readonly _tag: "Blocked" };
17
18export function markdownImageSourceFragment(source: string): string {
19 return splitMarkdownLinkSearchAndHash(normalizeMarkdownLinkDestination(source)).hash;
20}
21
22function joinWorkspacePath(workspaceRoot: string, relativePath: string): string {
23 const separator = isWindowsAbsolutePath(workspaceRoot) ? "\\" : "/";
24 const root = workspaceRoot.replace(/[\\/]+$/, "");
25 const path = relativePath.replace(/[\\/]/g, separator).replace(/^[\\/]+/, "");
26 return `${root}${separator}${path}`;
27}
28
29/**
30 * Classifies a markdown image or video source by where its bytes must be loaded from.
31 * Filesystem paths belong to the environment host and must never reach a
32 * browser or native image component without first becoming a signed asset URL.
33 */
34export function classifyMarkdownImageSource(
35 value: string | null | undefined,
36 workspaceRoot?: string | null,
37): MarkdownImageSource {
38 if (value === null || value === undefined) return { _tag: "Blocked" };
39
40 const source = normalizeMarkdownLinkDestination(value);
41 if (source.length === 0 || source.startsWith("#") || source.startsWith("?")) {
42 return { _tag: "Blocked" };
43 }
44 if (DIRECT_IMAGE_SOURCE_PATTERN.test(source)) {
45 return { _tag: "Direct", uri: source };
46 }
47
48 if (/^file:/i.test(source)) {
49 const target = parseFileUrlHref(source);
50 return target === null
51 ? { _tag: "Blocked" }
52 : {
53 _tag: "WorkspaceFile",
54 path: stripSlashPrefixedWindowsDrive(safeDecodeURIComponent(target.path)),
55 };
56 }
57
58 const path = stripSlashPrefixedWindowsDrive(
59 safeDecodeURIComponent(splitMarkdownLinkSearchAndHash(source).path),
60 );
61 if (path.length === 0) return { _tag: "Blocked" };
62 if (path.startsWith("/") || isWindowsAbsolutePath(path)) {
63 return { _tag: "WorkspaceFile", path };
64 }
65 if (URI_SCHEME_PATTERN.test(path) || path.startsWith("~/") || path.startsWith("~\\")) {
66 return { _tag: "Blocked" };
67 }
68 if (!workspaceRoot) return { _tag: "Blocked" };
69
70 return { _tag: "WorkspaceFile", path: joinWorkspacePath(workspaceRoot, path) };
71}