@aduptive/strapi-blockscene 2.0.0-alpha.14 → 2.0.0-alpha.15

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.
package/dist/server.cjs CHANGED
@@ -4,6 +4,223 @@ var __commonJS = (cb, mod) => function __require() {
4
4
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5
5
  };
6
6
 
7
+ // server/text-template.js
8
+ var require_text_template = __commonJS({
9
+ "server/text-template.js"(exports2, module2) {
10
+ "use strict";
11
+ var CUSTOM_FIELD = "plugin::blockscene.text-template";
12
+ var TAGS = ["h1", "h2", "h3", "h4", "h5", "h6", "p"];
13
+ var ROLES = ["eyebrow", "title", "description", "body"];
14
+ var MODIFIERS = ["uppercase", "lowercase", "underline"];
15
+ var ID = /^[a-z][a-z0-9_-]{0,47}$/;
16
+ var TOKEN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
17
+ var MAX_ENTRIES = 24;
18
+ var MAX_TEXT = 5e4;
19
+ var BINDING = /^[a-z][a-z0-9_-]*(?:::[a-z][a-z0-9_-]*)?(?:\.[A-Za-z_][A-Za-z0-9_-]*)+$/;
20
+ var plain = (value) => value && typeof value === "object" && !Array.isArray(value);
21
+ var codePoints = (value) => [...String(value || "")].length;
22
+ var cleanLineBreaks = (value) => String(value || "").replace(/\r\n?/g, "\n");
23
+ function templateError(message) {
24
+ const error = new Error(message);
25
+ error.name = "ValidationError";
26
+ throw error;
27
+ }
28
+ function validateTemplate(value) {
29
+ if (!Array.isArray(value) || value.length > MAX_ENTRIES) templateError(`Text template must contain at most ${MAX_ENTRIES} entries`);
30
+ const ids = /* @__PURE__ */ new Set();
31
+ return value.map((raw, index) => {
32
+ if (!plain(raw)) templateError(`Text template entry ${index + 1} must be an object`);
33
+ const allowed = ["id", "label", "role", "tag", "token", "modifiers", "required", "allowBreaks", "softLines", "maxChars", "demo"];
34
+ for (const key of Object.keys(raw)) if (!allowed.includes(key)) templateError(`Unknown text template option "${key}"`);
35
+ if (typeof raw.id !== "string" || !ID.test(raw.id) || ids.has(raw.id)) templateError(`Invalid or duplicate text template id "${raw.id}"`);
36
+ ids.add(raw.id);
37
+ if (typeof raw.label !== "string" || !raw.label.trim() || raw.label.length > 80) templateError(`Invalid label for text template entry "${raw.id}"`);
38
+ if (!ROLES.includes(raw.role)) templateError(`Invalid role for text template entry "${raw.id}"`);
39
+ if (!TAGS.includes(raw.tag)) templateError(`Invalid Tag HTML for text template entry "${raw.id}"`);
40
+ if (raw.token !== void 0 && (typeof raw.token !== "string" || raw.token && !TOKEN.test(raw.token))) templateError(`Invalid style token for text template entry "${raw.id}"`);
41
+ if (!Array.isArray(raw.modifiers) || raw.modifiers.some((item) => !MODIFIERS.includes(item)) || new Set(raw.modifiers).size !== raw.modifiers.length || raw.modifiers.includes("uppercase") && raw.modifiers.includes("lowercase")) templateError(`Invalid modifiers for text template entry "${raw.id}"`);
42
+ if (typeof raw.required !== "boolean" || typeof raw.allowBreaks !== "boolean") templateError(`Invalid rules for text template entry "${raw.id}"`);
43
+ if (raw.softLines !== void 0 && (!Number.isInteger(raw.softLines) || raw.softLines < 1 || raw.softLines > 20)) templateError(`Invalid line guidance for text template entry "${raw.id}"`);
44
+ if (raw.maxChars !== void 0 && (!Number.isInteger(raw.maxChars) || raw.maxChars < 1 || raw.maxChars > MAX_TEXT)) templateError(`Invalid character limit for text template entry "${raw.id}"`);
45
+ if (typeof raw.demo !== "string" || codePoints(raw.demo) > MAX_TEXT) templateError(`Invalid demo text for text template entry "${raw.id}"`);
46
+ if (!raw.allowBreaks && /[\r\n]/.test(raw.demo)) templateError(`Demo text for "${raw.id}" cannot contain line breaks`);
47
+ if (raw.maxChars && codePoints(raw.demo) > raw.maxChars) templateError(`Demo text for "${raw.id}" exceeds its character limit`);
48
+ return {
49
+ id: raw.id,
50
+ label: raw.label.trim(),
51
+ role: raw.role,
52
+ tag: raw.tag,
53
+ token: raw.token || "",
54
+ modifiers: [...raw.modifiers],
55
+ required: raw.required,
56
+ allowBreaks: raw.allowBreaks,
57
+ ...raw.softLines && { softLines: raw.softLines },
58
+ ...raw.maxChars && { maxChars: raw.maxChars },
59
+ demo: cleanLineBreaks(raw.demo)
60
+ };
61
+ });
62
+ }
63
+ function normalizeValue(value) {
64
+ if (value == null) return { version: 1, values: {} };
65
+ if (!plain(value) || value.version !== 1 || !plain(value.values)) templateError("Text template value must be { version: 1, values: { ... } }");
66
+ if (Object.keys(value.values).length > 100) templateError("Text template value has too many entries");
67
+ const values = {};
68
+ for (const [id, text] of Object.entries(value.values)) {
69
+ if (!ID.test(id) || typeof text !== "string" || codePoints(text) > MAX_TEXT) templateError(`Invalid text template value for "${id}"`);
70
+ values[id] = cleanLineBreaks(text);
71
+ }
72
+ return { version: 1, values };
73
+ }
74
+ function validateValue(template, value) {
75
+ const entries = validateTemplate(template);
76
+ const normalized = normalizeValue(value);
77
+ const errors = [];
78
+ for (const entry of entries) {
79
+ const text = normalized.values[entry.id] || "";
80
+ if (entry.required && !text.trim()) errors.push({ id: entry.id, code: "required", message: `${entry.label} is required` });
81
+ if (!entry.allowBreaks && /\n/.test(text)) errors.push({ id: entry.id, code: "lineBreak", message: `${entry.label} does not allow line breaks` });
82
+ if (entry.maxChars && codePoints(text) > entry.maxChars) errors.push({ id: entry.id, code: "maxChars", message: `${entry.label} exceeds ${entry.maxChars} characters` });
83
+ }
84
+ return { value: normalized, errors };
85
+ }
86
+ function demoValue(template) {
87
+ return { version: 1, values: Object.fromEntries(validateTemplate(template).map((entry) => [entry.id, entry.demo])) };
88
+ }
89
+ function renderTextTemplate(template, value) {
90
+ const entries = validateTemplate(template);
91
+ const normalized = normalizeValue(value);
92
+ return entries.flatMap((entry) => {
93
+ const text = normalized.values[entry.id] || "";
94
+ if (!text.trim()) return [];
95
+ return [{ id: entry.id, tag: entry.tag, text, lines: text.split("\n"), className: textTemplateClassName(entry) }];
96
+ });
97
+ }
98
+ function textTemplateClassName(entry) {
99
+ if (!plain(entry) || entry.token && !TOKEN.test(entry.token) || !Array.isArray(entry.modifiers) || entry.modifiers.some((item) => !MODIFIERS.includes(item))) return "";
100
+ return [entry.token, ...entry.modifiers].filter(Boolean).join(" ");
101
+ }
102
+ function renderTextTemplateElement(part, createElement) {
103
+ if (!plain(part) || !TAGS.includes(part.tag) || typeof part.text !== "string" || typeof createElement !== "function") templateError("Invalid projected text template part");
104
+ const children = part.text.split("\n").flatMap((line, index) => index ? [createElement("br", { key: `${part.id}-break-${index}` }), line] : [line]);
105
+ return createElement(part.tag, { key: part.id, className: part.className || void 0 }, ...children);
106
+ }
107
+ function binding(uid, name) {
108
+ return `${uid}.${name}`;
109
+ }
110
+ function isTextTemplateBinding(value) {
111
+ return typeof value === "string" && BINDING.test(value);
112
+ }
113
+ function resolveBinding(attribute, contentTypeUID, name) {
114
+ const explicit = attribute?.options?.blockscene?.textTemplate || attribute?.pluginOptions?.blockscene?.textTemplate;
115
+ if (isTextTemplateBinding(explicit)) return explicit;
116
+ return typeof contentTypeUID === "string" && contentTypeUID ? binding(contentTypeUID, String(name || "").split(".").pop()) : "";
117
+ }
118
+ function createEntryId(entries, values, nonce) {
119
+ const used = /* @__PURE__ */ new Set([...entries.map((entry) => entry.id), ...Object.keys(values || {})]);
120
+ const safe = String(nonce || "").toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 32);
121
+ const base = `text-${safe || Date.now().toString(36)}`;
122
+ let id = base, suffix = 2;
123
+ while (used.has(id)) id = `${base}-${suffix++}`;
124
+ return id;
125
+ }
126
+ function applyTextChange(entry, current, next, { composing = false, compositionStart = current } = {}) {
127
+ const value = entry.allowBreaks ? cleanLineBreaks(next) : String(next || "").replace(/[\r\n]+/g, " ");
128
+ const previous = String(compositionStart ?? current ?? "");
129
+ const exceeds = entry.maxChars && codePoints(value) > entry.maxChars;
130
+ const recoverable = codePoints(previous) > entry.maxChars && codePoints(value) <= codePoints(previous);
131
+ if (exceeds && !composing && !recoverable) return { accepted: false, value: previous, code: "maxChars" };
132
+ return { accepted: true, value };
133
+ }
134
+ function mergePartialDocument2(stored, patch) {
135
+ return { ...plain(stored) ? stored : {}, ...plain(patch) ? patch : {} };
136
+ }
137
+ function draftReadPlan2(localized, { documentId, locale } = {}, populate = {}, defaultLocale) {
138
+ const common = { status: "draft", ...Object.keys(populate).length && { populate } };
139
+ if (localized && locale === "*") return { many: true, query: { filters: { documentId }, locale: "*", ...common } };
140
+ return { many: false, query: { documentId, ...localized && { locale: locale || defaultLocale }, ...common } };
141
+ }
142
+ async function validateBulkUpdates2(query, where, populate, patch, validate) {
143
+ const entries = await query.findMany({ where: where || {}, ...Object.keys(populate || {}).length && { populate } });
144
+ for (const entry of entries) validate(mergePartialDocument2(entry, patch));
145
+ return entries.length;
146
+ }
147
+ function isTextTemplate(attr) {
148
+ return attr?.customField === CUSTOM_FIELD;
149
+ }
150
+ function textTemplateBindings(schemas2) {
151
+ const bindings = /* @__PURE__ */ new Set();
152
+ for (const [uid, schema] of Object.entries(schemas2 || {})) {
153
+ for (const [name, attribute] of Object.entries(schema?.attributes || {})) {
154
+ if (!isTextTemplate(attribute)) continue;
155
+ const key = resolveBinding(attribute, uid, name);
156
+ if (key) bindings.add(key);
157
+ }
158
+ }
159
+ return bindings;
160
+ }
161
+ function validateDocument2(schemas2, uid, data, templates, path = []) {
162
+ if (!plain(data)) return [];
163
+ const schema = schemas2?.[uid];
164
+ if (!schema) return [];
165
+ const errors = [];
166
+ for (const [name, attr] of Object.entries(schema.attributes || {})) {
167
+ const here = [...path, name];
168
+ if (isTextTemplate(attr)) {
169
+ const template = templates?.[resolveBinding(attr, uid, name)];
170
+ if (!template) continue;
171
+ let result;
172
+ try {
173
+ result = validateValue(template, data[name]);
174
+ } catch (error) {
175
+ errors.push({ path: here, code: "invalid", message: error.message });
176
+ continue;
177
+ }
178
+ errors.push(...result.errors.map((error) => ({ ...error, path: [...here, error.id] })));
179
+ } else if (!(name in data)) continue;
180
+ else if (attr?.type === "component") {
181
+ const list = attr.repeatable ? data[name] : [data[name]];
182
+ if (Array.isArray(list)) list.forEach((item, index) => errors.push(...validateDocument2(schemas2, attr.component, item, templates, attr.repeatable ? [...here, index] : here)));
183
+ } else if (attr?.type === "dynamiczone" && Array.isArray(data[name])) {
184
+ data[name].forEach((item, index) => errors.push(...validateDocument2(schemas2, item?.__component, item, templates, [...here, index])));
185
+ }
186
+ }
187
+ return errors;
188
+ }
189
+ function textTemplatePopulate2(schemas2, uid, templates) {
190
+ const active = new Set(Object.keys(templates || {}));
191
+ const walk = (schemaUid, seen = /* @__PURE__ */ new Set()) => {
192
+ if (seen.has(schemaUid)) return { configured: false, populate: {} };
193
+ const next = new Set(seen).add(schemaUid);
194
+ let configured = false;
195
+ const populate = {};
196
+ for (const [name, attr] of Object.entries(schemas2?.[schemaUid]?.attributes || {})) {
197
+ if (isTextTemplate(attr) && active.has(resolveBinding(attr, schemaUid, name))) configured = true;
198
+ else if (attr?.type === "component") {
199
+ const child = walk(attr.component, next);
200
+ if (child.configured) {
201
+ populate[name] = Object.keys(child.populate).length ? { populate: child.populate } : true;
202
+ configured = true;
203
+ }
204
+ } else if (attr?.type === "dynamiczone") {
205
+ const children = (attr.components || []).map((component) => [component, walk(component, next)]).filter(([, child]) => child.configured);
206
+ if (children.length) {
207
+ populate[name] = { on: Object.fromEntries(children.map(([component, child]) => [
208
+ component,
209
+ Object.keys(child.populate).length ? { populate: child.populate } : {}
210
+ ])) };
211
+ configured = true;
212
+ }
213
+ }
214
+ }
215
+ return { configured, populate };
216
+ };
217
+ const result = walk(uid);
218
+ return result.configured ? result.populate : null;
219
+ }
220
+ module2.exports = { CUSTOM_FIELD, TAGS, ROLES, MODIFIERS, MAX_ENTRIES, codePoints, validateTemplate, normalizeValue, validateValue, demoValue, renderTextTemplate, renderTextTemplateElement, textTemplateClassName, binding, isTextTemplateBinding, resolveBinding, createEntryId, applyTextChange, mergePartialDocument: mergePartialDocument2, draftReadPlan: draftReadPlan2, validateBulkUpdates: validateBulkUpdates2, isTextTemplate, textTemplateBindings, validateDocument: validateDocument2, textTemplatePopulate: textTemplatePopulate2 };
221
+ }
222
+ });
223
+
7
224
  // server/groups.js
8
225
  var require_groups = __commonJS({
9
226
  "server/groups.js"(exports2, module2) {
@@ -163,6 +380,7 @@ var require_groups = __commonJS({
163
380
  var require_settings = __commonJS({
164
381
  "server/settings.js"(exports2, module2) {
165
382
  "use strict";
383
+ var { validateTemplate, isTextTemplateBinding, textTemplateBindings } = require_text_template();
166
384
  var PLUGIN2 = "blockscene";
167
385
  var TEMPLATES2 = ["generic", "banner", "cards", "imageText", "faq", "none"];
168
386
  var INITIAL_STATES = ["closed", "open", "remember"];
@@ -203,6 +421,8 @@ var require_settings = __commonJS({
203
421
  // Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
204
422
  // previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
205
423
  contentTypes: {},
424
+ // Developer-owned templates for plugin::blockscene.text-template fields, keyed by "schema.uid.attribute".
425
+ textTemplates: {},
206
426
  // Version history (Strapi 5, see README "Version history"): off until turned on. contentTypes: 'all' (every api:: type)
207
427
  // or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays; deleted
208
428
  // documents stay in the trash trashDays (fixed per document when it is deleted).
@@ -270,7 +490,7 @@ var require_settings = __commonJS({
270
490
  const name = `${uid.split(".").pop()} ${displayName}`.toLowerCase();
271
491
  return GUESSES.find(([, pattern]) => pattern.test(name))?.[0] || "text";
272
492
  }
273
- function facetsOf(uid, schema, schemas = {}) {
493
+ function facetsOf(uid, schema, schemas2 = {}) {
274
494
  const facets = /* @__PURE__ */ new Set();
275
495
  const read = (attributes, nested) => {
276
496
  for (const attr of Object.values(attributes || {})) {
@@ -283,7 +503,7 @@ var require_settings = __commonJS({
283
503
  else if (attr?.type === "relation") facets.add("dynamic");
284
504
  else if (attr?.type === "component") {
285
505
  if (attr.repeatable) facets.add("list");
286
- if (!nested) read(schemas[attr.component]?.attributes, true);
506
+ if (!nested) read(schemas2[attr.component]?.attributes, true);
287
507
  }
288
508
  }
289
509
  };
@@ -340,7 +560,7 @@ var require_settings = __commonJS({
340
560
  groups: safeGroups2(config.groups, config.componentUids)
341
561
  };
342
562
  }
343
- function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
563
+ function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas2 = null) {
344
564
  const types = typeMap(contentTypeUids2);
345
565
  if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
346
566
  if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
@@ -380,7 +600,7 @@ var require_settings = __commonJS({
380
600
  if (typeof entry.showRowThumbnail !== "boolean") fail2(`Invalid row thumbnail setting for "${uid}"`);
381
601
  clean.showRowThumbnail = entry.showRowThumbnail;
382
602
  } else if (key === "layout") {
383
- if (!validLayout(entry.layout, schemas && (schemas[uid]?.attributes || {}))) fail2(`Invalid layout for "${uid}": columnsField (and mobileColumnsField) must name a number, string or numeric enumeration attribute of the component; maxColumns 1 to 12`);
603
+ if (!validLayout(entry.layout, schemas2 && (schemas2[uid]?.attributes || {}))) fail2(`Invalid layout for "${uid}": columnsField (and mobileColumnsField) must name a number, string or numeric enumeration attribute of the component; maxColumns 1 to 12`);
384
604
  clean.layout = structuredClone(entry.layout);
385
605
  } else fail2(`Unknown component setting "${key}"`);
386
606
  }
@@ -397,6 +617,17 @@ var require_settings = __commonJS({
397
617
  }
398
618
  if (Object.keys(entry).length) out.contentTypes[uid] = { ...structuredClone(entry), ...entry.previewDevices && { previewDevices: cleanDevices(entry.previewDevices) } };
399
619
  }
620
+ const textTemplates = input.textTemplates || {};
621
+ if (!plain(textTemplates) || Object.keys(textTemplates).length > 200) fail2("textTemplates must be an object with at most 200 field bindings");
622
+ const bindings = schemas2 ? textTemplateBindings(schemas2) : null;
623
+ for (const [key, entries] of Object.entries(textTemplates)) {
624
+ if (!isTextTemplateBinding(key) || bindings && !bindings.has(key)) fail2(`Unknown text template field "${key}"`);
625
+ try {
626
+ out.textTemplates[key] = validateTemplate(entries);
627
+ } catch (error) {
628
+ fail2(`${key}: ${error.message}`);
629
+ }
630
+ }
400
631
  const history = input.history || {};
401
632
  if (typeof history !== "object" || Array.isArray(history)) fail2("history must be an object");
402
633
  for (const [key, value] of Object.entries(history)) {
@@ -407,8 +638,9 @@ var require_settings = __commonJS({
407
638
  if (out.history.eventDays < out.history.retentionDays) fail2("history.eventDays must be at least history.retentionDays");
408
639
  return out;
409
640
  }
410
- function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
641
+ function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas2 = null) {
411
642
  const types = typeMap(contentTypeUids2);
643
+ const templateBindings = schemas2 ? textTemplateBindings(schemas2) : null;
412
644
  const out = structuredClone(DEFAULTS2);
413
645
  if (!saved || typeof saved !== "object") return out;
414
646
  for (const key of Object.keys(DEFAULTS2.palette)) if (COLOR.test(String(saved.palette?.[key]))) out.palette[key] = saved.palette[key];
@@ -438,7 +670,7 @@ var require_settings = __commonJS({
438
670
  if (!clean.tags.length) delete clean.tags;
439
671
  }
440
672
  if ("showRowThumbnail" in clean && typeof clean.showRowThumbnail !== "boolean") delete clean.showRowThumbnail;
441
- if ("layout" in clean && !validLayout(clean.layout, schemas && (schemas[uid]?.attributes || {}))) delete clean.layout;
673
+ if ("layout" in clean && !validLayout(clean.layout, schemas2 && (schemas2[uid]?.attributes || {}))) delete clean.layout;
442
674
  if (Object.keys(clean).length) out.components[uid] = clean;
443
675
  }
444
676
  for (const [uid, entry] of Object.entries(saved.contentTypes || {})) {
@@ -447,6 +679,14 @@ var require_settings = __commonJS({
447
679
  const clean = Object.fromEntries(Object.entries(entry).map(([key, value]) => [key, fix(key, value)]).filter(([key, value]) => TYPE_KEYS[key]?.(value, types[uid])));
448
680
  if (Object.keys(clean).length) out.contentTypes[uid] = clean;
449
681
  }
682
+ for (const [key, entries] of Object.entries(saved.textTemplates || {})) {
683
+ if (!isTextTemplateBinding(key) || templateBindings && !templateBindings.has(key)) continue;
684
+ try {
685
+ out.textTemplates[key] = validateTemplate(entries);
686
+ } catch (error) {
687
+ if (error?.name !== "ValidationError") throw error;
688
+ }
689
+ }
450
690
  for (const key of Object.keys(HISTORY_KEYS)) {
451
691
  let value = saved.history?.[key];
452
692
  if (key === "contentTypes" && Array.isArray(value)) value = value.filter((uid) => HISTORY_KEYS.contentTypes([uid], historyTypes));
@@ -455,7 +695,7 @@ var require_settings = __commonJS({
455
695
  out.history.eventDays = Math.max(out.history.eventDays, out.history.retentionDays);
456
696
  return out;
457
697
  }
458
- var ENTRY_KEYS = ["components", "contentTypes"];
698
+ var ENTRY_KEYS = ["components", "contentTypes", "textTemplates"];
459
699
  var UNSET_WHEN_EMPTY = ["previewUrl", "blockPreviewUrl"];
460
700
  var withoutEmpty = (editor) => Object.fromEntries(Object.entries(editor).filter(([key, value]) => !(UNSET_WHEN_EMPTY.includes(key) && value === "")));
461
701
  function layer2(base, saved) {
@@ -478,7 +718,7 @@ var require_settings = __commonJS({
478
718
  var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
479
719
  var plain = (value) => value && typeof value === "object" && !Array.isArray(value);
480
720
  var fieldText = (value, max) => typeof value === "string" ? value.trim() !== "" && value.length <= max : plain(value) && Object.keys(value).length > 0 && Object.keys(value).length <= 30 && Object.entries(value).every(([locale, text2]) => LOCALE.test(locale) && fieldText(text2, max));
481
- function validateFields2(input, schemas = {}, stale = []) {
721
+ function validateFields2(input, schemas2 = {}, stale = []) {
482
722
  if (input === null || input === void 0) return {};
483
723
  if (!plain(input)) fail2("fields must be an object");
484
724
  if (JSON.stringify(input).length > 256 * 1024) fail2("fields config too large");
@@ -491,7 +731,7 @@ var require_settings = __commonJS({
491
731
  if (!(key in FIELD_TEXT)) fail2(`Unknown field setting "${key}" of "${uid}.${name}"`);
492
732
  if (!fieldText(value, FIELD_TEXT[key])) fail2(`Invalid ${key} of "${uid}.${name}": a string of 1 to ${FIELD_TEXT[key]} characters, or { "<locale>": string }`);
493
733
  }
494
- if (!schemas[uid]?.includes(name)) {
734
+ if (!schemas2[uid]?.includes(name)) {
495
735
  stale.push(`${uid}.${name}`);
496
736
  continue;
497
737
  }
@@ -506,7 +746,7 @@ var require_settings = __commonJS({
506
746
  var localize = (holder, key, source, locale, text2) => {
507
747
  holder[key] = { ...plain(holder[key]) ? holder[key] : { en: holder[key] ?? source }, [locale]: text2 };
508
748
  };
509
- function schemaMetadata2(schemas = {}, translations = null, notes = []) {
749
+ function schemaMetadata2(schemas2 = {}, translations = null, notes = []) {
510
750
  const blocks = {}, fields = {};
511
751
  const own = (holder, where) => {
512
752
  const value = holder?.pluginOptions?.blockscene;
@@ -514,7 +754,7 @@ var require_settings = __commonJS({
514
754
  notes.push(`${where}: pluginOptions.blockscene must be an object`);
515
755
  return {};
516
756
  };
517
- for (const [uid, schema] of Object.entries(schemas)) {
757
+ for (const [uid, schema] of Object.entries(schemas2)) {
518
758
  if (!uid.includes("::")) {
519
759
  const block = {};
520
760
  for (const [key, value] of Object.entries(own(schema, uid))) {
@@ -546,10 +786,10 @@ var require_settings = __commonJS({
546
786
  continue;
547
787
  }
548
788
  for (const [key, text2] of Object.entries(entries)) {
549
- const uid = Object.keys(schemas).find((uid2) => key === uid2 || key.startsWith(`${uid2}.`));
789
+ const uid = Object.keys(schemas2).find((uid2) => key === uid2 || key.startsWith(`${uid2}.`));
550
790
  const [name, part = "label", extra] = uid ? key.slice(uid.length + 1).split(".") : [];
551
791
  const block = uid && !uid.includes("::");
552
- const target = block && key === uid ? [blocks[uid] ||= {}, "label", schemas[uid]?.info?.displayName || uid, 80] : block && key === `${uid}.description` ? [blocks[uid] ||= {}, "description", null, 300] : uid && Object.hasOwn(schemas[uid]?.attributes || {}, name) && Object.hasOwn(FIELD_TEXT, part) && extra === void 0 ? [null, part, null, FIELD_TEXT[part]] : null;
792
+ const target = block && key === uid ? [blocks[uid] ||= {}, "label", schemas2[uid]?.info?.displayName || uid, 80] : block && key === `${uid}.description` ? [blocks[uid] ||= {}, "description", null, 300] : uid && Object.hasOwn(schemas2[uid]?.attributes || {}, name) && Object.hasOwn(FIELD_TEXT, part) && extra === void 0 ? [null, part, null, FIELD_TEXT[part]] : null;
553
793
  if (!target) {
554
794
  stale.push(`${locale} "${key}"`);
555
795
  continue;
@@ -588,7 +828,7 @@ var require_settings = __commonJS({
588
828
  blocks: Array.isArray,
589
829
  enumeration: (value, attr) => (attr.enum || []).includes(value)
590
830
  };
591
- function variantValues(values, schema, schemas, path, skipped) {
831
+ function variantValues(values, schema, schemas2, path, skipped) {
592
832
  if (!plain(values)) throw new Error(`${path} must be an object`);
593
833
  const out = {};
594
834
  for (const [name, value] of Object.entries(values)) {
@@ -600,22 +840,22 @@ var require_settings = __commonJS({
600
840
  continue;
601
841
  }
602
842
  if (attr.type === "component") {
603
- const nested = schemas[attr.component], max = Math.min(attr.max ?? Infinity, VARIANT.items);
843
+ const nested = schemas2[attr.component], max = Math.min(attr.max ?? Infinity, VARIANT.items);
604
844
  if (!nested) throw new Error(`${at}: unknown component "${attr.component}"`);
605
- if (!attr.repeatable) out[name] = value === null ? null : variantValues(value, nested, schemas, at, skipped);
845
+ if (!attr.repeatable) out[name] = value === null ? null : variantValues(value, nested, schemas2, at, skipped);
606
846
  else if (!Array.isArray(value) || value.length > max) throw new Error(`${at} must be a list of at most ${max} items`);
607
- else out[name] = value.map((item, index) => variantValues(item, nested, schemas, `${at}[${index}]`, skipped));
847
+ else out[name] = value.map((item, index) => variantValues(item, nested, schemas2, `${at}[${index}]`, skipped));
608
848
  } else if (value === null || SCALARS[attr.type]?.(value, attr)) out[name] = structuredClone(value);
609
849
  else throw new Error(`${at}: ${SCALARS[attr.type] ? `not a valid ${attr.type}` : `${attr.type} values are not supported`}`);
610
850
  }
611
851
  return out;
612
852
  }
613
- function validateVariants2(config, schemas = {}, notes = []) {
853
+ function validateVariants2(config, schemas2 = {}, notes = []) {
614
854
  const out = {};
615
855
  let total = 0;
616
856
  for (const [uid, entry] of Object.entries(plain(config) ? config : {})) {
617
857
  if (!plain(entry) || entry.variants === void 0) continue;
618
- if (!schemas[uid]) {
858
+ if (!schemas2[uid]) {
619
859
  notes.push(`${uid}: unknown component`);
620
860
  continue;
621
861
  }
@@ -631,7 +871,7 @@ var require_settings = __commonJS({
631
871
  if (!text(variant.id) || !VARIANT.id.test(variant.id) || ids.has(variant.id)) throw new Error('id must be 1 to 40 lowercase letters, digits, "-" or "_", unique per component');
632
872
  if (!fieldText(variant.label, 60)) throw new Error('label must be a string of 1 to 60 characters, or { "<locale>": string }');
633
873
  const skipped = [];
634
- const values = variantValues(variant.values ?? {}, schemas[uid], schemas, "values", skipped);
874
+ const values = variantValues(variant.values ?? {}, schemas2[uid], schemas2, "values", skipped);
635
875
  const size = JSON.stringify(values).length;
636
876
  if (size > VARIANT.bytes) throw new Error(`values over ${VARIANT.bytes / 1024} KB`);
637
877
  if (total + size > VARIANT.total) throw new Error(`all variants together over ${VARIANT.total / 1024} KB`);
@@ -646,7 +886,7 @@ var require_settings = __commonJS({
646
886
  }
647
887
  return out;
648
888
  }
649
- function requiredProblems(schema, values, schemas, path, media, problems) {
889
+ function requiredProblems(schema, values, schemas2, path, media, problems) {
650
890
  for (const [field, attr] of Object.entries(schema?.attributes || {})) {
651
891
  const at = `${path}.${field}`;
652
892
  const supplied = Object.hasOwn(values, field);
@@ -656,7 +896,7 @@ var require_settings = __commonJS({
656
896
  continue;
657
897
  }
658
898
  if (attr.type === "component") {
659
- const nested = schemas[attr.component];
899
+ const nested = schemas2[attr.component];
660
900
  if (!nested) {
661
901
  problems.push(`${at}: unknown component "${attr.component}"`);
662
902
  continue;
@@ -665,16 +905,16 @@ var require_settings = __commonJS({
665
905
  const minimum = Math.max(attr.min ?? 0, attr.required ? 1 : 0);
666
906
  const items = supplied ? value : Array.from({ length: minimum }, () => ({}));
667
907
  if (!Array.isArray(items) || items.length < minimum) problems.push(`${at} requires at least ${minimum} item${minimum === 1 ? "" : "s"}`);
668
- else items.forEach((item, index) => requiredProblems(nested, item, schemas, `${at}[${index}]`, media, problems));
908
+ else items.forEach((item, index) => requiredProblems(nested, item, schemas2, `${at}[${index}]`, media, problems));
669
909
  } else if (supplied && value === null) {
670
910
  if (attr.required) problems.push(`${at} is required`);
671
- } else if (supplied || attr.required) requiredProblems(nested, supplied ? value : {}, schemas, at, media, problems);
911
+ } else if (supplied || attr.required) requiredProblems(nested, supplied ? value : {}, schemas2, at, media, problems);
672
912
  continue;
673
913
  }
674
914
  if (attr.required && (value === void 0 || value === null || value === "")) problems.push(`${at} is required`);
675
915
  }
676
916
  }
677
- function validateKits2(input, contentTypes = {}, schemas = {}, groups = null, notes = []) {
917
+ function validateKits2(input, contentTypes = {}, schemas2 = {}, groups = null, notes = []) {
678
918
  if (input === null || input === void 0) return {};
679
919
  if (!plain(input) || JSON.stringify(input).length > 256 * 1024) {
680
920
  notes.push("expected an object up to 256 KB");
@@ -710,18 +950,18 @@ var require_settings = __commonJS({
710
950
  const row = rows[index];
711
951
  if (!plain(row) || !text(row.__component)) throw new Error(`${zoneName}[${index}] must have __component`);
712
952
  const component = row.__component;
713
- if (!zone.components?.includes(component) || !schemas[component]) throw new Error(`${zoneName}[${index}]: component "${component}" is not allowed`);
953
+ if (!zone.components?.includes(component) || !schemas2[component]) throw new Error(`${zoneName}[${index}]: component "${component}" is not allowed`);
714
954
  const skipped = [];
715
- const values = variantValues(row, schemas[component], schemas, `${zoneName}[${index}]`, skipped);
955
+ const values = variantValues(row, schemas2[component], schemas2, `${zoneName}[${index}]`, skipped);
716
956
  if (skipped.length) notes.push(`${name}: media and relation values are not inserted (${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ", ..." : ""})`);
717
957
  const media = [], required = [];
718
- requiredProblems(schemas[component], values, schemas, `${zoneName}[${index}]`, media, required);
958
+ requiredProblems(schemas2[component], values, schemas2, `${zoneName}[${index}]`, media, required);
719
959
  if (required.length) throw new Error(required.slice(0, 5).join("; "));
720
960
  if (media.length) notes.push(`${name}: required media or relations must be completed before publishing (${media.slice(0, 5).join(", ")}${media.length > 5 ? ", ..." : ""})`);
721
961
  clean.push({ __component: component, values });
722
962
  const close = groups?.[component];
723
963
  if (close) {
724
- if (!zone.components?.includes(close) || !schemas[close]) throw new Error(`${zoneName}[${index}]: close component "${close}" is not allowed`);
964
+ if (!zone.components?.includes(close) || !schemas2[close]) throw new Error(`${zoneName}[${index}]: close component "${close}" is not allowed`);
725
965
  clean.push({ __component: close, values: {} });
726
966
  }
727
967
  }
@@ -1838,6 +2078,7 @@ var { safeGroups, validateGroups, invalidComponents, layoutFields } = require_gr
1838
2078
  var { hiddenName, injectHidden, hideFromBuilder, registerHiddenStrip } = require_hidden();
1839
2079
  var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
1840
2080
  var { trashContentType, trashService, trashController } = require_trash();
2081
+ var { validateDocument, textTemplatePopulate, mergePartialDocument, draftReadPlan, validateBulkUpdates } = require_text_template();
1841
2082
  var STRAPI5 = true;
1842
2083
  var withHidden = [];
1843
2084
  var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
@@ -1853,6 +2094,7 @@ async function readSettings(strapi) {
1853
2094
  return legacy;
1854
2095
  }
1855
2096
  var componentUids = (strapi) => Object.keys(strapi.components || {});
2097
+ var schemas = (strapi) => ({ ...strapi.contentTypes || {}, ...strapi.components || {} });
1856
2098
  var findMedia = (strapi, id) => strapi.db.query("plugin::upload.file").findOne({ where: { id }, select: ["id", "url", "mime", "name"] });
1857
2099
  async function resolveMedia(strapi, settings) {
1858
2100
  const resolved = {};
@@ -1968,6 +2210,63 @@ function registerZoneGuards(strapi) {
1968
2210
  }
1969
2211
  });
1970
2212
  }
2213
+ function registerTextTemplateGuard(strapi) {
2214
+ const check = (uid, data, templates) => {
2215
+ const errors = validateDocument(schemas(strapi), uid, data, templates);
2216
+ if (errors.length) fail("Text template content is invalid.", { plugin: PLUGIN, errors: errors.map((error) => ({ ...error, name: "ValidationError" })) });
2217
+ };
2218
+ if (strapi.documents?.use) {
2219
+ const defaultLocale = async () => {
2220
+ try {
2221
+ return await strapi.plugin("i18n")?.service("locales")?.getDefaultLocale?.();
2222
+ } catch {
2223
+ return void 0;
2224
+ }
2225
+ };
2226
+ strapi.documents.use(async (ctx, next) => {
2227
+ const { action, params = {} } = ctx;
2228
+ if (!ctx.uid || !["create", "update", "publish"].includes(action)) return next();
2229
+ const templates = (await strapi.plugin(PLUGIN).service("settings").get()).textTemplates;
2230
+ const populate = textTemplatePopulate(schemas(strapi), ctx.uid, templates);
2231
+ if (!populate) return next();
2232
+ if (action === "create") {
2233
+ check(ctx.uid, params.data || {}, templates);
2234
+ return next();
2235
+ }
2236
+ const localized = Boolean(strapi.getModel(ctx.uid)?.pluginOptions?.i18n?.localized);
2237
+ const plan = draftReadPlan(localized, params, populate, await defaultLocale());
2238
+ const stored = plan.many ? await strapi.documents(ctx.uid).findMany(plan.query) : [await strapi.documents(ctx.uid).findOne(plan.query)];
2239
+ for (const entry of stored) check(ctx.uid, action === "update" ? mergePartialDocument(entry, params.data) : entry || {}, templates);
2240
+ return next();
2241
+ });
2242
+ return;
2243
+ }
2244
+ strapi.db.lifecycles.subscribe({
2245
+ beforeCreate: async (event) => {
2246
+ const templates = (await strapi.plugin(PLUGIN).service("settings").get()).textTemplates;
2247
+ check(event.model.uid, event.params?.data || {}, templates);
2248
+ },
2249
+ beforeUpdate: async (event) => {
2250
+ const templates = (await strapi.plugin(PLUGIN).service("settings").get()).textTemplates;
2251
+ const populate = textTemplatePopulate(schemas(strapi), event.model.uid, templates);
2252
+ if (!populate) return;
2253
+ const stored = await strapi.db.query(event.model.uid).findOne({ where: event.params?.where || {}, ...Object.keys(populate).length && { populate } });
2254
+ check(event.model.uid, mergePartialDocument(stored, event.params?.data), templates);
2255
+ },
2256
+ beforeUpdateMany: async (event) => {
2257
+ const templates = (await strapi.plugin(PLUGIN).service("settings").get()).textTemplates;
2258
+ const populate = textTemplatePopulate(schemas(strapi), event.model.uid, templates);
2259
+ if (!populate) return;
2260
+ await validateBulkUpdates(
2261
+ strapi.db.query(event.model.uid),
2262
+ event.params?.where,
2263
+ populate,
2264
+ event.params?.data,
2265
+ (entry) => check(event.model.uid, entry, templates)
2266
+ );
2267
+ }
2268
+ });
2269
+ }
1971
2270
  module.exports = {
1972
2271
  config: {
1973
2272
  default: {
@@ -2000,6 +2299,7 @@ module.exports = {
2000
2299
  },
2001
2300
  // Before the database schema sync, so the column exists. A name the host already uses with another type is skipped.
2002
2301
  register({ strapi }) {
2302
+ strapi.customFields.register({ name: "text-template", plugin: PLUGIN, type: "json", inputSize: { default: 12, isResizable: true } });
2003
2303
  const plugin = strapi.plugin(PLUGIN);
2004
2304
  const name = hiddenName(plugin.config("hiddenAttribute"));
2005
2305
  withHidden = [];
@@ -2027,6 +2327,7 @@ module.exports = {
2027
2327
  strapi.plugin(PLUGIN).service("settings").variants();
2028
2328
  strapi.plugin(PLUGIN).service("settings").kits();
2029
2329
  registerZoneGuards(strapi);
2330
+ registerTextTemplateGuard(strapi);
2030
2331
  registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
2031
2332
  if (withHidden.length) hideFromBuilder(typeof strapi.get === "function" ? strapi.get("services") : strapi.container.get("services"), withHidden, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")));
2032
2333
  if (STRAPI5) {
@@ -2046,7 +2347,7 @@ module.exports = {
2046
2347
  const code = strapi.plugin(PLUGIN).config("settings");
2047
2348
  project = null;
2048
2349
  if (code) try {
2049
- project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
2350
+ project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), schemas(strapi));
2050
2351
  } catch (error) {
2051
2352
  strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
2052
2353
  }
@@ -2063,11 +2364,11 @@ module.exports = {
2063
2364
  let fields;
2064
2365
  const fieldTexts = () => {
2065
2366
  if (fields !== void 0) return fields;
2066
- const schemas = Object.fromEntries(Object.entries({ ...strapi.contentTypes, ...strapi.components }).map(([uid, schema]) => [uid, Object.keys(schema?.attributes || {})]));
2367
+ const schemas2 = Object.fromEntries(Object.entries({ ...strapi.contentTypes, ...strapi.components }).map(([uid, schema]) => [uid, Object.keys(schema?.attributes || {})]));
2067
2368
  const stale = [];
2068
2369
  let code = {};
2069
2370
  try {
2070
- code = validateFields(strapi.plugin(PLUGIN).config("fields"), schemas, stale);
2371
+ code = validateFields(strapi.plugin(PLUGIN).config("fields"), schemas2, stale);
2071
2372
  } catch (error) {
2072
2373
  strapi.log?.warn(`[${PLUGIN}] "fields" config ignored: ${error.message}. Field labels fall back to the schema metadata and the Content Manager's own.`);
2073
2374
  }
@@ -2092,7 +2393,7 @@ module.exports = {
2092
2393
  for (const note of notes) strapi.log?.warn(`[${PLUGIN}] starter kits: ${note}.`);
2093
2394
  return kits;
2094
2395
  };
2095
- const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
2396
+ const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), schemas(strapi));
2096
2397
  return {
2097
2398
  projectDefaults,
2098
2399
  get,
@@ -2101,7 +2402,7 @@ module.exports = {
2101
2402
  variants: insertVariants,
2102
2403
  kits: starterKits,
2103
2404
  async set(value) {
2104
- const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
2405
+ const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), schemas(strapi));
2105
2406
  for (const [uid, entry] of Object.entries(next.components)) {
2106
2407
  if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
2107
2408
  }
@@ -2154,6 +2455,7 @@ module.exports = {
2154
2455
  kits: plugin.service("settings").kits(),
2155
2456
  palette: settings.palette,
2156
2457
  contentTypes: settings.contentTypes,
2458
+ textTemplates: settings.textTemplates,
2157
2459
  hiddenAttribute: hidden,
2158
2460
  fields: plugin.service("settings").fields(),
2159
2461
  types,