Open History to see the last commit
1import { isWindowsAbsolutePath } from "@t3tools/shared/path";
2
3import { safeDecodeURIComponent } from "@t3tools/shared/markdownLinks";
4
5export type MediaReference =
6 | {
7 readonly kind: "file";
8 readonly path: string;
9 readonly relativePath?: string;
10 }
11 | { readonly kind: "url"; readonly url: string };
12
13function absolutePathParts(path: string) {
14 const windows = isWindowsAbsolutePath(path) || path.startsWith("//");
15 const normalized = windows ? path.replaceAll("\\", "/") : path;
16 const prefix = windows
17 ? /^(?:[a-z]:\/|\/\/[^/]+\/[^/]+(?:\/|$))/i.exec(normalized)?.[0]
18 : normalized.startsWith("/")
19 ? "/"
20 : undefined;
21 if (!prefix) return undefined;
22
23 const segments: string[] = [];
24 for (const segment of normalized.slice(prefix.length).split("/")) {
25 if (!segment || segment === ".") continue;
26 if (segment === "..") segments.pop();
27 else segments.push(segment);
28 }
29 const root = prefix.replace(/\/$/, "");
30 return { root: windows ? root.toLowerCase() : root, segments, windows };
31}
32
33export function mediaFileReference(
34 path: string,
35 workspaceRoot?: string | null,
36): Extract<MediaReference, { kind: "file" }> {
37 const target = absolutePathParts(path);
38 const workspace = workspaceRoot ? absolutePathParts(workspaceRoot) : undefined;
39 if (
40 !target ||
41 !workspace ||
42 target.windows !== workspace.windows ||
43 target.root !== workspace.root ||
44 target.segments.length <= workspace.segments.length ||
45 !workspace.segments.every((segment, index) =>
46 workspace.windows
47 ? segment.toLowerCase() === target.segments[index]?.toLowerCase()
48 : segment === target.segments[index],
49 )
50 ) {
51 return { kind: "file", path };
52 }
53 return {
54 kind: "file",
55 path,
56 relativePath: target.segments.slice(workspace.segments.length).join("/"),
57 };
58}
59
60/** Pass the authored source, not a generated URL used by the media player. */
61export function mediaUrlReference(
62 url: string,
63): Extract<MediaReference, { kind: "url" }> | undefined {
64 if (!/^(?:https?:\/\/|\/\/)/i.test(url)) return undefined;
65 try {
66 const parsed = new URL(url.startsWith("//") ? `https:${url}` : url);
67 return parsed.protocol === "http:" || parsed.protocol === "https:"
68 ? { kind: "url", url }
69 : undefined;
70 } catch {
71 return undefined;
72 }
73}
74
75/** Local paths are already decoded; URL filename escapes are decoded exactly once. */
76export function mediaReferenceFileName(reference: MediaReference): string | undefined {
77 if (reference.kind === "file") {
78 const windows = isWindowsAbsolutePath(reference.path) || reference.path.startsWith("//");
79 return reference.path.split(windows ? /[\\/]/ : "/").at(-1) || undefined;
80 }
81
82 let basename: string | undefined;
83 try {
84 const url = reference.url;
85 basename = new URL(url.startsWith("//") ? `https:${url}` : url).pathname.split("/").at(-1);
86 } catch {
87 return undefined;
88 }
89 return basename ? safeDecodeURIComponent(basename) : undefined;
90}