| 1 | import { isWindowsAbsolutePath, stripSlashPrefixedWindowsDrive } from "@t3tools/shared/path"; |
| 2 | |
| 3 | import { |
| 4 | normalizeMarkdownLinkDestination, |
| 5 | safeDecodeURIComponent, |
| 6 | splitMarkdownLinkSearchAndHash, |
| 7 | } from "@t3tools/shared/markdownLinks"; |
| 8 | import { parseFileUrlHref } from "@t3tools/shared/fileLinks"; |
| 9 | |
| 10 | const DIRECT_IMAGE_SOURCE_PATTERN = /^(?:https?:|data:|blob:|\/\/)/i; |
| 11 | const URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/; |
| 12 | |
| 13 | export type MarkdownImageSource = |
| 14 | | { readonly _tag: "Direct"; readonly uri: string } |
| 15 | | { readonly _tag: "WorkspaceFile"; readonly path: string } |
| 16 | | { readonly _tag: "Blocked" }; |
| 17 | |
| 18 | export function markdownImageSourceFragment(source: string): string { |
| 19 | return splitMarkdownLinkSearchAndHash(normalizeMarkdownLinkDestination(source)).hash; |
| 20 | } |
| 21 | |
| 22 | function 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 | */ |
| 34 | export 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 | } |