@standhigher/react-email-editor 0.4.1 → 0.5.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
@@ -142,7 +142,7 @@ var DEFAULT_EMAIL_BODY = {
142
142
  textColor: "#000000",
143
143
  backgroundColor: "#ffffff",
144
144
  contentWidth: CANVAS_DEVICE_WIDTH.desktop,
145
- fontFamily: "Arial, Helvetica, sans-serif",
145
+ fontFamily: "",
146
146
  fontWeight: FONT_WEIGHTS.normal,
147
147
  linkColor: "#005bd3",
148
148
  linkDecoration: LINK_DECORATIONS.underline,
@@ -379,7 +379,7 @@ var DEFAULT_TYPOGRAPHY = Object.freeze({
379
379
  function typographyWithBody(body) {
380
380
  return __spreadValues(__spreadValues({}, DEFAULT_TYPOGRAPHY), body ? {
381
381
  color: body.textColor,
382
- fontFamily: body.fontFamily,
382
+ fontFamily: cssFont(body.fontFamily, DEFAULT_TYPOGRAPHY.fontFamily),
383
383
  fontWeight: body.fontWeight,
384
384
  linkColor: body.linkColor,
385
385
  linkDecoration: body.linkDecoration
@@ -2661,9 +2661,34 @@ function createEmailDocument(blocks = []) {
2661
2661
  function createEmptyDocument() {
2662
2662
  return { version: DOCUMENT_VERSION, subject: "", body: resolveEmailBody(), blocks: [] };
2663
2663
  }
2664
+ function isRecord(value) {
2665
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2666
+ }
2664
2667
  function ensureEmailDocument(document) {
2665
- if (document.version !== DOCUMENT_VERSION) {
2666
- throw new Error(`Unsupported email document version: ${document.version}. Expected ${DOCUMENT_VERSION}.`);
2668
+ if (!isRecord(document)) {
2669
+ throw new TypeError("Invalid email document: expected an object.");
2670
+ }
2671
+ if (!Array.isArray(document.blocks)) {
2672
+ throw new TypeError("Invalid email document: blocks must be an array.");
2673
+ }
2674
+ if (document.body != null && !isRecord(document.body)) {
2675
+ throw new TypeError("Invalid email document: body must be an object when provided.");
2676
+ }
2677
+ if (document.subject !== void 0 && typeof document.subject !== "string") {
2678
+ throw new TypeError("Invalid email document: subject must be a string when provided.");
2679
+ }
2680
+ for (const [index, block] of document.blocks.entries()) {
2681
+ const path = `Invalid email document: blocks[${index}]`;
2682
+ if (!isRecord(block)) throw new TypeError(`${path} must be an object.`);
2683
+ if (typeof block.id !== "string" || !block.id.trim()) {
2684
+ throw new TypeError(`${path}.id must be a non-empty string.`);
2685
+ }
2686
+ if (typeof block.type !== "string" || !block.type.trim()) {
2687
+ throw new TypeError(`${path}.type must be a non-empty string.`);
2688
+ }
2689
+ if (!isRecord(block.controls)) {
2690
+ throw new TypeError(`${path}.controls must be an object.`);
2691
+ }
2667
2692
  }
2668
2693
  return __spreadProps(__spreadValues({}, document), { body: resolveEmailBody(document.body) });
2669
2694
  }
@@ -2807,7 +2832,7 @@ var EmailControls = {
2807
2832
 
2808
2833
  // src/plugins/define-block.ts
2809
2834
  function defineBlock(input) {
2810
- var _a, _b, _c;
2835
+ var _a, _b, _c, _d;
2811
2836
  const options = snapshotValue(input);
2812
2837
  if (!options.type.trim() || !options.title.trim()) throw new Error("Block type and title are required.");
2813
2838
  const schema = (_a = options.controls) != null ? _a : {};
@@ -2825,7 +2850,7 @@ function defineBlock(input) {
2825
2850
  type: options.type,
2826
2851
  title: options.title,
2827
2852
  description: options.description,
2828
- icon: options.icon,
2853
+ icon: (_c = options.icon) != null ? _c : esc(Array.from(options.title.trim())[0]),
2829
2854
  override: options.override,
2830
2855
  schema,
2831
2856
  controls: Object.entries(schema).map(([name, spec]) => __spreadProps(__spreadValues({}, spec), { name })),
@@ -2834,7 +2859,7 @@ function defineBlock(input) {
2834
2859
  inspectorSections: options.inspectorSections,
2835
2860
  defaultStyle: options.defaultStyle,
2836
2861
  defaultData: options.defaultData,
2837
- inlineControl: options.Canvas ? void 0 : (_c = inline[0]) == null ? void 0 : _c[0],
2862
+ inlineControl: options.Canvas ? void 0 : (_d = inline[0]) == null ? void 0 : _d[0],
2838
2863
  canvas: bindPluginComponent(options.Canvas),
2839
2864
  inspector: bindPluginComponent(options.Inspector),
2840
2865
  renderEmail: (ctx) => {
@@ -3471,7 +3496,7 @@ var BODY_CONTROL_SECTIONS = [
3471
3496
  { title: "General", controls: [
3472
3497
  { id: "textColor", group: "typography", label: "Text color", control: "color" },
3473
3498
  { id: "linkColor", group: "typography", label: "Link color", control: "color" },
3474
- { id: "fontFamily", group: "typography", label: "Font family", control: "select", options: copyFontFamilies() },
3499
+ { id: "fontFamily", group: "typography", label: "Font family", control: "select", options: [{ label: "Default", value: "" }, ...copyFontFamilies()] },
3475
3500
  __spreadProps(__spreadValues({}, FONT_WEIGHT), { id: "fontWeight", label: "Font weight" })
3476
3501
  ] },
3477
3502
  { title: "Content", controls: [
@@ -3840,7 +3865,7 @@ function buildRenderContext(node, mode, body = DEFAULT_EMAIL_BODY, definition =
3840
3865
  }
3841
3866
 
3842
3867
  // src/inspector/InspectorControls.tsx
3843
- import { useId as useId10 } from "react";
3868
+ import { useId as useId11 } from "react";
3844
3869
 
3845
3870
  // src/constants/style-groups.ts
3846
3871
  var STYLE_GROUPS = [
@@ -3885,7 +3910,7 @@ function inheritedStyleValue(id, body) {
3885
3910
  }
3886
3911
 
3887
3912
  // src/inspector/components/BasicControl.tsx
3888
- import { useRef as useRef9, useId as useId6 } from "react";
3913
+ import { useRef as useRef10, useId as useId7 } from "react";
3889
3914
 
3890
3915
  // src/inspector/HtmlCodeEditor.tsx
3891
3916
  import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect6, useRef as useRef8 } from "react";
@@ -4079,41 +4104,147 @@ function HtmlCodeEditor({
4079
4104
  );
4080
4105
  }
4081
4106
 
4082
- // src/inspector/VariablesDropdown.tsx
4083
- import { useState as useState12 } from "react";
4107
+ // src/components/PersonalizationMenu.tsx
4108
+ import { useId as useId6, useRef as useRef9, useState as useState13 } from "react";
4109
+ import { createPortal as createPortal2 } from "react-dom";
4110
+
4111
+ // src/components/usePersonalizationMenu.ts
4112
+ import { useLayoutEffect as useLayoutEffect7, useState as useState12 } from "react";
4113
+ function usePersonalizationMenu(open, rootRef, menuRef, focusOnOpen, setOpen) {
4114
+ const [position, setPosition] = useState12({ left: 0, top: 0, maxHeight: 240 });
4115
+ useLayoutEffect7(() => {
4116
+ var _a;
4117
+ const root = rootRef.current;
4118
+ const menu = menuRef.current;
4119
+ if (!open || !root || !menu) return;
4120
+ const ownerDocument = root.ownerDocument;
4121
+ const ownerWindow = ownerDocument.defaultView;
4122
+ if (!ownerWindow) return;
4123
+ const place = () => {
4124
+ var _a2, _b;
4125
+ const anchor = root.getBoundingClientRect();
4126
+ const inputBounds = (_b = (_a2 = root.closest(".ree-input-with-variable")) == null ? void 0 : _a2.getBoundingClientRect()) != null ? _b : anchor;
4127
+ const width = menu.offsetWidth;
4128
+ const height = Math.min(menu.scrollHeight + 2, 240);
4129
+ const below = Math.max(0, ownerWindow.innerHeight - inputBounds.bottom - 8);
4130
+ const above = Math.max(0, inputBounds.top - 8);
4131
+ const up = below < height && above > below;
4132
+ const maxHeight = Math.min(240, up ? above : below);
4133
+ const left = anchor.left + width > ownerWindow.innerWidth - 8 ? anchor.right - width : anchor.left;
4134
+ setPosition({
4135
+ left: Math.max(8, Math.min(left, ownerWindow.innerWidth - width - 8)),
4136
+ top: up ? inputBounds.top - Math.min(height, maxHeight) : inputBounds.bottom,
4137
+ maxHeight
4138
+ });
4139
+ };
4140
+ const onPointerDown = (event) => {
4141
+ if (!root.contains(event.target) && !menu.contains(event.target)) setOpen(false);
4142
+ };
4143
+ const onKeyDown = (event) => {
4144
+ var _a2, _b;
4145
+ const target = event.target;
4146
+ if (!root.contains(target) && !menu.contains(target)) return;
4147
+ if (event.key === "Tab" && menu.contains(target)) {
4148
+ const buttons = menu.querySelectorAll("button");
4149
+ if (target !== (event.shiftKey ? buttons[0] : buttons[buttons.length - 1])) return;
4150
+ if (event.shiftKey) event.preventDefault();
4151
+ setOpen(false);
4152
+ (_a2 = root.querySelector("button")) == null ? void 0 : _a2.focus({ preventScroll: true });
4153
+ return;
4154
+ }
4155
+ if (event.key !== "Escape") return;
4156
+ event.preventDefault();
4157
+ event.stopPropagation();
4158
+ setOpen(false);
4159
+ (_b = root.querySelector("button")) == null ? void 0 : _b.focus({ preventScroll: true });
4160
+ };
4161
+ menu.setAttribute("popover", "manual");
4162
+ menu.showPopover();
4163
+ place();
4164
+ if (focusOnOpen.current) (_a = menu.querySelector("button")) == null ? void 0 : _a.focus({ preventScroll: true });
4165
+ const observer = new ResizeObserver(place);
4166
+ observer.observe(root);
4167
+ observer.observe(menu);
4168
+ ownerWindow.addEventListener("resize", place);
4169
+ ownerDocument.addEventListener("scroll", place, true);
4170
+ ownerDocument.addEventListener("pointerdown", onPointerDown, true);
4171
+ ownerDocument.addEventListener("keydown", onKeyDown, true);
4172
+ return () => {
4173
+ menu.hidePopover();
4174
+ observer.disconnect();
4175
+ ownerWindow.removeEventListener("resize", place);
4176
+ ownerDocument.removeEventListener("scroll", place, true);
4177
+ ownerDocument.removeEventListener("pointerdown", onPointerDown, true);
4178
+ ownerDocument.removeEventListener("keydown", onKeyDown, true);
4179
+ };
4180
+ }, [open, rootRef, menuRef, focusOnOpen, setOpen]);
4181
+ return position;
4182
+ }
4183
+
4184
+ // src/components/PersonalizationMenu.tsx
4084
4185
  import { jsx as jsx40, jsxs as jsxs29 } from "react/jsx-runtime";
4085
- function VariablesDropdown({ variables, onInsert }) {
4086
- const [open, setOpen] = useState12(false);
4186
+ function PersonalizationMenu({
4187
+ variables,
4188
+ onInsert,
4189
+ suffix = false
4190
+ }) {
4191
+ const [open, setOpen] = useState13(false);
4192
+ const [host, setHost] = useState13(null);
4193
+ const id = useId6();
4194
+ const rootRef = useRef9(null);
4195
+ const menuRef = useRef9(null);
4196
+ const focusOnOpen = useRef9(false);
4197
+ const position = usePersonalizationMenu(open && variables.length > 0, rootRef, menuRef, focusOnOpen, setOpen);
4087
4198
  if (variables.length === 0) return null;
4088
- return /* @__PURE__ */ jsxs29("div", { className: "ree-var-wrap", children: [
4089
- /* @__PURE__ */ jsx40(
4199
+ return /* @__PURE__ */ jsxs29("div", { className: suffix ? "ree-personalization ree-personalization-suffix" : "ree-personalization", ref: rootRef, children: [
4200
+ /* @__PURE__ */ jsxs29(
4090
4201
  "button",
4091
4202
  {
4092
4203
  type: "button",
4093
- className: "ree-var-btn",
4094
- onClick: () => setOpen((current) => !current),
4095
- title: "Insert variable",
4096
- "aria-label": "Insert variable",
4204
+ className: suffix ? "ree-personalization-suffix-btn" : "ree-personalization-btn",
4205
+ "aria-label": suffix ? "Insert variable" : void 0,
4206
+ title: suffix ? "Personalization" : void 0,
4097
4207
  "aria-expanded": open,
4098
- children: "{ }"
4099
- }
4100
- ),
4101
- open ? /* @__PURE__ */ jsx40("div", { className: "ree-var-menu", children: variables.map((variable) => /* @__PURE__ */ jsxs29(
4102
- "button",
4103
- {
4104
- type: "button",
4105
- className: "ree-var-item",
4106
- onClick: () => {
4107
- onInsert(variable.name);
4108
- setOpen(false);
4208
+ "aria-controls": open ? id : void 0,
4209
+ onClick: (event) => {
4210
+ var _a;
4211
+ focusOnOpen.current = event.detail === 0;
4212
+ const root = rootRef.current;
4213
+ if (root) setHost((_a = root.closest(".ree-root")) != null ? _a : root.ownerDocument.body);
4214
+ setOpen((current) => !current);
4215
+ },
4216
+ onKeyDown: (event) => {
4217
+ var _a, _b;
4218
+ if (event.key === "Tab" && open && !event.shiftKey) {
4219
+ event.preventDefault();
4220
+ (_b = (_a = menuRef.current) == null ? void 0 : _a.querySelector("button")) == null ? void 0 : _b.focus();
4221
+ }
4109
4222
  },
4110
4223
  children: [
4111
- /* @__PURE__ */ jsx40("span", { className: "ree-var-name", children: variable.name }),
4112
- variable.description ? /* @__PURE__ */ jsx40("span", { className: "ree-var-label", children: variable.description }) : null
4224
+ suffix ? "{ }" : "Personalization",
4225
+ suffix ? null : /* @__PURE__ */ jsx40("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx40("path", { d: "M5.2 7.5 10 12.3l4.8-4.8 1.2 1.2L10 14.7 4 8.7l1.2-1.2Z" }) })
4113
4226
  ]
4114
- },
4115
- variable.name
4116
- )) }) : null
4227
+ }
4228
+ ),
4229
+ open && host ? createPortal2(
4230
+ /* @__PURE__ */ jsx40("div", { id, ref: menuRef, className: "ree-var-menu ree-personalization-menu", style: position, children: variables.map((variable) => /* @__PURE__ */ jsxs29(
4231
+ "button",
4232
+ {
4233
+ type: "button",
4234
+ className: "ree-var-item",
4235
+ onClick: () => {
4236
+ onInsert(variable.name);
4237
+ setOpen(false);
4238
+ },
4239
+ children: [
4240
+ /* @__PURE__ */ jsx40("span", { className: "ree-var-name", children: variable.name }),
4241
+ variable.description ? /* @__PURE__ */ jsx40("span", { className: "ree-var-label", children: variable.description }) : null
4242
+ ]
4243
+ },
4244
+ variable.name
4245
+ )) }),
4246
+ host
4247
+ ) : null
4117
4248
  ] });
4118
4249
  }
4119
4250
 
@@ -4121,8 +4252,8 @@ function VariablesDropdown({ variables, onInsert }) {
4121
4252
  import { jsx as jsx41, jsxs as jsxs30 } from "react/jsx-runtime";
4122
4253
  function BasicControl({ control, value, variables, onChange }) {
4123
4254
  var _a, _b, _c, _d, _e;
4124
- const ref = useRef9(null);
4125
- const id = useId6();
4255
+ const ref = useRef10(null);
4256
+ const id = useId7();
4126
4257
  const shown = value == null || typeof value === "number" && !Number.isFinite(value) ? "" : String(value);
4127
4258
  const insertVariable = (token) => {
4128
4259
  var _a2, _b2;
@@ -4130,6 +4261,11 @@ function BasicControl({ control, value, variables, onChange }) {
4130
4261
  const start = (_a2 = element == null ? void 0 : element.selectionStart) != null ? _a2 : shown.length;
4131
4262
  const end = (_b2 = element == null ? void 0 : element.selectionEnd) != null ? _b2 : shown.length;
4132
4263
  onChange(shown.slice(0, start) + token + shown.slice(end));
4264
+ requestAnimationFrame(() => {
4265
+ var _a3, _b3;
4266
+ (_a3 = ref.current) == null ? void 0 : _a3.focus();
4267
+ (_b3 = ref.current) == null ? void 0 : _b3.setSelectionRange(start + token.length, start + token.length);
4268
+ });
4133
4269
  };
4134
4270
  if (control.kind === "boolean") return /* @__PURE__ */ jsx41(CheckboxControl, { label: control.label, checked: Boolean(value), onChange });
4135
4271
  if (control.kind === "color") return /* @__PURE__ */ jsx41(ColorControl, { label: control.label, value: shown, resetTo: String((_a = control.defaultValue) != null ? _a : ""), onChange });
@@ -4138,30 +4274,32 @@ function BasicControl({ control, value, variables, onChange }) {
4138
4274
  /* @__PURE__ */ jsx41(HtmlCodeEditor, { value: shown, placeholder: control.placeholder, rows: control.rows, onChange })
4139
4275
  ] });
4140
4276
  const textual = control.kind === "text" || control.kind === "textarea" || control.kind === "url";
4277
+ const hasVariables = textual && variables.length > 0;
4141
4278
  const invalidUrl = control.kind === "url" && shown.trim() && !safeEmailUrl(shown);
4279
+ const input = control.kind === "select" ? /* @__PURE__ */ jsx41(SelectControl, { id, label: control.label, value: shown, options: (_b = control.options) != null ? _b : [], emptyLabel: (_c = control.placeholder) != null ? _c : "None", onChange }) : control.kind === "textarea" ? /* @__PURE__ */ jsx41("textarea", { id, ref: (element) => {
4280
+ ref.current = element;
4281
+ }, className: "ree-input ree-textarea", value: shown, placeholder: control.placeholder, rows: (_d = control.rows) != null ? _d : 4, onChange: (event) => onChange(event.target.value) }) : /* @__PURE__ */ jsx41(TextInput, { id, ref: (element) => {
4282
+ ref.current = element;
4283
+ }, type: control.kind === "number" ? "number" : "text", value: shown, min: control.min, max: control.max, step: control.step, placeholder: control.placeholder, "aria-invalid": Boolean(invalidUrl) || Boolean((_e = control.validateValue) == null ? void 0 : _e.call(control, value)), onChange: (event) => onChange(control.kind === "number" && event.target.value !== "" ? Number(event.target.value) : event.target.value) });
4142
4284
  return /* @__PURE__ */ jsxs30("div", { className: "ree-field", children: [
4143
- /* @__PURE__ */ jsxs30("div", { className: "ree-field-label", children: [
4144
- /* @__PURE__ */ jsxs30("label", { htmlFor: id, children: [
4145
- control.label,
4146
- control.unit ? ` (${control.unit})` : ""
4147
- ] }),
4148
- textual ? /* @__PURE__ */ jsx41(VariablesDropdown, { variables, onInsert: insertVariable }) : null
4149
- ] }),
4150
- control.kind === "select" ? /* @__PURE__ */ jsx41(SelectControl, { id, label: control.label, value: shown, options: (_b = control.options) != null ? _b : [], emptyLabel: (_c = control.placeholder) != null ? _c : "None", onChange }) : control.kind === "textarea" ? /* @__PURE__ */ jsx41("textarea", { id, ref: (element) => {
4151
- ref.current = element;
4152
- }, className: "ree-input ree-textarea", value: shown, placeholder: control.placeholder, rows: (_d = control.rows) != null ? _d : 4, onChange: (event) => onChange(event.target.value) }) : /* @__PURE__ */ jsx41(TextInput, { id, ref: (element) => {
4153
- ref.current = element;
4154
- }, type: control.kind === "number" ? "number" : "text", value: shown, min: control.min, max: control.max, step: control.step, placeholder: control.placeholder, "aria-invalid": Boolean(invalidUrl) || Boolean((_e = control.validateValue) == null ? void 0 : _e.call(control, value)), onChange: (event) => onChange(control.kind === "number" && event.target.value !== "" ? Number(event.target.value) : event.target.value) })
4285
+ /* @__PURE__ */ jsx41("div", { className: "ree-field-label", children: /* @__PURE__ */ jsxs30("label", { htmlFor: id, children: [
4286
+ control.label,
4287
+ control.unit ? ` (${control.unit})` : ""
4288
+ ] }) }),
4289
+ hasVariables ? /* @__PURE__ */ jsxs30("div", { className: "ree-input-with-variable", children: [
4290
+ input,
4291
+ /* @__PURE__ */ jsx41(PersonalizationMenu, { suffix: true, variables, onInsert: insertVariable })
4292
+ ] }) : input
4155
4293
  ] });
4156
4294
  }
4157
4295
 
4158
4296
  // src/inspector/components/ListControl.tsx
4159
- import { useId as useId7 } from "react";
4297
+ import { useId as useId8 } from "react";
4160
4298
 
4161
4299
  // src/inspector/components/useListRows.ts
4162
- import { useLayoutEffect as useLayoutEffect7, useRef as useRef10, useState as useState13 } from "react";
4300
+ import { useLayoutEffect as useLayoutEffect8, useRef as useRef11, useState as useState14 } from "react";
4163
4301
  function useListRows(value, onChange) {
4164
- const [state, setState] = useState13({ source: void 0, rows: [], nextKey: 0 });
4302
+ const [state, setState] = useState14({ source: void 0, rows: [], nextKey: 0 });
4165
4303
  if (state.source !== value) {
4166
4304
  const available = [...state.rows];
4167
4305
  let nextKey = state.nextKey;
@@ -4173,8 +4311,8 @@ function useListRows(value, onChange) {
4173
4311
  });
4174
4312
  setState({ source: value, rows, nextKey });
4175
4313
  }
4176
- const latest = useRef10(state);
4177
- useLayoutEffect7(() => {
4314
+ const latest = useRef11(state);
4315
+ useLayoutEffect8(() => {
4178
4316
  latest.current = state;
4179
4317
  return () => {
4180
4318
  latest.current = null;
@@ -4219,7 +4357,7 @@ import { jsx as jsx42, jsxs as jsxs31 } from "react/jsx-runtime";
4219
4357
  function ListControl(_a) {
4220
4358
  var _b = _a, { control, value, onChange } = _b, props = __objRest(_b, ["control", "value", "onChange"]);
4221
4359
  var _a2;
4222
- const id = useId7();
4360
+ const id = useId8();
4223
4361
  const schema = (_a2 = control.itemControls) != null ? _a2 : {};
4224
4362
  const { rows, updateRow, moveRow, removeRow, addRow } = useListRows(value, onChange);
4225
4363
  return /* @__PURE__ */ jsxs31("fieldset", { className: "ree-plugin-list", children: [
@@ -4239,15 +4377,15 @@ function ListControl(_a) {
4239
4377
  }
4240
4378
 
4241
4379
  // src/inspector/components/ImageControl.tsx
4242
- import { useContext as useContext4, useEffect as useEffect5, useId as useId8, useRef as useRef11, useState as useState14 } from "react";
4380
+ import { useContext as useContext4, useEffect as useEffect5, useId as useId9, useRef as useRef12, useState as useState15 } from "react";
4243
4381
  import { jsx as jsx43, jsxs as jsxs32 } from "react/jsx-runtime";
4244
4382
  function ImageControl({ control, value, onChange }) {
4245
4383
  var _a;
4246
4384
  const config = useContext4(EditorConfigContext);
4247
- const controller = useRef11(null);
4248
- const id = useId8();
4249
- const [loading, setLoading] = useState14(false);
4250
- const [error, setError] = useState14("");
4385
+ const controller = useRef12(null);
4386
+ const id = useId9();
4387
+ const [loading, setLoading] = useState15(false);
4388
+ const [error, setError] = useState15("");
4251
4389
  const shown = String(value != null ? value : "");
4252
4390
  const url = safeEmailUrl(shown, true);
4253
4391
  useEffect5(() => () => {
@@ -4284,12 +4422,12 @@ function ImageControl({ control, value, onChange }) {
4284
4422
  }
4285
4423
 
4286
4424
  // src/inspector/components/RichTextControl.tsx
4287
- import { useContext as useContext5, useId as useId9 } from "react";
4425
+ import { useContext as useContext5, useId as useId10 } from "react";
4288
4426
  import { jsx as jsx44, jsxs as jsxs33 } from "react/jsx-runtime";
4289
4427
  function RichTextControl({ control, value, variables, body, onChange }) {
4290
4428
  var _a, _b;
4291
4429
  const parent = useContext5(PluginEditorContext);
4292
- const id = useId9();
4430
+ const id = useId10();
4293
4431
  const context = {
4294
4432
  block: (_a = parent == null ? void 0 : parent.block) != null ? _a : { id, type: "control", controls: {} },
4295
4433
  controls: { [control.name]: value },
@@ -4347,7 +4485,7 @@ function InspectorControls({
4347
4485
  onCommitStyle
4348
4486
  }) {
4349
4487
  var _a, _b, _c, _d, _e;
4350
- const toolbarId = useId10();
4488
+ const toolbarId = useId11();
4351
4489
  const contentControls = ((_a = definition.controls) != null ? _a : []).filter((control) => control.name !== definition.inlineControl);
4352
4490
  const CustomInspector = definition.inspector;
4353
4491
  const context = buildRenderContext(block, "canvas", body, definition);
@@ -4476,6 +4614,9 @@ function Inspector({
4476
4614
  ] });
4477
4615
  }
4478
4616
 
4617
+ // src/stage/Stage.tsx
4618
+ import { useMemo as useMemo5 } from "react";
4619
+
4479
4620
  // src/stage/canvas/Canvas.tsx
4480
4621
  import { useDroppable } from "@dnd-kit/core";
4481
4622
  import {
@@ -4499,26 +4640,95 @@ function viewportWidth(device, body) {
4499
4640
  }
4500
4641
 
4501
4642
  // src/stage/canvas/CanvasBlock.tsx
4502
- import { useId as useId12, useMemo as useMemo3, useState as useState15 } from "react";
4643
+ import { useId as useId13, useMemo as useMemo3, useState as useState16 } from "react";
4503
4644
  import { useSortable } from "@dnd-kit/sortable";
4504
4645
  import { CSS } from "@dnd-kit/utilities";
4505
4646
 
4506
4647
  // src/stage/canvas/components/PluginCanvasContent.tsx
4507
4648
  import { useMemo as useMemo2 } from "react";
4508
4649
 
4509
- // src/render/template-preview.ts
4510
- var TOKEN_PATTERN = /\{\{-?([\s\S]*?)-?\}\}/g;
4650
+ // src/render/default-template-engine.ts
4651
+ var TOKEN_PATTERN = /\{\{(-[ \t\r\n])?([\s\S]*?)([ \t\r\n]-)?\}\}/g;
4652
+ var UNSUPPORTED_IDENTIFIERS = /* @__PURE__ */ new Set([
4653
+ "and",
4654
+ "or",
4655
+ "not",
4656
+ "len",
4657
+ "index",
4658
+ "slice",
4659
+ "call",
4660
+ "print",
4661
+ "printf",
4662
+ "println",
4663
+ "html",
4664
+ "js",
4665
+ "urlquery",
4666
+ "eq",
4667
+ "ne",
4668
+ "lt",
4669
+ "le",
4670
+ "gt",
4671
+ "ge",
4672
+ "break",
4673
+ "continue",
4674
+ "else",
4675
+ "end"
4676
+ ]);
4677
+ var PATH_PATTERN = /^(?:\.|\$|(?:\$\.|\.)?[\p{L}_][\p{L}\p{Nd}_]*(?:\.[\p{L}_][\p{L}\p{Nd}_]*)*)$/u;
4678
+ function supportsExpression(expression) {
4679
+ if (PATH_PATTERN.test(expression) && !UNSUPPORTED_IDENTIFIERS.has(expression)) return true;
4680
+ if (/^-?\d+(?:\.\d+)?$/.test(expression)) return true;
4681
+ if (/^"(?:[^"\\]|\\.)*"$/.test(expression)) {
4682
+ try {
4683
+ JSON.parse(expression);
4684
+ return true;
4685
+ } catch (e) {
4686
+ return false;
4687
+ }
4688
+ }
4689
+ return false;
4690
+ }
4691
+ function supportsTokens(tokens) {
4692
+ var _a;
4693
+ const branches = [];
4694
+ for (const token of tokens) {
4695
+ if (token.text !== void 0) {
4696
+ if (token.text.includes("{{")) return false;
4697
+ continue;
4698
+ }
4699
+ const tag = (_a = token.tag) != null ? _a : "";
4700
+ const branch = /^(if|range|with)\s+([\s\S]+)$/.exec(tag);
4701
+ if (branch) {
4702
+ if (!supportsExpression(branch[2].trim())) return false;
4703
+ branches.push({ kind: branch[1], hasElse: false });
4704
+ } else if (tag === "end") {
4705
+ if (!branches.pop()) return false;
4706
+ } else if (tag === "else" || /^else\s+if\s+/.test(tag)) {
4707
+ const current = branches.at(-1);
4708
+ if (!current || current.hasElse) return false;
4709
+ if (tag === "else") current.hasElse = true;
4710
+ else if (current.kind !== "if" || !supportsExpression(tag.replace(/^else\s+if\s+/, ""))) return false;
4711
+ } else if (!supportsExpression(tag)) return false;
4712
+ }
4713
+ return branches.length === 0;
4714
+ }
4511
4715
  function tokenize(template) {
4512
4716
  var _a;
4513
4717
  const tokens = [];
4514
4718
  let lastIndex = 0;
4719
+ let trimNext = false;
4515
4720
  for (const match of template.matchAll(TOKEN_PATTERN)) {
4516
4721
  const index = (_a = match.index) != null ? _a : 0;
4517
- if (index > lastIndex) tokens.push({ text: template.slice(lastIndex, index) });
4518
- tokens.push({ tag: match[1].trim() });
4722
+ let text = template.slice(lastIndex, index);
4723
+ if (trimNext) text = text.replace(/^[ \t\r\n]+/, "");
4724
+ if (match[1]) text = text.replace(/[ \t\r\n]+$/, "");
4725
+ if (text) tokens.push({ text });
4726
+ tokens.push({ tag: match[2].trim() });
4727
+ trimNext = Boolean(match[3]);
4519
4728
  lastIndex = index + match[0].length;
4520
4729
  }
4521
- if (lastIndex < template.length) tokens.push({ text: template.slice(lastIndex) });
4730
+ const tail = template.slice(lastIndex);
4731
+ if (tail) tokens.push({ text: trimNext ? tail.replace(/^[ \t\r\n]+/, "") : tail });
4522
4732
  return tokens;
4523
4733
  }
4524
4734
  function parseNodes(tokens, startIndex) {
@@ -4533,11 +4743,11 @@ function parseNodes(tokens, startIndex) {
4533
4743
  continue;
4534
4744
  }
4535
4745
  const tag = (_a = token.tag) != null ? _a : "";
4536
- if (tag === "else" || tag.startsWith("else if ")) {
4746
+ if (tag === "else" || /^else\s+if\s+/.test(tag)) {
4537
4747
  return { nodes, nextIndex: index, stopTag: "else" };
4538
4748
  }
4539
4749
  if (tag === "end") return { nodes, nextIndex: index, stopTag: "end" };
4540
- const branchMatch = /^(if|range|with)\s+(.+)$/.exec(tag);
4750
+ const branchMatch = /^(if|range|with)\s+([\s\S]+)$/.exec(tag);
4541
4751
  if (branchMatch) {
4542
4752
  const branch = parseBranch(
4543
4753
  tokens,
@@ -4561,7 +4771,7 @@ function parseBranch(tokens, startIndex, kind, expression) {
4561
4771
  let nextIndex = body.nextIndex;
4562
4772
  if (body.stopTag === "else") {
4563
4773
  const elseTag = (_b = (_a = tokens[body.nextIndex]) == null ? void 0 : _a.tag) != null ? _b : "";
4564
- const elseIfMatch = /^else\s+if\s+(.+)$/.exec(elseTag);
4774
+ const elseIfMatch = /^else\s+if\s+([\s\S]+)$/.exec(elseTag);
4565
4775
  if (elseIfMatch) {
4566
4776
  const nested = parseBranch(
4567
4777
  tokens,
@@ -4585,42 +4795,44 @@ function parseBranch(tokens, startIndex, kind, expression) {
4585
4795
  function readPath(value, path) {
4586
4796
  if (path === "") return value;
4587
4797
  return path.split(".").reduce((current, key) => {
4588
- if (current && typeof current === "object") return current[key];
4798
+ if (current && typeof current === "object" && Object.hasOwn(current, key)) return current[key];
4589
4799
  return void 0;
4590
4800
  }, value);
4591
4801
  }
4592
4802
  function resolveExpression(expression, root, current) {
4593
4803
  const trimmed = expression.trim();
4594
4804
  if (trimmed === ".") return current;
4805
+ if (trimmed === "$") return root;
4595
4806
  if (trimmed === "true") return true;
4596
4807
  if (trimmed === "false") return false;
4597
4808
  if (trimmed === "nil") return null;
4598
- if (/^\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
4599
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
4600
- return trimmed.slice(1, -1);
4601
- }
4809
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
4810
+ if (trimmed.startsWith('"')) return JSON.parse(trimmed);
4602
4811
  if (trimmed.startsWith("$.")) return readPath(root, trimmed.slice(2));
4603
4812
  if (trimmed.startsWith(".")) return readPath(current, trimmed.slice(1));
4604
4813
  return readPath(current, trimmed);
4605
4814
  }
4606
4815
  function isTruthy(value) {
4607
4816
  if (value == null || value === false || value === "" || value === 0) return false;
4608
- return !Array.isArray(value) || value.length > 0;
4609
- }
4610
- function escapeHtml(value) {
4611
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4817
+ if (Array.isArray(value)) return value.length > 0;
4818
+ if (value instanceof Map || value instanceof Set) return value.size > 0;
4819
+ if (typeof value === "object") {
4820
+ const prototype = Object.getPrototypeOf(value);
4821
+ if (prototype === Object.prototype || prototype === null) return Object.keys(value).length > 0;
4822
+ }
4823
+ return true;
4612
4824
  }
4613
4825
  function renderNodes(nodes, root, current) {
4614
4826
  return nodes.map((node) => {
4615
4827
  if (node.kind === "text") return node.value;
4616
4828
  if (node.kind === "expression") {
4617
4829
  const value2 = resolveExpression(node.expression, root, current);
4618
- return value2 == null ? "" : escapeHtml(String(value2));
4830
+ return value2 == null ? "" : esc(String(value2)).replaceAll("'", "&#39;");
4619
4831
  }
4620
4832
  const value = resolveExpression(node.expression, root, current);
4621
4833
  if (node.kind === "range") {
4622
- const items = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
4623
- return items.map((item) => renderNodes(node.children, root, item)).join("");
4834
+ const items = Array.isArray(value) ? value : value && typeof value === "object" ? Object.keys(value).sort().map((key) => value[key]) : [];
4835
+ return items.length > 0 ? items.map((item) => renderNodes(node.children, root, item)).join("") : renderNodes(node.alternate, root, current);
4624
4836
  }
4625
4837
  if (!isTruthy(value)) {
4626
4838
  return renderNodes(node.alternate, root, current);
@@ -4629,9 +4841,40 @@ function renderNodes(nodes, root, current) {
4629
4841
  return renderNodes(node.children, root, childContext);
4630
4842
  }).join("");
4631
4843
  }
4632
- function renderTemplatePreview(html, defaultData) {
4633
- if (defaultData === void 0) return html;
4634
- return renderNodes(parseNodes(tokenize(html), 0).nodes, defaultData, defaultData);
4844
+ var defaultTemplateEngine = Object.freeze({
4845
+ render(template, data) {
4846
+ const tokens = tokenize(template);
4847
+ if (!supportsTokens(tokens)) return template;
4848
+ return renderNodes(parseNodes(tokens, 0).nodes, data, data);
4849
+ }
4850
+ });
4851
+
4852
+ // src/render/template-preview.ts
4853
+ function normalizeEngineError(error) {
4854
+ return error instanceof Error ? error : new Error(String(error) || "Template engine failed.", { cause: error });
4855
+ }
4856
+ function renderTemplatePreview(template, data, options = {}) {
4857
+ var _a;
4858
+ const engine = (_a = options.templateEngine) != null ? _a : defaultTemplateEngine;
4859
+ if (data === void 0 && engine === defaultTemplateEngine) return template;
4860
+ try {
4861
+ const html = engine.render(template, structuredClone(data != null ? data : {}));
4862
+ if (typeof html !== "string") throw new TypeError("TemplateEngine.render must return a string synchronously.");
4863
+ return html;
4864
+ } catch (error) {
4865
+ throw normalizeEngineError(error);
4866
+ }
4867
+ }
4868
+ function validateTemplate(template, options = {}) {
4869
+ var _a, _b;
4870
+ const engine = (_a = options.templateEngine) != null ? _a : defaultTemplateEngine;
4871
+ try {
4872
+ const issues = (_b = engine.validate) == null ? void 0 : _b.call(engine, template);
4873
+ if (issues !== void 0 && !Array.isArray(issues)) throw new TypeError("TemplateEngine.validate must return an array synchronously.");
4874
+ return issues;
4875
+ } catch (error) {
4876
+ throw normalizeEngineError(error);
4877
+ }
4635
4878
  }
4636
4879
 
4637
4880
  // src/render/block-html.ts
@@ -4656,7 +4899,7 @@ function variableControlName(name) {
4656
4899
  const token = /^\{\{-?\s*([\s\S]*?)\s*-?\}\}$/.exec(name.trim());
4657
4900
  const inner = (token ? token[1] : name).trim();
4658
4901
  const control = inner.startsWith(".") ? inner.slice(1) : inner;
4659
- if (!control || !/^[A-Za-z_][\w.]*$/.test(control)) return null;
4902
+ if (!control || !/^[\p{L}_][\p{L}\p{Nd}_]*(?:\.[\p{L}_][\p{L}\p{Nd}_]*)*$/u.test(control)) return null;
4660
4903
  return control;
4661
4904
  }
4662
4905
  function dottedListedToken(name) {
@@ -4722,12 +4965,12 @@ function renderBlockContent(node, definitions = DEFAULT_BLOCKS, mode = "email",
4722
4965
  }
4723
4966
  function resolveBlockContent(rendered, node, options) {
4724
4967
  var _a, _b;
4725
- if (!options.resolveHtmlTemplate) {
4726
- return applyListedVariableDots(rendered, (_a = options.variables) != null ? _a : []);
4727
- }
4968
+ const normalized = applyListedVariableDots(rendered, (_a = options.variables) != null ? _a : []);
4969
+ if (!options.resolveHtmlTemplate) return normalized;
4728
4970
  return renderTemplatePreview(
4729
- rendered,
4730
- (_b = options.templateData) != null ? _b : node.defaultData
4971
+ normalized,
4972
+ (_b = options.templateData) != null ? _b : node.defaultData,
4973
+ options
4731
4974
  );
4732
4975
  }
4733
4976
  function renderBlock(node, definitions = DEFAULT_BLOCKS, mode = "email", body = DEFAULT_EMAIL_BODY) {
@@ -4753,7 +4996,7 @@ function blockToHtml(node, definitions = DEFAULT_BLOCKS, mode = "email", options
4753
4996
  }
4754
4997
 
4755
4998
  // src/stage/canvas/components/HtmlCanvas.tsx
4756
- import { useCallback, useEffect as useEffect6, useRef as useRef12 } from "react";
4999
+ import { useCallback, useEffect as useEffect6, useRef as useRef13 } from "react";
4757
5000
 
4758
5001
  // src/render/head-styles.ts
4759
5002
  function extractHeadStyles(html) {
@@ -4774,9 +5017,9 @@ function frameHtml(html, baseCss) {
4774
5017
  }
4775
5018
  function HtmlCanvas({ html, baseCss = "", onSelect }) {
4776
5019
  const hasHtmlTag = /<\/?[a-z][\w:-]*(?:\s[^>]*)?>/i.test(html);
4777
- const containerRef = useRef12(null);
4778
- const frameRef = useRef12(null);
4779
- const cleanupRef = useRef12();
5020
+ const containerRef = useRef13(null);
5021
+ const frameRef = useRef13(null);
5022
+ const cleanupRef = useRef13();
4780
5023
  const observeContent = useCallback(() => {
4781
5024
  var _a;
4782
5025
  (_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
@@ -4869,15 +5112,17 @@ function HtmlCanvas({ html, baseCss = "", onSelect }) {
4869
5112
 
4870
5113
  // src/stage/canvas/components/PluginCanvasContent.tsx
4871
5114
  import { jsx as jsx49, jsxs as jsxs38 } from "react/jsx-runtime";
4872
- function PluginCanvasContent({ block, body, definition, variables, selected, toolbarId, toolbarCollapsed, select, onCommitControl, onCommitControls, onCommitStyle }) {
5115
+ function PluginCanvasContent({ block, body, definition, variables, templateEngine, selected, toolbarId, toolbarCollapsed, select, onCommitControl, onCommitControls, onCommitStyle }) {
4873
5116
  var _a, _b;
4874
5117
  const variableSnapshot = useMemo2(() => structuredClone(variables), [variables]);
4875
5118
  const context = useMemo2(() => definition ? buildRenderContext(block, "canvas", body, definition) : null, [block, body, definition]);
4876
5119
  const renderedHtml = useMemo2(() => definition && context ? renderBlockContentFromContext(context, definition, {
4877
5120
  resolveHtmlTemplate: true,
5121
+ templateEngine,
5122
+ variables,
4878
5123
  templateData: mergeTemplateData(variables, block.defaultData),
4879
5124
  body
4880
- }) : "", [block.defaultData, body, context, definition, variables]);
5125
+ }) : "", [block.defaultData, body, context, definition, variables, templateEngine]);
4881
5126
  if (!definition || !context) return /* @__PURE__ */ jsxs38("div", { className: "ree-plugin-error", role: "alert", children: [
4882
5127
  "Missing block plugin: ",
4883
5128
  block.type,
@@ -4897,7 +5142,7 @@ function PluginCanvasContent({ block, body, definition, variables, selected, too
4897
5142
  }
4898
5143
 
4899
5144
  // src/stage/canvas/components/BlockActions.tsx
4900
- import { useId as useId11 } from "react";
5145
+ import { useId as useId12 } from "react";
4901
5146
  import { jsx as jsx50, jsxs as jsxs39 } from "react/jsx-runtime";
4902
5147
  function BlockActions({
4903
5148
  inlineEdit,
@@ -4907,7 +5152,7 @@ function BlockActions({
4907
5152
  onDuplicate,
4908
5153
  onRemove
4909
5154
  }) {
4910
- const tooltipId = useId11();
5155
+ const tooltipId = useId12();
4911
5156
  const toggleLabel = toolbarCollapsed ? "Expand Toolbar" : "Collapse Toolbar";
4912
5157
  return /* @__PURE__ */ jsxs39(
4913
5158
  "div",
@@ -4994,6 +5239,7 @@ function CanvasBlock({
4994
5239
  selected,
4995
5240
  dropEdge,
4996
5241
  variables,
5242
+ templateEngine,
4997
5243
  onSelect,
4998
5244
  onCommitControl,
4999
5245
  onCommitControls,
@@ -5003,8 +5249,9 @@ function CanvasBlock({
5003
5249
  }) {
5004
5250
  var _a, _b, _c;
5005
5251
  const block = useMemo3(() => __spreadProps(__spreadValues({}, sourceBlock), { style: resolveBlockStyle(sourceBlock.style, definition) }), [sourceBlock, definition]);
5006
- const toolbarId = useId12();
5007
- const [toolbarCollapsed, setToolbarCollapsed] = useState15(false);
5252
+ const renderKey = useMemo3(() => ({ definition, templateEngine, variables }), [definition, templateEngine, variables]);
5253
+ const toolbarId = useId13();
5254
+ const [toolbarCollapsed, setToolbarCollapsed] = useState16(false);
5008
5255
  const dragData = { kind: "canvas" };
5009
5256
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: block.id, data: dragData });
5010
5257
  const select = (event) => {
@@ -5064,7 +5311,7 @@ function CanvasBlock({
5064
5311
  {
5065
5312
  className: `ree-block-content${(definition == null ? void 0 : definition.inlineControl) ? " is-inline" : ""}`,
5066
5313
  style: canvasBlockStyle(block, body),
5067
- children: /* @__PURE__ */ jsx51(BlockErrorBoundary, { identity: sourceBlock, resetKey: definition, children: /* @__PURE__ */ jsx51(PluginCanvasContent, { block, body, definition, variables, selected, toolbarId, toolbarCollapsed, select: onSelect, onCommitControl, onCommitControls, onCommitStyle }) })
5314
+ children: /* @__PURE__ */ jsx51(BlockErrorBoundary, { identity: sourceBlock, resetKey: renderKey, children: /* @__PURE__ */ jsx51(PluginCanvasContent, { block, body, definition, variables, templateEngine, selected, toolbarId, toolbarCollapsed, select: onSelect, onCommitControl, onCommitControls, onCommitStyle }) })
5068
5315
  }
5069
5316
  ),
5070
5317
  definition ? /* @__PURE__ */ jsx51("span", { className: "ree-block-badge", children: definition.title }) : null
@@ -5083,6 +5330,7 @@ function Canvas({
5083
5330
  dropTargetId,
5084
5331
  dropEdge,
5085
5332
  variables,
5333
+ templateEngine,
5086
5334
  device,
5087
5335
  onSelect,
5088
5336
  onCommitControl,
@@ -5149,6 +5397,7 @@ function Canvas({
5149
5397
  selected: block.id === selectedId,
5150
5398
  dropEdge: dropTargetId === block.id ? dropEdge : null,
5151
5399
  variables,
5400
+ templateEngine,
5152
5401
  onSelect: () => onSelect(block.id),
5153
5402
  onCommitControl,
5154
5403
  onCommitControls,
@@ -5171,68 +5420,17 @@ function Canvas({
5171
5420
  }
5172
5421
 
5173
5422
  // src/stage/preview/Preview.tsx
5174
- import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef13 } from "react";
5175
-
5176
- // src/render/document-html.ts
5177
- function documentToHtml(document, definitions = DEFAULT_BLOCKS, options = {}) {
5178
- const source = ensureEmailDocument(document);
5179
- const body = source.body;
5180
- const baseCss = emailBaseCss(body);
5181
- const rendered = source.blocks.map((node, index) => {
5182
- var _a;
5183
- const blockData = __spreadValues(__spreadValues({}, options.templateData), node.defaultData);
5184
- const block = blockToHtml(node, definitions, "email", {
5185
- resolveHtmlTemplate: options.resolveHtmlTemplates,
5186
- body,
5187
- variables: options.variables,
5188
- templateData: mergeTemplateData(
5189
- (_a = options.variables) != null ? _a : [],
5190
- Object.keys(blockData).length > 0 ? blockData : void 0
5191
- )
5192
- });
5193
- const gap = index < source.blocks.length - 1 ? spacerHtml(body.blockGap) : "";
5194
- return block + gap;
5195
- }).join("\n");
5196
- const { html: content, styles } = extractHeadStyles(rendered);
5197
- return [
5198
- "<!DOCTYPE html>",
5199
- '<html lang="en">',
5200
- "<head>",
5201
- '<meta charset="utf-8" />',
5202
- '<meta name="viewport" content="width=device-width, initial-scale=1" />',
5203
- "<title>Email</title>",
5204
- styles,
5205
- "</head>",
5206
- `<body style="margin:0;padding:0;${baseCss};background-color:#ffffff;word-spacing:normal;">`,
5207
- `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="${baseCss};background-color:#ffffff;table-layout:fixed;">`,
5208
- "<tr>",
5209
- `<td align="center" style="padding:0;${baseCss};">`,
5210
- `<table role="presentation" align="center" width="${body.contentWidth}" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:${body.contentWidth}px;margin-left:auto;margin-right:auto;table-layout:fixed;">`,
5211
- "<tr>",
5212
- `<td style="box-sizing:border-box;${styleToCss(surfaceStyle(body))};">`,
5213
- content,
5214
- "</td>",
5215
- "</tr>",
5216
- "</table>",
5217
- "</td>",
5218
- "</tr>",
5219
- "</table>",
5220
- "</body>",
5221
- "</html>"
5222
- ].join("\n");
5223
- }
5224
-
5225
- // src/stage/preview/Preview.tsx
5423
+ import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef14 } from "react";
5226
5424
  import { jsx as jsx53 } from "react/jsx-runtime";
5227
- function Preview({ document, definitions, variables, device }) {
5228
- const frameRef = useRef13(null);
5229
- const cleanupRef = useRef13();
5425
+ function Preview({ document, config, variables, device }) {
5426
+ const frameRef = useRef14(null);
5427
+ const cleanupRef = useRef14();
5230
5428
  const html = useMemo4(
5231
- () => documentToHtml(document, definitions, {
5429
+ () => config.toHtml(document, {
5232
5430
  resolveHtmlTemplates: true,
5233
5431
  variables
5234
5432
  }),
5235
- [document, definitions, variables]
5433
+ [document, config, variables]
5236
5434
  );
5237
5435
  const observeContent = useCallback2(() => {
5238
5436
  var _a;
@@ -5378,6 +5576,7 @@ function Stage({
5378
5576
  onDeviceChange,
5379
5577
  onSelectBlock
5380
5578
  }) {
5579
+ const previewKey = useMemo5(() => ({ config: session.config, variables: session.variables }), [session.config, session.variables]);
5381
5580
  const { targetId, dropEdge } = useEditorDragState();
5382
5581
  return /* @__PURE__ */ jsxs42("div", { className: "ree-col ree-col-center", children: [
5383
5582
  /* @__PURE__ */ jsx55(
@@ -5399,6 +5598,7 @@ function Stage({
5399
5598
  dropTargetId: targetId,
5400
5599
  dropEdge,
5401
5600
  variables: session.variables,
5601
+ templateEngine: session.config.templateEngine,
5402
5602
  device,
5403
5603
  onSelect: onSelectBlock,
5404
5604
  onCommitControl: session.updateControl,
@@ -5407,11 +5607,11 @@ function Stage({
5407
5607
  onDuplicate: session.duplicateBlock,
5408
5608
  onRemove: session.removeBlock
5409
5609
  }
5410
- ) }) : /* @__PURE__ */ jsx55("div", { className: "ree-stage-view", children: /* @__PURE__ */ jsx55(BlockErrorBoundary, { identity: session.document, resetKey: session.definitions, children: /* @__PURE__ */ jsx55(
5610
+ ) }) : /* @__PURE__ */ jsx55("div", { className: "ree-stage-view", children: /* @__PURE__ */ jsx55(BlockErrorBoundary, { identity: session.document, resetKey: previewKey, children: /* @__PURE__ */ jsx55(
5411
5611
  Preview,
5412
5612
  {
5413
5613
  document: session.document,
5414
- definitions: session.definitions,
5614
+ config: session.config,
5415
5615
  variables: session.variables,
5416
5616
  device
5417
5617
  }
@@ -5420,75 +5620,22 @@ function Stage({
5420
5620
  }
5421
5621
 
5422
5622
  // src/shell/SubjectBar.tsx
5423
- import { useId as useId13, useRef as useRef15 } from "react";
5623
+ import { useId as useId14, useRef as useRef15 } from "react";
5424
5624
 
5425
5625
  // src/shell/constants.ts
5426
5626
  var EMPTY_VARIABLES = [];
5427
5627
  var PERSONALIZATION_HINT = "Personalization fields will appear blank if no data is available.";
5428
5628
 
5429
- // src/shell/PersonalizationMenu.tsx
5430
- import { useEffect as useEffect8, useRef as useRef14, useState as useState16 } from "react";
5431
- import { jsx as jsx56, jsxs as jsxs43 } from "react/jsx-runtime";
5432
- function PersonalizationMenu({
5433
- variables,
5434
- onInsert
5435
- }) {
5436
- const [open, setOpen] = useState16(false);
5437
- const rootRef = useRef14(null);
5438
- useEffect8(() => {
5439
- const root = rootRef.current;
5440
- if (!open || !root) return;
5441
- const ownerDocument = root.ownerDocument;
5442
- const onPointerDown = (event) => {
5443
- if (!root.contains(event.target)) setOpen(false);
5444
- };
5445
- ownerDocument.addEventListener("pointerdown", onPointerDown, true);
5446
- return () => ownerDocument.removeEventListener("pointerdown", onPointerDown, true);
5447
- }, [open]);
5448
- if (variables.length === 0) return null;
5449
- return /* @__PURE__ */ jsxs43("div", { className: "ree-personalization", ref: rootRef, children: [
5450
- /* @__PURE__ */ jsxs43(
5451
- "button",
5452
- {
5453
- type: "button",
5454
- className: "ree-personalization-btn",
5455
- "aria-expanded": open,
5456
- onClick: () => setOpen((current) => !current),
5457
- children: [
5458
- "Personalization",
5459
- /* @__PURE__ */ jsx56("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx56("path", { d: "M5.2 7.5 10 12.3l4.8-4.8 1.2 1.2L10 14.7 4 8.7l1.2-1.2Z" }) })
5460
- ]
5461
- }
5462
- ),
5463
- open ? /* @__PURE__ */ jsx56("div", { className: "ree-var-menu ree-personalization-menu", children: variables.map((variable) => /* @__PURE__ */ jsxs43(
5464
- "button",
5465
- {
5466
- type: "button",
5467
- className: "ree-var-item",
5468
- onClick: () => {
5469
- onInsert(variable.name);
5470
- setOpen(false);
5471
- },
5472
- children: [
5473
- /* @__PURE__ */ jsx56("span", { className: "ree-var-name", children: variable.name }),
5474
- variable.description ? /* @__PURE__ */ jsx56("span", { className: "ree-var-label", children: variable.description }) : null
5475
- ]
5476
- },
5477
- variable.name
5478
- )) }) : null
5479
- ] });
5480
- }
5481
-
5482
5629
  // src/shell/SubjectBar.tsx
5483
- import { Fragment as Fragment5, jsx as jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
5630
+ import { Fragment as Fragment5, jsx as jsx56, jsxs as jsxs43 } from "react/jsx-runtime";
5484
5631
  function SubjectBar({
5485
5632
  subject,
5486
5633
  variables,
5487
5634
  onSubjectChange
5488
5635
  }) {
5489
5636
  const inputRef = useRef15(null);
5490
- const subjectId = useId13();
5491
- const countId = useId13();
5637
+ const subjectId = useId14();
5638
+ const countId = useId14();
5492
5639
  const insertVariable = (token) => {
5493
5640
  var _a, _b;
5494
5641
  const input = inputRef.current;
@@ -5506,10 +5653,10 @@ function SubjectBar({
5506
5653
  (_b2 = inputRef.current) == null ? void 0 : _b2.setSelectionRange(cursor, cursor);
5507
5654
  });
5508
5655
  };
5509
- return /* @__PURE__ */ jsxs44("div", { className: "ree-subject-bar", children: [
5510
- /* @__PURE__ */ jsx57("label", { className: "ree-subject-label", htmlFor: subjectId, children: "Email subject" }),
5511
- /* @__PURE__ */ jsxs44("div", { className: "ree-subject-field", children: [
5512
- /* @__PURE__ */ jsx57(
5656
+ return /* @__PURE__ */ jsxs43("div", { className: "ree-subject-bar", children: [
5657
+ /* @__PURE__ */ jsx56("label", { className: "ree-subject-label", htmlFor: subjectId, children: "Email subject" }),
5658
+ /* @__PURE__ */ jsxs43("div", { className: "ree-subject-field", children: [
5659
+ /* @__PURE__ */ jsx56(
5513
5660
  TextInput,
5514
5661
  {
5515
5662
  id: subjectId,
@@ -5521,15 +5668,15 @@ function SubjectBar({
5521
5668
  onChange: (event) => onSubjectChange(event.target.value)
5522
5669
  }
5523
5670
  ),
5524
- /* @__PURE__ */ jsxs44("span", { id: countId, className: "ree-subject-count", children: [
5671
+ /* @__PURE__ */ jsxs43("span", { id: countId, className: "ree-subject-count", children: [
5525
5672
  subject.length,
5526
5673
  "/",
5527
5674
  SUBJECT_MAX_LENGTH
5528
5675
  ] })
5529
5676
  ] }),
5530
- variables.length > 0 ? /* @__PURE__ */ jsxs44(Fragment5, { children: [
5531
- /* @__PURE__ */ jsx57(PersonalizationMenu, { variables, onInsert: insertVariable }),
5532
- /* @__PURE__ */ jsx57("p", { className: "ree-subject-hint", children: PERSONALIZATION_HINT })
5677
+ variables.length > 0 ? /* @__PURE__ */ jsxs43(Fragment5, { children: [
5678
+ /* @__PURE__ */ jsx56(PersonalizationMenu, { variables, onInsert: insertVariable }),
5679
+ /* @__PURE__ */ jsx56("p", { className: "ree-subject-hint", children: PERSONALIZATION_HINT })
5533
5680
  ] }) : null
5534
5681
  ] });
5535
5682
  }
@@ -5634,9 +5781,59 @@ function createBlockNode(type, definitions) {
5634
5781
  };
5635
5782
  }
5636
5783
 
5784
+ // src/render/document-html.ts
5785
+ function documentToHtml(document, definitions = DEFAULT_BLOCKS, options = {}) {
5786
+ const source = ensureEmailDocument(document);
5787
+ const body = source.body;
5788
+ const baseCss = emailBaseCss(body);
5789
+ const rendered = source.blocks.map((node, index) => {
5790
+ var _a;
5791
+ const blockData = __spreadValues(__spreadValues({}, options.templateData), node.defaultData);
5792
+ const block = blockToHtml(node, definitions, "email", {
5793
+ resolveHtmlTemplate: options.resolveHtmlTemplates,
5794
+ templateEngine: options.templateEngine,
5795
+ body,
5796
+ variables: options.variables,
5797
+ templateData: mergeTemplateData(
5798
+ (_a = options.variables) != null ? _a : [],
5799
+ Object.keys(blockData).length > 0 ? blockData : void 0
5800
+ )
5801
+ });
5802
+ const gap = index < source.blocks.length - 1 ? spacerHtml(body.blockGap) : "";
5803
+ return block + gap;
5804
+ }).join("\n");
5805
+ const { html: content, styles } = extractHeadStyles(rendered);
5806
+ return [
5807
+ "<!DOCTYPE html>",
5808
+ '<html lang="en">',
5809
+ "<head>",
5810
+ '<meta charset="utf-8" />',
5811
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
5812
+ "<title>Email</title>",
5813
+ styles,
5814
+ "</head>",
5815
+ `<body style="margin:0;padding:0;${baseCss};background-color:#ffffff;word-spacing:normal;">`,
5816
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="${baseCss};background-color:#ffffff;table-layout:fixed;">`,
5817
+ "<tr>",
5818
+ `<td align="center" style="padding:0;${baseCss};">`,
5819
+ `<table role="presentation" align="center" width="${body.contentWidth}" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:${body.contentWidth}px;margin-left:auto;margin-right:auto;table-layout:fixed;">`,
5820
+ "<tr>",
5821
+ `<td style="box-sizing:border-box;${styleToCss(surfaceStyle(body))};">`,
5822
+ content,
5823
+ "</td>",
5824
+ "</tr>",
5825
+ "</table>",
5826
+ "</td>",
5827
+ "</tr>",
5828
+ "</table>",
5829
+ "</body>",
5830
+ "</html>"
5831
+ ].join("\n");
5832
+ }
5833
+
5637
5834
  // src/plugins/config.ts
5638
5835
  function buildEmailEditorConfig(options = {}) {
5639
- var _a, _b, _c;
5836
+ var _a, _b, _c, _d;
5640
5837
  const definitions = new Map(DEFAULT_BLOCKS.map((block) => [block.type, block]));
5641
5838
  const customTypes = /* @__PURE__ */ new Set();
5642
5839
  for (const plugin of (_a = options.plugins) != null ? _a : []) {
@@ -5651,16 +5848,21 @@ function buildEmailEditorConfig(options = {}) {
5651
5848
  for (const type of visibleBuiltins) if (!builtinTypes.has(type)) throw new Error(`Unknown builtin block: ${type}`);
5652
5849
  const all = Object.freeze([...definitions.values()].map((plugin) => snapshotValue(plugin)));
5653
5850
  const registered = new Map(all.map((plugin) => [plugin.type, plugin]));
5851
+ const templateEngine = (_c = options.templateEngine) != null ? _c : defaultTemplateEngine;
5654
5852
  return Object.freeze({
5853
+ templateEngine,
5655
5854
  definitions: all,
5656
- library: Object.freeze([...visibleBuiltins.map((type) => registered.get(type)), ...((_c = options.plugins) != null ? _c : []).filter((plugin) => !builtinTypes.has(plugin.type)).map((plugin) => registered.get(plugin.type))]),
5855
+ library: Object.freeze([...visibleBuiltins.map((type) => registered.get(type)), ...((_d = options.plugins) != null ? _d : []).filter((plugin) => !builtinTypes.has(plugin.type)).map((plugin) => registered.get(plugin.type))]),
5657
5856
  pickImage: options.pickImage,
5658
5857
  createBlock: (type) => {
5659
5858
  const block = createBlockNode(type, all);
5660
5859
  if (!block) throw new Error(`Unknown block plugin: ${type}`);
5661
5860
  return block;
5662
5861
  },
5663
- toHtml: (document, htmlOptions) => documentToHtml(document, all, htmlOptions),
5862
+ toHtml: (document, htmlOptions) => {
5863
+ var _a2;
5864
+ return documentToHtml(document, all, __spreadProps(__spreadValues({}, htmlOptions), { templateEngine: (_a2 = htmlOptions == null ? void 0 : htmlOptions.templateEngine) != null ? _a2 : templateEngine }));
5865
+ },
5664
5866
  validate: (document) => validateDocument(document, all)
5665
5867
  });
5666
5868
  }
@@ -5897,7 +6099,7 @@ function useEditorSession(editor) {
5897
6099
  }
5898
6100
 
5899
6101
  // src/shell/EmailEditorShell.tsx
5900
- import { jsx as jsx58, jsxs as jsxs45 } from "react/jsx-runtime";
6102
+ import { jsx as jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
5901
6103
  function EmailEditorShell({ editor, height = "100%" }) {
5902
6104
  var _a;
5903
6105
  const session = useEditorSession(editor);
@@ -5935,8 +6137,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
5935
6137
  selectBlock(null);
5936
6138
  };
5937
6139
  const showCanvas = () => setViewMode("edit");
5938
- return /* @__PURE__ */ jsx58(EditorConfigContext.Provider, { value: session.config, children: /* @__PURE__ */ jsx58(RichTextFocusContext.Provider, { value: { active: activeRichText, activate: setActiveRichText }, children: /* @__PURE__ */ jsxs45("div", { className: "ree-root", style: { height }, onKeyDown: handleKeyDown, children: [
5939
- /* @__PURE__ */ jsx58(
6140
+ return /* @__PURE__ */ jsx57(EditorConfigContext.Provider, { value: session.config, children: /* @__PURE__ */ jsx57(RichTextFocusContext.Provider, { value: { active: activeRichText, activate: setActiveRichText }, children: /* @__PURE__ */ jsxs44("div", { className: "ree-root", style: { height }, onKeyDown: handleKeyDown, children: [
6141
+ /* @__PURE__ */ jsx57(
5940
6142
  SubjectBar,
5941
6143
  {
5942
6144
  subject: (_a = session.document.subject) != null ? _a : "",
@@ -5944,8 +6146,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
5944
6146
  onSubjectChange: session.setSubject
5945
6147
  }
5946
6148
  ),
5947
- /* @__PURE__ */ jsxs45("div", { className: "ree-body", children: [
5948
- /* @__PURE__ */ jsxs45(
6149
+ /* @__PURE__ */ jsxs44("div", { className: "ree-body", children: [
6150
+ /* @__PURE__ */ jsxs44(
5949
6151
  EditorDnd,
5950
6152
  {
5951
6153
  blocks: session.document.blocks,
@@ -5957,7 +6159,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
5957
6159
  onReorder: session.reorderBlocks,
5958
6160
  onLibraryDragStart: showCanvas,
5959
6161
  children: [
5960
- /* @__PURE__ */ jsx58(
6162
+ /* @__PURE__ */ jsx57(
5961
6163
  BlockLibrary,
5962
6164
  {
5963
6165
  definitions: session.config.library,
@@ -5967,7 +6169,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
5967
6169
  }
5968
6170
  }
5969
6171
  ),
5970
- /* @__PURE__ */ jsx58(
6172
+ /* @__PURE__ */ jsx57(
5971
6173
  Stage,
5972
6174
  {
5973
6175
  session,
@@ -5981,7 +6183,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
5981
6183
  ]
5982
6184
  }
5983
6185
  ),
5984
- /* @__PURE__ */ jsx58(
6186
+ /* @__PURE__ */ jsx57(
5985
6187
  Inspector,
5986
6188
  {
5987
6189
  panel: inspectorPanel,
@@ -6001,21 +6203,21 @@ function EmailEditorShell({ editor, height = "100%" }) {
6001
6203
  }
6002
6204
 
6003
6205
  // src/EmailEditor.tsx
6004
- import { jsx as jsx59 } from "react/jsx-runtime";
6206
+ import { jsx as jsx58 } from "react/jsx-runtime";
6005
6207
  function EmailEditor(props) {
6006
- return /* @__PURE__ */ jsx59(EmailEditorShell, __spreadValues({}, props));
6208
+ return /* @__PURE__ */ jsx58(EmailEditorShell, __spreadValues({}, props));
6007
6209
  }
6008
6210
 
6009
6211
  // src/plugins/useEmailEditorConfig.ts
6010
- import { useMemo as useMemo5 } from "react";
6212
+ import { useMemo as useMemo6 } from "react";
6011
6213
  function useEmailEditorConfig(options = {}) {
6012
- const { plugins, builtins, pickImage } = options;
6013
- return useMemo5(() => buildEmailEditorConfig({ plugins, builtins, pickImage }), [plugins, builtins, pickImage]);
6214
+ const { plugins, builtins, pickImage, templateEngine } = options;
6215
+ return useMemo6(() => buildEmailEditorConfig({ plugins, builtins, pickImage, templateEngine }), [plugins, builtins, pickImage, templateEngine]);
6014
6216
  }
6015
6217
 
6016
6218
  // src/shell/useEmailEditor.ts
6017
- import { useEffect as useEffect9, useInsertionEffect, useLayoutEffect as useLayoutEffect8, useState as useState19 } from "react";
6018
- var useOptionsEffect = typeof window === "undefined" ? useEffect9 : useLayoutEffect8;
6219
+ import { useEffect as useEffect8, useInsertionEffect, useLayoutEffect as useLayoutEffect9, useState as useState19 } from "react";
6220
+ var useOptionsEffect = typeof window === "undefined" ? useEffect8 : useLayoutEffect9;
6019
6221
  function useEmailEditor(options = {}) {
6020
6222
  const [store] = useState19(() => createEditorStore(options));
6021
6223
  useInsertionEffect(() => {
@@ -6033,12 +6235,12 @@ function useEmailEditorState(editor) {
6033
6235
  }
6034
6236
 
6035
6237
  // src/plugins/components/ControlEditor.tsx
6036
- import { jsx as jsx60 } from "react/jsx-runtime";
6238
+ import { jsx as jsx59 } from "react/jsx-runtime";
6037
6239
  function ControlEditor({ control }) {
6038
6240
  const context = usePluginEditor();
6039
6241
  const definition = context.controlDefinitions.find((item) => item.name === control);
6040
6242
  if (!definition) throw new Error(`Unknown plugin control: ${control}`);
6041
- return /* @__PURE__ */ jsx60(ControlRenderer, { control: definition, value: context.controls[control], body: context.body, variables: context.variables, onChange: (value) => context.setControl(control, value) });
6243
+ return /* @__PURE__ */ jsx59(ControlRenderer, { control: definition, value: context.controls[control], body: context.body, variables: context.variables, onChange: (value) => context.setControl(control, value) });
6042
6244
  }
6043
6245
  export {
6044
6246
  BLOCK_TYPES,
@@ -6070,6 +6272,7 @@ export {
6070
6272
  createDimensionControls,
6071
6273
  createEmailDocument,
6072
6274
  createTypographyControls,
6275
+ defaultTemplateEngine,
6073
6276
  defineBlock,
6074
6277
  documentToHtml,
6075
6278
  generateId,
@@ -6082,5 +6285,6 @@ export {
6082
6285
  useEmailEditor,
6083
6286
  useEmailEditorConfig,
6084
6287
  useEmailEditorState,
6288
+ validateTemplate,
6085
6289
  variablesToTemplateData
6086
6290
  };