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