@standhigher/react-email-editor 0.4.3 → 0.5.2

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.
@@ -0,0 +1,81 @@
1
+ # Template preview and custom engines
2
+
3
+ English | [简体中文](https://github.com/standhigher/react-email-editor/blob/main/docs/template-engines.zh-CN.md)
4
+
5
+ Inject a synchronous `TemplateEngine` with `useEmailEditorConfig({ templateEngine })`. Canvas `renderedHtml`, editor preview, `getRenderedHtml()` and `getData().renderedHtml` share it. Rich-text editing regions keep editable source tokens.
6
+
7
+ ```ts
8
+ interface TemplateEngine {
9
+ render: (template: string, data: TemplateData) => string;
10
+ validate?: (template: string) => TemplateValidationIssue[];
11
+ }
12
+
13
+ interface TemplateValidationIssue {
14
+ message: string;
15
+ offset?: number; // Zero-based character offset in the original template
16
+ }
17
+
18
+ interface TemplateEngineOptions {
19
+ templateEngine?: TemplateEngine;
20
+ }
21
+ ```
22
+
23
+ Wrap your own engine; this package does not bundle a complete Go engine. `renderTemplatePreview(template, data?, { templateEngine })` and `validateTemplate(template, { templateEngine })` use the same engine selection. Omitted engines use the exported `defaultTemplateEngine`. Replacing the engine refreshes rendering without resetting the document or invoking document-change callbacks.
24
+
25
+ - Each block's complete `renderEmail` HTML is sent to the selected engine after the same listed-token dot normalization used by sending HTML: exact `variables` tokens gain a leading dot while whitespace and trim markers are preserved. Other expressions pass through unchanged. Standalone preview passes the caller's template directly. No default parser runs first. Variables cannot span blocks because blocks execute separately.
26
+ - Preview data must be structured-cloneable. The engine receives a detached copy; mutating it does not change caller data or stored documents.
27
+ - The engine owns functions, pipelines, scope and escaping. Output is not escaped again. Match the backend's `text/template` or `html/template` behavior and escaping policy; the adapter alone does not guarantee equivalent engines.
28
+ - Rendering returns a string synchronously; Promises are unsupported. Existing `Error` objects propagate unchanged; other thrown values become an `Error` with a nonempty message and the original `cause`, for both rendering and validation. There is no fallback. The UI uses its existing error and Retry display; explicit API calls throw.
29
+ - Without data, the basic engine preserves source. An explicitly selected external engine still runs with `{}`.
30
+ - `validateTemplate()` only invokes the selected optional validator on explicit calls. `[]` means no issues were found; `undefined` means validation is unavailable. The basic engine has no Go validator. Rendering, `editor.validate()` and saving never invoke template validation automatically.
31
+ - `getHtml()`, `getJson()` and default `documentToHtml()` preserve templates. Existing listed-token dot normalization still applies. Engines only run when template resolution is requested.
32
+
33
+ The basic engine supports field paths, dot context, `$` / `$.field`, legacy bare paths, booleans/nil, decimal numbers, JSON-compatible double-quoted strings, basic `if/else if/else`, `range/else`, `with/else` and whitespace trimming. Arrays retain order; object keys are sorted. Missing fields produce empty strings and interpolated values are HTML-escaped.
34
+
35
+ Field names support Unicode letters, digits and underscores, starting with a letter or underscore. Conditions treat `null/undefined/false/0/empty strings`, empty arrays, plain objects without own enumerable keys and empty `Map/Set` values as false; nonempty collections and other objects such as `Date` are true. This only defines `Map/Set` truthiness, without adding field-access or iteration semantics.
36
+
37
+ Functions (including `eq/ne/and/or/not/len/index/printf/html`), pipelines, parentheses, local variables, template definitions/calls, comments and other full Go features require an external engine. Unsupported syntax or incomplete control structures preserve the **entire input for that render**, without partial evaluation. In the editor this preserves that block's HTML template.
38
+
39
+ ## Switching between editing and preview
40
+
41
+ The editing canvas stays mounted, so switching modes preserves rich-text editors and HTML block iframes. Editing and preview retain their own scroll positions. While preview is requested, the canvas is excluded from focus, keyboard and drag interactions, and rich-text toolbars are hidden.
42
+
43
+ Changes to the document, configuration, variables or device width schedule a complete email preview after a 400ms debounce, using an idle callback with a maximum wait of 2 seconds during continuous changes. Browsers without `requestIdleCallback` use a timer. Clicking Preview or Retry bypasses the wait and prepares the latest revision; subsequent changes in preview mode still use the same debounce. HTML generation still runs synchronously on the main thread; custom engines and `renderEmail` should be pure and allow repeated background calls.
44
+
45
+ Two iframe buffers keep loading and measurement in the standby layer. A buffer becomes visible only after it has loaded and its height has been measured. Until the latest revision is ready, the current view remains visible with a preparation status. Stale revisions cannot replace newer content. Identical HTML and width reuse a ready or loading buffer to avoid unnecessary reloads. Generation, load or measurement failures, including a 15-second load/measurement timeout, show an error and Retry while retaining any existing preview. Retry forces a new load. Preview iframes use `sandbox="allow-same-origin"` to block scripts and event handlers while allowing the host to measure height; this is not complete HTML sanitization and does not rewrite exported HTML. This cache only serves the UI; explicit export APIs synchronously generate from the latest document at call time.
46
+
47
+ ## Integration example
48
+
49
+ Wrap your template library as TemplateEngine and pass the same adapter to editing and standalone preview. The caller provides templateEngine in this example:
50
+
51
+ ```tsx
52
+ import {
53
+ EmailEditor, useEmailEditor, useEmailEditorConfig,
54
+ renderTemplatePreview, validateTemplate,
55
+ type TemplateEngine, type TemplateData,
56
+ } from "@standhigher/react-email-editor";
57
+
58
+ interface EmailPageProps {
59
+ templateEngine: TemplateEngine;
60
+ }
61
+
62
+ export function EmailPage({ templateEngine }: EmailPageProps) {
63
+ const config = useEmailEditorConfig({ templateEngine });
64
+ const editor = useEmailEditor({ config });
65
+ return <EmailEditor editor={editor} height={720} />;
66
+ }
67
+
68
+ export function previewHtml(
69
+ template: string,
70
+ data: TemplateData,
71
+ templateEngine: TemplateEngine,
72
+ ) {
73
+ return renderTemplatePreview(template, data, { templateEngine });
74
+ }
75
+
76
+ export function inspectTemplate(template: string, templateEngine: TemplateEngine) {
77
+ return validateTemplate(template, { templateEngine });
78
+ }
79
+ ```
80
+
81
+ validateTemplate() returns undefined when validation is unavailable and [] when no issues are found. It never blocks saving automatically. Rendering is per block and synchronous; the engine owns escaping. The basic engine preserves the entire input when it contains unsupported functions or pipelines.
@@ -0,0 +1,81 @@
1
+ # 模板预览与自定义引擎
2
+
3
+ [English](https://github.com/standhigher/react-email-editor/blob/main/docs/template-engines.md) | 简体中文
4
+
5
+ 通过 `useEmailEditorConfig({ templateEngine })` 注入同一个同步适配器,画布生成的 `renderedHtml`、编辑器预览、`getRenderedHtml()` 和 `getData().renderedHtml` 都使用该引擎。富文本编辑区域继续显示可编辑的模板原文。
6
+
7
+ ```ts
8
+ interface TemplateEngine {
9
+ render: (template: string, data: TemplateData) => string;
10
+ validate?: (template: string) => TemplateValidationIssue[];
11
+ }
12
+
13
+ interface TemplateValidationIssue {
14
+ message: string;
15
+ offset?: number; // 从 0 开始的原始模板字符位置
16
+ }
17
+
18
+ interface TemplateEngineOptions {
19
+ templateEngine?: TemplateEngine;
20
+ }
21
+ ```
22
+
23
+ 业务侧将自己的引擎包装为 `TemplateEngine`,包不额外安装完整 Go 引擎。`renderTemplatePreview(template, data?, { templateEngine })` 和 `validateTemplate(template, { templateEngine })` 使用相同的选择规则;省略引擎时使用公开的 `defaultTemplateEngine`。配置引用稳定时会复用;更换引擎对象会刷新展示与导出,不重置文档,也不触发文档变更回调。
24
+
25
+ - 引擎收到每个区块 `renderEmail` 生成的完整 HTML;预览与发送先按 `variables` 清单精确匹配 token 并补点,保留空白与 trim 标记,其他表达式原样传递,不先运行默认解析器。单独调用 `renderTemplatePreview()` 时则直接传入调用者的模板。区块分别执行,不能跨区块声明和引用局部变量。
26
+ - `TemplateData` 是键值对象;预览数据需可被 `structuredClone()` 复制。传给引擎的是独立副本,修改该副本不影响文档或调用方数据。
27
+ - 外部引擎负责函数、管道、作用域和 HTML 转义,包不再转义其返回值。应选择与后端一致的 `text/template` 或 `html/template` 语义及转义策略;仅注入适配器不保证不同引擎之间语义一致。
28
+ - 接口同步返回字符串,不支持 Promise。已有 `Error` 原样抛出,其他异常归一化为带原始 `cause` 和非空消息的 `Error`;渲染与校验遵循同一规则。界面使用现有错误提示及 Retry,不静默回退到默认引擎。显式 API 调用抛出异常。
29
+ - 默认引擎省略 `data` 时保留原文;显式配置外部引擎时,即使没有预览数据也调用它,并传入 `{}`。
30
+ - `validateTemplate()` 仅在显式调用时执行,返回诊断数组;`[]` 表示校验器未发现问题,`undefined` 表示所选引擎没有提供校验能力。默认引擎不提供 Go 语法校验;渲染、`editor.validate()` 和保存均不会自动执行模板校验。
31
+ - `getHtml()`、`getJson()` 和默认 `documentToHtml()` 保留模板;原有变量列表补点规则不变。引擎只在请求解析模板时运行。
32
+
33
+ 默认引擎支持 `.field`、嵌套字段、当前上下文 `.`、根变量 `$` / `$.field`,以及历史支持的无点字段名;支持 `true/false/nil`、十进制数、JSON 兼容的双引号字符串,基础 `if/else if/else`、`range/else`、`with/else` 和 `{{- -}}` 空白裁剪。数组按顺序遍历,对象按键排序;`range/with` 切换当前上下文,`if` 保持上下文。缺失字段输出空字符串,普通插值做 HTML 转义。
34
+
35
+ 字段名支持 Unicode 字母、数字和下划线,首字符为字母或下划线。条件中的 `null/undefined/false/0/空字符串`、空数组、无自有可枚举键的普通对象及空 `Map/Set` 为假;非空集合和其他对象(如 `Date`)为真。此处 `Map/Set` 仅约定条件真值,不扩展字段访问或遍历语义。
36
+
37
+ 默认引擎不支持函数调用(含 `eq/ne/and/or/not/len/index/printf/html`)、管道、括号表达式、局部变量、模板定义/调用、注释和其他完整 Go 语法。输入包含不支持的语法或不完整的控制结构时,**保留该次渲染的完整输入,不执行部分替换**;编辑器中意味着保留对应区块的 HTML 模板。需要完整 Go 兼容性时接入外部引擎。
38
+
39
+ ## 编辑与预览切换
40
+
41
+ 编辑画布保持挂载,切换预览不会重建富文本编辑器或 HTML 区块 iframe。编辑区与预览区分别保留滚动位置;预览时编辑画布不参与焦点、键盘和拖拽交互,富文本工具栏也会隐藏。
42
+
43
+ 文档、配置、变量或设备宽度变化后,完整邮件预览在停止修改 400ms 后排入空闲任务,连续修改时最长等待 2 秒;不支持 `requestIdleCallback` 时使用定时器。点击 Preview 或 Retry 会跳过等待并准备最新版本;进入预览后的后续修改仍使用相同防抖规则。HTML 生成仍在主线程同步执行,自定义引擎和 `renderEmail` 应保持纯函数,允许后台重复调用。
44
+
45
+ 预览使用两个 iframe 缓冲:最新内容在备用层加载并测量高度,完成后才交换显示。首次预览或最新内容尚未就绪时保留当前画面并显示准备提示;过期版本不会覆盖新内容。HTML 和宽度相同时复用已就绪或正在加载的缓冲,避免无意义重载。生成、加载或测量失败,以及加载测量超过 15 秒时显示错误和 Retry;已有预览仍保留,Retry 强制创建新加载。预览 iframe 使用 `sandbox="allow-same-origin"`,阻止脚本及事件属性执行,同时允许宿主测量高度;这不等同于完整 HTML 消毒,也不改写导出的 HTML。预览缓存仅服务于界面,显式导出 API 仍按调用时的最新文档同步生成。
46
+
47
+ ## 接入示例
48
+
49
+ 业务侧把实际模板库封装为 `TemplateEngine`,再把同一个适配器用于编辑器和独立预览。下面示例的 `templateEngine` 由调用方提供:
50
+
51
+ ```tsx
52
+ import {
53
+ EmailEditor, useEmailEditor, useEmailEditorConfig,
54
+ renderTemplatePreview, validateTemplate,
55
+ type TemplateEngine, type TemplateData,
56
+ } from "@standhigher/react-email-editor";
57
+
58
+ interface EmailPageProps {
59
+ templateEngine: TemplateEngine;
60
+ }
61
+
62
+ export function EmailPage({ templateEngine }: EmailPageProps) {
63
+ const config = useEmailEditorConfig({ templateEngine });
64
+ const editor = useEmailEditor({ config });
65
+ return <EmailEditor editor={editor} height={720} />;
66
+ }
67
+
68
+ export function previewHtml(
69
+ template: string,
70
+ data: TemplateData,
71
+ templateEngine: TemplateEngine,
72
+ ) {
73
+ return renderTemplatePreview(template, data, { templateEngine });
74
+ }
75
+
76
+ export function inspectTemplate(template: string, templateEngine: TemplateEngine) {
77
+ return validateTemplate(template, { templateEngine });
78
+ }
79
+ ```
80
+
81
+ `validateTemplate()` 返回 `undefined` 表示该引擎不提供校验,`[]` 表示校验器未发现问题。校验按需调用,不自动阻止保存。渲染以区块为单位;引擎负责转义,不支持异步返回。默认引擎不计算函数或管道,遇到未支持语法会保留完整输入。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@standhigher/react-email-editor",
3
- "version": "0.4.3",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "React email template editor with drag-and-drop blocks, rich text, JSON storage, and HTML export.",
6
6
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  "bugs": {
27
27
  "url": "https://github.com/standhigher/react-email-editor/issues"
28
28
  },
29
- "packageManager": "pnpm@10.16.1",
29
+ "packageManager": "npm@11.13.0",
30
30
  "exports": {
31
31
  ".": {
32
32
  "types": "./dist/index.d.ts",
@@ -54,12 +54,12 @@
54
54
  "build": "tsup",
55
55
  "build:watch": "tsup --watch src",
56
56
  "test": "node --test tests/*.test.mjs tests/*.test.ts",
57
- "check": "pnpm typecheck && pnpm lint && pnpm build && pnpm test",
58
- "prepack": "pnpm build",
59
- "prepublishOnly": "pnpm check",
57
+ "check": "npm run typecheck && npm run lint && npm run build && npm test",
58
+ "prepack": "npm run build",
59
+ "prepublishOnly": "npm run check",
60
60
  "typecheck": "tsc --noEmit && tsc --noEmit -p playground",
61
61
  "lint": "oxlint . --deny-warnings --report-unused-disable-directives",
62
- "lint:fix": "pnpm lint --fix"
62
+ "lint:fix": "npm run lint -- --fix"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "react": "^18",
@@ -78,11 +78,6 @@
78
78
  "typescript": "^5",
79
79
  "vite": "^8.3.5"
80
80
  },
81
- "pnpm": {
82
- "onlyBuiltDependencies": [
83
- "esbuild"
84
- ]
85
- },
86
81
  "dependencies": {
87
82
  "@codemirror/commands": "^6.11.1",
88
83
  "@codemirror/language": "^6.13.1",