@unlayer/react-elements 0.1.22 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,9 +1,349 @@
1
- import { ButtonDefaults, DividerDefaults, HeadingDefaults, HtmlDefaults, ImageDefaults, MenuDefaults, ParagraphDefaults, SocialDefaults, TableDefaults, VideoDefaults, BodyDefaults, RowDefaults, ColumnDefaults, ButtonExporters, DividerExporters, HeadingExporters, HtmlExporters, ImageExporters, MenuExporters, ParagraphExporters, SocialExporters, TableExporters, VideoExporters, schemaVersion as schemaVersion$1, RowExporters, ContentExporters, ColumnExporters, BodyExporters, heads } from '@unlayer/exporters';
2
- import { jsx, jsxs } from 'react/jsx-runtime';
3
- import React, { useMemo, useContext, createContext } from 'react';
4
- import ReactDOMServer, { renderToStaticMarkup } from 'react-dom/server';
1
+ import { ButtonDefaults, DividerDefaults, HeadingDefaults, HtmlDefaults, ImageDefaults, MenuDefaults, ParagraphDefaults, SocialDefaults, TableDefaults, VideoDefaults, BodyDefaults, RowDefaults, ColumnDefaults, RowExporters, ContentExporters, ColumnExporters, ButtonExporters, DividerExporters, HeadingExporters, HtmlExporters, ImageExporters, MenuExporters, ParagraphExporters, SocialExporters, TableExporters, VideoExporters, BodyExporters, schemaVersion as schemaVersion$1, heads } from '@unlayer/exporters';
2
+ import React4, { Suspense, useMemo, useContext, createContext } from 'react';
3
+ import * as defaultBuild from 'react-dom/server';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/components/Button.tsx
7
+ var SHARED = Symbol.for("@unlayer/react-elements/collecting");
8
+ var frames = globalThis[SHARED] ?? (globalThis[SHARED] = []);
9
+ function collecting() {
10
+ return frames[frames.length - 1] ?? void 0;
11
+ }
12
+ function within(frame, run) {
13
+ frames.push(frame);
14
+ try {
15
+ return run();
16
+ } finally {
17
+ frames.pop();
18
+ }
19
+ }
20
+ var MARK = "unlayer-mark";
21
+ var RECORDS = Symbol.for("@unlayer/react-elements/records");
22
+ var COLLECTING_ID = "unlayer-collect-";
23
+ function marker(collection, at, edge) {
24
+ return React4.createElement(MARK, { key: `${at}${edge ?? ""}`, "data-n": collection.nonce, "data-i": at, ...edge ? { "data-e": edge } : {} });
25
+ }
26
+ var SHARED2 = Symbol.for("@unlayer/react-elements/render-builds");
27
+ var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
28
+ var same = (a, b) => a.renderToString === b.renderToString;
29
+ function dispatcher() {
30
+ const internals = React4;
31
+ return internals.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.H ?? internals.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentDispatcher?.current ?? null;
32
+ }
33
+ var dispatchers = /* @__PURE__ */ new Map();
34
+ function dispatcherOf(build) {
35
+ if (!dispatchers.has(build)) {
36
+ let seen = null;
37
+ try {
38
+ class Probe extends React4.Component {
39
+ render() {
40
+ seen = dispatcher();
41
+ return null;
42
+ }
43
+ }
44
+ build.renderToStaticMarkup(React4.createElement(Probe));
45
+ } catch {
46
+ }
47
+ dispatchers.set(build, seen);
48
+ }
49
+ return dispatchers.get(build);
50
+ }
51
+ var second;
52
+ function secondBuild(outer) {
53
+ if (second === void 0) {
54
+ second = null;
55
+ const node = globalThis.process;
56
+ const requirers = [
57
+ // (`module`'s, read so that a bundler doesn't take it for a require of a path worked out when it runs.)
58
+ () => {
59
+ return void 0;
60
+ },
61
+ // From this module's URL (as a stack names it: webpack 4 can't parse the syntax for it), the app's script, then
62
+ // the folder Node started in; only where `react` is this React. Another React's isn't loaded (React 19's browser
63
+ // build keeps Node running).
64
+ ...[/file:\/\/\S+?(?=:\d+:\d+)/.exec(String(new Error().stack))?.[0], node?.argv?.[1], `${node?.cwd?.()}/`].map((from) => () => {
65
+ const required = node?.getBuiltinModule?.("module")?.createRequire(from);
66
+ return required?.("react")?.createElement === React4.createElement ? required : void 0;
67
+ })
68
+ ];
69
+ const ids = ["react-dom/server.bun", ...Number.parseInt(React4.version) >= 19 ? [] : ["react-dom/server.browser"], "react-dom/server.node"];
70
+ search: for (const id of ids) {
71
+ for (const requirer of requirers) {
72
+ try {
73
+ const build = requirer()?.(id);
74
+ if (build?.renderToStaticMarkup && !same(build, defaultBuild) && ![null, outer].includes(dispatcherOf(build))) {
75
+ second = build;
76
+ break search;
77
+ }
78
+ } catch {
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return second ?? void 0;
84
+ }
85
+ var buildOf = (outer) => outer === null ? void 0 : [defaultBuild, second].find((b) => !!b && dispatcherOf(b) === outer);
86
+ function withBuild(render, own = false) {
87
+ const outer = own ? null : dispatcher();
88
+ let build = current();
89
+ const theirs = !own && (using.length === 0 || collecting() !== void 0) ? buildOf(outer) : void 0;
90
+ const busy = [];
91
+ if (theirs) {
92
+ const free = (b) => !same(b, theirs) && !using.some((u) => same(u, b));
93
+ const other = free(defaultBuild) ? defaultBuild : secondBuild(outer);
94
+ if (other && free(other)) {
95
+ busy.push(theirs);
96
+ build = other;
97
+ } else build = theirs;
98
+ }
99
+ using.push(...busy, build);
100
+ try {
101
+ return render();
102
+ } catch (error) {
103
+ if (build !== theirs && !rendersHere(build)) throw Object.assign(new Error("[Unlayer] react-dom can't render with this React (React Server Components', as in Next.js's edge runtime): render emails in the Node.js runtime."), { cause: error });
104
+ throw error;
105
+ } finally {
106
+ using.length -= busy.length + 1;
107
+ }
108
+ }
109
+ function rendersHere(build) {
110
+ try {
111
+ build.renderToStaticMarkup(React4.createElement(() => null));
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ function current() {
118
+ return using[using.length - 1] ?? defaultBuild;
119
+ }
120
+ var renderToStaticMarkup = (element2, options) => current().renderToStaticMarkup(element2, options);
121
+ var renderToString = (element2, options) => current().renderToString(element2, options);
122
+
123
+ // src/utils/unwrap-root.ts
124
+ var ROOT_NAMES = /* @__PURE__ */ new Set(["Body", "Email", "Page", "Document"]);
125
+ var CONTAINER_NAMES = /* @__PURE__ */ new Set([...ROOT_NAMES, "Row", "Column"]);
126
+ var MEMO = Symbol.for("react.memo");
127
+ var FORWARD_REF = Symbol.for("react.forward_ref");
128
+ var PROVIDER = Symbol.for("react.provider");
129
+ var CONTEXT = Symbol.for("react.context");
130
+ function getDisplayName(element2) {
131
+ return element2.type?.displayName;
132
+ }
133
+ function componentName(element2) {
134
+ const type = element2.type;
135
+ return type?.displayName || type?.name;
136
+ }
137
+ function isElementsType(type) {
138
+ return !!type?.[UNLAYER_RENDER_KEY] || CONTAINER_NAMES.has(type?.displayName);
139
+ }
140
+ function isElementsComponent(element2) {
141
+ return isElementsType(element2.type);
142
+ }
143
+ var ASYNC_TEMPLATE = "async and suspending templates aren't supported: load the data first and pass it as props";
144
+ var isThenable = (value) => typeof value?.then === "function";
145
+ var settle = (value) => {
146
+ if (isThenable(value)) value.then(void 0, () => {
147
+ });
148
+ };
149
+ function isAsyncComponent(type) {
150
+ while (type?.$$typeof === MEMO) type = type.type;
151
+ return (type?.$$typeof === FORWARD_REF ? type.render : type)?.[Symbol.toStringTag] === "AsyncFunction";
152
+ }
153
+ function callIsolated(call, prefix, providers = []) {
154
+ let result;
155
+ let error;
156
+ let ran = false;
157
+ let threw = false;
158
+ let calling = false;
159
+ const Probe = () => {
160
+ if (calling) return null;
161
+ ran = calling = true;
162
+ threw = false;
163
+ try {
164
+ result = call();
165
+ } catch (cause) {
166
+ threw = true;
167
+ error = cause;
168
+ } finally {
169
+ calling = false;
170
+ }
171
+ return null;
172
+ };
173
+ const probe = providers.reduceRight((inside, provider) => React4.cloneElement(provider, void 0, inside), React4.createElement(Probe));
174
+ renderToStaticMarkup(probe, prefix ? { identifierPrefix: prefix } : void 0);
175
+ settle(error);
176
+ settle(result);
177
+ if (threw) throw isThenable(error) ? new Error(ASYNC_TEMPLATE) : error;
178
+ if (!ran) throw new Error("the template wasn't rendered");
179
+ if (isThenable(result)) throw new Error(ASYNC_TEMPLATE);
180
+ return result;
181
+ }
182
+ function unwrapRoot(element2, why = {}) {
183
+ let current2 = element2;
184
+ const providers = [];
185
+ for (let depth = 0; ; depth++) {
186
+ if (ROOT_NAMES.has(getDisplayName(current2) ?? "")) break;
187
+ if (elementsOwn(current2)) break;
188
+ if (depth === 20) {
189
+ why.unread = "it wraps more than 20 components";
190
+ break;
191
+ }
192
+ const type = current2.type;
193
+ const props = current2.props;
194
+ if (type?.$$typeof === MEMO) {
195
+ current2 = React4.createElement(type.type, props);
196
+ continue;
197
+ }
198
+ if ((type?.$$typeof === PROVIDER || type?.$$typeof === CONTEXT) && "value" in props) {
199
+ const inside = React4.Children.toArray(props.children);
200
+ if (inside.length !== 1 || !React4.isValidElement(inside[0])) break;
201
+ providers.push(current2);
202
+ current2 = inside[0];
203
+ continue;
204
+ }
205
+ if (type === React4.Fragment || type === React4.StrictMode || type === React4.Profiler) {
206
+ const inside = React4.Children.toArray(props.children);
207
+ if (inside.length !== 1 || !React4.isValidElement(inside[0])) break;
208
+ current2 = inside[0];
209
+ continue;
210
+ }
211
+ let produced;
212
+ const prefix = depth ? `w${depth}-` : void 0;
213
+ if (type?.$$typeof === FORWARD_REF) produced = callIsolated(() => type.render({ ...props }, null), prefix, providers);
214
+ else if (typeof type === "function" && !type.prototype?.isReactComponent) produced = callIsolated(() => type({ ...props }), prefix, providers);
215
+ else {
216
+ if (type?.prototype?.isReactComponent) why.unread = `<${componentName(current2)}> is a class component, which isn't called to find it`;
217
+ break;
218
+ }
219
+ if (!React4.isValidElement(produced)) {
220
+ if (holdsRoot(produced)) why.unread = "it doesn't return a root element";
221
+ break;
222
+ }
223
+ current2 = produced;
224
+ }
225
+ if (!providers.length || !ROOT_NAMES.has(getDisplayName(current2) ?? "")) return current2;
226
+ const children = providers.reduceRight((inside, provider) => React4.cloneElement(provider, void 0, inside), current2.props.children);
227
+ return React4.cloneElement(current2, void 0, children);
228
+ }
229
+ var elementsOwn = (element2) => {
230
+ const type = element2.type;
231
+ return !!(type?.[UNLAYER_RENDER_KEY] || type?.[RECORDS]);
232
+ };
233
+ function holdsRoot(node) {
234
+ if (Array.isArray(node)) return node.some(holdsRoot);
235
+ if (!React4.isValidElement(node)) return false;
236
+ return ROOT_NAMES.has(getDisplayName(node) ?? "") || !elementsOwn(node) && holdsRoot(node.props.children);
237
+ }
238
+ var UNWRAP_ADVICE = "Pass the root element (<Email>\u2026</Email>), or a function component that returns one.";
239
+ function htmlRoot(element2, api) {
240
+ if (typeof element2.type === "string" || isElementsComponent(element2)) return element2;
241
+ let reason;
242
+ try {
243
+ const why = {};
244
+ const root = unwrapRoot(element2, why);
245
+ if (ROOT_NAMES.has(getDisplayName(root) ?? "")) return root;
246
+ if (!why.unread && !holdsRoot(root)) return element2;
247
+ reason = why.unread ?? "its root is inside other markup";
248
+ } catch (cause) {
249
+ if (cause instanceof Error && cause.message === ASYNC_TEMPLATE) throw new Error(`[Unlayer] ${api}: ${ASYNC_TEMPLATE}.`);
250
+ reason = cause instanceof Error ? cause.message : String(cause);
251
+ }
252
+ console.warn(`[Unlayer] ${api}: couldn't unwrap <${componentName(element2) || "wrapper"}> (${reason}): its root's fonts, lang, dir and phone styles are ignored. ${UNWRAP_ADVICE}`);
253
+ return element2;
254
+ }
255
+ function guard(children, collection, inContainer = false) {
256
+ return React4.Children.toArray(children).map((child) => {
257
+ const placed = guarded(child, collection, inContainer);
258
+ return placed === child && React4.isValidElement(child) ? React4.cloneElement(child) : placed;
259
+ });
260
+ }
261
+ function guarded(child, collection, inContainer) {
262
+ if (!React4.isValidElement(child)) return child;
263
+ if (isElementsType(child.type)) {
264
+ if (child.type[RECORDS]) return child;
265
+ if (!olderContainer(child.type)) return bounded(child, collection);
266
+ return recordedForOlder(child, collection, inContainer);
267
+ }
268
+ if (!child.type || typeof child.type === "object" && !("$$typeof" in child.type)) return child;
269
+ if (typeof child.type === "string") {
270
+ const inside = child.props.children;
271
+ const element2 = inside !== void 0 && hasComponent(inside) ? React4.cloneElement(child, void 0, guard(inside, collection, inContainer)) : child;
272
+ return collection.markup ? bounded(element2, collection) : element2;
273
+ }
274
+ if (child.type === React4.Fragment) {
275
+ return React4.createElement(React4.Fragment, { key: child.key }, guard(child.props.children, collection, inContainer));
276
+ }
277
+ return bounded(child, collection);
278
+ }
279
+ function bounded(child, collection) {
280
+ const at = collection.recorded.push({ kind: "boundary", element: child }) - 1;
281
+ if (isAsyncComponent(child.type)) return marker(collection, at, "f");
282
+ return React4.createElement(
283
+ Suspense,
284
+ { key: child.key ?? `b${at}`, fallback: marker(collection, at, "f") },
285
+ marker(collection, at, "o"),
286
+ child,
287
+ marker(collection, at, "c")
288
+ );
289
+ }
290
+ function olderContainer(type) {
291
+ return !/^(Row|Column)$/.test(type?.displayName ?? "") || /\b_config\b/.test(String(type));
292
+ }
293
+ function recordedForOlder(child, collection, inContainer) {
294
+ const type = child.type;
295
+ const props = child.props;
296
+ if (type.displayName === "Row" || type.displayName === "Column") {
297
+ const at2 = collection.recorded.push({ kind: type.displayName === "Row" ? "row" : "column", props }) - 1;
298
+ return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
299
+ marker(collection, at2, "o"),
300
+ guard(props.children, collection, true),
301
+ marker(collection, at2, "c")
302
+ ] }, child.key ?? `r${at2}`);
303
+ }
304
+ if (!type.__unlayerRender && !inContainer) return child;
305
+ if (!type.__unlayerRender) {
306
+ console.warn(`[Unlayer] <${type.displayName}> is a root: it can't be placed in a Row or Column, so it's left out.`);
307
+ return null;
308
+ }
309
+ const at = collection.recorded.push({ kind: "block", type: child.type, props }) - 1;
310
+ return React4.cloneElement(marker(collection, at), { key: child.key ?? `b${at}` });
311
+ }
312
+ function hasComponent(children) {
313
+ return React4.Children.toArray(children).some(
314
+ (child) => React4.isValidElement(child) && (typeof child.type === "string" || child.type === React4.Fragment ? hasComponent(child.props.children) : !isElementsType(child.type))
315
+ );
316
+ }
317
+ function Sentinel({ record, real }) {
318
+ let collected2 = !!collecting()?.blind;
319
+ if (!collected2) {
320
+ try {
321
+ collected2 = React4.useId().slice(1).startsWith(COLLECTING_ID);
322
+ } catch {
323
+ }
324
+ }
325
+ return collected2 ? record() : within(null, real);
326
+ }
327
+ var named = /* @__PURE__ */ new Map();
328
+ function whileCollecting(record, real, name) {
329
+ let type = named.get(name);
330
+ if (!type) named.set(name, type = Object.assign((props) => Sentinel(props), { displayName: name }));
331
+ return React4.createElement(type, { record, real });
332
+ }
333
+ function recordContainer(kind, props) {
334
+ const collection = collecting();
335
+ const at = collection.recorded.push({ kind, props }) - 1;
336
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
337
+ marker(collection, at, "o"),
338
+ guard(props.children, collection, true),
339
+ marker(collection, at, "c")
340
+ ] });
341
+ }
342
+ function recordBlock(type, props) {
343
+ const collection = collecting();
344
+ const at = collection.recorded.push({ kind: "block", type, props }) - 1;
345
+ return marker(collection, at);
346
+ }
7
347
 
8
348
  // src/utils/image-sizing.ts
9
349
  function toPx(value) {
@@ -15,11 +355,11 @@ function toPx(value) {
15
355
  function edges(value) {
16
356
  if (value == null) return { left: 0, right: 0 };
17
357
  if (typeof value === "number") return { left: value, right: value };
18
- const parts = String(value).trim().split(/\s+/).map((p) => parseFloat(p) || 0);
19
- if (parts.length === 1) return { left: parts[0], right: parts[0] };
20
- if (parts.length === 2 || parts.length === 3)
21
- return { left: parts[1], right: parts[1] };
22
- return { left: parts[3] || 0, right: parts[1] || 0 };
358
+ const parts2 = String(value).trim().split(/\s+/).map((p) => parseFloat(p) || 0);
359
+ if (parts2.length === 1) return { left: parts2[0], right: parts2[0] };
360
+ if (parts2.length === 2 || parts2.length === 3)
361
+ return { left: parts2[1], right: parts2[1] };
362
+ return { left: parts2[3] || 0, right: parts2[1] || 0 };
23
363
  }
24
364
  function borderEdges(border) {
25
365
  if (!border || typeof border !== "object") return { left: 0, right: 0 };
@@ -209,7 +549,7 @@ function paragraphAlign(format) {
209
549
  }
210
550
  return void 0;
211
551
  }
212
- function paragraphNodeToHtml(node, childrenHtml) {
552
+ function paragraphNodeToHtml(node, childrenHtml22) {
213
553
  const isInlineTool = !!node.isInlineTool;
214
554
  const tag = isInlineTool ? "span" : "p";
215
555
  const dir = dirAttribute(node.direction);
@@ -233,7 +573,7 @@ function paragraphNodeToHtml(node, childrenHtml) {
233
573
  styleParts.push(`text-align: ${align}`);
234
574
  }
235
575
  const style = styleParts.length > 0 ? ` style="${escapeHtml(styleParts.join("; ") + ";")}"` : "";
236
- return `<${tag}${dir}${style}>${childrenHtml || "<br>"}</${tag}>`;
576
+ return `<${tag}${dir}${style}>${childrenHtml22 || "<br>"}</${tag}>`;
237
577
  }
238
578
  function nodeToHtml(node) {
239
579
  if (!node || typeof node !== "object") return "";
@@ -244,32 +584,32 @@ function nodeToHtml(node) {
244
584
  if (type === "linebreak") {
245
585
  return "<br>";
246
586
  }
247
- const childrenHtml = (node.children || []).map(nodeToHtml).join("");
587
+ const childrenHtml22 = (node.children || []).map(nodeToHtml).join("");
248
588
  switch (type) {
249
589
  case "root":
250
- return childrenHtml;
590
+ return childrenHtml22;
251
591
  case "paragraph":
252
592
  case "extended-paragraph":
253
- return paragraphNodeToHtml(node, childrenHtml);
593
+ return paragraphNodeToHtml(node, childrenHtml22);
254
594
  case "heading": {
255
595
  const tag = /^h[1-6]$/.test(node.tag) ? node.tag : "h1";
256
596
  const dir = dirAttribute(node.direction);
257
- return `<${tag}${dir}>${childrenHtml}</${tag}>`;
597
+ return `<${tag}${dir}>${childrenHtml22}</${tag}>`;
258
598
  }
259
599
  case "link": {
260
600
  const href = node.url ? ` href="${escapeHtml(node.url)}"` : "";
261
601
  const target = node.target ? ` target="${escapeHtml(node.target)}"` : "";
262
602
  const rel = node.rel ? ` rel="${escapeHtml(node.rel)}"` : "";
263
- return `<a${href}${target}${rel}>${childrenHtml}</a>`;
603
+ return `<a${href}${target}${rel}>${childrenHtml22}</a>`;
264
604
  }
265
605
  case "list": {
266
606
  const tag = node.listType === "number" ? "ol" : "ul";
267
- return `<${tag}>${childrenHtml}</${tag}>`;
607
+ return `<${tag}>${childrenHtml22}</${tag}>`;
268
608
  }
269
609
  case "listitem":
270
- return `<li>${childrenHtml}</li>`;
610
+ return `<li>${childrenHtml22}</li>`;
271
611
  default:
272
- return childrenHtml;
612
+ return childrenHtml22;
273
613
  }
274
614
  }
275
615
  function generateHtmlFromTextJson(textJson) {
@@ -300,6 +640,57 @@ function mergeValues(defaults, userValues) {
300
640
  }
301
641
  return result;
302
642
  }
643
+ var CHANNEL = /^-?(?:\d+(?:\.\d*)?|\.\d+)%?$/;
644
+ function normalizeColor(value) {
645
+ const text = value.trim();
646
+ const open = text.indexOf("(");
647
+ if (!/^rgba?$/i.test(text.slice(0, open)) || !text.endsWith(")")) return value;
648
+ const [rgb, slashed, extra] = text.slice(open + 1, -1).split("/");
649
+ const channels = rgb.trim().split(/\s*,\s*|\s+/);
650
+ if (slashed !== void 0) channels.push(slashed.trim());
651
+ if (extra !== void 0 || channels.length > 4 || channels.length < (slashed === void 0 ? 3 : 4) || !channels.every((c) => CHANNEL.test(c))) return value;
652
+ const num = (text2, full) => text2.endsWith("%") ? parseFloat(text2) / 100 * full : parseFloat(text2);
653
+ const [r, g, b] = channels.slice(0, 3).map((text2) => Math.max(0, Math.min(255, Math.round(num(text2, 255)))));
654
+ const alpha = channels[3] === void 0 ? 1 : num(channels[3], 1);
655
+ if (alpha < 1) return `rgba(${r}, ${g}, ${b}, ${Math.max(0, alpha)})`;
656
+ return `#${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
657
+ }
658
+ function cssValueSafe(text) {
659
+ if (/[<>{};]|\/\*|\*\//.test(text)) return false;
660
+ const open = [];
661
+ for (const c of text) {
662
+ if (c === "(" || c === "[") open.push(c === "(" ? ")" : "]");
663
+ else if ((c === ")" || c === "]") && open.pop() !== c) return false;
664
+ }
665
+ return !open.length;
666
+ }
667
+ function cssSafeValues(value) {
668
+ if (typeof value === "string") return cssValueSafe(value) ? value : void 0;
669
+ if (Array.isArray(value)) return value.map(cssSafeValues);
670
+ if (!isPlainObject(value)) return value;
671
+ const out = {};
672
+ for (const [k, v] of Object.entries(value)) {
673
+ const safe = cssSafeValues(v);
674
+ if (safe !== void 0 || v === void 0) out[k] = safe;
675
+ }
676
+ return out;
677
+ }
678
+ function normalizeFontStack(value) {
679
+ return value.replace(/"([^"]*)"?/g, (_, name) => `'${name.replace(/'/g, "\\'")}'`);
680
+ }
681
+ function normalizeCssValues(value, key) {
682
+ if (typeof value === "string") return key === "fontFamily" ? normalizeFontStack(value) : key === "color" || key?.endsWith("Color") ? normalizeColor(value) : value;
683
+ if (Array.isArray(value)) return value.map((item) => normalizeCssValues(item));
684
+ if (!isPlainObject(value)) return value;
685
+ const out = {};
686
+ for (const [k, v] of Object.entries(value)) out[k] = normalizeCssValues(v, key === "fontFamily" ? key : k);
687
+ return out;
688
+ }
689
+ function isPlainObject(value) {
690
+ if (value === null || typeof value !== "object") return false;
691
+ const proto = Object.getPrototypeOf(value);
692
+ return proto === Object.prototype || proto === null;
693
+ }
303
694
  var nestedStructureCache = /* @__PURE__ */ new WeakMap();
304
695
  function analyzeNestedStructure(defaultValues) {
305
696
  const cached = nestedStructureCache.get(defaultValues);
@@ -344,24 +735,39 @@ function normalizeCssProps(props) {
344
735
  }
345
736
  }
346
737
  }
347
- function flattenChildrenText(node) {
738
+ var ENTITIES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
739
+ function escapeText(text, quotes) {
740
+ return text.replace(quotes ? /[&<>"']/g : /[&<>]/g, (c) => ENTITIES[c]);
741
+ }
742
+ function escapeKeepingTags(text, quotes, escape = (part) => escapeText(part, quotes)) {
743
+ return text.split(/(<%[\w.]+%>)/).map((part, i) => i % 2 ? part : escape(part)).join("");
744
+ }
745
+ var menuText = (text) => escapeKeepingTags(text, false, (part) => part.replace(/&(?!(?:#\d+|#x[\da-f]+|[a-z][\da-z]*);)|[<>"]/gi, (c) => ENTITIES[c]));
746
+ function flattenChildrenText(node, breaks) {
348
747
  if (node == null || typeof node === "boolean") return "";
349
748
  if (typeof node === "string") return node;
350
749
  if (typeof node === "number") return String(node);
351
- if (Array.isArray(node)) return node.map(flattenChildrenText).join("");
750
+ if (Array.isArray(node)) return node.map((child) => flattenChildrenText(child, breaks)).join("");
352
751
  if (typeof node === "object" && "props" in node) {
353
- return flattenChildrenText(node.props?.children);
752
+ return breaks && node.type === "br" ? "<br/>" : flattenChildrenText(node.props?.children, breaks);
354
753
  }
355
754
  return "";
356
755
  }
756
+ function childrenHtml(children) {
757
+ return flattenChildrenText(children, true).split(/(<br\s*\/?>)/i).map((part, i) => i % 2 ? "<br/>" : escapeKeepingTags(part)).join("");
758
+ }
357
759
  function mapSemanticProps(props, defaultValues, componentType) {
358
- const { children, values, ...restProps } = props;
760
+ const { children, values, mobile, hideOnMobile: hideMobileProp, hideOnDesktop: hideDesktopProp, ...restProps } = props;
761
+ const hideOnMobile = componentType === "Column" ? void 0 : hideMobileProp;
762
+ const hideOnDesktop = componentType === "Column" ? void 0 : hideDesktopProp;
359
763
  const userProps = { ...restProps };
360
764
  const result = values ? { ...values } : {};
361
765
  if (children !== void 0 && !result.text && !result.textJson) {
362
766
  const textContent = typeof children === "string" ? children : flattenChildrenText(children);
363
767
  if (componentType === "Paragraph") {
364
768
  result.textJson = textToTextJson(textContent);
769
+ } else if (componentType === "Heading" || componentType === "Button") {
770
+ result.text = childrenHtml(children);
365
771
  } else {
366
772
  result.text = textContent;
367
773
  }
@@ -439,15 +845,15 @@ function mapSemanticProps(props, defaultValues, componentType) {
439
845
  }
440
846
  if (defaultValues && typeof defaultValues === "object" && "border" in defaultValues) {
441
847
  const borderSideRe = /^border(Top|Right|Bottom|Left)(Width|Style|Color)$/;
442
- const collected = {};
848
+ const collected2 = {};
443
849
  for (const key of Object.keys(final)) {
444
850
  if (borderSideRe.test(key)) {
445
- collected[key] = final[key];
851
+ collected2[key] = final[key];
446
852
  delete final[key];
447
853
  }
448
854
  }
449
- if (Object.keys(collected).length > 0) {
450
- final.border = { ...final.border || {}, ...collected };
855
+ if (Object.keys(collected2).length > 0) {
856
+ final.border = { ...final.border || {}, ...collected2 };
451
857
  }
452
858
  if (final.border && typeof final.border === "object") {
453
859
  const b = { ...final.border };
@@ -460,7 +866,31 @@ function mapSemanticProps(props, defaultValues, componentType) {
460
866
  final.border = b;
461
867
  }
462
868
  }
463
- return final;
869
+ if (mobile || hideOnMobile !== void 0 || hideOnDesktop !== void 0) {
870
+ const overrides = { ...final._override };
871
+ if (mobile || hideOnMobile !== void 0) {
872
+ const { width, maxWidth, autoWidth, ...phone } = mobile || {};
873
+ const mapped = mapSemanticProps(phone, defaultValues, componentType);
874
+ if (componentType === "Image" && (width !== void 0 || maxWidth !== void 0 || autoWidth !== void 0)) {
875
+ mapped.src = {
876
+ ...overrides.mobile?.src || {},
877
+ ...autoWidth !== void 0 ? { autoWidth } : { autoWidth: false },
878
+ ...width !== void 0 || maxWidth !== void 0 ? { maxWidth: width ?? maxWidth } : {}
879
+ };
880
+ } else if (width !== void 0 || autoWidth !== void 0) {
881
+ const key = componentType === "Button" ? "size" : "width";
882
+ mapped[key] = { autoWidth: autoWidth ?? false, ...width !== void 0 ? { width: typeof width === "number" ? `${width}px` : width } : {} };
883
+ }
884
+ overrides.mobile = { ...overrides.mobile, ...cssSafeValues(mapped), ...hideOnMobile !== void 0 ? { hideMobile: hideOnMobile } : {} };
885
+ }
886
+ if (hideOnDesktop !== void 0) overrides.desktop = { ...overrides.desktop, hideDesktop: hideOnDesktop };
887
+ final._override = overrides;
888
+ }
889
+ if (componentType === "Row" && final.noStackMobile === true) {
890
+ final._override = { ...final._override, mobile: { ...final._override?.mobile, noStackMobile: true } };
891
+ final.noStackMobile = false;
892
+ }
893
+ return normalizeCssValues(final);
464
894
  }
465
895
  function normalizeLinkValue(value) {
466
896
  if (value == null) return void 0;
@@ -482,26 +912,39 @@ function normalizeLinkValue(value) {
482
912
  }
483
913
  return void 0;
484
914
  }
485
- function normalizeValuesForExporter(values, componentName) {
915
+ function safeLink(link) {
916
+ const url = link.url === void 0 || link.url === null ? link.url : String(link.url);
917
+ return typeof url === "string" ? { ...link, url: url.replace(/<%[\w.]+%>|["<>]/g, (c) => c.length > 1 ? c : encodeURIComponent(c)) } : link;
918
+ }
919
+ function normalizeValuesForExporter(values, componentName2, mode) {
486
920
  if (values == null || typeof values !== "object") return values;
487
921
  const out = { ...values };
488
922
  for (const key of ["href", "action"]) {
489
923
  if (out[key] !== void 0) {
490
924
  const normalized = normalizeLinkValue(out[key]);
491
- if (normalized !== void 0) out[key] = normalized;
925
+ if (normalized !== void 0) out[key] = safeLink(normalized);
492
926
  }
493
927
  }
494
- if (componentName === "Menu" && out.menu && Array.isArray(out.menu.items)) {
928
+ if (componentName2 === "Button" && typeof out.text === "string") {
929
+ out.text = escapeKeepingTags(out.text, true);
930
+ }
931
+ if (componentName2 === "Menu" && out.menu && Array.isArray(out.menu.items)) {
495
932
  out.menu = {
496
933
  ...out.menu,
497
934
  items: out.menu.items.map((item) => {
498
- if (!item || item.link === void 0) return item;
499
- const normalized = normalizeLinkValue(item.link);
500
- if (normalized === void 0) return item;
501
- return { ...item, link: normalized };
935
+ if (!item) return item;
936
+ const next = mode === "email" && typeof item.text === "string" ? { ...item, text: menuText(item.text) } : item;
937
+ if (next.link === void 0) return next;
938
+ const normalized = normalizeLinkValue(next.link);
939
+ if (normalized === void 0) return next;
940
+ return { ...next, link: safeLink(normalized) };
502
941
  })
503
942
  };
504
943
  }
944
+ if (componentName2 === "Social" && out.icons && Array.isArray(out.icons.icons)) {
945
+ const named2 = (icon) => mode === "email" && typeof icon.name === "string" ? { ...icon, name: escapeText(icon.name, true) } : icon;
946
+ out.icons = { ...out.icons, icons: out.icons.icons.map((icon) => icon && typeof icon === "object" ? named2(safeLink(icon)) : icon) };
947
+ }
505
948
  return out;
506
949
  }
507
950
  var HTML_ENTITIES = {
@@ -594,9 +1037,11 @@ function htmlToPlainText(html) {
594
1037
  text = text.replace(/<head[\s\S]*?<\/head>/gi, "");
595
1038
  text = text.replace(/<style[\s\S]*?<\/style>/gi, "");
596
1039
  text = text.replace(/<script[\s\S]*?<\/script>/gi, "");
1040
+ text = text.replace(/<br\s*\/?>/gi, "\n");
597
1041
  text = text.replace(/<h[12][^>]*>([\s\S]*?)<\/h[12]>/gi, (_, content) => {
598
1042
  const clean = content.replace(/<[^>]+>/g, "").trim();
599
- return "\n" + clean.toUpperCase() + "\n";
1043
+ const upper = clean.replace(/&#?\w+;|[^&]+|&/g, (part) => /^&#?\w+;$/.test(part) ? part : part.toUpperCase());
1044
+ return "\n" + upper + "\n";
600
1045
  });
601
1046
  text = text.replace(/<h[3-6][^>]*>([\s\S]*?)<\/h[3-6]>/gi, (_, content) => {
602
1047
  const clean = content.replace(/<[^>]+>/g, "").trim();
@@ -618,7 +1063,6 @@ function htmlToPlainText(html) {
618
1063
  const clean = content.replace(/<[^>]+>/g, "").trim();
619
1064
  return "- " + clean + "\n";
620
1065
  });
621
- text = text.replace(/<br\s*\/?>/gi, "\n");
622
1066
  text = text.replace(/<\/(?:p|div|tr|blockquote|section|article|header|footer|main|nav|aside)>/gi, "\n");
623
1067
  text = text.replace(/<\/(?:ul|ol|table|thead|tbody|tfoot)>/gi, "\n");
624
1068
  text = text.replace(/<\/(?:td|th)>/gi, " ");
@@ -665,6 +1109,15 @@ function nextHtmlId(config, prefix) {
665
1109
  ids[prefix] = (ids[prefix] || 0) + 1;
666
1110
  return `${prefix}_${ids[prefix]}`;
667
1111
  }
1112
+ function safeName(value, spaces = false) {
1113
+ const text = String(value ?? "");
1114
+ return spaces ? text.replace(/[\t\n\f\r]/g, " ").replace(/["'<>&`\u0000-\u001f\u007f]/g, "") : text.replace(/["'<>&`\s\u0000-\u001f\u007f]/g, "");
1115
+ }
1116
+ var safeMeta = (meta) => ({ ...meta, htmlID: safeName(meta.htmlID), htmlClassNames: safeName(meta.htmlClassNames, true) });
1117
+ function textDirectionOf(value) {
1118
+ const dir = typeof value === "string" ? value.trim().toLowerCase() : "";
1119
+ return dir === "ltr" || dir === "rtl" || dir === "auto" ? dir : void 0;
1120
+ }
668
1121
  function renderComponent(config) {
669
1122
  const { type, values, className, style, args = [], innerHTML, _config, exporter, metaContext } = config;
670
1123
  try {
@@ -672,7 +1125,7 @@ function renderComponent(config) {
672
1125
  const exporterConfig = {
673
1126
  generateHtmlFromTextJson,
674
1127
  toSafeHtml: cfg.toSafeHtml,
675
- textDirection: cfg.textDirection,
1128
+ textDirection: textDirectionOf(cfg.textDirection),
676
1129
  cdnBaseUrl: cfg.cdnBaseUrl
677
1130
  };
678
1131
  let html;
@@ -736,21 +1189,20 @@ function createItemComponent(config) {
736
1189
  });
737
1190
  const finalValues = mergeValues(config.defaultValues, mappedValues);
738
1191
  const metaName = config.metaName ?? config.name.toLowerCase();
739
- const valuesWithMeta = {
740
- ...finalValues,
741
- _meta: {
742
- htmlID: _metaHtmlId ?? `u_content_${metaName}_${index + 1}`,
743
- htmlClassNames: `u_content_${metaName}`,
744
- ...finalValues._meta || {}
745
- }
1192
+ const meta = {
1193
+ htmlID: _metaHtmlId ?? `u_content_${metaName}_${index + 1}`,
1194
+ htmlClassNames: `u_content_${metaName}`,
1195
+ ...finalValues._meta || {}
746
1196
  };
1197
+ const valuesWithMeta = { ...finalValues, _meta: safeMeta(meta) };
747
1198
  const safeBodyValues = {
748
1199
  contentWidth: "500px",
749
1200
  ...bodyValues
750
1201
  };
751
1202
  const valuesForExporter = normalizeValuesForExporter(
752
1203
  valuesWithMeta,
753
- config.name
1204
+ config.name,
1205
+ mode
754
1206
  );
755
1207
  const exportSrc = valuesForExporter.src;
756
1208
  if (exportSrc && typeof exportSrc === "object" && exportSrc.autoWidth === false) {
@@ -785,10 +1237,12 @@ function createItemComponent(config) {
785
1237
  });
786
1238
  }
787
1239
  const ItemComponent = (props) => {
1240
+ if (collecting()) return whileCollecting(() => recordBlock(ItemComponent, props), () => renderFn(props), ItemComponent.displayName ?? "Block");
788
1241
  return renderFn(props);
789
1242
  };
790
1243
  ItemComponent.displayName = config.displayName || config.name;
791
1244
  ItemComponent[UNLAYER_RENDER_KEY] = renderFn;
1245
+ ItemComponent[RECORDS] = true;
792
1246
  ItemComponent[UNLAYER_CONFIG_KEY] = {
793
1247
  name: config.name,
794
1248
  defaultValues: config.defaultValues,
@@ -817,9 +1271,9 @@ var Button = createItemComponent({
817
1271
  DEFAULT_VALUES,
818
1272
  "Button"
819
1273
  );
820
- const size = mapped.size;
821
- if (size && typeof size === "object" && !Array.isArray(size)) {
822
- const s = size;
1274
+ const size2 = mapped.size;
1275
+ if (size2 && typeof size2 === "object" && !Array.isArray(size2)) {
1276
+ const s = size2;
823
1277
  if (typeof s.width === "number") {
824
1278
  s.width = `${s.width}px`;
825
1279
  } else if (typeof s.width === "string" && /^\d+(?:\.\d+)?$/.test(s.width.trim())) {
@@ -1228,6 +1682,35 @@ var {
1228
1682
  var BODY_DEFAULTS = bodyRest;
1229
1683
  var ROW_DEFAULTS = { ...RowDefaults };
1230
1684
  var COLUMN_DEFAULTS = { ...ColumnDefaults };
1685
+
1686
+ // src/utils/render-scope.ts
1687
+ var scoped;
1688
+ function scopeCache(key, make) {
1689
+ if (!scoped) return make();
1690
+ if (!scoped.has(key)) scoped.set(key, make());
1691
+ return scoped.get(key);
1692
+ }
1693
+ function withRenderScope(render, own = false) {
1694
+ const scope = () => {
1695
+ if (scoped) return render();
1696
+ scoped = /* @__PURE__ */ new WeakMap();
1697
+ try {
1698
+ return render();
1699
+ } finally {
1700
+ scoped = void 0;
1701
+ }
1702
+ };
1703
+ return withBuild(() => within(null, scope), own);
1704
+ }
1705
+ var warnedLoose = /* @__PURE__ */ new Set();
1706
+ function looseText(child, container) {
1707
+ const text = String(child);
1708
+ if (text.trim() && !warnedLoose.has(container)) {
1709
+ warnedLoose.add(container);
1710
+ console.warn(`${container}: text placed straight in a ${container} shows as text and isn't kept in the editor's design. Put it in a <Paragraph>, or HTML in an <Html> block.`);
1711
+ }
1712
+ return escapeKeepingTags(text, true);
1713
+ }
1231
1714
  var DEFAULT_VALUES12 = ROW_DEFAULTS;
1232
1715
  var DEFAULT_BODY_VALUES = BODY_DEFAULTS;
1233
1716
  function getWidthPercentages(cells) {
@@ -1257,8 +1740,8 @@ ${maxQuery} {
1257
1740
  .u-row { width: 100% !important; }
1258
1741
  .u-row .u-col { display: block !important; width: 100% !important; min-width: 320px !important; max-width: 100% !important; }
1259
1742
  .u-row .u-col > div { margin: 0 auto; }
1260
- .no-stack .u-col { min-width: 0 !important; display: table-cell !important; }
1261
- ${widths.map(({ value, className }) => ` .no-stack .u-col-${className} { width: ${value}% !important; }`).join("\n")}
1743
+ .u-row.no-stack .u-col { min-width: 0 !important; display: table-cell !important; }
1744
+ ${widths.map(({ value, className }) => ` .u-row.no-stack .u-col-${className} { width: ${value}% !important; }`).join("\n")}
1262
1745
  }`;
1263
1746
  }
1264
1747
  const baseCSS = `
@@ -1298,17 +1781,18 @@ function renderRowToHtml(innerHTML, values, bodyValues, mode, cells, collection
1298
1781
  variant: mode
1299
1782
  });
1300
1783
  const contentWidth = toContentWidthPx(bodyValues);
1301
- const css = generateGridCSS(cells, mode, contentWidth);
1302
- return css ? `<style>${css}</style>${html}` : html;
1784
+ const css2 = generateGridCSS(cells, mode, contentWidth);
1785
+ return css2 ? `<style>${css2}</style>${html}` : html;
1303
1786
  }
1304
1787
  function processChildren(children, cells, bodyValues, rowValues, mode, _config) {
1305
1788
  if (!children) return "";
1306
1789
  let innerHTML = "";
1307
- const childrenArray = React.Children.toArray(children);
1308
- childrenArray.forEach((child, index) => {
1309
- if (!React.isValidElement(child)) {
1790
+ const childrenArray = React4.Children.toArray(children);
1791
+ let index = 0;
1792
+ childrenArray.forEach((child) => {
1793
+ if (!React4.isValidElement(child)) {
1310
1794
  if (typeof child === "string" || typeof child === "number") {
1311
- innerHTML += String(child);
1795
+ innerHTML += looseText(child, "Row");
1312
1796
  }
1313
1797
  return;
1314
1798
  }
@@ -1317,7 +1801,7 @@ function processChildren(children, cells, bodyValues, rowValues, mode, _config)
1317
1801
  if (isColumn && typeof child.type === "function") {
1318
1802
  const rendered = child.type({
1319
1803
  ...child.props,
1320
- index,
1804
+ index: index++,
1321
1805
  cells,
1322
1806
  bodyValues,
1323
1807
  rowValues,
@@ -1327,7 +1811,7 @@ function processChildren(children, cells, bodyValues, rowValues, mode, _config)
1327
1811
  if (rendered?.props?.dangerouslySetInnerHTML?.__html) {
1328
1812
  innerHTML += rendered.props.dangerouslySetInnerHTML.__html;
1329
1813
  }
1330
- } else if (React.isValidElement(child)) {
1814
+ } else if (React4.isValidElement(child)) {
1331
1815
  const name = child.type?.displayName || child.type?.name || "Unknown";
1332
1816
  console.warn(
1333
1817
  `Row: <${name}> is not a valid Row child. Only <Column> components can be direct children of <Row>. Wrap it in a <Column>: <Row><Column><${name} /></Column></Row>`
@@ -1336,7 +1820,7 @@ function processChildren(children, cells, bodyValues, rowValues, mode, _config)
1336
1820
  });
1337
1821
  return innerHTML;
1338
1822
  }
1339
- var Row = (props) => {
1823
+ function renderRow(props) {
1340
1824
  const {
1341
1825
  layout,
1342
1826
  cells: propsCells,
@@ -1351,16 +1835,16 @@ var Row = (props) => {
1351
1835
  ...semanticProps
1352
1836
  } = props;
1353
1837
  const mode = modeProp ?? _config?.mode ?? "web";
1838
+ const columnCount = React4.Children.toArray(children).filter(
1839
+ (c) => React4.isValidElement(c) && /^Column$/.test(c.type?.displayName || c.type?.name || "")
1840
+ ).length;
1354
1841
  let cells;
1355
1842
  if (layout) {
1356
- validateColumnLayout(layout, React.Children.count(children));
1843
+ if (columnCount !== layout.expectedColumns) validateColumnLayout(layout, React4.Children.count(children));
1357
1844
  cells = layout.cells;
1358
1845
  } else if (propsCells) {
1359
1846
  cells = propsCells;
1360
1847
  } else {
1361
- const columnCount = React.Children.toArray(children).filter(
1362
- (c) => React.isValidElement(c) && /^Column$/.test(c.type?.displayName || c.type?.name || "")
1363
- ).length;
1364
1848
  cells = Array(Math.max(1, columnCount)).fill(1);
1365
1849
  }
1366
1850
  const safeBodyValues = { ...DEFAULT_BODY_VALUES, ...bodyValues };
@@ -1369,15 +1853,7 @@ var Row = (props) => {
1369
1853
  DEFAULT_VALUES12,
1370
1854
  "Row"
1371
1855
  );
1372
- const valuesWithMeta = {
1373
- ...values,
1374
- cells,
1375
- _meta: {
1376
- htmlID: nextHtmlId(_config, "u_row"),
1377
- htmlClassNames: "u_row",
1378
- ...values._meta || {}
1379
- }
1380
- };
1856
+ const valuesWithMeta = { ...values, cells, _meta: safeMeta({ htmlID: nextHtmlId(_config, "u_row"), htmlClassNames: "u_row", ...values._meta || {} }) };
1381
1857
  const innerHTML = processChildren(
1382
1858
  children,
1383
1859
  cells,
@@ -1400,10 +1876,11 @@ var Row = (props) => {
1400
1876
  console.error("Row rendering failed:", error);
1401
1877
  return /* @__PURE__ */ jsx("div", { className, style, children });
1402
1878
  }
1403
- };
1879
+ }
1880
+ var Row = (props) => collecting() ? whileCollecting(() => recordContainer("row", props), () => renderRow(props), "Row") : renderRow(props);
1404
1881
  Row.displayName = "Row";
1882
+ Row[RECORDS] = true;
1405
1883
  var Row_default = Row;
1406
- var DEFAULT_CONTAINER_PADDING2 = "10px";
1407
1884
  var DEFAULT_VALUES13 = COLUMN_DEFAULTS;
1408
1885
  function renderColumnToHtml(innerHTML, values, index, cells, bodyValues, rowValues, mode) {
1409
1886
  const columnExporter = ColumnExporters[mode] || ColumnExporters.web;
@@ -1414,6 +1891,7 @@ function renderContentToHtml(innerHTML, values, bodyValues, mode) {
1414
1891
  return contentExporter(innerHTML, values, bodyValues, {});
1415
1892
  }
1416
1893
  var Column = (props) => {
1894
+ if (collecting()) return whileCollecting(() => recordContainer("column", props), () => Column(props), "Column");
1417
1895
  const {
1418
1896
  children,
1419
1897
  index = 0,
@@ -1432,27 +1910,20 @@ var Column = (props) => {
1432
1910
  DEFAULT_VALUES13,
1433
1911
  "Column"
1434
1912
  );
1435
- const valuesWithMeta = {
1436
- ...values,
1437
- _meta: {
1438
- htmlID: nextHtmlId(_config, "u_column"),
1439
- htmlClassNames: "u_column",
1440
- ...values._meta || {}
1441
- }
1442
- };
1913
+ const valuesWithMeta = { ...values, _meta: safeMeta({ htmlID: nextHtmlId(_config, "u_column"), htmlClassNames: "u_column", ...values._meta || {} }) };
1443
1914
  let innerHTML = "";
1444
1915
  if (children) {
1445
1916
  try {
1446
- const childrenArray = React.Children.toArray(children);
1917
+ const childrenArray = React4.Children.toArray(children);
1447
1918
  childrenArray.forEach((child, childIndex) => {
1448
1919
  if (typeof child === "string" || typeof child === "number") {
1449
- innerHTML += String(child);
1450
- } else if (React.isValidElement(child)) {
1920
+ innerHTML += looseText(child, "Column");
1921
+ } else if (React4.isValidElement(child)) {
1451
1922
  if (typeof child.type === "function") {
1452
1923
  const ComponentType = child.type;
1453
1924
  const renderFn = ComponentType[UNLAYER_RENDER_KEY] || ComponentType;
1454
- const componentName = ComponentType?.[UNLAYER_CONFIG_KEY]?.metaName ?? (ComponentType?.displayName || ComponentType?.name || "component").toLowerCase();
1455
- const contentHtmlId = nextHtmlId(_config, `u_content_${componentName}`);
1925
+ const componentName2 = ComponentType?.[UNLAYER_CONFIG_KEY]?.metaName ?? (ComponentType?.displayName || ComponentType?.name || "component").toLowerCase();
1926
+ const contentHtmlId = nextHtmlId(_config, `u_content_${componentName2}`);
1456
1927
  const rendered = renderFn({
1457
1928
  ...child.props,
1458
1929
  _config,
@@ -1467,13 +1938,26 @@ var Column = (props) => {
1467
1938
  const componentHTML = rendered.props.dangerouslySetInnerHTML.__html;
1468
1939
  if (!componentHTML && ComponentType?.[UNLAYER_CONFIG_KEY]?.omitEmptyOutput) return;
1469
1940
  const childProps = child.props;
1470
- const rawContainerPadding = childProps.containerPadding ?? childProps.values?.containerPadding ?? DEFAULT_CONTAINER_PADDING2;
1941
+ const rawContainerPadding = childProps.containerPadding ?? childProps.values?.containerPadding ?? DEFAULT_CONTAINER_PADDING;
1471
1942
  const containerPadding = typeof rawContainerPadding === "number" ? `${rawContainerPadding}px` : rawContainerPadding;
1943
+ const device = ComponentType?.[UNLAYER_CONFIG_KEY]?.contentType === "custom" ? {} : childProps;
1472
1944
  const contentValues = {
1473
1945
  containerPadding,
1946
+ _override: {
1947
+ ...childProps.values?._override,
1948
+ mobile: {
1949
+ ...childProps.values?._override?.mobile,
1950
+ ...device.hideOnMobile !== void 0 ? { hideMobile: device.hideOnMobile } : {}
1951
+ },
1952
+ desktop: {
1953
+ ...childProps.values?._override?.desktop,
1954
+ ...device.hideOnDesktop !== void 0 ? { hideDesktop: device.hideOnDesktop } : {}
1955
+ }
1956
+ },
1474
1957
  _meta: {
1475
1958
  htmlID: contentHtmlId,
1476
- htmlClassNames: `u_content_${componentName}`
1959
+ htmlClassNames: `u_content_${componentName2}`,
1960
+ ...childProps.values?._meta?.htmlID ? { htmlID: safeName(childProps.values._meta.htmlID) } : {}
1477
1961
  }
1478
1962
  };
1479
1963
  innerHTML += renderContentToHtml(
@@ -1512,7 +1996,332 @@ var Column = (props) => {
1512
1996
  }
1513
1997
  };
1514
1998
  Column.displayName = "Column";
1999
+ Column[RECORDS] = true;
1515
2000
  var Column_default = Column;
2001
+ var KEPT = Symbol.for("@unlayer/react-elements/kept");
2002
+ function parse(html, collection) {
2003
+ const root = [];
2004
+ const open = [root];
2005
+ const pattern = new RegExp(`<${MARK} data-n="${collection.nonce}" data-i="(\\d+)"(?: data-e="([ocf])")?></${MARK}>`, "g");
2006
+ let last = 0;
2007
+ for (const match of html.matchAll(pattern)) {
2008
+ const into = open[open.length - 1];
2009
+ if (match.index > last) into.push({ kind: "raw", html: html.slice(last, match.index) });
2010
+ last = match.index + match[0].length;
2011
+ const at = Number(match[1]);
2012
+ const edge = match[2];
2013
+ if (edge === "o") {
2014
+ const children = [];
2015
+ into.push({ kind: collection.recorded[at].kind, at, children });
2016
+ open.push(children);
2017
+ } else if (edge === "c") open.pop();
2018
+ else into.push({ kind: edge === "f" ? "failed" : "block", at });
2019
+ }
2020
+ if (last < html.length) open[open.length - 1].push({ kind: "raw", html: html.slice(last) });
2021
+ if (open.length !== 1) throw new Error("[Unlayer] a collected render's markers don't pair up");
2022
+ return root;
2023
+ }
2024
+ var TEXT_ENTITIES = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#x27;": "'" };
2025
+ var VOID = /^(area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)$/i;
2026
+ function parts(html) {
2027
+ const out = [];
2028
+ let at = 0;
2029
+ const text = (s) => s && out.push({ text: s.replace(/&amp;|&lt;|&gt;|&quot;|&#x27;/g, (e) => TEXT_ENTITIES[e]) });
2030
+ while (at < html.length) {
2031
+ const start = html.indexOf("<", at);
2032
+ if (start < 0) {
2033
+ text(html.slice(at));
2034
+ break;
2035
+ }
2036
+ text(html.slice(at, start));
2037
+ const name = /^<([a-zA-Z][\w:-]*)/.exec(html.slice(start))?.[1];
2038
+ if (!name) {
2039
+ out.push({ tag: "#", html: html.slice(start) });
2040
+ break;
2041
+ }
2042
+ const openEnd = html.indexOf(">", start);
2043
+ if (VOID.test(name) || html[openEnd - 1] === "/") {
2044
+ out.push({ tag: name, html: html.slice(start, openEnd + 1) });
2045
+ at = openEnd + 1;
2046
+ continue;
2047
+ }
2048
+ const tags = new RegExp(`<(/?)${name}(?=[\\s>/])`, "gi");
2049
+ tags.lastIndex = openEnd + 1;
2050
+ let depth = 1;
2051
+ let end = -1;
2052
+ for (let m = tags.exec(html); m; m = tags.exec(html)) {
2053
+ depth += m[1] ? -1 : 1;
2054
+ if (depth === 0) {
2055
+ end = m.index;
2056
+ break;
2057
+ }
2058
+ }
2059
+ if (end < 0) {
2060
+ out.push({ tag: "#", html: html.slice(start) });
2061
+ break;
2062
+ }
2063
+ const close = html.indexOf(">", end) + 1;
2064
+ out.push({ tag: name, html: html.slice(start, close), inner: html.slice(openEnd + 1, end) });
2065
+ at = close;
2066
+ }
2067
+ return out;
2068
+ }
2069
+ function innerType(type) {
2070
+ while (type?.$$typeof === MEMO) type = type.type;
2071
+ return type;
2072
+ }
2073
+ var recorded = (c, node) => c.collection.recorded[node.at];
2074
+ var nameOf = (element2) => {
2075
+ if (typeof element2.type === "string") return element2.type;
2076
+ const type = innerType(element2.type);
2077
+ return type?.displayName || type?.name || "component";
2078
+ };
2079
+ function leftOut(c, node) {
2080
+ if (c.fatal) throw c.fatal;
2081
+ if (!c.told.has(node.at)) {
2082
+ c.told.add(node.at);
2083
+ try {
2084
+ tell(recorded(c, node).element);
2085
+ } catch (error) {
2086
+ c.fatal = error;
2087
+ throw error;
2088
+ }
2089
+ }
2090
+ return [];
2091
+ }
2092
+ function calledAgain(element2) {
2093
+ const type = innerType(element2.type);
2094
+ if (type?.$$typeof !== FORWARD_REF && (typeof type !== "function" || type.prototype?.isReactComponent)) return void 0;
2095
+ const props = { ...element2.props };
2096
+ for (const key in type.defaultProps) if (props[key] === void 0) props[key] = type.defaultProps[key];
2097
+ return { out: callIsolated(() => type.$$typeof === FORWARD_REF ? type.render(props, null) : type(props)) };
2098
+ }
2099
+ function tell(element2) {
2100
+ const type = innerType(element2.type);
2101
+ const name = nameOf(element2);
2102
+ const async = () => console.warn(`[Unlayer] <${name}> is async or suspends: Elements renders synchronously, so it's left out. Load its data first and pass it as props.`);
2103
+ if (isAsyncComponent(type)) return async();
2104
+ try {
2105
+ const again = calledAgain(element2);
2106
+ const inner = again ? collected(again.out) : collected(element2, false);
2107
+ const failed = (nodes) => nodes.flatMap((n) => n.kind === "failed" ? [n] : "children" in n ? failed(n.children) : []);
2108
+ const found = failed(inner.nodes);
2109
+ for (const node of found) tell(recorded(inner, node).element);
2110
+ if (!found.length) console.error(`[Unlayer] <${name}> failed while rendering, so it's left out.`);
2111
+ } catch (error) {
2112
+ const message = String(error?.message);
2113
+ if (/Element type is invalid|Objects are not valid as a React child|Minified React error #(130|31)\b/.test(message)) throw error;
2114
+ if (isThenable(error) || /async and suspending|suspended|Minified React error #426\b/.test(message)) async();
2115
+ else console.error(`[Unlayer] <${name}> threw while rendering, so it's left out:`, error);
2116
+ }
2117
+ }
2118
+ function markupOnly(c, nodes) {
2119
+ let out = "";
2120
+ for (const node of nodes) {
2121
+ if (node.kind === "raw") out += node.html;
2122
+ else if (node.kind === "failed") leftOut(c, node);
2123
+ else if (node.kind !== "boundary") return void 0;
2124
+ else {
2125
+ const inner = markupOnly(c, node.children);
2126
+ if (inner === void 0) return void 0;
2127
+ out += inner;
2128
+ }
2129
+ }
2130
+ return out;
2131
+ }
2132
+ function keptMarkup(c, node) {
2133
+ c.kept ?? (c.kept = /* @__PURE__ */ new Map());
2134
+ if (!c.kept.has(node.at)) c.kept.set(node.at, keptMarkupOnce(c, node));
2135
+ return c.kept.get(node.at);
2136
+ }
2137
+ function keptMarkupOnce(c, node) {
2138
+ const raw = markupOnly(c, node.children);
2139
+ if (raw === void 0) return void 0;
2140
+ const list = parts(raw);
2141
+ if (!list.some((p) => "tag" in p)) return void 0;
2142
+ const element2 = recorded(c, node).element;
2143
+ const one = list.length === 1 || list.some((p) => "tag" in p && p.tag === "#") ? /^<([a-zA-Z][\w:-]*)\b[^>]*>([\s\S]*)<\/\1>$/.exec(raw) : null;
2144
+ let given = false;
2145
+ if (one) {
2146
+ try {
2147
+ const out = calledAgain(element2)?.out;
2148
+ given = React4.isValidElement(out) && typeof out.type === "string" && out.props.dangerouslySetInnerHTML != null;
2149
+ } catch {
2150
+ given = one[1].toLowerCase() === "div";
2151
+ }
2152
+ }
2153
+ const html = one && given ? one[2] : raw;
2154
+ const Rendered = () => React4.createElement("div", { dangerouslySetInnerHTML: { __html: html } });
2155
+ Rendered.displayName = nameOf(element2);
2156
+ const { containerPadding, hideOnMobile, hideOnDesktop } = element2.props ?? {};
2157
+ Rendered[KEPT] = React4.createElement(Html_default, {
2158
+ html,
2159
+ ...containerPadding !== void 0 ? { containerPadding } : {},
2160
+ ...hideOnMobile !== void 0 ? { hideOnMobile } : {},
2161
+ ...hideOnDesktop !== void 0 ? { hideOnDesktop } : {}
2162
+ });
2163
+ return React4.createElement(Rendered, { ...element2.props });
2164
+ }
2165
+ function resolvedProps(c, props) {
2166
+ let out = props;
2167
+ for (const [key, value] of Object.entries(props)) {
2168
+ if (typeof value !== "string" || !value.includes(MARK)) continue;
2169
+ let html = value;
2170
+ try {
2171
+ if (value.includes(c.collection.nonce)) html = markup(c, parse(value, c.collection), (block) => within(null, () => renderToStaticMarkup(block)));
2172
+ } catch {
2173
+ }
2174
+ out = out === props ? { ...props } : out;
2175
+ out[key] = withoutMarkers(html, c.collection.nonce);
2176
+ }
2177
+ return out;
2178
+ }
2179
+ function withoutMarkers(html, nonce2) {
2180
+ const left = new RegExp(`<${MARK} data-n="${nonce2}" data-i="\\d+"(?: data-e="[ocf]")?></${MARK}>`, "g");
2181
+ if (!html.includes(nonce2) || !html.match(left)) return html;
2182
+ console.error(`[Unlayer] an Elements block rendered inside a component left a marker behind; it's taken out, and the block it stood for is missing.`);
2183
+ return html.replace(left, "");
2184
+ }
2185
+ function markup(c, nodes, render) {
2186
+ return nodes.map((node) => {
2187
+ switch (node.kind) {
2188
+ case "raw":
2189
+ return node.html;
2190
+ case "boundary":
2191
+ return markup(c, node.children, render);
2192
+ case "failed":
2193
+ leftOut(c, node);
2194
+ return "";
2195
+ default:
2196
+ return render(element(c, node));
2197
+ }
2198
+ }).join("");
2199
+ }
2200
+ var keyed = (list) => list.map((child, i) => React4.isValidElement(child) && child.key == null ? React4.cloneElement(child, { key: `k${i}` }) : child);
2201
+ function element(c, node) {
2202
+ const r = recorded(c, node);
2203
+ if (node.kind === "block") return React4.createElement(r.type, resolvedProps(c, r.props));
2204
+ if (node.kind === "column") return React4.createElement(Column_default, { ...r.props, children: keyed(elements(c, node.children, "column")) });
2205
+ const props = { ...r.props };
2206
+ const children = keyed(elements(c, node.children, "row"));
2207
+ if (props.layout) {
2208
+ const columns = children.filter((child) => React4.isValidElement(child) && child.type === Column_default).length;
2209
+ if (columns === props.layout.expectedColumns || React4.Children.count(props.children) === props.layout.expectedColumns) {
2210
+ props.cells = props.layout.cells;
2211
+ delete props.layout;
2212
+ }
2213
+ }
2214
+ return React4.createElement(Row_default, { ...props, children });
2215
+ }
2216
+ function spanned(nodes) {
2217
+ const out = [];
2218
+ const spans = [];
2219
+ for (const node of nodes) {
2220
+ if (node.kind !== "raw") {
2221
+ out.push({ node, inside: spans.length > 0 });
2222
+ continue;
2223
+ }
2224
+ const html = node.html;
2225
+ const open = [];
2226
+ let closed = spans.length ? -1 : 0;
2227
+ for (const m of html.matchAll(/<!--[\s\S]*?(?:-->|$)|<(\/?)([a-zA-Z][\w:-]*)\b[^>]*>/g)) {
2228
+ if (!m[2]) continue;
2229
+ const name = m[2].toLowerCase();
2230
+ if (m[1]) {
2231
+ const i = open.map((o) => o.name).lastIndexOf(name);
2232
+ const across = spans.lastIndexOf(name);
2233
+ if (i >= 0) open.length = i;
2234
+ else if (across >= 0) {
2235
+ spans.length = across;
2236
+ open.length = 0;
2237
+ if (!spans.length) closed = m.index + m[0].length;
2238
+ }
2239
+ } else if (!VOID.test(name) && !m[0].endsWith("/>")) open.push({ name, at: m.index });
2240
+ }
2241
+ const piece = (from, to, inside) => {
2242
+ if (to > from) out.push({ node: { kind: "raw", html: html.slice(from, to) }, inside });
2243
+ };
2244
+ if (closed < 0) piece(0, html.length, true);
2245
+ else {
2246
+ piece(0, closed, true);
2247
+ piece(closed, open[0]?.at ?? html.length, false);
2248
+ if (open.length) piece(open[0].at, html.length, true);
2249
+ }
2250
+ spans.push(...open.map((o) => o.name));
2251
+ }
2252
+ return out;
2253
+ }
2254
+ function elements(c, nodes, place, owner, inMarkup = false) {
2255
+ const inColumn = place === "column";
2256
+ return (place === "root" || inMarkup ? nodes.map((node) => ({ node, inside: inMarkup })) : spanned(nodes)).flatMap(({ node, inside }) => {
2257
+ switch (node.kind) {
2258
+ case "failed":
2259
+ return leftOut(c, node);
2260
+ case "boundary": {
2261
+ const kept = inColumn && !inside ? keptMarkup(c, node) : void 0;
2262
+ if (kept) return [kept];
2263
+ const Named = () => null;
2264
+ Named.displayName = nameOf(recorded(c, node).element);
2265
+ if (inColumn && !inside && !c.told.has(node.at) && node.children.some((n) => n.kind === "raw" && parts(n.html).some((p) => "tag" in p))) {
2266
+ c.told.add(node.at);
2267
+ console.warn(`Column: <${Named.displayName}> rendered markup that isn't a block, so it's left out. Put HTML in an <Html> block.`);
2268
+ }
2269
+ return elements(c, node.children, place, Named, inside);
2270
+ }
2271
+ case "raw":
2272
+ if (inside) {
2273
+ const name = /^<([a-zA-Z][\w:-]*)/.exec(node.html)?.[1];
2274
+ return place === "row" && name && !inMarkup ? [React4.createElement(owner ?? name)] : [];
2275
+ }
2276
+ return parts(node.html).flatMap((p) => "text" in p ? [p.text] : inColumn ? [] : [React4.createElement(owner ?? (p.tag === "#" ? "div" : p.tag))]);
2277
+ default:
2278
+ return [element(c, node)];
2279
+ }
2280
+ });
2281
+ }
2282
+ function collected(children, guarded2 = true) {
2283
+ const once = (markup2) => {
2284
+ const collection = { nonce: nonce(), recorded: [], ...markup2 ? { markup: markup2 } : {}, ...idsSeen() ? {} : { blind: true } };
2285
+ const html = within(collection, () => renderToStaticMarkup(/* @__PURE__ */ jsx(Fragment, { children: guarded2 ? guard(children, collection) : children }), { identifierPrefix: COLLECTING_ID }));
2286
+ return { nodes: parse(html, collection), collection, told: /* @__PURE__ */ new Set() };
2287
+ };
2288
+ try {
2289
+ return once(false);
2290
+ } catch (error) {
2291
+ if (!guarded2) throw error;
2292
+ return once(true);
2293
+ }
2294
+ }
2295
+ function idsSeen() {
2296
+ let seen = false;
2297
+ const Probe = () => {
2298
+ try {
2299
+ seen = React4.useId().slice(1).startsWith(COLLECTING_ID);
2300
+ } catch {
2301
+ }
2302
+ return null;
2303
+ };
2304
+ renderToStaticMarkup(/* @__PURE__ */ jsx(Probe, {}), { identifierPrefix: COLLECTING_ID });
2305
+ return seen;
2306
+ }
2307
+ var RENDERS = Symbol.for("@unlayer/react-elements/renders");
2308
+ function nonce() {
2309
+ const shared = globalThis;
2310
+ const clock = globalThis.performance;
2311
+ return [shared[RENDERS] = (shared[RENDERS] ?? 0) + 1, (clock?.timeOrigin ?? 0) * 1e3, (clock?.now() ?? 0) * 1e6].map((n) => Math.floor(n).toString(36)).join("_");
2312
+ }
2313
+ var collect = (children) => children !== null && typeof children === "object" ? scopeCache(children, () => collected(children)) : collected(children);
2314
+ function childrenHtml2(children, place) {
2315
+ const c = collect(children);
2316
+ return withoutMarkers(markup(c, c.nodes, (child) => within(null, () => renderToString(place(child)))), c.collection.nonce);
2317
+ }
2318
+ function normalizedChildren(children) {
2319
+ const c = collect(children);
2320
+ return elements(c, c.nodes, "root");
2321
+ }
2322
+ function normalizedRoot(root) {
2323
+ return React4.cloneElement(root, void 0, ...normalizedChildren(root.props.children));
2324
+ }
1516
2325
  var DEFAULT_VALUES14 = BODY_DEFAULTS;
1517
2326
  var MAX_PREVIEW_LENGTH = 150;
1518
2327
  var PADDING_CHARS = [
@@ -1524,6 +2333,9 @@ var PADDING_CHARS = [
1524
2333
  "\u200F",
1525
2334
  "\uFEFF"
1526
2335
  ];
2336
+ function escapePreviewText(text) {
2337
+ return text.split(/(<%[\w.]+%>)/).map((part, i) => i % 2 ? part : part.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).join("");
2338
+ }
1527
2339
  function generatePreviewHtml(text) {
1528
2340
  if (!text || text.trim().length === 0) return "";
1529
2341
  const truncated = text.length > MAX_PREVIEW_LENGTH ? text.slice(0, MAX_PREVIEW_LENGTH) : text;
@@ -1532,7 +2344,7 @@ function generatePreviewHtml(text) {
1532
2344
  for (let i = 0; i < paddingLength; i++) {
1533
2345
  padding += PADDING_CHARS[i % PADDING_CHARS.length];
1534
2346
  }
1535
- return `<div data-skip-in-text="true" style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">` + truncated + padding + `</div>`;
2347
+ return `<div data-skip-in-text="true" style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">` + escapePreviewText(truncated) + padding + `</div>`;
1536
2348
  }
1537
2349
  function renderBodyToHtml(innerHTML, values, mode, previewText) {
1538
2350
  let finalInnerHtml = innerHTML;
@@ -1553,7 +2365,7 @@ function renderBodyToHtml(innerHTML, values, mode, previewText) {
1553
2365
  return raw.replace("min-height: 100vh; ", "").replace("min-height: 100vh;", "");
1554
2366
  }
1555
2367
  var Body = (props) => {
1556
- const { children, mode: modeProp, className, style, index = 0, config: configProp, previewText, ...semanticProps } = props;
2368
+ const { children, mode: modeProp, className, style, index = 0, config: configProp, previewText, fonts: _fonts, lang: _lang, ...semanticProps } = props;
1557
2369
  const resolvedConfig = { ...DEFAULT_CONFIG, ...configProp };
1558
2370
  const mode = modeProp ?? resolvedConfig.mode ?? "web";
1559
2371
  const _config = { ...resolvedConfig, mode };
@@ -1570,22 +2382,12 @@ var Body = (props) => {
1570
2382
  ...values._meta || {}
1571
2383
  }
1572
2384
  };
1573
- let enrichedChildren = children;
1574
- if (children) {
1575
- enrichedChildren = React.Children.map(children, (child) => {
1576
- if (React.isValidElement(child)) {
1577
- return React.cloneElement(child, {
1578
- _config,
1579
- bodyValues: values
1580
- });
1581
- }
1582
- return child;
1583
- });
1584
- }
1585
2385
  let innerHTML = "";
1586
- if (enrichedChildren) {
2386
+ if (children) {
1587
2387
  try {
1588
- innerHTML = ReactDOMServer.renderToString(enrichedChildren);
2388
+ innerHTML = withRenderScope(() => {
2389
+ return childrenHtml2(children, (child) => React4.cloneElement(child, { _config, bodyValues: values }));
2390
+ }, true);
1589
2391
  } catch (error) {
1590
2392
  console.error("Body: Failed to render children:", error);
1591
2393
  innerHTML = "";
@@ -1643,29 +2445,104 @@ UnlayerProvider.displayName = "UnlayerProvider";
1643
2445
  function useUnlayerConfig() {
1644
2446
  return useContext(getUnlayerContext());
1645
2447
  }
1646
- function ensureMeta(values, type, index = 0) {
1647
- return {
1648
- ...values,
1649
- _meta: {
1650
- htmlID: `u_content_${type.toLowerCase()}_${index + 1}`,
1651
- htmlClassNames: `u_content_${type.toLowerCase()}`,
1652
- ...values._meta || {}
2448
+
2449
+ // src/utils/device-overrides.ts
2450
+ var contentOptions = {
2451
+ Button: ["size", "fontSize", "lineHeight", "textAlign", "padding"],
2452
+ Image: ["src", "textAlign"],
2453
+ Heading: ["fontSize", "textAlign", "lineHeight"],
2454
+ Paragraph: ["fontSize", "textAlign", "lineHeight"],
2455
+ Menu: ["fontSize", "padding"],
2456
+ Divider: ["width", "textAlign"]
2457
+ };
2458
+ function css(value) {
2459
+ const text = String(value).trim();
2460
+ return text && !/[!\\"'\n\r]/.test(text) && cssValueSafe(text) ? text : void 0;
2461
+ }
2462
+ function ident(id) {
2463
+ return id.replace(/^-?\d|^-$|[^\w\u0080-\uffff-]/g, (c) => /\d/.test(c.slice(-1)) ? `${c.slice(0, -1)}\\${c.slice(-1).charCodeAt(0).toString(16)} ` : `\\${c}`);
2464
+ }
2465
+ function size(value) {
2466
+ const number = parseFloat(String(value));
2467
+ if (!Number.isFinite(number)) return;
2468
+ const rounded = parseFloat(number.toFixed(2));
2469
+ if (typeof value === "number" || String(value).endsWith("px")) return `${rounded}px`;
2470
+ if (String(value).endsWith("%")) return `${rounded}%`;
2471
+ }
2472
+ function collectDeviceStyles(values, collection, name, mode, styles) {
2473
+ const given = safeName(values._meta?.htmlID);
2474
+ if (!given) return;
2475
+ const id = ident(given);
2476
+ const options = collection === "contents" ? ["containerPadding", ...contentOptions[name] || []] : collection === "columns" ? ["padding", "border"] : ["padding"];
2477
+ for (const device of mode === "document" ? ["desktop"] : ["desktop", "mobile"]) {
2478
+ const overrides = values._override?.[device];
2479
+ if (!overrides) continue;
2480
+ for (const option of options) {
2481
+ if (!(option in overrides)) continue;
2482
+ const value = overrides[option];
2483
+ const property = option.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
2484
+ const prefix = collection === "rows" ? "row-" : collection === "columns" ? "col-" : "";
2485
+ const selector = (attribute) => `#${id} .v-${prefix}${property === attribute ? property : `${property}-${attribute}`}`;
2486
+ const push = (rule) => (styles[device] || (styles[device] = [])).push(rule);
2487
+ if (option === "padding" || option === "containerPadding") {
2488
+ if (!(value || value === 0) || css(value) === void 0) continue;
2489
+ const direct = collection === "rows" || mode === "web" && collection === "contents" && option === "containerPadding";
2490
+ const target = direct ? selector("padding").replace(`#${id} `, `#${id}`) : selector("padding");
2491
+ push(`${target} { padding: ${css(value)} !important; }`);
2492
+ if (mode === "email" && collection === "rows") {
2493
+ const parts2 = String(value).trim().split(/\s+/);
2494
+ const [top, bottom] = [parts2[0], parts2[2] || parts2[0]].map((part) => size(part) ?? css(part));
2495
+ if (top !== void 0 && bottom !== void 0) push(`${target}--vertical { padding-top: ${top} !important; padding-bottom: ${bottom} !important; }`);
2496
+ }
2497
+ } else if (option === "border") {
2498
+ if (value) push(`${selector("border")} { ${["Top", "Left", "Right", "Bottom"].map((d) => {
2499
+ const w = css(value[`border${d}Width`] || "0px") ?? "0px";
2500
+ return `border-${d.toLowerCase()}: ${w} ${css(value[`border${d}Style`] || "solid") ?? "solid"} ${parseInt(w) > 0 && css(value[`border${d}Color`]) || "transparent"} !important;`;
2501
+ }).join("")} }`);
2502
+ } else if (option === "src") {
2503
+ if (!value) continue;
2504
+ const merged = { ...values.src, ...value };
2505
+ const maximum = merged.autoWidth === false ? size(merged.maxWidth) : void 0;
2506
+ push(`${selector("width")} { width: ${maximum || "100%"} !important; }`);
2507
+ push(`${selector("max-width")} { max-width: ${size(maximum || merged.width || "100%")} !important; }`);
2508
+ } else if (option === "size" || option === "width") {
2509
+ if (!value) continue;
2510
+ push(`${selector("width")} { width: ${size(value.width) && !value.autoWidth ? size(value.width) : "auto"} !important; }`);
2511
+ push(`${selector("max-width")} { max-width: 100% !important; }`);
2512
+ } else {
2513
+ if (!(value || option === "lineHeight" && `${value}` === "0")) continue;
2514
+ const rendered = css(option === "fontSize" && typeof value === "number" ? `${value}px` : value);
2515
+ if (rendered === void 0) continue;
2516
+ push(`${selector(property)} { ${property}: ${rendered} !important; }`);
2517
+ }
1653
2518
  }
1654
- };
2519
+ }
1655
2520
  }
1656
- function getDisplayName(element) {
1657
- const type = element.type;
1658
- return type?.displayName || type?.name;
2521
+ function deviceStylesCss(styles, mode) {
2522
+ return Object.entries(styles).filter(([, rules]) => rules.length).map(([device, rules]) => {
2523
+ const query = device === "mobile" ? "@media (max-width: 480px)" : `@media (min-width: ${mode === "document" ? 1 : 481}px)`;
2524
+ return `${query} { ${rules.join(" ")} }`;
2525
+ }).join("\n");
1659
2526
  }
1660
- function collectChildren(node) {
1661
- const result = [];
1662
- React.Children.forEach(node, (child) => {
1663
- if (React.isValidElement(child)) {
1664
- result.push(child);
1665
- }
1666
- });
1667
- return result;
2527
+ function deviceVisibilityCss(mode) {
2528
+ if (mode === "document") return "";
2529
+ const extra = mode === "email" ? "max-height: 0px; overflow: hidden; " : "";
2530
+ const reset = mode === "email" ? "@media (min-width: 0px) { .hide-default__display-block { display: block !important; mso-hide: unset !important; } .hide-default__display-table { display: table !important; mso-hide: unset !important; } }\n" : "";
2531
+ return reset + `@media (max-width: 480px) { .hide-mobile { ${extra}display: none !important; } }
2532
+ @media (min-width: 481px) { .hide-desktop { ${extra}display: none !important; } }`;
2533
+ }
2534
+ var schemaVersion = schemaVersion$1 ?? 24;
2535
+ function unwrapForJson(element2) {
2536
+ try {
2537
+ return unwrapRoot(element2);
2538
+ } catch (cause) {
2539
+ const detail = cause instanceof Error ? cause.message : String(cause);
2540
+ throw new Error(
2541
+ `[Unlayer] renderToJson: could not unwrap <${componentName(element2) || "wrapper"}>. ${UNWRAP_ADVICE} (${detail})`
2542
+ );
2543
+ }
1668
2544
  }
2545
+ var collectChildren = (node) => React4.Children.toArray(node).filter(React4.isValidElement);
1669
2546
  function extractSemanticProps(props, extraKeys = []) {
1670
2547
  const internalKeys = /* @__PURE__ */ new Set([
1671
2548
  "children",
@@ -1680,6 +2557,8 @@ function extractSemanticProps(props, extraKeys = []) {
1680
2557
  "_config",
1681
2558
  "config",
1682
2559
  "previewText",
2560
+ "fonts",
2561
+ "lang",
1683
2562
  "layout",
1684
2563
  "collection",
1685
2564
  ...extraKeys
@@ -1696,68 +2575,313 @@ function nextCounter(counters, key) {
1696
2575
  counters[key] = (counters[key] || 0) + 1;
1697
2576
  return counters[key];
1698
2577
  }
1699
- function callHead(head, values, bodyValues, displayMode, headConfig, styles, scripts, tags) {
1700
- if (!head) return;
1701
- const headArgs = [
1702
- values,
1703
- bodyValues,
1704
- {
1705
- displayMode,
1706
- isViewer: false,
1707
- variant: null,
1708
- type: "rows",
1709
- headConfig
2578
+ function toContentType(displayName) {
2579
+ if (displayName === "Paragraph") return "text";
2580
+ return displayName.toLowerCase();
2581
+ }
2582
+ function makeId(prefix, counter) {
2583
+ return `${prefix}_${counter}`;
2584
+ }
2585
+ function extractTextFromTextJson(textJson) {
2586
+ try {
2587
+ const parsed = JSON.parse(textJson);
2588
+ if (parsed.__html) return parsed.__html;
2589
+ const root = parsed?.root;
2590
+ if (!root?.children) return "";
2591
+ const paragraphs = [];
2592
+ for (const paragraph of root.children) {
2593
+ if (!paragraph?.children) continue;
2594
+ const parts2 = [];
2595
+ for (const node of paragraph.children) {
2596
+ if (node?.text) parts2.push(node.text);
2597
+ }
2598
+ paragraphs.push(parts2.join(""));
1710
2599
  }
1711
- ];
1712
- const css = head.css?.(...headArgs);
1713
- if (css) styles.push(css);
1714
- const js = head.js?.(...headArgs);
1715
- if (js) scripts.push(js);
1716
- const headTags = head.tags?.(...headArgs);
1717
- if (headTags) tags.push(...headTags);
2600
+ return paragraphs.join("\n");
2601
+ } catch {
2602
+ return "";
2603
+ }
1718
2604
  }
1719
- function walkItem(element, bodyValues, displayMode, headConfig, counters, styles, scripts, tags) {
1720
- const componentType = element.type;
2605
+ function processItem(element2, counters, layout = {}, metaName) {
2606
+ const componentType = element2.type;
1721
2607
  const config = componentType[UNLAYER_CONFIG_KEY];
1722
- if (!config) return;
1723
- const { name, defaultValues, propMapper } = config;
1724
- const { children, ...restProps } = element.props;
2608
+ if (!config && componentType?.[KEPT]) {
2609
+ return processItem(componentType[KEPT], counters, layout, (componentType.displayName || componentType.name || "component").toLowerCase());
2610
+ }
2611
+ if (!config) {
2612
+ const name2 = componentName(element2) || "Unknown";
2613
+ throw new Error(
2614
+ `[Unlayer] renderToJson: <${name2}> is not a recognized Unlayer item component. Only components created with createItemComponent are supported.`
2615
+ );
2616
+ }
2617
+ const { name, defaultValues, propMapper, slug } = config;
2618
+ const contentType = config.contentType ?? toContentType(name);
2619
+ const metaBase = `u_content_${metaName ?? config.metaName ?? contentType}`;
2620
+ const count = nextCounter(counters, metaBase);
2621
+ const id = makeId(metaBase, count);
2622
+ const { children, ...restProps } = element2.props;
1725
2623
  const mappedValues = propMapper({ children, ...restProps });
1726
2624
  const finalValues = mergeValues(defaultValues, mappedValues);
1727
- const contentType = config.metaName ?? name.toLowerCase();
2625
+ if (contentType === "text" && finalValues.textJson) {
2626
+ finalValues.text = extractTextFromTextJson(finalValues.textJson);
2627
+ delete finalValues.textJson;
2628
+ }
2629
+ const values = {
2630
+ containerPadding: "10px",
2631
+ ...finalValues,
2632
+ _meta: {
2633
+ htmlID: id,
2634
+ htmlClassNames: metaBase,
2635
+ ...finalValues._meta || {}
2636
+ },
2637
+ selectable: true,
2638
+ draggable: true,
2639
+ duplicatable: true,
2640
+ deletable: true,
2641
+ hideable: true
2642
+ };
2643
+ const itemSrc = values.src;
2644
+ if (itemSrc && typeof itemSrc === "object" && itemSrc.autoWidth === false) {
2645
+ const availableWidth = contentSlotWidth({
2646
+ ...layout,
2647
+ containerPadding: values.containerPadding
2648
+ });
2649
+ values.src = pinImageSrc(itemSrc, availableWidth);
2650
+ }
2651
+ return slug ? { type: contentType, slug, values } : { type: contentType, values };
2652
+ }
2653
+ function processColumn(element2, counters, layout = {}) {
2654
+ const count = nextCounter(counters, "u_column");
2655
+ const id = makeId("u_column", count);
2656
+ const semanticProps = extractSemanticProps(element2.props);
2657
+ const mapped = mapSemanticProps(semanticProps, COLUMN_DEFAULTS, "Column");
2658
+ const values = mergeValues(COLUMN_DEFAULTS, mapped);
2659
+ const valuesWithMeta = {
2660
+ ...values,
2661
+ _meta: {
2662
+ htmlID: id,
2663
+ htmlClassNames: "u_column",
2664
+ ...values._meta || {}
2665
+ }
2666
+ };
2667
+ const contents = [];
2668
+ const children = collectChildren(element2.props.children);
2669
+ const itemLayout = { ...layout, columnValues: valuesWithMeta };
2670
+ for (const child of children) {
2671
+ contents.push(processItem(child, counters, itemLayout));
2672
+ }
2673
+ return { contents, values: valuesWithMeta };
2674
+ }
2675
+ function processRow(element2, counters, parentLayout = {}) {
2676
+ const count = nextCounter(counters, "u_row");
2677
+ const id = makeId("u_row", count);
2678
+ const { layout, cells: propsCells } = element2.props;
2679
+ let cells;
2680
+ if (layout) {
2681
+ cells = layout.cells;
2682
+ } else if (propsCells) {
2683
+ cells = propsCells;
2684
+ } else {
2685
+ const columnCount = Math.max(
2686
+ 1,
2687
+ collectChildren(element2.props.children).filter(
2688
+ (child) => getDisplayName(child) === "Column"
2689
+ ).length
2690
+ );
2691
+ cells = Array(columnCount).fill(1);
2692
+ }
2693
+ const semanticProps = extractSemanticProps(element2.props, ["layout"]);
2694
+ const mapped = mapSemanticProps(semanticProps, ROW_DEFAULTS, "Row");
2695
+ const values = mergeValues(ROW_DEFAULTS, mapped);
2696
+ const valuesWithMeta = {
2697
+ ...values,
2698
+ columns: values.columns ?? false,
2699
+ cells,
2700
+ _meta: {
2701
+ htmlID: id,
2702
+ htmlClassNames: "u_row",
2703
+ ...values._meta || {}
2704
+ },
2705
+ selectable: true,
2706
+ draggable: true,
2707
+ duplicatable: true,
2708
+ deletable: true,
2709
+ hideable: true
2710
+ };
2711
+ const columns = [];
2712
+ const children = collectChildren(element2.props.children);
2713
+ const columnLayout = {
2714
+ bodyValues: parentLayout.bodyValues,
2715
+ rowValues: valuesWithMeta,
2716
+ rowCells: cells
2717
+ };
2718
+ let columnIndex = 0;
2719
+ for (const child of children) {
2720
+ const name = getDisplayName(child);
2721
+ if (name === "Column") {
2722
+ columns.push(
2723
+ processColumn(child, counters, { ...columnLayout, columnIndex })
2724
+ );
2725
+ columnIndex += 1;
2726
+ } else {
2727
+ console.warn(
2728
+ `[Unlayer] renderToJson: <${componentName(child) ?? String(child.type)}> is not a valid Row child. Only <Column> is allowed.`
2729
+ );
2730
+ }
2731
+ }
2732
+ return { cells, columns, values: valuesWithMeta };
2733
+ }
2734
+ function processBody(element2, counters) {
2735
+ const id = "u_body";
2736
+ const semanticProps = extractSemanticProps(element2.props);
2737
+ const mapped = mapSemanticProps(semanticProps, BODY_DEFAULTS, "Body");
2738
+ const values = mergeValues(BODY_DEFAULTS, mapped);
2739
+ if ("textDirection" in values) {
2740
+ const direction = textDirectionOf(values.textDirection);
2741
+ if (direction) values.textDirection = direction;
2742
+ else delete values.textDirection;
2743
+ }
2744
+ const previewText = element2.props.previewText;
2745
+ if (previewText !== void 0) {
2746
+ values.preheaderText = previewText;
2747
+ }
2748
+ const valuesWithMeta = {
2749
+ ...values,
2750
+ _meta: {
2751
+ htmlID: id,
2752
+ htmlClassNames: "u_body",
2753
+ ...values._meta || {}
2754
+ }
2755
+ };
2756
+ const rows = [];
2757
+ const children = collectChildren(element2.props.children);
2758
+ for (const child of children) {
2759
+ const name = getDisplayName(child);
2760
+ if (name === "Row") {
2761
+ rows.push(processRow(child, counters, { bodyValues: valuesWithMeta }));
2762
+ } else {
2763
+ console.warn(
2764
+ `[Unlayer] renderToJson: <${componentName(child) ?? String(child.type)}> is not a valid Body child. Only <Row> is allowed.`
2765
+ );
2766
+ }
2767
+ }
2768
+ return {
2769
+ rows,
2770
+ values: valuesWithMeta
2771
+ };
2772
+ }
2773
+ function renderRowToJson(element2) {
2774
+ const displayName = getDisplayName(element2);
2775
+ if (displayName !== "Row") {
2776
+ throw new Error(
2777
+ `[Unlayer] renderRowToJson: Element must be <Row>, but got <${componentName(element2) || "unknown"}>. For full designs, use renderToJson instead.`
2778
+ );
2779
+ }
2780
+ const counters = {};
2781
+ return withRenderScope(() => processRow(normalizedChildren(element2).find((c) => React4.isValidElement(c)) ?? element2, counters));
2782
+ }
2783
+ function renderToJson(element2) {
2784
+ return withRenderScope(() => designJson(element2));
2785
+ }
2786
+ function designJson(element2) {
2787
+ element2 = unwrapForJson(element2);
2788
+ const displayName = getDisplayName(element2);
2789
+ if (displayName && ROOT_NAMES.has(displayName)) element2 = normalizedRoot(element2);
2790
+ if (!displayName || !ROOT_NAMES.has(displayName)) {
2791
+ throw new Error(
2792
+ `[Unlayer] renderToJson: Root element must be <Body>, <Email>, <Page>, or <Document>, but got <${componentName(element2) || "unknown"}>. Wrap your content: <Body><Row><Column>...</Column></Row></Body>`
2793
+ );
2794
+ }
2795
+ const counters = {};
2796
+ const body = processBody(element2, counters);
2797
+ return {
2798
+ counters,
2799
+ body,
2800
+ schemaVersion
2801
+ };
2802
+ }
2803
+
2804
+ // src/utils/extract-head.ts
2805
+ function ensureMeta(values, type, index = 0) {
2806
+ return {
2807
+ ...values,
2808
+ _meta: {
2809
+ htmlID: `${["row", "column"].includes(type) ? "u" : "u_content"}_${type.toLowerCase()}_${index + 1}`,
2810
+ htmlClassNames: `u_content_${type.toLowerCase()}`,
2811
+ ...values._meta || {}
2812
+ }
2813
+ };
2814
+ }
2815
+ var collectChildren2 = (node) => React4.Children.toArray(node).filter(React4.isValidElement);
2816
+ function callHead(head, values, bodyValues, displayMode, headConfig, styles, scripts, tags) {
2817
+ if (!head) return;
2818
+ const headArgs = [
2819
+ values,
2820
+ bodyValues,
2821
+ {
2822
+ displayMode,
2823
+ isViewer: false,
2824
+ variant: null,
2825
+ type: "rows",
2826
+ headConfig
2827
+ }
2828
+ ];
2829
+ const css2 = head.css?.(...headArgs);
2830
+ if (css2) styles.push(css2);
2831
+ const js = head.js?.(...headArgs);
2832
+ if (js) scripts.push(js);
2833
+ const headTags = head.tags?.(...headArgs);
2834
+ if (headTags) tags.push(...headTags);
2835
+ }
2836
+ function walkItem(element2, bodyValues, displayMode, headConfig, counters, styles, scripts, tags, deviceStyles) {
2837
+ const componentType = element2.type;
2838
+ const config = componentType[UNLAYER_CONFIG_KEY];
2839
+ if (!config) {
2840
+ if (componentType?.[KEPT]) nextCounter(counters, `u_content_${(componentType.displayName || componentType.name || "component").toLowerCase()}`);
2841
+ return;
2842
+ }
2843
+ const { name, defaultValues, propMapper } = config;
2844
+ const { children, ...restProps } = element2.props;
2845
+ const mappedValues = propMapper({ children, ...restProps });
2846
+ const finalValues = mergeValues(defaultValues, mappedValues);
2847
+ const contentType = config.metaName ?? name.toLowerCase();
1728
2848
  const count = nextCounter(counters, `u_content_${contentType}`);
1729
2849
  const valuesWithMeta = ensureMeta(finalValues, contentType, count - 1);
2850
+ collectDeviceStyles(valuesWithMeta, "contents", name, displayMode, deviceStyles);
1730
2851
  const head = config.head ?? heads[name];
1731
2852
  callHead(head, valuesWithMeta, bodyValues, displayMode, headConfig, styles, scripts, tags);
1732
2853
  }
1733
- function walkColumn(element, bodyValues, rowValues, displayMode, headConfig, counters, styles, scripts, tags) {
1734
- const semanticProps = extractSemanticProps(element.props);
2854
+ function walkColumn(element2, bodyValues, rowValues, displayMode, headConfig, counters, styles, scripts, tags, deviceStyles) {
2855
+ const semanticProps = extractSemanticProps(element2.props);
1735
2856
  const mapped = mapSemanticProps(semanticProps, COLUMN_DEFAULTS, "Column");
1736
2857
  const count = nextCounter(counters, "u_column");
1737
2858
  const columnValues = ensureMeta(mergeValues(COLUMN_DEFAULTS, mapped), "column", count - 1);
2859
+ collectDeviceStyles(columnValues, "columns", "Column", displayMode, deviceStyles);
1738
2860
  const columnHead = heads["Column"];
1739
2861
  callHead(columnHead, columnValues, bodyValues, displayMode, headConfig, styles, scripts, tags);
1740
- const children = collectChildren(element.props.children);
2862
+ const children = collectChildren2(element2.props.children);
1741
2863
  for (const child of children) {
1742
- walkItem(child, bodyValues, displayMode, headConfig, counters, styles, scripts, tags);
2864
+ walkItem(child, bodyValues, displayMode, headConfig, counters, styles, scripts, tags, deviceStyles);
1743
2865
  }
1744
2866
  }
1745
- function walkRow(element, bodyValues, displayMode, headConfig, counters, styles, scripts, tags) {
1746
- const semanticProps = extractSemanticProps(element.props, ["layout"]);
2867
+ function walkRow(element2, bodyValues, displayMode, headConfig, counters, styles, scripts, tags, deviceStyles) {
2868
+ const semanticProps = extractSemanticProps(element2.props, ["layout"]);
1747
2869
  const mapped = mapSemanticProps(semanticProps, ROW_DEFAULTS, "Row");
1748
2870
  const count = nextCounter(counters, "u_row");
1749
2871
  const rowValues = ensureMeta(mergeValues(ROW_DEFAULTS, mapped), "row", count - 1);
2872
+ collectDeviceStyles(rowValues, "rows", "Row", displayMode, deviceStyles);
1750
2873
  const rowHead = heads["Row"];
1751
2874
  callHead(rowHead, rowValues, bodyValues, displayMode, headConfig, styles, scripts, tags);
1752
- const children = collectChildren(element.props.children);
2875
+ const children = collectChildren2(element2.props.children);
1753
2876
  for (const child of children) {
1754
2877
  const name = getDisplayName(child);
1755
2878
  if (name === "Column") {
1756
- walkColumn(child, bodyValues, rowValues, displayMode, headConfig, counters, styles, scripts, tags);
2879
+ walkColumn(child, bodyValues, rowValues, displayMode, headConfig, counters, styles, scripts, tags, deviceStyles);
1757
2880
  }
1758
2881
  }
1759
2882
  }
1760
- function extractHeadFromTree(element, options) {
2883
+ function extractHeadFromTree(element2, options) {
2884
+ element2 = normalizedRoot(element2);
1761
2885
  const { displayMode } = options;
1762
2886
  const headConfig = {
1763
2887
  hasFeature: options.headConfig?.hasFeature ?? (() => false),
@@ -1767,18 +2891,22 @@ function extractHeadFromTree(element, options) {
1767
2891
  const scripts = [];
1768
2892
  const tags = [];
1769
2893
  const counters = {};
1770
- const semanticProps = extractSemanticProps(element.props);
2894
+ const deviceStyles = {};
2895
+ const semanticProps = extractSemanticProps(element2.props);
1771
2896
  const mapped = mapSemanticProps(semanticProps, BODY_DEFAULTS, "Body");
1772
2897
  const bodyValues = ensureMeta(mergeValues(BODY_DEFAULTS, mapped), "body", 0);
1773
2898
  const bodyHead = heads["Body"];
1774
2899
  callHead(bodyHead, bodyValues, bodyValues, displayMode, headConfig, styles, scripts, tags);
1775
- const children = collectChildren(element.props.children);
2900
+ const children = collectChildren2(element2.props.children);
1776
2901
  for (const child of children) {
1777
2902
  const name = getDisplayName(child);
1778
2903
  if (name === "Row") {
1779
- walkRow(child, bodyValues, displayMode, headConfig, counters, styles, scripts, tags);
2904
+ walkRow(child, bodyValues, displayMode, headConfig, counters, styles, scripts, tags, deviceStyles);
1780
2905
  }
1781
2906
  }
2907
+ styles.unshift(deviceVisibilityCss(displayMode));
2908
+ const overrides = deviceStylesCss(deviceStyles, displayMode);
2909
+ if (overrides) styles.push(overrides);
1782
2910
  const uniqueTags = [...new Set(tags.filter(Boolean))];
1783
2911
  return {
1784
2912
  css: styles.filter(Boolean).join("\n"),
@@ -1798,7 +2926,7 @@ function emailLayout(args) {
1798
2926
  fontLinks && `<!--[if !mso]><!-->${fontLinks}<!--<![endif]-->`
1799
2927
  ].filter(Boolean).join("\n ");
1800
2928
  return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1801
- <html${dir} xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
2929
+ <html${dir}${args.lang || ""} xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
1802
2930
  <head>
1803
2931
  <!--[if gte mso 9]>
1804
2932
  <xml>
@@ -1822,7 +2950,7 @@ function documentLayout(args) {
1822
2950
  const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
1823
2951
  const head = [titleTag, tagLines, styleTag, scriptTag, fontLinks].filter(Boolean).join("\n ");
1824
2952
  return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1825
- <html${dir} xmlns="http://www.w3.org/1999/xhtml">
2953
+ <html${dir}${args.lang || ""} xmlns="http://www.w3.org/1999/xhtml">
1826
2954
  <head>
1827
2955
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
1828
2956
  ${head}
@@ -1835,7 +2963,7 @@ function webLayout(args) {
1835
2963
  const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
1836
2964
  const head = [titleTag, fontLinks, tagLines, styleTag, scriptTag].filter(Boolean).join("\n ");
1837
2965
  return `<!doctype html>
1838
- <html${dir}>
2966
+ <html${dir}${args.lang || ""}>
1839
2967
  <head>
1840
2968
  <meta charset="utf-8">
1841
2969
  <meta http-equiv="x-ua-compatible" content="ie=edge">
@@ -1856,20 +2984,25 @@ var MODE_BY_WRAPPER = {
1856
2984
  Page: "web",
1857
2985
  Document: "document"
1858
2986
  };
1859
- function resolveDisplayMode(element, mergedConfig) {
1860
- const displayName = element.type?.displayName;
2987
+ function resolveConfig(element2, config) {
2988
+ const props = element2.props;
2989
+ const textDirection = textDirectionOf(props.textDirection ?? props.values?.textDirection);
2990
+ return { ...DEFAULT_CONFIG, ...textDirection !== void 0 ? { textDirection } : {}, ...config };
2991
+ }
2992
+ function resolveDisplayMode(element2, mergedConfig) {
2993
+ const displayName = element2.type?.displayName;
1861
2994
  const wrapperMode = displayName ? MODE_BY_WRAPPER[displayName] : void 0;
1862
- return wrapperMode ?? element.props.mode ?? mergedConfig.mode ?? "web";
2995
+ return wrapperMode ?? element2.props.mode ?? mergedConfig.mode ?? "web";
1863
2996
  }
1864
- function renderBody(element, config) {
1865
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
2997
+ function renderBody(element2, config, name = "renderToHtml") {
2998
+ const mergedConfig = resolveConfig(element2, config);
1866
2999
  try {
1867
- const enriched = React.cloneElement(element, { config: mergedConfig });
3000
+ const enriched = React4.cloneElement(element2, { config: mergedConfig });
1868
3001
  return renderToStaticMarkup(enriched);
1869
3002
  } catch (error) {
1870
3003
  const message = error instanceof Error ? error.message : String(error);
1871
3004
  throw new Error(
1872
- `[Unlayer] renderToHtml failed: ${message}
3005
+ `[Unlayer] ${name} failed: ${message}
1873
3006
  Tip: Ensure your tree uses Body > Row > Column > Item structure.`
1874
3007
  );
1875
3008
  }
@@ -1881,30 +3014,41 @@ function stripOuterDiv(html) {
1881
3014
  function escapeForHtml(text) {
1882
3015
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1883
3016
  }
1884
- function renderToHtml(element, options) {
1885
- const { title, fonts = [], ...config } = options ?? {};
1886
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1887
- const displayMode = resolveDisplayMode(element, mergedConfig);
1888
- const wrapperName = element.type?.displayName;
3017
+ function renderToHtml(element2, options) {
3018
+ return withRenderScope(() => htmlDocument(element2, options));
3019
+ }
3020
+ function styleText(css2) {
3021
+ return css2.replace(/<\/(style)/gi, "<\\/$1");
3022
+ }
3023
+ function htmlDocument(element2, options) {
3024
+ const { title, lang: optionLang, fonts: optionFonts = [], ...config } = options ?? {};
3025
+ element2 = htmlRoot(element2, "renderToHtml");
3026
+ const rootFonts = element2.props?.fonts ?? [];
3027
+ const fonts = [...rootFonts, ...optionFonts].filter((font, i, all) => font?.url && all.findIndex((f) => f?.url === font.url) === i);
3028
+ const lang = optionLang ?? element2.props.lang;
3029
+ const mergedConfig = resolveConfig(element2, config);
3030
+ const displayMode = resolveDisplayMode(element2, mergedConfig);
3031
+ const wrapperName = element2.type?.displayName;
1889
3032
  const isUnlayerWrapper = wrapperName === "Body" || (wrapperName ? wrapperName in MODE_BY_WRAPPER : false);
1890
- const rawBody = renderBody(element, config);
3033
+ const rawBody = renderBody(element2, config);
1891
3034
  const body = isUnlayerWrapper ? stripOuterDiv(rawBody) : rawBody;
1892
- const { css, js, tags } = extractHeadFromTree(element, {
3035
+ const { css: css2, js, tags } = extractHeadFromTree(element2, {
1893
3036
  displayMode,
1894
3037
  headConfig: mergedConfig.headConfig
1895
3038
  });
1896
3039
  const layoutArgs = {
3040
+ lang: lang ? ` lang="${escapeForHtml(String(lang))}"` : "",
1897
3041
  dir: mergedConfig.textDirection ? ` dir="${escapeForHtml(String(mergedConfig.textDirection))}"` : "",
1898
3042
  titleTag: title ? `<title>${escapeForHtml(title.trim())}</title>` : "",
1899
- styleTag: css ? `<style type="text/css">
1900
- ${css}
3043
+ styleTag: css2 ? `<style type="text/css">
3044
+ ${styleText(css2)}
1901
3045
  </style>` : "",
1902
3046
  scriptTag: js ? `<script type="application/javascript">
1903
3047
  ${js}
1904
3048
  </script>` : "",
1905
3049
  tagLines: tags.join("\n"),
1906
3050
  fontLinks: fonts.filter((font) => font?.url).map(
1907
- (font) => `<link href="${escapeForHtml(font.url)}" rel="stylesheet" type="text/css">`
3051
+ (font) => `<link href="${escapeForHtml(String(font.url))}" rel="stylesheet" type="text/css">`
1908
3052
  ).join(""),
1909
3053
  body
1910
3054
  };
@@ -1912,21 +3056,29 @@ ${js}
1912
3056
  if (displayMode === "document") return documentLayout(layoutArgs);
1913
3057
  return webLayout(layoutArgs);
1914
3058
  }
1915
- function renderToPlainText(element, config) {
1916
- const html = renderBody(element, config);
3059
+ function renderToPlainText(element2, config) {
3060
+ const html = withRenderScope(() => {
3061
+ const root = htmlRoot(element2, "renderToPlainText");
3062
+ normalizedRoot(root);
3063
+ return renderBody(root, config, "renderToPlainText");
3064
+ });
1917
3065
  return htmlToPlainText(html);
1918
3066
  }
1919
- function renderToHtmlParts(element, config) {
1920
- const body = renderBody(element, config);
1921
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1922
- const displayMode = resolveDisplayMode(element, mergedConfig);
1923
- const { css, js, tags } = extractHeadFromTree(element, {
3067
+ function renderToHtmlParts(element2, config) {
3068
+ return withRenderScope(() => htmlParts(element2, config));
3069
+ }
3070
+ function htmlParts(element2, config) {
3071
+ element2 = htmlRoot(element2, "renderToHtmlParts");
3072
+ const body = renderBody(element2, config, "renderToHtmlParts");
3073
+ const mergedConfig = resolveConfig(element2, config);
3074
+ const displayMode = resolveDisplayMode(element2, mergedConfig);
3075
+ const { css: css2, js, tags } = extractHeadFromTree(element2, {
1924
3076
  displayMode,
1925
3077
  headConfig: mergedConfig.headConfig
1926
3078
  });
1927
3079
  const headParts = [];
1928
- if (css) {
1929
- headParts.push(`<style>${css}</style>`);
3080
+ if (css2) {
3081
+ headParts.push(`<style>${styleText(css2)}</style>`);
1930
3082
  }
1931
3083
  if (js) {
1932
3084
  headParts.push(`<script>${js}</script>`);
@@ -1937,291 +3089,11 @@ function renderToHtmlParts(element, config) {
1937
3089
  return {
1938
3090
  head: headParts.join("\n"),
1939
3091
  body,
1940
- css,
3092
+ css: css2,
1941
3093
  js,
1942
3094
  tags
1943
3095
  };
1944
3096
  }
1945
- var schemaVersion = schemaVersion$1 ?? 24;
1946
- function getDisplayName2(element) {
1947
- const type = element.type;
1948
- return type?.displayName || type?.name;
1949
- }
1950
- var VALID_ROOTS = /* @__PURE__ */ new Set(["Body", "Email", "Page", "Document"]);
1951
- function unwrapRoot(element) {
1952
- let current = element;
1953
- for (let depth = 0; depth < 10; depth++) {
1954
- const name = getDisplayName2(current);
1955
- if (name && VALID_ROOTS.has(name)) break;
1956
- const type = current.type;
1957
- const isPlainFunctionComponent = typeof type === "function" && !type.prototype?.isReactComponent;
1958
- if (!isPlainFunctionComponent) break;
1959
- let produced;
1960
- try {
1961
- produced = type({ ...current.props });
1962
- } catch (cause) {
1963
- const detail = cause instanceof Error ? cause.message : String(cause);
1964
- throw new Error(
1965
- `[Unlayer] renderToJson: could not unwrap <${name || "wrapper"}>. A wrapper must be a plain component that synchronously returns a root (<Email>, <Page>, <Document>, or <Body>) and uses no React hooks. Pass the root element directly \u2014 e.g. renderToJson(<Email>\u2026</Email>). (${detail})`
1966
- );
1967
- }
1968
- if (!React.isValidElement(produced)) break;
1969
- current = produced;
1970
- }
1971
- return current;
1972
- }
1973
- function collectChildren2(node) {
1974
- const result = [];
1975
- React.Children.forEach(node, (child) => {
1976
- if (React.isValidElement(child)) {
1977
- result.push(child);
1978
- }
1979
- });
1980
- return result;
1981
- }
1982
- function extractSemanticProps2(props, extraKeys = []) {
1983
- const internalKeys = /* @__PURE__ */ new Set([
1984
- "children",
1985
- "mode",
1986
- "className",
1987
- "style",
1988
- "index",
1989
- "colIndex",
1990
- "cells",
1991
- "bodyValues",
1992
- "rowValues",
1993
- "_config",
1994
- "config",
1995
- "previewText",
1996
- "layout",
1997
- "collection",
1998
- ...extraKeys
1999
- ]);
2000
- const result = {};
2001
- for (const [key, value] of Object.entries(props)) {
2002
- if (!internalKeys.has(key) && value !== void 0) {
2003
- result[key] = value;
2004
- }
2005
- }
2006
- return result;
2007
- }
2008
- function nextCounter2(counters, key) {
2009
- counters[key] = (counters[key] || 0) + 1;
2010
- return counters[key];
2011
- }
2012
- function toContentType(displayName) {
2013
- if (displayName === "Paragraph") return "text";
2014
- return displayName.toLowerCase();
2015
- }
2016
- function makeId(prefix, counter) {
2017
- return `${prefix}_${counter}`;
2018
- }
2019
- function extractTextFromTextJson(textJson) {
2020
- try {
2021
- const parsed = JSON.parse(textJson);
2022
- if (parsed.__html) return parsed.__html;
2023
- const root = parsed?.root;
2024
- if (!root?.children) return "";
2025
- const paragraphs = [];
2026
- for (const paragraph of root.children) {
2027
- if (!paragraph?.children) continue;
2028
- const parts = [];
2029
- for (const node of paragraph.children) {
2030
- if (node?.text) parts.push(node.text);
2031
- }
2032
- paragraphs.push(parts.join(""));
2033
- }
2034
- return paragraphs.join("\n");
2035
- } catch {
2036
- return "";
2037
- }
2038
- }
2039
- function processItem(element, counters, layout = {}) {
2040
- const componentType = element.type;
2041
- const config = componentType[UNLAYER_CONFIG_KEY];
2042
- if (!config) {
2043
- const name2 = getDisplayName2(element) || "Unknown";
2044
- throw new Error(
2045
- `[Unlayer] renderToJson: <${name2}> is not a recognized Unlayer item component. Only components created with createItemComponent are supported.`
2046
- );
2047
- }
2048
- const { name, defaultValues, propMapper, slug } = config;
2049
- const contentType = config.contentType ?? toContentType(name);
2050
- const metaBase = `u_content_${config.metaName ?? contentType}`;
2051
- const count = nextCounter2(counters, metaBase);
2052
- const id = makeId(metaBase, count);
2053
- const { children, ...restProps } = element.props;
2054
- const mappedValues = propMapper({ children, ...restProps });
2055
- const finalValues = mergeValues(defaultValues, mappedValues);
2056
- if (contentType === "text" && finalValues.textJson) {
2057
- finalValues.text = extractTextFromTextJson(finalValues.textJson);
2058
- delete finalValues.textJson;
2059
- }
2060
- const values = {
2061
- containerPadding: "10px",
2062
- ...finalValues,
2063
- _meta: {
2064
- htmlID: id,
2065
- htmlClassNames: metaBase,
2066
- ...finalValues._meta || {}
2067
- },
2068
- selectable: true,
2069
- draggable: true,
2070
- duplicatable: true,
2071
- deletable: true,
2072
- hideable: true
2073
- };
2074
- const itemSrc = values.src;
2075
- if (itemSrc && typeof itemSrc === "object" && itemSrc.autoWidth === false) {
2076
- const availableWidth = contentSlotWidth({
2077
- ...layout,
2078
- containerPadding: values.containerPadding
2079
- });
2080
- values.src = pinImageSrc(itemSrc, availableWidth);
2081
- }
2082
- return slug ? { type: contentType, slug, values } : { type: contentType, values };
2083
- }
2084
- function processColumn(element, counters, layout = {}) {
2085
- const count = nextCounter2(counters, "u_column");
2086
- const id = makeId("u_column", count);
2087
- const semanticProps = extractSemanticProps2(element.props);
2088
- const mapped = mapSemanticProps(semanticProps, COLUMN_DEFAULTS, "Column");
2089
- const values = mergeValues(COLUMN_DEFAULTS, mapped);
2090
- const valuesWithMeta = {
2091
- ...values,
2092
- _meta: {
2093
- htmlID: id,
2094
- htmlClassNames: "u_column",
2095
- ...values._meta || {}
2096
- }
2097
- };
2098
- const contents = [];
2099
- const children = collectChildren2(element.props.children);
2100
- const itemLayout = { ...layout, columnValues: valuesWithMeta };
2101
- for (const child of children) {
2102
- contents.push(processItem(child, counters, itemLayout));
2103
- }
2104
- return { contents, values: valuesWithMeta };
2105
- }
2106
- function processRow(element, counters, parentLayout = {}) {
2107
- const count = nextCounter2(counters, "u_row");
2108
- const id = makeId("u_row", count);
2109
- const { layout, cells: propsCells } = element.props;
2110
- let cells;
2111
- if (layout) {
2112
- cells = layout.cells;
2113
- } else if (propsCells) {
2114
- cells = propsCells;
2115
- } else {
2116
- const columnCount = Math.max(
2117
- 1,
2118
- collectChildren2(element.props.children).filter(
2119
- (child) => getDisplayName2(child) === "Column"
2120
- ).length
2121
- );
2122
- cells = Array(columnCount).fill(1);
2123
- }
2124
- const semanticProps = extractSemanticProps2(element.props, ["layout"]);
2125
- const mapped = mapSemanticProps(semanticProps, ROW_DEFAULTS, "Row");
2126
- const values = mergeValues(ROW_DEFAULTS, mapped);
2127
- const valuesWithMeta = {
2128
- ...values,
2129
- columns: values.columns ?? false,
2130
- cells,
2131
- _meta: {
2132
- htmlID: id,
2133
- htmlClassNames: "u_row",
2134
- ...values._meta || {}
2135
- },
2136
- selectable: true,
2137
- draggable: true,
2138
- duplicatable: true,
2139
- deletable: true,
2140
- hideable: true
2141
- };
2142
- const columns = [];
2143
- const children = collectChildren2(element.props.children);
2144
- const columnLayout = {
2145
- bodyValues: parentLayout.bodyValues,
2146
- rowValues: valuesWithMeta,
2147
- rowCells: cells
2148
- };
2149
- let columnIndex = 0;
2150
- for (const child of children) {
2151
- const name = getDisplayName2(child);
2152
- if (name === "Column") {
2153
- columns.push(
2154
- processColumn(child, counters, { ...columnLayout, columnIndex })
2155
- );
2156
- columnIndex += 1;
2157
- } else {
2158
- console.warn(
2159
- `[Unlayer] renderToJson: <${name}> is not a valid Row child. Only <Column> is allowed.`
2160
- );
2161
- }
2162
- }
2163
- return { cells, columns, values: valuesWithMeta };
2164
- }
2165
- function processBody(element, counters) {
2166
- const id = "u_body";
2167
- const semanticProps = extractSemanticProps2(element.props);
2168
- const mapped = mapSemanticProps(semanticProps, BODY_DEFAULTS, "Body");
2169
- const values = mergeValues(BODY_DEFAULTS, mapped);
2170
- const previewText = element.props.previewText;
2171
- if (previewText !== void 0) {
2172
- values.preheaderText = previewText;
2173
- }
2174
- const valuesWithMeta = {
2175
- ...values,
2176
- _meta: {
2177
- htmlID: id,
2178
- htmlClassNames: "u_body",
2179
- ...values._meta || {}
2180
- }
2181
- };
2182
- const rows = [];
2183
- const children = collectChildren2(element.props.children);
2184
- for (const child of children) {
2185
- const name = getDisplayName2(child);
2186
- if (name === "Row") {
2187
- rows.push(processRow(child, counters, { bodyValues: valuesWithMeta }));
2188
- } else {
2189
- console.warn(
2190
- `[Unlayer] renderToJson: <${name}> is not a valid Body child. Only <Row> is allowed.`
2191
- );
2192
- }
2193
- }
2194
- return {
2195
- rows,
2196
- values: valuesWithMeta
2197
- };
2198
- }
2199
- function renderRowToJson(element) {
2200
- const displayName = getDisplayName2(element);
2201
- if (displayName !== "Row") {
2202
- throw new Error(
2203
- `[Unlayer] renderRowToJson: Element must be <Row>, but got <${displayName || "unknown"}>. For full designs, use renderToJson instead.`
2204
- );
2205
- }
2206
- const counters = {};
2207
- return processRow(element, counters);
2208
- }
2209
- function renderToJson(element) {
2210
- element = unwrapRoot(element);
2211
- const displayName = getDisplayName2(element);
2212
- if (!displayName || !VALID_ROOTS.has(displayName)) {
2213
- throw new Error(
2214
- `[Unlayer] renderToJson: Root element must be <Body>, <Email>, <Page>, or <Document>, but got <${displayName || "unknown"}>. Wrap your content: <Body><Row><Column>...</Column></Row></Body>`
2215
- );
2216
- }
2217
- const counters = {};
2218
- const body = processBody(element, counters);
2219
- return {
2220
- counters,
2221
- body,
2222
- schemaVersion
2223
- };
2224
- }
2225
3097
 
2226
3098
  // src/utils/register-tool.tsx
2227
3099
  function initialValuesFromOptions(options) {