nextjs-cms 0.9.42 → 0.10.1

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.
Files changed (37) hide show
  1. package/dist/api/actions/pages.d.ts +3 -3
  2. package/dist/api/trpc/root.d.ts +3 -3
  3. package/dist/api/trpc/routers/navigation.d.ts +3 -3
  4. package/dist/api/trpc/server.d.ts +9 -9
  5. package/dist/core/config/config-loader.d.ts +2 -2
  6. package/dist/core/fields/date-range.d.ts +4 -4
  7. package/dist/core/fields/inline-image.d.ts +45 -0
  8. package/dist/core/fields/inline-image.d.ts.map +1 -0
  9. package/dist/core/fields/inline-image.js +30 -0
  10. package/dist/core/fields/photo.d.ts.map +1 -1
  11. package/dist/core/fields/photo.js +1 -9
  12. package/dist/core/fields/richText.d.ts +162 -42
  13. package/dist/core/fields/richText.d.ts.map +1 -1
  14. package/dist/core/fields/richText.js +104 -35
  15. package/dist/core/fields/select.d.ts +1 -1
  16. package/dist/core/helpers/background.d.ts +18 -0
  17. package/dist/core/helpers/background.d.ts.map +1 -0
  18. package/dist/core/helpers/background.js +18 -0
  19. package/dist/core/helpers/index.d.ts +5 -1
  20. package/dist/core/helpers/index.d.ts.map +1 -1
  21. package/dist/core/helpers/index.js +3 -1
  22. package/dist/core/helpers/inline-image.d.ts +46 -0
  23. package/dist/core/helpers/inline-image.d.ts.map +1 -0
  24. package/dist/core/helpers/inline-image.js +30 -0
  25. package/dist/core/sections/category.d.ts +4 -4
  26. package/dist/core/sections/hasItems.d.ts +4 -4
  27. package/dist/core/sections/section.d.ts +3 -3
  28. package/dist/core/sections/simple.d.ts +4 -4
  29. package/dist/utils/index.d.ts +1 -1
  30. package/dist/utils/index.d.ts.map +1 -1
  31. package/dist/utils/index.js +1 -1
  32. package/dist/utils/utils.d.ts +13 -0
  33. package/dist/utils/utils.d.ts.map +1 -1
  34. package/dist/utils/utils.js +15 -0
  35. package/dist/validators/richText.d.ts.map +1 -1
  36. package/dist/validators/richText.js +35 -1
  37. package/package.json +3 -3
@@ -1,39 +1,93 @@
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 { base64ByteLength, humanReadableFileSize, maxFileSizeToBytes } from '../../utils/index.js';
11
11
  import { getCMSConfig } from '../config/index.js';
12
+ import { backgroundSchema, entityKind } from '../helpers/index.js';
13
+ import { DEFAULT_INLINE_IMAGE_SIZE, DEFAULT_MAX_FILE_SIZE, inlineImageResizeOptions } from '../helpers/inline-image.js';
12
14
  import { sanitizeRichText } from '../security/dom.js';
15
+ import { baseFieldConfigSchema, Field } from './field.js';
13
16
  const positiveInt = z.number().int().positive();
14
17
  const nonNegativeInt = z.number().int().nonnegative();
18
+ /**
19
+ * Bounds each inline image is fitted into, discriminated on `fit` so `background` only exists
20
+ * where it means something. Mirrors the `fit` vocabulary of the photo field, using sharp's own
21
+ * names so the behaviour is not renamed in translation.
22
+ */
23
+ const inlineImageSizeSchema = z.discriminatedUnion('fit', [
24
+ z.strictObject({
25
+ width: positiveInt,
26
+ height: positiveInt,
27
+ fit: z.literal('inside').describe('Scale down to fit within the box, keeping aspect ratio (sharp fit: inside)'),
28
+ }),
29
+ z.strictObject({
30
+ width: positiveInt,
31
+ height: positiveInt,
32
+ fit: z.literal('cover').describe('Crop to fill dimensions (sharp fit: cover)'),
33
+ }),
34
+ z.strictObject({
35
+ width: positiveInt,
36
+ height: positiveInt,
37
+ fit: z.literal('contain').describe('Pad with background to fill dimensions (sharp fit: contain)'),
38
+ /**
39
+ * Falls back to `media.images.background` from the CMS config, which is transparent by
40
+ * default. Transparency needs an alpha-capable `format` (webp or png) — jpeg flattens it
41
+ * to black, so set an opaque colour when writing jpeg.
42
+ */
43
+ background: backgroundSchema.optional().describe('Background color for padding'),
44
+ }),
45
+ ]);
46
+ /**
47
+ * Matches the `{name}` placeholder (whitespace tolerated: `{ name }`).
48
+ *
49
+ * Built on demand rather than shared as a single global regex: a `/g` regex is stateful via
50
+ * `lastIndex`, so reusing one instance across `test()` calls silently alternates its result.
51
+ */
52
+ const NAME_PLACEHOLDER = String.raw `\{\s*name\s*\}`;
53
+ const hasNamePlaceholder = (value) => new RegExp(NAME_PLACEHOLDER).test(value);
54
+ const resolvePublicURL = (template, name) => template.replace(new RegExp(NAME_PLACEHOLDER, 'g'), name);
15
55
  const allowImageUploadsSchema = z.strictObject({
16
56
  /**
17
- * The public URL prefix for inline photos.
57
+ * The public URL template for inline photos.
18
58
  * This url should be handled by the website app, not the NextJS-CMS app
19
59
  * 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
60
+ *
61
+ * `{name}` marks where the generated filename goes, so it can sit anywhere in the URL —
62
+ * a path segment (`https://site.com/photos/{name}`) or a query parameter
63
+ * (`https://site.com/api/photo?folder=posts&name={name}`).
21
64
  */
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),
65
+ publicURL: z
66
+ .url()
67
+ .refine(hasNamePlaceholder, {
68
+ message: 'publicURL must contain the {name} placeholder, e.g. https://site.com/photos/{name}',
28
69
  })
29
- .optional(),
70
+ .describe('The public URL template for inline photos; {name} is replaced with the generated filename'),
71
+ /**
72
+ * Bounds applied to each inline image before it is written to disk.
73
+ * Defaults to {@link DEFAULT_INLINE_IMAGE_SIZE}.
74
+ *
75
+ * `fit: 'inside'` scales the image down to fit within the box, keeping its aspect ratio and
76
+ * never enlarging a smaller image. `fit: 'cover'` fills the box exactly, cropping the overflow.
77
+ * `fit: 'contain'` pads the image out to the full box with `background`.
78
+ */
79
+ size: inlineImageSizeSchema.optional(),
80
+ /**
81
+ * Largest accepted inline image, measured on the decoded image rather than the base64 text.
82
+ * Rejected during submission, before the item is saved. Defaults to {@link DEFAULT_MAX_FILE_SIZE}.
83
+ */
30
84
  maxFileSize: z
31
85
  .strictObject({
32
86
  size: positiveInt,
33
87
  unit: z.enum(['kb', 'mb']),
34
88
  })
35
89
  .optional(),
36
- handleMethod: z.enum(['base64', 'tempSave']).optional(),
90
+ // handleMethod: z.enum(['base64', 'tempSave']).optional(),
37
91
  /**
38
92
  * Omit the extension of the image when saving it to disk
39
93
  */
@@ -96,13 +150,13 @@ export class RichTextField extends Field {
96
150
  this.placeholder = config.placeholder;
97
151
  this.sanitize = config.sanitize ?? true;
98
152
  this.rtl = config.rtl;
99
- if (config.allowMedia && config.allowImageUploads?.publicURLPrefix) {
153
+ if (config.allowMedia && config.allowImageUploads?.publicURL) {
100
154
  this.allowMedia = true;
101
155
  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',
156
+ // handleMethod: config.allowImageUploads.handleMethod ?? 'base64',
157
+ publicURL: config.allowImageUploads.publicURL,
158
+ size: config.allowImageUploads.size ?? DEFAULT_INLINE_IMAGE_SIZE,
159
+ maxFileSize: config.allowImageUploads.maxFileSize ?? DEFAULT_MAX_FILE_SIZE,
106
160
  omitExtension: config.allowImageUploads.omitExtension ?? true,
107
161
  format: config.allowImageUploads.format ?? 'webp',
108
162
  };
@@ -164,9 +218,24 @@ export class RichTextField extends Field {
164
218
  return;
165
219
  const regex = /<img.*?src="(data:image\/.*?;base64,.*?)".*?>/g;
166
220
  const matches = this.value.matchAll(regex);
221
+ const maxFileSize = this.allowImageUploads.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
222
+ const maxBytes = maxFileSizeToBytes(maxFileSize);
167
223
  for (const match of matches) {
168
224
  const base64String = match[1];
169
225
  const extension = base64String.split(';')[0].split('/')[1];
226
+ /**
227
+ * Reject oversized images here rather than in `writeInlineImages`: extraction runs
228
+ * during submission, so throwing prevents the item from being saved at all, whereas
229
+ * the write step runs afterwards and the row would already exist.
230
+ */
231
+ const byteLength = base64ByteLength(base64String.split(',')[1] ?? '');
232
+ if (byteLength > maxBytes) {
233
+ throw new Error(getString('fileSizeExceedsMax', this.language, {
234
+ field: this.getLocalizedLabel(),
235
+ actual: humanReadableFileSize(byteLength),
236
+ max: `${maxFileSize.size} ${maxFileSize.unit}`,
237
+ }));
238
+ }
170
239
  const randomString = customAlphabet('1234567890abcdef', 21)();
171
240
  const name = `${randomString}${this.allowImageUploads.omitExtension ? '' : `.${extension}`}`;
172
241
  this._inlinePhotos.push({
@@ -175,9 +244,9 @@ export class RichTextField extends Field {
175
244
  name: name,
176
245
  });
177
246
  /**
178
- * Replace the inline image src with the name of the image
247
+ * Replace the inline image src with the public URL of the image
179
248
  */
180
- this.value = this.value.replace(base64String, `${this.allowImageUploads?.publicURLPrefix}/${name}`);
249
+ this.value = this.value.replace(base64String, resolvePublicURL(this.allowImageUploads.publicURL, name));
181
250
  }
182
251
  }
183
252
  async writeInlineImages(sectionName, itemId, locale) {
@@ -203,7 +272,9 @@ export class RichTextField extends Field {
203
272
  sharp.cache({ files: 0 });
204
273
  sharp.cache(false);
205
274
  const image = sharp(buffer);
275
+ const size = this.allowImageUploads.size ?? DEFAULT_INLINE_IMAGE_SIZE;
206
276
  await image
277
+ .resize(inlineImageResizeOptions(size, cmsConfig.media.images.background))
207
278
  .toFormat(this.allowImageUploads.format ?? 'webp')
208
279
  .toFile(path.join(uploadsFolder, '.photos', sectionName, photo.name));
209
280
  /**
@@ -237,7 +308,7 @@ export class RichTextField extends Field {
237
308
  /**
238
309
  * Sanitize the value
239
310
  */
240
- this.sanitizeValue();
311
+ await this.sanitizeValue();
241
312
  if (!this.value)
242
313
  return;
243
314
  /**
@@ -291,19 +362,17 @@ export class RichTextField extends Field {
291
362
  if (tablePhotos.length === 0)
292
363
  return;
293
364
  /**
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
365
+ * Check if the photos in the database are still present in the value.
366
+ *
367
+ * Matched on the generated filename rather than the full public URL: the name is a unique
368
+ * random token, while the URL is presentation. It can be re-encoded by the sanitizer
369
+ * (`&` -> `&amp;`), or change wholesale when `publicURL` or the site's domain changes —
370
+ * none of which mean the admin removed the image. A stale match here deletes a file that
371
+ * is still in use, so the check errs towards keeping (an orphan file is recoverable, a
372
+ * deleted one is not).
304
373
  */
305
374
  for (const tablePhoto of tablePhotos) {
306
- if (!photosInValue.includes(tablePhoto.name)) {
375
+ if (!this.value.includes(tablePhoto.name)) {
307
376
  /**
308
377
  * Delete the photo from the database
309
378
  */
@@ -379,8 +379,8 @@ declare const selectFieldConfigSchema: z.ZodIntersection<z.ZodUnion<readonly [z.
379
379
  }, z.core.$strict>]>, z.ZodObject<{
380
380
  type: z.ZodLiteral<"select">;
381
381
  optionsType: z.ZodEnum<{
382
- db: "db";
383
382
  section: "section";
383
+ db: "db";
384
384
  static: "static";
385
385
  }>;
386
386
  build: z.ZodFunction<z.core.$ZodFunctionArgs, z.ZodCustom<SelectField, SelectField>>;
@@ -0,0 +1,18 @@
1
+ import * as z from 'zod';
2
+ /**
3
+ * Padding colour schema — shared by every field that resizes images, so the shape stays
4
+ * identical across them. Only meaningful alongside `fit: 'contain'`, where the image is padded
5
+ * out to the full box; fields fall back to `media.images.background` from the CMS config when
6
+ * it is omitted.
7
+ *
8
+ * `alpha: 0` only survives in formats with an alpha channel (webp, png). Encoding transparent
9
+ * padding to jpeg flattens it to black, so pick an opaque colour for jpeg output.
10
+ */
11
+ export declare const backgroundSchema: z.ZodObject<{
12
+ r: z.ZodNumber;
13
+ g: z.ZodNumber;
14
+ b: z.ZodNumber;
15
+ alpha: z.ZodNumber;
16
+ }, z.core.$strict>;
17
+ export type Background = z.infer<typeof backgroundSchema>;
18
+ //# sourceMappingURL=background.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../../../src/core/helpers/background.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAExB;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;;;;kBAO+C,CAAA;AAE5E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA"}
@@ -0,0 +1,18 @@
1
+ import * as z from 'zod';
2
+ /**
3
+ * Padding colour schema — shared by every field that resizes images, so the shape stays
4
+ * identical across them. Only meaningful alongside `fit: 'contain'`, where the image is padded
5
+ * out to the full box; fields fall back to `media.images.background` from the CMS config when
6
+ * it is omitted.
7
+ *
8
+ * `alpha: 0` only survives in formats with an alpha channel (webp, png). Encoding transparent
9
+ * padding to jpeg flattens it to black, so pick an opaque colour for jpeg output.
10
+ */
11
+ export const backgroundSchema = z
12
+ .strictObject({
13
+ r: z.number().min(0).max(255).describe('Red channel (0-255)'),
14
+ g: z.number().min(0).max(255).describe('Green channel (0-255)'),
15
+ b: z.number().min(0).max(255).describe('Blue channel (0-255)'),
16
+ alpha: z.number().min(0).max(1).describe('Alpha channel (0-1)'),
17
+ })
18
+ .describe('Background color for image padding (used with fit: contain)');
@@ -2,7 +2,11 @@ export { is } from './entity.js';
2
2
  export { entityKind } from './entity.js';
3
3
  export type { LZEntity } from './entity.js';
4
4
  export { hasOwnEntityKind } from './entity.js';
5
- export { evaluateConditionalRule, evaluateConditionalRuleGroup, normalizeConditionalRules } from './conditional-rules.js';
5
+ export { evaluateConditionalRule, evaluateConditionalRuleGroup, normalizeConditionalRules, } from './conditional-rules.js';
6
+ export { backgroundSchema } from './background.js';
7
+ export { inlineImageResizeOptions, DEFAULT_INLINE_IMAGE_SIZE, DEFAULT_MAX_FILE_SIZE, INLINE_IMAGE_REGEX, } from './inline-image.js';
8
+ export type { InlineImageSize, InlineImageMaxFileSize } from './inline-image.js';
9
+ export type { Background } from './background.js';
6
10
  export { watermarkPositionEnum, watermarkObjectSchema, watermarkConfigSchema, WATERMARK_DEFAULTS, resolveWatermark, } from './watermark.js';
7
11
  export type { WatermarkPosition, WatermarkObject, WatermarkConfig, ResolvedWatermark } from './watermark.js';
8
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/helpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAI9C,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AACzH,OAAO,EACH,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,GACnB,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/helpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAI9C,OAAO,EACH,uBAAuB,EACvB,4BAA4B,EAC5B,yBAAyB,GAC5B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EACH,wBAAwB,EACxB,yBAAyB,EACzB,qBAAqB,EACrB,kBAAkB,GACrB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAChF,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EACH,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,GACnB,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
@@ -4,5 +4,7 @@ export { hasOwnEntityKind } from './entity.js';
4
4
  // Note: the `and` / `or` authoring wrappers are re-exported from `core/fields` (alongside the field
5
5
  // factories that section configs use), so they are intentionally not re-exported here to avoid a
6
6
  // duplicate export when `core/index` merges both barrels.
7
- export { evaluateConditionalRule, evaluateConditionalRuleGroup, normalizeConditionalRules } from './conditional-rules.js';
7
+ export { evaluateConditionalRule, evaluateConditionalRuleGroup, normalizeConditionalRules, } from './conditional-rules.js';
8
+ export { backgroundSchema } from './background.js';
9
+ export { inlineImageResizeOptions, DEFAULT_INLINE_IMAGE_SIZE, DEFAULT_MAX_FILE_SIZE, INLINE_IMAGE_REGEX, } from './inline-image.js';
8
10
  export { watermarkPositionEnum, watermarkObjectSchema, watermarkConfigSchema, WATERMARK_DEFAULTS, resolveWatermark, } from './watermark.js';
@@ -0,0 +1,46 @@
1
+ import type sharp from 'sharp';
2
+ import type { Background } from './background.js';
3
+ /**
4
+ * Inline-image sizing shared by the rich-text field, the editor upload route, and the client-side
5
+ * validator.
6
+ *
7
+ * Deliberately a leaf module with type-only imports. The validators are environment-neutral —
8
+ * the admin form calls them in the browser, and a consumer may call them server-side — so reaching
9
+ * these through a barrel that pulls the database client, sharp, or fs would break the client build.
10
+ * Import this file directly rather than via `core/helpers/index.js` for the same reason.
11
+ */
12
+ export type InlineImageSize = {
13
+ width: number;
14
+ height: number;
15
+ fit: 'inside';
16
+ } | {
17
+ width: number;
18
+ height: number;
19
+ fit: 'cover';
20
+ } | {
21
+ width: number;
22
+ height: number;
23
+ fit: 'contain';
24
+ background?: Background;
25
+ };
26
+ export type InlineImageMaxFileSize = {
27
+ size: number;
28
+ unit: 'kb' | 'mb';
29
+ };
30
+ /**
31
+ * Bounds every inline image is fitted into unless the field overrides `size`.
32
+ * 1200x675 is 16:9 at a width that covers typical article body content, scaled down to fit so a
33
+ * body image is never cropped or padded unless the field asks for it.
34
+ */
35
+ export declare const DEFAULT_INLINE_IMAGE_SIZE: InlineImageSize;
36
+ /** Largest accepted inline image unless the field overrides `maxFileSize`. */
37
+ export declare const DEFAULT_MAX_FILE_SIZE: InlineImageMaxFileSize;
38
+ /** Same shape `extractInlineImages` looks for, so every side agrees on what an inline image is. */
39
+ export declare const INLINE_IMAGE_REGEX: RegExp;
40
+ /**
41
+ * Sharp resize options for an inline image, so every place that writes one — the field on save and
42
+ * the editor upload route — sizes it identically. `background` is the CMS-config fallback, used
43
+ * only when the field asks for `contain` without naming its own.
44
+ */
45
+ export declare function inlineImageResizeOptions(size: InlineImageSize, background: Background): sharp.ResizeOptions;
46
+ //# sourceMappingURL=inline-image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-image.d.ts","sourceRoot":"","sources":["../../../src/core/helpers/inline-image.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;;;;GAQG;AAEH,MAAM,MAAM,eAAe,GACrB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,QAAQ,CAAA;CAAE,GAChD;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,SAAS,CAAC;IAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CAAE,CAAA;AAEhF,MAAM,MAAM,sBAAsB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;CAAE,CAAA;AAExE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,eAIvC,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,EAAE,sBAAgD,CAAA;AAEpF,mGAAmG;AACnG,eAAO,MAAM,kBAAkB,QAAmD,CAAA;AAElF;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,GAAG,KAAK,CAAC,aAAa,CAU3G"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bounds every inline image is fitted into unless the field overrides `size`.
3
+ * 1200x675 is 16:9 at a width that covers typical article body content, scaled down to fit so a
4
+ * body image is never cropped or padded unless the field asks for it.
5
+ */
6
+ export const DEFAULT_INLINE_IMAGE_SIZE = {
7
+ width: 1200,
8
+ height: 675,
9
+ fit: 'inside',
10
+ };
11
+ /** Largest accepted inline image unless the field overrides `maxFileSize`. */
12
+ export const DEFAULT_MAX_FILE_SIZE = { size: 1, unit: 'mb' };
13
+ /** Same shape `extractInlineImages` looks for, so every side agrees on what an inline image is. */
14
+ export const INLINE_IMAGE_REGEX = /<img.*?src="(data:image\/.*?;base64,.*?)".*?>/g;
15
+ /**
16
+ * Sharp resize options for an inline image, so every place that writes one — the field on save and
17
+ * the editor upload route — sizes it identically. `background` is the CMS-config fallback, used
18
+ * only when the field asks for `contain` without naming its own.
19
+ */
20
+ export function inlineImageResizeOptions(size, background) {
21
+ return {
22
+ width: size.width,
23
+ height: size.height,
24
+ fit: size.fit,
25
+ // `inside` only ever scales down — enlarging a smaller image to fill the box would cost
26
+ // quality for nothing. `cover` and `contain` produce the exact box, so they may enlarge.
27
+ withoutEnlargement: size.fit === 'inside',
28
+ ...(size.fit === 'contain' ? { background: size.background ?? background } : {}),
29
+ };
30
+ }