@standhigher/react-email-editor 0.4.3 → 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/README.md +7 -6
- package/README.zh-CN.md +7 -6
- package/dist/index.d.ts +44 -17
- package/dist/index.js +246 -102
- package/docs/advanced-examples.md +5 -1
- package/docs/advanced-examples.zh-CN.md +5 -1
- package/docs/api.md +23 -11
- package/docs/api.zh-CN.md +23 -11
- package/docs/changelog.md +44 -0
- package/docs/changelog.zh-CN.md +44 -0
- package/docs/custom-blocks.md +3 -3
- package/docs/custom-blocks.zh-CN.md +3 -3
- package/docs/release.md +4 -4
- package/docs/release.zh-CN.md +4 -4
- package/docs/template-engines.md +73 -0
- package/docs/template-engines.zh-CN.md +73 -0
- package/package.json +6 -11
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: "
|
|
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
|
|
2666
|
-
throw new
|
|
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 : (
|
|
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: [
|
|
@@ -4589,6 +4614,9 @@ function Inspector({
|
|
|
4589
4614
|
] });
|
|
4590
4615
|
}
|
|
4591
4616
|
|
|
4617
|
+
// src/stage/Stage.tsx
|
|
4618
|
+
import { useMemo as useMemo5 } from "react";
|
|
4619
|
+
|
|
4592
4620
|
// src/stage/canvas/Canvas.tsx
|
|
4593
4621
|
import { useDroppable } from "@dnd-kit/core";
|
|
4594
4622
|
import {
|
|
@@ -4619,19 +4647,88 @@ import { CSS } from "@dnd-kit/utilities";
|
|
|
4619
4647
|
// src/stage/canvas/components/PluginCanvasContent.tsx
|
|
4620
4648
|
import { useMemo as useMemo2 } from "react";
|
|
4621
4649
|
|
|
4622
|
-
// src/render/template-
|
|
4623
|
-
var TOKEN_PATTERN = /\{\{
|
|
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
|
+
}
|
|
4624
4715
|
function tokenize(template) {
|
|
4625
4716
|
var _a;
|
|
4626
4717
|
const tokens = [];
|
|
4627
4718
|
let lastIndex = 0;
|
|
4719
|
+
let trimNext = false;
|
|
4628
4720
|
for (const match of template.matchAll(TOKEN_PATTERN)) {
|
|
4629
4721
|
const index = (_a = match.index) != null ? _a : 0;
|
|
4630
|
-
|
|
4631
|
-
|
|
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]);
|
|
4632
4728
|
lastIndex = index + match[0].length;
|
|
4633
4729
|
}
|
|
4634
|
-
|
|
4730
|
+
const tail = template.slice(lastIndex);
|
|
4731
|
+
if (tail) tokens.push({ text: trimNext ? tail.replace(/^[ \t\r\n]+/, "") : tail });
|
|
4635
4732
|
return tokens;
|
|
4636
4733
|
}
|
|
4637
4734
|
function parseNodes(tokens, startIndex) {
|
|
@@ -4646,11 +4743,11 @@ function parseNodes(tokens, startIndex) {
|
|
|
4646
4743
|
continue;
|
|
4647
4744
|
}
|
|
4648
4745
|
const tag = (_a = token.tag) != null ? _a : "";
|
|
4649
|
-
if (tag === "else" ||
|
|
4746
|
+
if (tag === "else" || /^else\s+if\s+/.test(tag)) {
|
|
4650
4747
|
return { nodes, nextIndex: index, stopTag: "else" };
|
|
4651
4748
|
}
|
|
4652
4749
|
if (tag === "end") return { nodes, nextIndex: index, stopTag: "end" };
|
|
4653
|
-
const branchMatch = /^(if|range|with)\s+(
|
|
4750
|
+
const branchMatch = /^(if|range|with)\s+([\s\S]+)$/.exec(tag);
|
|
4654
4751
|
if (branchMatch) {
|
|
4655
4752
|
const branch = parseBranch(
|
|
4656
4753
|
tokens,
|
|
@@ -4674,7 +4771,7 @@ function parseBranch(tokens, startIndex, kind, expression) {
|
|
|
4674
4771
|
let nextIndex = body.nextIndex;
|
|
4675
4772
|
if (body.stopTag === "else") {
|
|
4676
4773
|
const elseTag = (_b = (_a = tokens[body.nextIndex]) == null ? void 0 : _a.tag) != null ? _b : "";
|
|
4677
|
-
const elseIfMatch = /^else\s+if\s+(
|
|
4774
|
+
const elseIfMatch = /^else\s+if\s+([\s\S]+)$/.exec(elseTag);
|
|
4678
4775
|
if (elseIfMatch) {
|
|
4679
4776
|
const nested = parseBranch(
|
|
4680
4777
|
tokens,
|
|
@@ -4698,42 +4795,44 @@ function parseBranch(tokens, startIndex, kind, expression) {
|
|
|
4698
4795
|
function readPath(value, path) {
|
|
4699
4796
|
if (path === "") return value;
|
|
4700
4797
|
return path.split(".").reduce((current, key) => {
|
|
4701
|
-
if (current && typeof current === "object") return current[key];
|
|
4798
|
+
if (current && typeof current === "object" && Object.hasOwn(current, key)) return current[key];
|
|
4702
4799
|
return void 0;
|
|
4703
4800
|
}, value);
|
|
4704
4801
|
}
|
|
4705
4802
|
function resolveExpression(expression, root, current) {
|
|
4706
4803
|
const trimmed = expression.trim();
|
|
4707
4804
|
if (trimmed === ".") return current;
|
|
4805
|
+
if (trimmed === "$") return root;
|
|
4708
4806
|
if (trimmed === "true") return true;
|
|
4709
4807
|
if (trimmed === "false") return false;
|
|
4710
4808
|
if (trimmed === "nil") return null;
|
|
4711
|
-
if (
|
|
4712
|
-
if (trimmed.startsWith('"')
|
|
4713
|
-
return trimmed.slice(1, -1);
|
|
4714
|
-
}
|
|
4809
|
+
if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
4810
|
+
if (trimmed.startsWith('"')) return JSON.parse(trimmed);
|
|
4715
4811
|
if (trimmed.startsWith("$.")) return readPath(root, trimmed.slice(2));
|
|
4716
4812
|
if (trimmed.startsWith(".")) return readPath(current, trimmed.slice(1));
|
|
4717
4813
|
return readPath(current, trimmed);
|
|
4718
4814
|
}
|
|
4719
4815
|
function isTruthy(value) {
|
|
4720
4816
|
if (value == null || value === false || value === "" || value === 0) return false;
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
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;
|
|
4725
4824
|
}
|
|
4726
4825
|
function renderNodes(nodes, root, current) {
|
|
4727
4826
|
return nodes.map((node) => {
|
|
4728
4827
|
if (node.kind === "text") return node.value;
|
|
4729
4828
|
if (node.kind === "expression") {
|
|
4730
4829
|
const value2 = resolveExpression(node.expression, root, current);
|
|
4731
|
-
return value2 == null ? "" :
|
|
4830
|
+
return value2 == null ? "" : esc(String(value2)).replaceAll("'", "'");
|
|
4732
4831
|
}
|
|
4733
4832
|
const value = resolveExpression(node.expression, root, current);
|
|
4734
4833
|
if (node.kind === "range") {
|
|
4735
|
-
const items = Array.isArray(value) ? value : value && typeof value === "object" ? Object.
|
|
4736
|
-
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);
|
|
4737
4836
|
}
|
|
4738
4837
|
if (!isTruthy(value)) {
|
|
4739
4838
|
return renderNodes(node.alternate, root, current);
|
|
@@ -4742,9 +4841,40 @@ function renderNodes(nodes, root, current) {
|
|
|
4742
4841
|
return renderNodes(node.children, root, childContext);
|
|
4743
4842
|
}).join("");
|
|
4744
4843
|
}
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
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
|
+
}
|
|
4748
4878
|
}
|
|
4749
4879
|
|
|
4750
4880
|
// src/render/block-html.ts
|
|
@@ -4769,7 +4899,7 @@ function variableControlName(name) {
|
|
|
4769
4899
|
const token = /^\{\{-?\s*([\s\S]*?)\s*-?\}\}$/.exec(name.trim());
|
|
4770
4900
|
const inner = (token ? token[1] : name).trim();
|
|
4771
4901
|
const control = inner.startsWith(".") ? inner.slice(1) : inner;
|
|
4772
|
-
if (!control || !/^[
|
|
4902
|
+
if (!control || !/^[\p{L}_][\p{L}\p{Nd}_]*(?:\.[\p{L}_][\p{L}\p{Nd}_]*)*$/u.test(control)) return null;
|
|
4773
4903
|
return control;
|
|
4774
4904
|
}
|
|
4775
4905
|
function dottedListedToken(name) {
|
|
@@ -4835,12 +4965,12 @@ function renderBlockContent(node, definitions = DEFAULT_BLOCKS, mode = "email",
|
|
|
4835
4965
|
}
|
|
4836
4966
|
function resolveBlockContent(rendered, node, options) {
|
|
4837
4967
|
var _a, _b;
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
}
|
|
4968
|
+
const normalized = applyListedVariableDots(rendered, (_a = options.variables) != null ? _a : []);
|
|
4969
|
+
if (!options.resolveHtmlTemplate) return normalized;
|
|
4841
4970
|
return renderTemplatePreview(
|
|
4842
|
-
|
|
4843
|
-
(_b = options.templateData) != null ? _b : node.defaultData
|
|
4971
|
+
normalized,
|
|
4972
|
+
(_b = options.templateData) != null ? _b : node.defaultData,
|
|
4973
|
+
options
|
|
4844
4974
|
);
|
|
4845
4975
|
}
|
|
4846
4976
|
function renderBlock(node, definitions = DEFAULT_BLOCKS, mode = "email", body = DEFAULT_EMAIL_BODY) {
|
|
@@ -4982,15 +5112,17 @@ function HtmlCanvas({ html, baseCss = "", onSelect }) {
|
|
|
4982
5112
|
|
|
4983
5113
|
// src/stage/canvas/components/PluginCanvasContent.tsx
|
|
4984
5114
|
import { jsx as jsx49, jsxs as jsxs38 } from "react/jsx-runtime";
|
|
4985
|
-
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 }) {
|
|
4986
5116
|
var _a, _b;
|
|
4987
5117
|
const variableSnapshot = useMemo2(() => structuredClone(variables), [variables]);
|
|
4988
5118
|
const context = useMemo2(() => definition ? buildRenderContext(block, "canvas", body, definition) : null, [block, body, definition]);
|
|
4989
5119
|
const renderedHtml = useMemo2(() => definition && context ? renderBlockContentFromContext(context, definition, {
|
|
4990
5120
|
resolveHtmlTemplate: true,
|
|
5121
|
+
templateEngine,
|
|
5122
|
+
variables,
|
|
4991
5123
|
templateData: mergeTemplateData(variables, block.defaultData),
|
|
4992
5124
|
body
|
|
4993
|
-
}) : "", [block.defaultData, body, context, definition, variables]);
|
|
5125
|
+
}) : "", [block.defaultData, body, context, definition, variables, templateEngine]);
|
|
4994
5126
|
if (!definition || !context) return /* @__PURE__ */ jsxs38("div", { className: "ree-plugin-error", role: "alert", children: [
|
|
4995
5127
|
"Missing block plugin: ",
|
|
4996
5128
|
block.type,
|
|
@@ -5107,6 +5239,7 @@ function CanvasBlock({
|
|
|
5107
5239
|
selected,
|
|
5108
5240
|
dropEdge,
|
|
5109
5241
|
variables,
|
|
5242
|
+
templateEngine,
|
|
5110
5243
|
onSelect,
|
|
5111
5244
|
onCommitControl,
|
|
5112
5245
|
onCommitControls,
|
|
@@ -5116,6 +5249,7 @@ function CanvasBlock({
|
|
|
5116
5249
|
}) {
|
|
5117
5250
|
var _a, _b, _c;
|
|
5118
5251
|
const block = useMemo3(() => __spreadProps(__spreadValues({}, sourceBlock), { style: resolveBlockStyle(sourceBlock.style, definition) }), [sourceBlock, definition]);
|
|
5252
|
+
const renderKey = useMemo3(() => ({ definition, templateEngine, variables }), [definition, templateEngine, variables]);
|
|
5119
5253
|
const toolbarId = useId13();
|
|
5120
5254
|
const [toolbarCollapsed, setToolbarCollapsed] = useState16(false);
|
|
5121
5255
|
const dragData = { kind: "canvas" };
|
|
@@ -5177,7 +5311,7 @@ function CanvasBlock({
|
|
|
5177
5311
|
{
|
|
5178
5312
|
className: `ree-block-content${(definition == null ? void 0 : definition.inlineControl) ? " is-inline" : ""}`,
|
|
5179
5313
|
style: canvasBlockStyle(block, body),
|
|
5180
|
-
children: /* @__PURE__ */ jsx51(BlockErrorBoundary, { identity: sourceBlock, resetKey:
|
|
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 }) })
|
|
5181
5315
|
}
|
|
5182
5316
|
),
|
|
5183
5317
|
definition ? /* @__PURE__ */ jsx51("span", { className: "ree-block-badge", children: definition.title }) : null
|
|
@@ -5196,6 +5330,7 @@ function Canvas({
|
|
|
5196
5330
|
dropTargetId,
|
|
5197
5331
|
dropEdge,
|
|
5198
5332
|
variables,
|
|
5333
|
+
templateEngine,
|
|
5199
5334
|
device,
|
|
5200
5335
|
onSelect,
|
|
5201
5336
|
onCommitControl,
|
|
@@ -5262,6 +5397,7 @@ function Canvas({
|
|
|
5262
5397
|
selected: block.id === selectedId,
|
|
5263
5398
|
dropEdge: dropTargetId === block.id ? dropEdge : null,
|
|
5264
5399
|
variables,
|
|
5400
|
+
templateEngine,
|
|
5265
5401
|
onSelect: () => onSelect(block.id),
|
|
5266
5402
|
onCommitControl,
|
|
5267
5403
|
onCommitControls,
|
|
@@ -5285,67 +5421,16 @@ function Canvas({
|
|
|
5285
5421
|
|
|
5286
5422
|
// src/stage/preview/Preview.tsx
|
|
5287
5423
|
import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef14 } from "react";
|
|
5288
|
-
|
|
5289
|
-
// src/render/document-html.ts
|
|
5290
|
-
function documentToHtml(document, definitions = DEFAULT_BLOCKS, options = {}) {
|
|
5291
|
-
const source = ensureEmailDocument(document);
|
|
5292
|
-
const body = source.body;
|
|
5293
|
-
const baseCss = emailBaseCss(body);
|
|
5294
|
-
const rendered = source.blocks.map((node, index) => {
|
|
5295
|
-
var _a;
|
|
5296
|
-
const blockData = __spreadValues(__spreadValues({}, options.templateData), node.defaultData);
|
|
5297
|
-
const block = blockToHtml(node, definitions, "email", {
|
|
5298
|
-
resolveHtmlTemplate: options.resolveHtmlTemplates,
|
|
5299
|
-
body,
|
|
5300
|
-
variables: options.variables,
|
|
5301
|
-
templateData: mergeTemplateData(
|
|
5302
|
-
(_a = options.variables) != null ? _a : [],
|
|
5303
|
-
Object.keys(blockData).length > 0 ? blockData : void 0
|
|
5304
|
-
)
|
|
5305
|
-
});
|
|
5306
|
-
const gap = index < source.blocks.length - 1 ? spacerHtml(body.blockGap) : "";
|
|
5307
|
-
return block + gap;
|
|
5308
|
-
}).join("\n");
|
|
5309
|
-
const { html: content, styles } = extractHeadStyles(rendered);
|
|
5310
|
-
return [
|
|
5311
|
-
"<!DOCTYPE html>",
|
|
5312
|
-
'<html lang="en">',
|
|
5313
|
-
"<head>",
|
|
5314
|
-
'<meta charset="utf-8" />',
|
|
5315
|
-
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
5316
|
-
"<title>Email</title>",
|
|
5317
|
-
styles,
|
|
5318
|
-
"</head>",
|
|
5319
|
-
`<body style="margin:0;padding:0;${baseCss};background-color:#ffffff;word-spacing:normal;">`,
|
|
5320
|
-
`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="${baseCss};background-color:#ffffff;table-layout:fixed;">`,
|
|
5321
|
-
"<tr>",
|
|
5322
|
-
`<td align="center" style="padding:0;${baseCss};">`,
|
|
5323
|
-
`<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;">`,
|
|
5324
|
-
"<tr>",
|
|
5325
|
-
`<td style="box-sizing:border-box;${styleToCss(surfaceStyle(body))};">`,
|
|
5326
|
-
content,
|
|
5327
|
-
"</td>",
|
|
5328
|
-
"</tr>",
|
|
5329
|
-
"</table>",
|
|
5330
|
-
"</td>",
|
|
5331
|
-
"</tr>",
|
|
5332
|
-
"</table>",
|
|
5333
|
-
"</body>",
|
|
5334
|
-
"</html>"
|
|
5335
|
-
].join("\n");
|
|
5336
|
-
}
|
|
5337
|
-
|
|
5338
|
-
// src/stage/preview/Preview.tsx
|
|
5339
5424
|
import { jsx as jsx53 } from "react/jsx-runtime";
|
|
5340
|
-
function Preview({ document,
|
|
5425
|
+
function Preview({ document, config, variables, device }) {
|
|
5341
5426
|
const frameRef = useRef14(null);
|
|
5342
5427
|
const cleanupRef = useRef14();
|
|
5343
5428
|
const html = useMemo4(
|
|
5344
|
-
() =>
|
|
5429
|
+
() => config.toHtml(document, {
|
|
5345
5430
|
resolveHtmlTemplates: true,
|
|
5346
5431
|
variables
|
|
5347
5432
|
}),
|
|
5348
|
-
[document,
|
|
5433
|
+
[document, config, variables]
|
|
5349
5434
|
);
|
|
5350
5435
|
const observeContent = useCallback2(() => {
|
|
5351
5436
|
var _a;
|
|
@@ -5491,6 +5576,7 @@ function Stage({
|
|
|
5491
5576
|
onDeviceChange,
|
|
5492
5577
|
onSelectBlock
|
|
5493
5578
|
}) {
|
|
5579
|
+
const previewKey = useMemo5(() => ({ config: session.config, variables: session.variables }), [session.config, session.variables]);
|
|
5494
5580
|
const { targetId, dropEdge } = useEditorDragState();
|
|
5495
5581
|
return /* @__PURE__ */ jsxs42("div", { className: "ree-col ree-col-center", children: [
|
|
5496
5582
|
/* @__PURE__ */ jsx55(
|
|
@@ -5512,6 +5598,7 @@ function Stage({
|
|
|
5512
5598
|
dropTargetId: targetId,
|
|
5513
5599
|
dropEdge,
|
|
5514
5600
|
variables: session.variables,
|
|
5601
|
+
templateEngine: session.config.templateEngine,
|
|
5515
5602
|
device,
|
|
5516
5603
|
onSelect: onSelectBlock,
|
|
5517
5604
|
onCommitControl: session.updateControl,
|
|
@@ -5520,11 +5607,11 @@ function Stage({
|
|
|
5520
5607
|
onDuplicate: session.duplicateBlock,
|
|
5521
5608
|
onRemove: session.removeBlock
|
|
5522
5609
|
}
|
|
5523
|
-
) }) : /* @__PURE__ */ jsx55("div", { className: "ree-stage-view", children: /* @__PURE__ */ jsx55(BlockErrorBoundary, { identity: session.document, resetKey:
|
|
5610
|
+
) }) : /* @__PURE__ */ jsx55("div", { className: "ree-stage-view", children: /* @__PURE__ */ jsx55(BlockErrorBoundary, { identity: session.document, resetKey: previewKey, children: /* @__PURE__ */ jsx55(
|
|
5524
5611
|
Preview,
|
|
5525
5612
|
{
|
|
5526
5613
|
document: session.document,
|
|
5527
|
-
|
|
5614
|
+
config: session.config,
|
|
5528
5615
|
variables: session.variables,
|
|
5529
5616
|
device
|
|
5530
5617
|
}
|
|
@@ -5694,9 +5781,59 @@ function createBlockNode(type, definitions) {
|
|
|
5694
5781
|
};
|
|
5695
5782
|
}
|
|
5696
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
|
+
|
|
5697
5834
|
// src/plugins/config.ts
|
|
5698
5835
|
function buildEmailEditorConfig(options = {}) {
|
|
5699
|
-
var _a, _b, _c;
|
|
5836
|
+
var _a, _b, _c, _d;
|
|
5700
5837
|
const definitions = new Map(DEFAULT_BLOCKS.map((block) => [block.type, block]));
|
|
5701
5838
|
const customTypes = /* @__PURE__ */ new Set();
|
|
5702
5839
|
for (const plugin of (_a = options.plugins) != null ? _a : []) {
|
|
@@ -5711,16 +5848,21 @@ function buildEmailEditorConfig(options = {}) {
|
|
|
5711
5848
|
for (const type of visibleBuiltins) if (!builtinTypes.has(type)) throw new Error(`Unknown builtin block: ${type}`);
|
|
5712
5849
|
const all = Object.freeze([...definitions.values()].map((plugin) => snapshotValue(plugin)));
|
|
5713
5850
|
const registered = new Map(all.map((plugin) => [plugin.type, plugin]));
|
|
5851
|
+
const templateEngine = (_c = options.templateEngine) != null ? _c : defaultTemplateEngine;
|
|
5714
5852
|
return Object.freeze({
|
|
5853
|
+
templateEngine,
|
|
5715
5854
|
definitions: all,
|
|
5716
|
-
library: Object.freeze([...visibleBuiltins.map((type) => registered.get(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))]),
|
|
5717
5856
|
pickImage: options.pickImage,
|
|
5718
5857
|
createBlock: (type) => {
|
|
5719
5858
|
const block = createBlockNode(type, all);
|
|
5720
5859
|
if (!block) throw new Error(`Unknown block plugin: ${type}`);
|
|
5721
5860
|
return block;
|
|
5722
5861
|
},
|
|
5723
|
-
toHtml: (document, 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
|
+
},
|
|
5724
5866
|
validate: (document) => validateDocument(document, all)
|
|
5725
5867
|
});
|
|
5726
5868
|
}
|
|
@@ -6067,10 +6209,10 @@ function EmailEditor(props) {
|
|
|
6067
6209
|
}
|
|
6068
6210
|
|
|
6069
6211
|
// src/plugins/useEmailEditorConfig.ts
|
|
6070
|
-
import { useMemo as
|
|
6212
|
+
import { useMemo as useMemo6 } from "react";
|
|
6071
6213
|
function useEmailEditorConfig(options = {}) {
|
|
6072
|
-
const { plugins, builtins, pickImage } = options;
|
|
6073
|
-
return
|
|
6214
|
+
const { plugins, builtins, pickImage, templateEngine } = options;
|
|
6215
|
+
return useMemo6(() => buildEmailEditorConfig({ plugins, builtins, pickImage, templateEngine }), [plugins, builtins, pickImage, templateEngine]);
|
|
6074
6216
|
}
|
|
6075
6217
|
|
|
6076
6218
|
// src/shell/useEmailEditor.ts
|
|
@@ -6130,6 +6272,7 @@ export {
|
|
|
6130
6272
|
createDimensionControls,
|
|
6131
6273
|
createEmailDocument,
|
|
6132
6274
|
createTypographyControls,
|
|
6275
|
+
defaultTemplateEngine,
|
|
6133
6276
|
defineBlock,
|
|
6134
6277
|
documentToHtml,
|
|
6135
6278
|
generateId,
|
|
@@ -6142,5 +6285,6 @@ export {
|
|
|
6142
6285
|
useEmailEditor,
|
|
6143
6286
|
useEmailEditorConfig,
|
|
6144
6287
|
useEmailEditorState,
|
|
6288
|
+
validateTemplate,
|
|
6145
6289
|
variablesToTemplateData
|
|
6146
6290
|
};
|
|
@@ -262,4 +262,8 @@ export const fragment = blockToHtml(block, undefined, "email", {
|
|
|
262
262
|
});
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
Complete documents shallow-merge variables → templateData → block.defaultData, with later data winning. Single-block rendering does not merge variables automatically; this example merges explicitly and uses the singular resolveHtmlTemplate option. Your application chooses the actual sending engine
|
|
265
|
+
Complete documents shallow-merge variables → templateData → block.defaultData, with later data winning. Preview and sending apply listed-variable dot normalization while preserving the stored template source. Single-block rendering does not merge variables automatically; this example merges explicitly and uses the singular resolveHtmlTemplate option. Your application chooses the actual sending engine. The default preview supports basic syntax; inject templateEngine for complete Go semantics.
|
|
266
|
+
|
|
267
|
+
## Custom template engine and standalone preview
|
|
268
|
+
|
|
269
|
+
See [Template preview and custom engines](https://github.com/standhigher/react-email-editor/blob/main/docs/template-engines.md) for supported syntax, adapter behavior and integration examples.
|
|
@@ -262,4 +262,8 @@ export const fragment = blockToHtml(block, undefined, "email", {
|
|
|
262
262
|
});
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
完整文档自动按顶层浅合并 `variables → templateData → block.defaultData
|
|
265
|
+
完整文档自动按顶层浅合并 `variables → templateData → block.defaultData`,后者优先。预览和发送会先按变量清单补点,存储中的模板原文保持不变;单区块不会自动合并变量,示例显式合并,并使用单数 `resolveHtmlTemplate`。真实发送端的模板引擎由应用决定;默认预览仅支持基础语法,完整 Go 语义通过 `templateEngine` 注入。
|
|
266
|
+
|
|
267
|
+
## 自定义模板引擎与独立预览
|
|
268
|
+
|
|
269
|
+
完整说明与接入示例见 [模板预览与自定义引擎](https://github.com/standhigher/react-email-editor/blob/main/docs/template-engines.zh-CN.md)。
|