nextjs-cms 0.9.42 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -140,10 +140,10 @@ declare const configSchema: z.ZodObject<{
140
140
  * @hint 'jpg' is an alias for 'jpeg'
141
141
  */
142
142
  fileType: z.ZodOptional<z.ZodArray<z.ZodEnum<{
143
- jpeg: "jpeg";
143
+ webp: "webp";
144
144
  jpg: "jpg";
145
+ jpeg: "jpeg";
145
146
  png: "png";
146
- webp: "webp";
147
147
  }>>>;
148
148
  /**
149
149
  * Remove the extension from the file name
@@ -353,10 +353,10 @@ declare const optionsSchema: z.ZodObject<{
353
353
  * @hint 'jpg' is an alias for 'jpeg'
354
354
  */
355
355
  fileType: z.ZodOptional<z.ZodArray<z.ZodEnum<{
356
- jpeg: "jpeg";
356
+ webp: "webp";
357
357
  jpg: "jpg";
358
+ jpeg: "jpeg";
358
359
  png: "png";
359
- webp: "webp";
360
360
  }>>>;
361
361
  /**
362
362
  * Remove the extension from the file name
@@ -467,10 +467,10 @@ declare const photoFieldConfigSchema: z.ZodObject<{
467
467
  * @hint 'jpg' is an alias for 'jpeg'
468
468
  */
469
469
  fileType: z.ZodOptional<z.ZodArray<z.ZodEnum<{
470
- jpeg: "jpeg";
470
+ webp: "webp";
471
471
  jpg: "jpg";
472
+ jpeg: "jpeg";
472
473
  png: "png";
473
- webp: "webp";
474
474
  }>>>;
475
475
  /**
476
476
  * Remove the extension from the file name
@@ -1 +1 @@
1
- {"version":3,"file":"photo.d.ts","sourceRoot":"","sources":["../../../src/core/fields/photo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"photo.d.ts","sourceRoot":"","sources":["../../../src/core/fields/photo.ts"],"names":[],"mappings":"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"}
@@ -6,17 +6,9 @@ import * as z from 'zod';
6
6
  import getString from '../../translations/index.js';
7
7
  import { humanReadableFileSize } from '../../utils/index.js';
8
8
  import { getCMSConfig } from '../config/index.js';
9
- import { entityKind, resolveWatermark, watermarkConfigSchema } from '../helpers/index.js';
9
+ import { backgroundSchema, entityKind, resolveWatermark, watermarkConfigSchema } from '../helpers/index.js';
10
10
  import { baseFieldConfigSchema } from './field.js';
11
11
  import { FileField } from './fileField.js';
12
- const backgroundSchema = z
13
- .strictObject({
14
- r: z.number().min(0).max(255).describe('Red channel (0-255)'),
15
- g: z.number().min(0).max(255).describe('Green channel (0-255)'),
16
- b: z.number().min(0).max(255).describe('Blue channel (0-255)'),
17
- alpha: z.number().min(0).max(1).describe('Alpha channel (0-1)'),
18
- })
19
- .describe('Background color for image padding (used with fit: contain)');
20
12
  /**
21
13
  * Size schema — discriminated union of three variants:
22
14
  * - strict: true — user must upload exact dimensions, no server resize
@@ -1,20 +1,54 @@
1
+ import * as z from 'zod';
1
2
  import type { BaseFieldConfig, FieldClientConfig } from './field.js';
2
- import { Field } from './field.js';
3
3
  import { entityKind } from '../helpers/index.js';
4
- import * as z from 'zod';
4
+ import { Field } from './field.js';
5
5
  declare const allowImageUploadsSchema: z.ZodObject<{
6
6
  /**
7
- * The public URL prefix for inline photos.
7
+ * The public URL template for inline photos.
8
8
  * This url should be handled by the website app, not the NextJS-CMS app
9
9
  * This is the URL that will be used to access the photos publicly on the website
10
- * The name of the photo will be appended to this URL
10
+ *
11
+ * `{name}` marks where the generated filename goes, so it can sit anywhere in the URL —
12
+ * a path segment (`https://site.com/photos/{name}`) or a query parameter
13
+ * (`https://site.com/api/photo?folder=posts&name={name}`).
11
14
  */
12
- publicURLPrefix: z.ZodURL;
13
- size: z.ZodOptional<z.ZodObject<{
15
+ publicURL: z.ZodURL;
16
+ /**
17
+ * Bounds applied to each inline image before it is written to disk.
18
+ * Defaults to {@link DEFAULT_INLINE_IMAGE_SIZE}.
19
+ *
20
+ * `fit: 'inside'` scales the image down to fit within the box, keeping its aspect ratio and
21
+ * never enlarging a smaller image. `fit: 'cover'` fills the box exactly, cropping the overflow.
22
+ * `fit: 'contain'` pads the image out to the full box with `background`.
23
+ */
24
+ size: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
14
25
  width: z.ZodNumber;
15
26
  height: z.ZodNumber;
16
- crop: z.ZodDefault<z.ZodBoolean>;
17
- }, z.core.$strict>>;
27
+ fit: z.ZodLiteral<"inside">;
28
+ }, z.core.$strict>, z.ZodObject<{
29
+ width: z.ZodNumber;
30
+ height: z.ZodNumber;
31
+ fit: z.ZodLiteral<"cover">;
32
+ }, z.core.$strict>, z.ZodObject<{
33
+ width: z.ZodNumber;
34
+ height: z.ZodNumber;
35
+ fit: z.ZodLiteral<"contain">;
36
+ /**
37
+ * Falls back to `media.images.background` from the CMS config, which is transparent by
38
+ * default. Transparency needs an alpha-capable `format` (webp or png) — jpeg flattens it
39
+ * to black, so set an opaque colour when writing jpeg.
40
+ */
41
+ background: z.ZodOptional<z.ZodObject<{
42
+ r: z.ZodNumber;
43
+ g: z.ZodNumber;
44
+ b: z.ZodNumber;
45
+ alpha: z.ZodNumber;
46
+ }, z.core.$strict>>;
47
+ }, z.core.$strict>], "fit">>;
48
+ /**
49
+ * Largest accepted inline image, measured on the decoded image rather than the base64 text.
50
+ * Rejected during submission, before the item is saved. Defaults to {@link DEFAULT_MAX_FILE_SIZE}.
51
+ */
18
52
  maxFileSize: z.ZodOptional<z.ZodObject<{
19
53
  size: z.ZodNumber;
20
54
  unit: z.ZodEnum<{
@@ -22,19 +56,15 @@ declare const allowImageUploadsSchema: z.ZodObject<{
22
56
  mb: "mb";
23
57
  }>;
24
58
  }, z.core.$strict>>;
25
- handleMethod: z.ZodOptional<z.ZodEnum<{
26
- base64: "base64";
27
- tempSave: "tempSave";
28
- }>>;
29
59
  /**
30
60
  * Omit the extension of the image when saving it to disk
31
61
  */
32
62
  omitExtension: z.ZodOptional<z.ZodBoolean>;
33
63
  format: z.ZodOptional<z.ZodEnum<{
34
- jpeg: "jpeg";
64
+ webp: "webp";
35
65
  jpg: "jpg";
66
+ jpeg: "jpeg";
36
67
  png: "png";
37
- webp: "webp";
38
68
  }>>;
39
69
  }, z.core.$strict>;
40
70
  declare const configSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -46,17 +76,51 @@ declare const configSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
46
76
  allowMedia: z.ZodLiteral<true>;
47
77
  allowImageUploads: z.ZodOptional<z.ZodObject<{
48
78
  /**
49
- * The public URL prefix for inline photos.
79
+ * The public URL template for inline photos.
50
80
  * This url should be handled by the website app, not the NextJS-CMS app
51
81
  * This is the URL that will be used to access the photos publicly on the website
52
- * The name of the photo will be appended to this URL
82
+ *
83
+ * `{name}` marks where the generated filename goes, so it can sit anywhere in the URL —
84
+ * a path segment (`https://site.com/photos/{name}`) or a query parameter
85
+ * (`https://site.com/api/photo?folder=posts&name={name}`).
86
+ */
87
+ publicURL: z.ZodURL;
88
+ /**
89
+ * Bounds applied to each inline image before it is written to disk.
90
+ * Defaults to {@link DEFAULT_INLINE_IMAGE_SIZE}.
91
+ *
92
+ * `fit: 'inside'` scales the image down to fit within the box, keeping its aspect ratio and
93
+ * never enlarging a smaller image. `fit: 'cover'` fills the box exactly, cropping the overflow.
94
+ * `fit: 'contain'` pads the image out to the full box with `background`.
53
95
  */
54
- publicURLPrefix: z.ZodURL;
55
- size: z.ZodOptional<z.ZodObject<{
96
+ size: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
56
97
  width: z.ZodNumber;
57
98
  height: z.ZodNumber;
58
- crop: z.ZodDefault<z.ZodBoolean>;
59
- }, z.core.$strict>>;
99
+ fit: z.ZodLiteral<"inside">;
100
+ }, z.core.$strict>, z.ZodObject<{
101
+ width: z.ZodNumber;
102
+ height: z.ZodNumber;
103
+ fit: z.ZodLiteral<"cover">;
104
+ }, z.core.$strict>, z.ZodObject<{
105
+ width: z.ZodNumber;
106
+ height: z.ZodNumber;
107
+ fit: z.ZodLiteral<"contain">;
108
+ /**
109
+ * Falls back to `media.images.background` from the CMS config, which is transparent by
110
+ * default. Transparency needs an alpha-capable `format` (webp or png) — jpeg flattens it
111
+ * to black, so set an opaque colour when writing jpeg.
112
+ */
113
+ background: z.ZodOptional<z.ZodObject<{
114
+ r: z.ZodNumber;
115
+ g: z.ZodNumber;
116
+ b: z.ZodNumber;
117
+ alpha: z.ZodNumber;
118
+ }, z.core.$strict>>;
119
+ }, z.core.$strict>], "fit">>;
120
+ /**
121
+ * Largest accepted inline image, measured on the decoded image rather than the base64 text.
122
+ * Rejected during submission, before the item is saved. Defaults to {@link DEFAULT_MAX_FILE_SIZE}.
123
+ */
60
124
  maxFileSize: z.ZodOptional<z.ZodObject<{
61
125
  size: z.ZodNumber;
62
126
  unit: z.ZodEnum<{
@@ -64,19 +128,15 @@ declare const configSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
64
128
  mb: "mb";
65
129
  }>;
66
130
  }, z.core.$strict>>;
67
- handleMethod: z.ZodOptional<z.ZodEnum<{
68
- base64: "base64";
69
- tempSave: "tempSave";
70
- }>>;
71
131
  /**
72
132
  * Omit the extension of the image when saving it to disk
73
133
  */
74
134
  omitExtension: z.ZodOptional<z.ZodBoolean>;
75
135
  format: z.ZodOptional<z.ZodEnum<{
76
- jpeg: "jpeg";
136
+ webp: "webp";
77
137
  jpg: "jpg";
138
+ jpeg: "jpeg";
78
139
  png: "png";
79
- webp: "webp";
80
140
  }>>;
81
141
  }, z.core.$strict>>;
82
142
  }, z.core.$strict>, z.ZodObject<{
@@ -144,17 +204,51 @@ declare const optionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
144
204
  allowMedia: z.ZodLiteral<true>;
145
205
  allowImageUploads: z.ZodOptional<z.ZodObject<{
146
206
  /**
147
- * The public URL prefix for inline photos.
207
+ * The public URL template for inline photos.
148
208
  * This url should be handled by the website app, not the NextJS-CMS app
149
209
  * This is the URL that will be used to access the photos publicly on the website
150
- * The name of the photo will be appended to this URL
210
+ *
211
+ * `{name}` marks where the generated filename goes, so it can sit anywhere in the URL —
212
+ * a path segment (`https://site.com/photos/{name}`) or a query parameter
213
+ * (`https://site.com/api/photo?folder=posts&name={name}`).
214
+ */
215
+ publicURL: z.ZodURL;
216
+ /**
217
+ * Bounds applied to each inline image before it is written to disk.
218
+ * Defaults to {@link DEFAULT_INLINE_IMAGE_SIZE}.
219
+ *
220
+ * `fit: 'inside'` scales the image down to fit within the box, keeping its aspect ratio and
221
+ * never enlarging a smaller image. `fit: 'cover'` fills the box exactly, cropping the overflow.
222
+ * `fit: 'contain'` pads the image out to the full box with `background`.
151
223
  */
152
- publicURLPrefix: z.ZodURL;
153
- size: z.ZodOptional<z.ZodObject<{
224
+ size: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
154
225
  width: z.ZodNumber;
155
226
  height: z.ZodNumber;
156
- crop: z.ZodDefault<z.ZodBoolean>;
157
- }, z.core.$strict>>;
227
+ fit: z.ZodLiteral<"inside">;
228
+ }, z.core.$strict>, z.ZodObject<{
229
+ width: z.ZodNumber;
230
+ height: z.ZodNumber;
231
+ fit: z.ZodLiteral<"cover">;
232
+ }, z.core.$strict>, z.ZodObject<{
233
+ width: z.ZodNumber;
234
+ height: z.ZodNumber;
235
+ fit: z.ZodLiteral<"contain">;
236
+ /**
237
+ * Falls back to `media.images.background` from the CMS config, which is transparent by
238
+ * default. Transparency needs an alpha-capable `format` (webp or png) — jpeg flattens it
239
+ * to black, so set an opaque colour when writing jpeg.
240
+ */
241
+ background: z.ZodOptional<z.ZodObject<{
242
+ r: z.ZodNumber;
243
+ g: z.ZodNumber;
244
+ b: z.ZodNumber;
245
+ alpha: z.ZodNumber;
246
+ }, z.core.$strict>>;
247
+ }, z.core.$strict>], "fit">>;
248
+ /**
249
+ * Largest accepted inline image, measured on the decoded image rather than the base64 text.
250
+ * Rejected during submission, before the item is saved. Defaults to {@link DEFAULT_MAX_FILE_SIZE}.
251
+ */
158
252
  maxFileSize: z.ZodOptional<z.ZodObject<{
159
253
  size: z.ZodNumber;
160
254
  unit: z.ZodEnum<{
@@ -162,19 +256,15 @@ declare const optionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
162
256
  mb: "mb";
163
257
  }>;
164
258
  }, z.core.$strict>>;
165
- handleMethod: z.ZodOptional<z.ZodEnum<{
166
- base64: "base64";
167
- tempSave: "tempSave";
168
- }>>;
169
259
  /**
170
260
  * Omit the extension of the image when saving it to disk
171
261
  */
172
262
  omitExtension: z.ZodOptional<z.ZodBoolean>;
173
263
  format: z.ZodOptional<z.ZodEnum<{
174
- jpeg: "jpeg";
264
+ webp: "webp";
175
265
  jpg: "jpg";
266
+ jpeg: "jpeg";
176
267
  png: "png";
177
- webp: "webp";
178
268
  }>>;
179
269
  }, z.core.$strict>>;
180
270
  name: z.ZodString;
@@ -210,17 +300,51 @@ declare const richTextFieldConfigSchema: z.ZodIntersection<z.ZodDiscriminatedUni
210
300
  allowMedia: z.ZodLiteral<true>;
211
301
  allowImageUploads: z.ZodOptional<z.ZodObject<{
212
302
  /**
213
- * The public URL prefix for inline photos.
303
+ * The public URL template for inline photos.
214
304
  * This url should be handled by the website app, not the NextJS-CMS app
215
305
  * This is the URL that will be used to access the photos publicly on the website
216
- * The name of the photo will be appended to this URL
306
+ *
307
+ * `{name}` marks where the generated filename goes, so it can sit anywhere in the URL —
308
+ * a path segment (`https://site.com/photos/{name}`) or a query parameter
309
+ * (`https://site.com/api/photo?folder=posts&name={name}`).
310
+ */
311
+ publicURL: z.ZodURL;
312
+ /**
313
+ * Bounds applied to each inline image before it is written to disk.
314
+ * Defaults to {@link DEFAULT_INLINE_IMAGE_SIZE}.
315
+ *
316
+ * `fit: 'inside'` scales the image down to fit within the box, keeping its aspect ratio and
317
+ * never enlarging a smaller image. `fit: 'cover'` fills the box exactly, cropping the overflow.
318
+ * `fit: 'contain'` pads the image out to the full box with `background`.
217
319
  */
218
- publicURLPrefix: z.ZodURL;
219
- size: z.ZodOptional<z.ZodObject<{
320
+ size: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
220
321
  width: z.ZodNumber;
221
322
  height: z.ZodNumber;
222
- crop: z.ZodDefault<z.ZodBoolean>;
223
- }, z.core.$strict>>;
323
+ fit: z.ZodLiteral<"inside">;
324
+ }, z.core.$strict>, z.ZodObject<{
325
+ width: z.ZodNumber;
326
+ height: z.ZodNumber;
327
+ fit: z.ZodLiteral<"cover">;
328
+ }, z.core.$strict>, z.ZodObject<{
329
+ width: z.ZodNumber;
330
+ height: z.ZodNumber;
331
+ fit: z.ZodLiteral<"contain">;
332
+ /**
333
+ * Falls back to `media.images.background` from the CMS config, which is transparent by
334
+ * default. Transparency needs an alpha-capable `format` (webp or png) — jpeg flattens it
335
+ * to black, so set an opaque colour when writing jpeg.
336
+ */
337
+ background: z.ZodOptional<z.ZodObject<{
338
+ r: z.ZodNumber;
339
+ g: z.ZodNumber;
340
+ b: z.ZodNumber;
341
+ alpha: z.ZodNumber;
342
+ }, z.core.$strict>>;
343
+ }, z.core.$strict>], "fit">>;
344
+ /**
345
+ * Largest accepted inline image, measured on the decoded image rather than the base64 text.
346
+ * Rejected during submission, before the item is saved. Defaults to {@link DEFAULT_MAX_FILE_SIZE}.
347
+ */
224
348
  maxFileSize: z.ZodOptional<z.ZodObject<{
225
349
  size: z.ZodNumber;
226
350
  unit: z.ZodEnum<{
@@ -228,19 +352,15 @@ declare const richTextFieldConfigSchema: z.ZodIntersection<z.ZodDiscriminatedUni
228
352
  mb: "mb";
229
353
  }>;
230
354
  }, z.core.$strict>>;
231
- handleMethod: z.ZodOptional<z.ZodEnum<{
232
- base64: "base64";
233
- tempSave: "tempSave";
234
- }>>;
235
355
  /**
236
356
  * Omit the extension of the image when saving it to disk
237
357
  */
238
358
  omitExtension: z.ZodOptional<z.ZodBoolean>;
239
359
  format: z.ZodOptional<z.ZodEnum<{
240
- jpeg: "jpeg";
360
+ webp: "webp";
241
361
  jpg: "jpg";
362
+ jpeg: "jpeg";
242
363
  png: "png";
243
- webp: "webp";
244
364
  }>>;
245
365
  }, z.core.$strict>>;
246
366
  name: z.ZodString;
@@ -1 +1 @@
1
- {"version":3,"file":"richText.d.ts","sourceRoot":"","sources":["../../../src/core/fields/richText.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACpE,OAAO,EAAE,KAAK,EAAyB,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAcxB,QAAA,MAAM,uBAAuB;IACzB;;;;;OAKG;;;;;;;;;;;;;;;;;;IAgBH;;OAEG;;;;;;;;kBAGL,CAAA;AAwCF,QAAA,MAAM,YAAY;;;;;;;;QAlEd;;;;;WAKG;;;;;;;;;;;;;;;;;;QAgBH;;WAEG;;;;;;;;;;;;;;;;kCA8CL,CAAA;AAEF,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAE1C,qBAAa,aAAc,SAAQ,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC;IACzD,gBAAyB,CAAC,UAAU,CAAC,EAAE,MAAM,CAAkB;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,GAAG,KAAK,CAAA;IAC3E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAAA;IAEjC,OAAO,CAAC,aAAa,CAIb;gBAEI,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC;IAuB3C;;OAEG;IACH,QAAQ,IAAI,MAAM;IAIF,eAAe,IAAI,iBAAiB,GAAG;QACnD,IAAI,EAAE,WAAW,CAAA;QACjB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAChC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;QAChC,iBAAiB,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,GAAG,KAAK,CAAA;QACnE,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;QAC9B,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAC5B;IAaD;;OAEG;YACW,aAAa;IAY3B,aAAa;IAWb;;OAEG;IACH,mBAAmB;IAuBb,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;IAsD5E;;OAEG;IACG,oBAAoB;IA2BJ,UAAU,CAAC,EAC7B,WAAW,EACX,MAAM,EACN,MAAM,GACT,EAAE;QACC,WAAW,EAAE,MAAM,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;YAYH,yBAAyB;CAuE1C;AAED,MAAM,MAAM,yBAAyB,GAAG,UAAU,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAA;AAWpF,QAAA,MAAM,aAAa;;;;;;;;QA7Xf;;;;;WAKG;;;;;;;;;;;;;;;;;;QAgBH;;WAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAyWL,CAAA;AAEF,QAAA,MAAM,yBAAyB;;;;;;;;QAlY3B;;;;;WAKG;;;;;;;;;;;;;;;;;;QAgBH;;WAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAoXN,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAE3E;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,GAAG,mBAAmB,CAmBvF"}
1
+ {"version":3,"file":"richText.d.ts","sourceRoot":"","sources":["../../../src/core/fields/richText.ts"],"names":[],"mappings":"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"}
@@ -1,39 +1,113 @@
1
- import { Field, baseFieldConfigSchema } from './field.js';
2
- import { entityKind } from '../helpers/index.js';
3
- import * as z from 'zod';
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { and, eq, sql } from 'drizzle-orm';
4
4
  import { customAlphabet } from 'nanoid';
5
5
  import sharp from 'sharp';
6
- import path from 'path';
6
+ import * as z from 'zod';
7
7
  import { db } from '../../db/client.js';
8
8
  import { EditorPhotosTable } from '../../db/schema.js';
9
- import fs from 'fs';
10
- import { and, eq, sql } from 'drizzle-orm';
9
+ import getString from '../../translations/index.js';
10
+ import { humanReadableFileSize } from '../../utils/index.js';
11
11
  import { getCMSConfig } from '../config/index.js';
12
+ import { backgroundSchema, entityKind } from '../helpers/index.js';
12
13
  import { sanitizeRichText } from '../security/dom.js';
14
+ import { baseFieldConfigSchema, Field } from './field.js';
13
15
  const positiveInt = z.number().int().positive();
14
16
  const nonNegativeInt = z.number().int().nonnegative();
17
+ /**
18
+ * Bounds each inline image is fitted into, discriminated on `fit` so `background` only exists
19
+ * where it means something. Mirrors the `fit` vocabulary of the photo field, using sharp's own
20
+ * names so the behaviour is not renamed in translation.
21
+ */
22
+ const inlineImageSizeSchema = z.discriminatedUnion('fit', [
23
+ z.strictObject({
24
+ width: positiveInt,
25
+ height: positiveInt,
26
+ fit: z.literal('inside').describe('Scale down to fit within the box, keeping aspect ratio (sharp fit: inside)'),
27
+ }),
28
+ z.strictObject({
29
+ width: positiveInt,
30
+ height: positiveInt,
31
+ fit: z.literal('cover').describe('Crop to fill dimensions (sharp fit: cover)'),
32
+ }),
33
+ z.strictObject({
34
+ width: positiveInt,
35
+ height: positiveInt,
36
+ fit: z.literal('contain').describe('Pad with background to fill dimensions (sharp fit: contain)'),
37
+ /**
38
+ * Falls back to `media.images.background` from the CMS config, which is transparent by
39
+ * default. Transparency needs an alpha-capable `format` (webp or png) — jpeg flattens it
40
+ * to black, so set an opaque colour when writing jpeg.
41
+ */
42
+ background: backgroundSchema.optional().describe('Background color for padding'),
43
+ }),
44
+ ]);
45
+ /**
46
+ * Bounds every inline image is fitted into unless the field overrides `size`.
47
+ * 1200x675 is 16:9 at a width that covers typical article body content, scaled down to fit so a
48
+ * body image is never cropped or padded unless the field asks for it.
49
+ */
50
+ const DEFAULT_INLINE_IMAGE_SIZE = {
51
+ width: 1200,
52
+ height: 675,
53
+ fit: 'inside',
54
+ };
55
+ /** Largest accepted inline image unless the field overrides `maxFileSize`. */
56
+ const DEFAULT_MAX_FILE_SIZE = { size: 1, unit: 'mb' };
57
+ const BYTES_PER_UNIT = { kb: 1024, mb: 1024 * 1024 };
58
+ /**
59
+ * Decoded byte length of a base64 payload, without allocating the buffer — inline images are
60
+ * rejected before anything is decoded or written.
61
+ */
62
+ function base64ByteLength(base64) {
63
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
64
+ return Math.floor((base64.length * 3) / 4) - padding;
65
+ }
66
+ /**
67
+ * Matches the `{name}` placeholder (whitespace tolerated: `{ name }`).
68
+ *
69
+ * Built on demand rather than shared as a single global regex: a `/g` regex is stateful via
70
+ * `lastIndex`, so reusing one instance across `test()` calls silently alternates its result.
71
+ */
72
+ const NAME_PLACEHOLDER = String.raw `\{\s*name\s*\}`;
73
+ const hasNamePlaceholder = (value) => new RegExp(NAME_PLACEHOLDER).test(value);
74
+ const resolvePublicURL = (template, name) => template.replace(new RegExp(NAME_PLACEHOLDER, 'g'), name);
15
75
  const allowImageUploadsSchema = z.strictObject({
16
76
  /**
17
- * The public URL prefix for inline photos.
77
+ * The public URL template for inline photos.
18
78
  * This url should be handled by the website app, not the NextJS-CMS app
19
79
  * This is the URL that will be used to access the photos publicly on the website
20
- * The name of the photo will be appended to this URL
80
+ *
81
+ * `{name}` marks where the generated filename goes, so it can sit anywhere in the URL —
82
+ * a path segment (`https://site.com/photos/{name}`) or a query parameter
83
+ * (`https://site.com/api/photo?folder=posts&name={name}`).
21
84
  */
22
- publicURLPrefix: z.url().describe('The public URL prefix for inline photos'),
23
- size: z
24
- .strictObject({
25
- width: positiveInt,
26
- height: positiveInt,
27
- crop: z.boolean().default(false),
85
+ publicURL: z
86
+ .url()
87
+ .refine(hasNamePlaceholder, {
88
+ message: 'publicURL must contain the {name} placeholder, e.g. https://site.com/photos/{name}',
28
89
  })
29
- .optional(),
90
+ .describe('The public URL template for inline photos; {name} is replaced with the generated filename'),
91
+ /**
92
+ * Bounds applied to each inline image before it is written to disk.
93
+ * Defaults to {@link DEFAULT_INLINE_IMAGE_SIZE}.
94
+ *
95
+ * `fit: 'inside'` scales the image down to fit within the box, keeping its aspect ratio and
96
+ * never enlarging a smaller image. `fit: 'cover'` fills the box exactly, cropping the overflow.
97
+ * `fit: 'contain'` pads the image out to the full box with `background`.
98
+ */
99
+ size: inlineImageSizeSchema.optional(),
100
+ /**
101
+ * Largest accepted inline image, measured on the decoded image rather than the base64 text.
102
+ * Rejected during submission, before the item is saved. Defaults to {@link DEFAULT_MAX_FILE_SIZE}.
103
+ */
30
104
  maxFileSize: z
31
105
  .strictObject({
32
106
  size: positiveInt,
33
107
  unit: z.enum(['kb', 'mb']),
34
108
  })
35
109
  .optional(),
36
- handleMethod: z.enum(['base64', 'tempSave']).optional(),
110
+ // handleMethod: z.enum(['base64', 'tempSave']).optional(),
37
111
  /**
38
112
  * Omit the extension of the image when saving it to disk
39
113
  */
@@ -96,13 +170,13 @@ export class RichTextField extends Field {
96
170
  this.placeholder = config.placeholder;
97
171
  this.sanitize = config.sanitize ?? true;
98
172
  this.rtl = config.rtl;
99
- if (config.allowMedia && config.allowImageUploads?.publicURLPrefix) {
173
+ if (config.allowMedia && config.allowImageUploads?.publicURL) {
100
174
  this.allowMedia = true;
101
175
  this.allowImageUploads = {
102
- publicURLPrefix: config.allowImageUploads.publicURLPrefix,
103
- size: config.allowImageUploads.size ?? { width: 400, height: 400, crop: false },
104
- maxFileSize: config.allowImageUploads.maxFileSize ?? { size: 1, unit: 'mb' },
105
- handleMethod: config.allowImageUploads.handleMethod ?? 'base64',
176
+ // handleMethod: config.allowImageUploads.handleMethod ?? 'base64',
177
+ publicURL: config.allowImageUploads.publicURL,
178
+ size: config.allowImageUploads.size ?? DEFAULT_INLINE_IMAGE_SIZE,
179
+ maxFileSize: config.allowImageUploads.maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
106
180
  omitExtension: config.allowImageUploads.omitExtension ?? true,
107
181
  format: config.allowImageUploads.format ?? 'webp',
108
182
  };
@@ -164,9 +238,24 @@ export class RichTextField extends Field {
164
238
  return;
165
239
  const regex = /<img.*?src="(data:image\/.*?;base64,.*?)".*?>/g;
166
240
  const matches = this.value.matchAll(regex);
241
+ const maxFileSize = this.allowImageUploads.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
242
+ const maxBytes = maxFileSize.size * BYTES_PER_UNIT[maxFileSize.unit];
167
243
  for (const match of matches) {
168
244
  const base64String = match[1];
169
245
  const extension = base64String.split(';')[0].split('/')[1];
246
+ /**
247
+ * Reject oversized images here rather than in `writeInlineImages`: extraction runs
248
+ * during submission, so throwing prevents the item from being saved at all, whereas
249
+ * the write step runs afterwards and the row would already exist.
250
+ */
251
+ const byteLength = base64ByteLength(base64String.split(',')[1] ?? '');
252
+ if (byteLength > maxBytes) {
253
+ throw new Error(getString('fileSizeExceedsMax', this.language, {
254
+ field: this.getLocalizedLabel(),
255
+ actual: humanReadableFileSize(byteLength),
256
+ max: `${maxFileSize.size} ${maxFileSize.unit}`,
257
+ }));
258
+ }
170
259
  const randomString = customAlphabet('1234567890abcdef', 21)();
171
260
  const name = `${randomString}${this.allowImageUploads.omitExtension ? '' : `.${extension}`}`;
172
261
  this._inlinePhotos.push({
@@ -175,9 +264,9 @@ export class RichTextField extends Field {
175
264
  name: name,
176
265
  });
177
266
  /**
178
- * Replace the inline image src with the name of the image
267
+ * Replace the inline image src with the public URL of the image
179
268
  */
180
- this.value = this.value.replace(base64String, `${this.allowImageUploads?.publicURLPrefix}/${name}`);
269
+ this.value = this.value.replace(base64String, resolvePublicURL(this.allowImageUploads.publicURL, name));
181
270
  }
182
271
  }
183
272
  async writeInlineImages(sectionName, itemId, locale) {
@@ -203,7 +292,20 @@ export class RichTextField extends Field {
203
292
  sharp.cache({ files: 0 });
204
293
  sharp.cache(false);
205
294
  const image = sharp(buffer);
295
+ const size = this.allowImageUploads.size ?? DEFAULT_INLINE_IMAGE_SIZE;
206
296
  await image
297
+ .resize({
298
+ width: size.width,
299
+ height: size.height,
300
+ fit: size.fit,
301
+ // `inside` only ever scales down — enlarging a smaller image to fill the box
302
+ // would cost quality for nothing. `cover` and `contain` produce the exact box,
303
+ // so they are allowed to enlarge.
304
+ withoutEnlargement: size.fit === 'inside',
305
+ ...(size.fit === 'contain'
306
+ ? { background: size.background ?? cmsConfig.media.images.background }
307
+ : {}),
308
+ })
207
309
  .toFormat(this.allowImageUploads.format ?? 'webp')
208
310
  .toFile(path.join(uploadsFolder, '.photos', sectionName, photo.name));
209
311
  /**
@@ -237,7 +339,7 @@ export class RichTextField extends Field {
237
339
  /**
238
340
  * Sanitize the value
239
341
  */
240
- this.sanitizeValue();
342
+ await this.sanitizeValue();
241
343
  if (!this.value)
242
344
  return;
243
345
  /**
@@ -291,19 +393,17 @@ export class RichTextField extends Field {
291
393
  if (tablePhotos.length === 0)
292
394
  return;
293
395
  /**
294
- * Extract the photos from the value (only photos that has the publicURLPrefix)
295
- */
296
- const regex = new RegExp(`${this.allowImageUploads.publicURLPrefix}/(.*?)`, 'g');
297
- const matches = this.value.matchAll(regex);
298
- const photosInValue = [];
299
- for (const match of matches) {
300
- photosInValue.push(match[1]);
301
- }
302
- /**
303
- * Check if the photos in the database are still present in the value
396
+ * Check if the photos in the database are still present in the value.
397
+ *
398
+ * Matched on the generated filename rather than the full public URL: the name is a unique
399
+ * random token, while the URL is presentation. It can be re-encoded by the sanitizer
400
+ * (`&` -> `&amp;`), or change wholesale when `publicURL` or the site's domain changes —
401
+ * none of which mean the admin removed the image. A stale match here deletes a file that
402
+ * is still in use, so the check errs towards keeping (an orphan file is recoverable, a
403
+ * deleted one is not).
304
404
  */
305
405
  for (const tablePhoto of tablePhotos) {
306
- if (!photosInValue.includes(tablePhoto.name)) {
406
+ if (!this.value.includes(tablePhoto.name)) {
307
407
  /**
308
408
  * Delete the photo from the database
309
409
  */