-
Notifications
You must be signed in to change notification settings - Fork 31
/
cloudinary.ts
158 lines (141 loc) · 3.72 KB
/
cloudinary.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import {
UrlGenerator,
UrlGeneratorOptions,
UrlParser,
UrlTransformer,
} from "../types.ts";
import { roundIfNumeric, toUrl } from "../utils.ts";
// Thanks Colby!
const cloudinaryRegex =
/https?:\/\/(?<host>[^\/]+)\/(?<cloudName>[^\/]+)\/(?<assetType>image|video|raw)\/(?<deliveryType>upload|fetch|private|authenticated|sprite|facebook|twitter|youtube|vimeo)\/?(?<signature>s\-\-[a-zA-Z0-9]+\-\-)?\/?(?<transformations>(?:[^_\/]+_[^,\/]+,?)*)?\/(?:(?<version>v\d+)\/)?(?<idAndFormat>[^\s]+)$/g;
const parseTransforms = (transformations: string) => {
return transformations
? Object.fromEntries(transformations.split(",").map((t) => t.split("_")))
: {};
};
const formatUrl = (
{
host,
cloudName,
assetType,
deliveryType,
signature,
transformations = {},
version,
id,
format,
}: CloudinaryParams,
): string => {
if (format) {
transformations.f = format;
}
const transformString = Object.entries(transformations).map(
([key, value]) => `${key}_${value}`,
).join(",");
const pathSegments = [
host,
cloudName,
assetType,
deliveryType,
signature,
transformString,
version,
id,
].filter(Boolean).join("/");
return `https://${pathSegments}`;
};
export interface CloudinaryParams {
host?: string;
cloudName?: string;
assetType?: string;
deliveryType?: string;
signature?: string;
transformations: Record<string, string>;
version?: string;
id?: string;
format?: string;
}
export const parse: UrlParser<CloudinaryParams> = (
imageUrl,
) => {
const url = toUrl(imageUrl);
const matches = [...url.toString().matchAll(cloudinaryRegex)];
if (!matches.length) {
throw new Error("Invalid Cloudinary URL");
}
const group = matches[0].groups || {};
const {
transformations: transformString = "",
idAndFormat,
...baseParams
} = group;
delete group.idAndFormat;
const lastDotIndex = idAndFormat.lastIndexOf(".");
const id = lastDotIndex < 0
? idAndFormat
: idAndFormat.slice(0, lastDotIndex);
const originalFormat = lastDotIndex < 0
? undefined
: idAndFormat.slice(lastDotIndex + 1);
const { w, h, f, ...transformations } = parseTransforms(
transformString,
);
const format = (f && f !== "auto") ? f : originalFormat;
const base = formatUrl({ ...baseParams, id, transformations });
return {
base,
width: Number(w) || undefined,
height: Number(h) || undefined,
format,
cdn: "cloudinary",
params: {
...group,
id: group.deliveryType === "fetch" ? idAndFormat : id,
format,
transformations,
},
};
};
export const generate: UrlGenerator<CloudinaryParams> = (
{ base, width, height, format, params },
) => {
const parsed = parse(base.toString());
const props: CloudinaryParams = {
transformations: {},
...parsed.params,
...params,
format: format || "auto",
};
if (width) {
props.transformations.w = roundIfNumeric(width).toString();
}
if (height) {
props.transformations.h = roundIfNumeric(height).toString();
}
// Default crop to fill without upscaling
props.transformations.c ||= "lfill";
return formatUrl(props);
};
export const transform: UrlTransformer = (
{ url: originalUrl, width, height, format = "auto" },
) => {
const parsed = parse(originalUrl);
if (!parsed) {
throw new Error("Invalid Cloudinary URL");
}
if (parsed.params?.assetType !== "image") {
throw new Error("Cloudinary transformer only supports images");
}
if (parsed.params?.signature) {
throw new Error(
"Cloudinary transformer does not support signed URLs",
);
}
const props: UrlGeneratorOptions<CloudinaryParams> = {
...parsed,
width,
height,
format,
};
return generate(props);
};