@markopolo_ai_inc/markopolo-email-editor 1.0.3

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.
Files changed (91) hide show
  1. package/README.md +63 -0
  2. package/build/asset-manifest.json +12 -0
  3. package/build/favicon.ico +0 -0
  4. package/build/index.html +1 -0
  5. package/build/logo192.png +0 -0
  6. package/build/logo512.png +0 -0
  7. package/build/manifest.json +25 -0
  8. package/build/robots.txt +3 -0
  9. package/build/static/css/main.588cb535.css +9 -0
  10. package/build/static/js/206.a4343501.chunk.js +1 -0
  11. package/build/static/js/main.053d366a.js +2 -0
  12. package/build/static/js/main.053d366a.js.LICENSE.txt +56 -0
  13. package/package.json +64 -0
  14. package/public/favicon.ico +0 -0
  15. package/public/index.html +50 -0
  16. package/public/logo192.png +0 -0
  17. package/public/logo512.png +0 -0
  18. package/public/manifest.json +25 -0
  19. package/public/robots.txt +3 -0
  20. package/src/App.js +15 -0
  21. package/src/components/EmailEditor/assets/App.css +339 -0
  22. package/src/components/EmailEditor/assets/Columns.css +309 -0
  23. package/src/components/EmailEditor/assets/Header.css +174 -0
  24. package/src/components/EmailEditor/assets/ImageBlock.css +12 -0
  25. package/src/components/EmailEditor/assets/Preview.css +30 -0
  26. package/src/components/EmailEditor/assets/RichText.css +199 -0
  27. package/src/components/EmailEditor/assets/RightSettings.css +520 -0
  28. package/src/components/EmailEditor/assets/Sidebar.css +195 -0
  29. package/src/components/EmailEditor/components/BlockItems/ButtonBlock.js +25 -0
  30. package/src/components/EmailEditor/components/BlockItems/DividerBlock.js +19 -0
  31. package/src/components/EmailEditor/components/BlockItems/HeadingBlock.js +16 -0
  32. package/src/components/EmailEditor/components/BlockItems/ImageBlock.js +28 -0
  33. package/src/components/EmailEditor/components/BlockItems/MenuBlock.js +52 -0
  34. package/src/components/EmailEditor/components/BlockItems/SocialLinkBlocks.js +26 -0
  35. package/src/components/EmailEditor/components/BlockItems/SpacerBlock.js +23 -0
  36. package/src/components/EmailEditor/components/BlockItems/TextBlock.js +16 -0
  37. package/src/components/EmailEditor/components/BlockItems/index.js +25 -0
  38. package/src/components/EmailEditor/components/ColorPicker/index.js +26 -0
  39. package/src/components/EmailEditor/components/Column/index.js +253 -0
  40. package/src/components/EmailEditor/components/Header/index.js +243 -0
  41. package/src/components/EmailEditor/components/LeftSideBar/index.js +253 -0
  42. package/src/components/EmailEditor/components/Main/index.js +281 -0
  43. package/src/components/EmailEditor/components/Preview/index.js +97 -0
  44. package/src/components/EmailEditor/components/RichText/Bold.js +37 -0
  45. package/src/components/EmailEditor/components/RichText/FontColor.js +39 -0
  46. package/src/components/EmailEditor/components/RichText/InsertOrderedList.js +36 -0
  47. package/src/components/EmailEditor/components/RichText/InsertUnorderedList.js +36 -0
  48. package/src/components/EmailEditor/components/RichText/Italic.js +36 -0
  49. package/src/components/EmailEditor/components/RichText/Link.js +99 -0
  50. package/src/components/EmailEditor/components/RichText/RichTextLayout.js +53 -0
  51. package/src/components/EmailEditor/components/RichText/Strikethrough.js +36 -0
  52. package/src/components/EmailEditor/components/RichText/TextAlign.js +58 -0
  53. package/src/components/EmailEditor/components/RichText/Underline.js +36 -0
  54. package/src/components/EmailEditor/components/RichText/index.js +210 -0
  55. package/src/components/EmailEditor/components/RightSetting/index.js +126 -0
  56. package/src/components/EmailEditor/components/StyleSettings/ButtonStyleSettings.js +141 -0
  57. package/src/components/EmailEditor/components/StyleSettings/ColumnStyleSettings.js +241 -0
  58. package/src/components/EmailEditor/components/StyleSettings/DividerStyleSettings.js +111 -0
  59. package/src/components/EmailEditor/components/StyleSettings/HeadingStyleSettings.js +162 -0
  60. package/src/components/EmailEditor/components/StyleSettings/ImageStyleSettings.js +217 -0
  61. package/src/components/EmailEditor/components/StyleSettings/MenuStyleSettings.js +177 -0
  62. package/src/components/EmailEditor/components/StyleSettings/PaddingSettings.js +30 -0
  63. package/src/components/EmailEditor/components/StyleSettings/SocialLinkSettings.js +250 -0
  64. package/src/components/EmailEditor/components/StyleSettings/SpacerStyleSettings.js +38 -0
  65. package/src/components/EmailEditor/components/StyleSettings/TextStyleSettings.js +108 -0
  66. package/src/components/EmailEditor/components/StyleSettings/index.js +32 -0
  67. package/src/components/EmailEditor/configs/getBlockConfigsList.js +263 -0
  68. package/src/components/EmailEditor/configs/getColumnConfigFunc.js +59 -0
  69. package/src/components/EmailEditor/configs/getColumnsSettings.js +246 -0
  70. package/src/components/EmailEditor/configs/useDataSource.js +19 -0
  71. package/src/components/EmailEditor/index.js +93 -0
  72. package/src/components/EmailEditor/reducers/index.js +173 -0
  73. package/src/components/EmailEditor/translation/en.js +166 -0
  74. package/src/components/EmailEditor/translation/index.js +39 -0
  75. package/src/components/EmailEditor/translation/zh.js +166 -0
  76. package/src/components/EmailEditor/utils/classNames.js +5 -0
  77. package/src/components/EmailEditor/utils/dataToHTML.js +323 -0
  78. package/src/components/EmailEditor/utils/exportValidation.js +296 -0
  79. package/src/components/EmailEditor/utils/helpers.js +48 -0
  80. package/src/components/EmailEditor/utils/pexels.js +20 -0
  81. package/src/components/EmailEditor/utils/useSection.js +24 -0
  82. package/src/components/EmailEditor/utils/useStyleLayout.js +82 -0
  83. package/src/index.css +99 -0
  84. package/src/index.js +15 -0
  85. package/src/logo.svg +1 -0
  86. package/src/pages/AppPage/index.js +10 -0
  87. package/src/pages/Dashboard/Header.js +192 -0
  88. package/src/pages/Dashboard/defaultBlockList.json +1758 -0
  89. package/src/pages/Dashboard/index.js +48 -0
  90. package/src/reportWebVitals.js +13 -0
  91. package/src/setupTests.js +5 -0
@@ -0,0 +1,39 @@
1
+ import zh from "./zh";
2
+ import en from "./en";
3
+ import { useContext } from "react";
4
+ import { GlobalContext } from "../reducers";
5
+
6
+ const useTranslation = () => {
7
+ const { language, languageLibraries } = useContext(GlobalContext);
8
+ let langLibraries = { zh, en };
9
+
10
+ if (languageLibraries) {
11
+ langLibraries = {
12
+ ...langLibraries,
13
+ ...languageLibraries,
14
+ zh: {
15
+ ...zh,
16
+ ...(langLibraries["zh"] || {}),
17
+ },
18
+ en: {
19
+ ...en,
20
+ ...(langLibraries["en"] || {}),
21
+ },
22
+ };
23
+ }
24
+
25
+ const t = (key, metaTag) => {
26
+ const langLibrary = langLibraries[language] || {};
27
+ let translation = langLibrary[key];
28
+ if (metaTag && metaTag instanceof Object) {
29
+ Object.keys(metaTag).forEach((key) => {
30
+ translation = translation.replace(`{{${key}}}`, metaTag[key]);
31
+ });
32
+ }
33
+ return translation || key;
34
+ };
35
+
36
+ return { t };
37
+ };
38
+
39
+ export default useTranslation;
@@ -0,0 +1,166 @@
1
+ const zh = {
2
+ drag_block_here: "请将模块拖放到此处",
3
+ blocks: "模块",
4
+ photos: "图片",
5
+ powered_by_pexels: "图片由Pexels提供",
6
+ loading: "加载中...",
7
+ content: "内容",
8
+ body_settings: "邮件主题设置",
9
+ pre_header: "预标题",
10
+ pre_header_description: "预标题是在收件箱中查看电子邮件时跟随主题行的简短摘要文本。",
11
+ confirm: "确认",
12
+ cancel: "取消",
13
+ add_blocks: "请添加块",
14
+
15
+ // column block
16
+ column: "列",
17
+ columns: "列排版",
18
+ column_settings: "列设置",
19
+ column_styles: "列样式",
20
+ column_delete: "删除列",
21
+ column_delete_desc: "您确定删除多余的{{count}}列吗?",
22
+
23
+ //text block
24
+ text: "文本",
25
+ text_content: "这是一个文本,点击修改文本",
26
+ text_settings: "文本设置",
27
+ text_styles: "文本样式",
28
+ text_align: "文本对齐方式",
29
+
30
+ // heading block
31
+ heading: "标题",
32
+ heading_content: "这是一个标题,点击修改标题",
33
+ heading_settings: "标题设置",
34
+ heading_type: "标题类型",
35
+
36
+ // button block
37
+ button: "按钮",
38
+ button_settings: "按钮设置",
39
+ button_action: "点击按钮触发",
40
+ button_styles: "按钮样式",
41
+ button_padding: "按钮内边距",
42
+
43
+ // spacer block
44
+ spacer: "间距",
45
+ spacer_block: "间距块",
46
+ spacer_settings: "间距设置",
47
+ // divider block
48
+ divider: "分割线",
49
+ divider_settings: "分割线设置",
50
+ divider_type: "分割线类型",
51
+ divider_styles: "分割线样式",
52
+
53
+ // image block
54
+ image: "图片",
55
+ image_settings: "图片设置",
56
+ image_action: "点击图片跳转",
57
+ image_url: "图片URL",
58
+ image_alt: "图片alt",
59
+ image_styles: "图片样式",
60
+ image_source: "图片来源",
61
+ image_source_url: "输入URL",
62
+ image_source_upload: "上传图片",
63
+ image_upload: "上传",
64
+ image_upload_click: "点击上传",
65
+
66
+ // menu block
67
+ menu: "菜单",
68
+ menu_settings: "菜单设置",
69
+ menu_items: "菜单项",
70
+ menu_item: "菜单项",
71
+ menu_styles: "菜单样式",
72
+ add_new_item: "添加新项",
73
+ label: "标签",
74
+ page: "页面",
75
+ url: "链接地址",
76
+ target: "打开方式",
77
+ same_tab: "当前窗口",
78
+ new_tab: "新窗口",
79
+ separator: "分隔符",
80
+ letter_spacing: "字间距",
81
+ link_color: "链接颜色",
82
+
83
+ // social link block
84
+ social_link: "社交链接",
85
+ add_social_link: "添加社交链接",
86
+ social_link_size: "社交链接大小",
87
+ social_links: "社交链接",
88
+ social_link_settings: "社交链接设置",
89
+
90
+ // colors
91
+ content_background_color: "内容背景颜色",
92
+ background_color: "背景颜色",
93
+ text_color: "文本颜色",
94
+ email_theme_background_color: "邮件主题色",
95
+ font_color: "字体颜色",
96
+ button_color: "按钮颜色",
97
+ divider_color: "分割线颜色",
98
+
99
+ // styles
100
+ action_type: "按钮类型",
101
+ top: "上",
102
+ right: "右",
103
+ left: "左",
104
+ bottom: "下",
105
+ line_height: "行高",
106
+ link: "超链接",
107
+ link_url: "超链接URL",
108
+ padding_settings: "内边距设置",
109
+ width_auto: "宽度自适应",
110
+ width: "宽度",
111
+ height: "高度",
112
+ font_size: "字体大小",
113
+ font_family: "字体",
114
+ align: "对齐方式",
115
+ solid: "实线",
116
+ dotted: "虚线(点)",
117
+ dashed: "虚线(破折号)",
118
+ spacing: "间距",
119
+
120
+ // Dashboard / export
121
+ export_html: "导出html",
122
+ export_json: "导出JSON",
123
+ language_zh: "中文",
124
+
125
+ // Export validation modal
126
+ export_validation_title: "导出前校验",
127
+ export_validation_issues_found: "导出前发现以下问题",
128
+ export_validation_errors: "错误",
129
+ export_validation_warnings: "警告",
130
+ export_validation_info: "提示",
131
+ export_anyway: "仍要导出",
132
+ validation_body_content_width_invalid: "邮件内容宽度必须为正数。",
133
+ validation_body_background_color_missing: "主题背景色未设置。",
134
+ validation_body_text_color_missing: "主题文字颜色未设置。",
135
+ validation_body_font_family_missing: "主题字体未设置。",
136
+ validation_image_empty_src: "图片地址为空,图片将无法显示。",
137
+ validation_image_invalid_src: "图片地址格式无效。",
138
+ validation_image_empty_alt: "图片未设置 alt 文本,建议添加以提升可访问性。",
139
+ validation_button_empty_link: "按钮链接为空,将导出为 href=\"https://\"。",
140
+ validation_button_invalid_link: "按钮链接格式无效。",
141
+ validation_menu_empty_label: "菜单项标签为空,将显示为「链接」。",
142
+ validation_menu_empty_url: "菜单项链接为空,将导出为 href=\"#\"。",
143
+ validation_menu_invalid_url: "菜单项链接格式无效。",
144
+ validation_menu_no_items: "菜单块没有任何项。",
145
+ validation_social_empty_link: "社交链接地址为空,将导出为 href=\"https://\"。",
146
+ validation_social_invalid_link: "社交链接地址格式无效。",
147
+ validation_social_empty_icon: "社交链接图标地址为空,图标将无法显示。",
148
+ validation_social_invalid_icon: "社交链接图标地址格式无效。",
149
+ validation_social_no_links: "社交链接块没有任何链接。",
150
+
151
+ // Rich text tooltips
152
+ tooltip_bold: "加粗",
153
+ tooltip_italic: "斜体",
154
+ tooltip_underline: "下划线",
155
+ tooltip_strikethrough: "删除线",
156
+ tooltip_align_left: "居左",
157
+ tooltip_align_center: "居中",
158
+ tooltip_align_right: "居右",
159
+ tooltip_ordered_list: "有序列表",
160
+ tooltip_unordered_list: "无序列表",
161
+ tooltip_link: "超链接",
162
+ tooltip_remove_link: "删除超链接",
163
+ add_link_modal_title: "添加超链接",
164
+ };
165
+
166
+ export default zh;
@@ -0,0 +1,5 @@
1
+ const classNames = (...classes) => {
2
+ return classes.filter(Boolean).join(" ");
3
+ };
4
+
5
+ export default classNames;
@@ -0,0 +1,323 @@
1
+ const createStyleString = (className, styles) => {
2
+ const regex = new RegExp(/[A-Z]/g);
3
+ const kebabCase = (str) => str.replace(regex, (v) => `-${v.toLowerCase()}`);
4
+
5
+ let stylesConfig = {
6
+ className: className,
7
+ desktop: "",
8
+ mobile: "",
9
+ };
10
+
11
+ for (let item of Object.entries(styles.desktop)) {
12
+ if (item[1] && item[0] !== "contentBackground") {
13
+ stylesConfig.desktop += `${kebabCase(item[0])}:${typeof item[1] === "number" ? item[1] + "px" : item[1]};`;
14
+ }
15
+ }
16
+
17
+ if (Object.keys(styles.mobile).length) {
18
+ let mobile = "";
19
+ for (let item of Object.entries(styles.mobile)) {
20
+ if (item[1] && item[0] !== "contentBackground") {
21
+ mobile += `${kebabCase(item[0])}:${typeof item[1] === "number" ? item[1] + "px" : item[1]} !important;`;
22
+ }
23
+ }
24
+
25
+ stylesConfig.mobile += `@media(max-width:620px){
26
+ .${className} {
27
+ ${mobile}
28
+ }
29
+ }`;
30
+ }
31
+
32
+ return stylesConfig;
33
+ };
34
+
35
+ const createStyleTag = (list, styles = "", parentIndex) => {
36
+ const newBlockList = list.map((item, index) => {
37
+ let newItem = item;
38
+ newItem.styleConfig = createStyleString(parentIndex ? `${parentIndex}-${item.key}-${index}` : `${item.key}-${index}`, item.styles);
39
+
40
+ if (newItem.contentStyles) {
41
+ newItem.contentStyleConfig = createStyleString(`${item.key}-content-${index}`, item.contentStyles);
42
+
43
+ if (newItem.contentStyleConfig.mobile) {
44
+ styles += newItem.contentStyleConfig.mobile;
45
+ }
46
+ }
47
+ if (newItem.styleConfig.mobile) {
48
+ styles += newItem.styleConfig.mobile;
49
+ }
50
+
51
+ if (item.styles.desktop.contentBackground) {
52
+ newItem.contentStyleConfig = {
53
+ className: `${item.key}-content-${index}`,
54
+ desktop: `background-color:${item.styles.desktop.contentBackground};`,
55
+ mobile: "",
56
+ };
57
+ }
58
+
59
+ // contentBackground => column content background
60
+ if (item.styles.mobile.contentBackground) {
61
+ newItem.contentStyleConfig.mobile = ` @media(max-width:620px){
62
+ .${newItem.contentStyleConfig.className} {background-color:${item.styles.mobile.contentBackground};}
63
+ }`;
64
+ styles += newItem.contentStyleConfig.mobile;
65
+ }
66
+
67
+ if (item.children?.length) {
68
+ const { newBlockList: childrenList, styles: childrenStyles } = createStyleTag(item.children, styles, index);
69
+ styles += childrenStyles;
70
+ newItem.children = childrenList;
71
+ }
72
+
73
+ return {
74
+ ...newItem,
75
+ };
76
+ });
77
+
78
+ return {
79
+ newBlockList: newBlockList,
80
+ styles: styles,
81
+ };
82
+ };
83
+
84
+ const createImageString = (imageConfig) => {
85
+ return `<div ${imageConfig.contentStyleConfig.mobile ? `class="${imageConfig.contentStyleConfig.className}"` : ""}
86
+ style="${imageConfig.contentStyleConfig.desktop}">
87
+ <img src="${imageConfig.src}" alt="${imageConfig.alt}" style="max-width:100%;${imageConfig.styleConfig.desktop}"
88
+ ${imageConfig.styleConfig.mobile ? `class="${imageConfig.styleConfig.className}"` : ""}/>
89
+ </div>`;
90
+ };
91
+
92
+ const createTextString = (textBlock) => {
93
+ return `<div ${textBlock.styleConfig.mobile ? `class="${textBlock.styleConfig.className}"` : ""}
94
+ style="${textBlock.styleConfig.desktop}">${textBlock.text}</div>`;
95
+ };
96
+
97
+ const createHeaderString = (headerBlock) => {
98
+ return `<${headerBlock.type} ${headerBlock.styleConfig.mobile ? `class="${headerBlock.styleConfig.className}"` : ""}
99
+ style="${headerBlock.styleConfig.desktop}">
100
+ ${headerBlock.text}
101
+ </${headerBlock.type}>`;
102
+ };
103
+
104
+ const createButtonString = (buttonBlock) => {
105
+ return `<div ${buttonBlock.contentStyleConfig.mobile ? `class="${buttonBlock.contentStyleConfig.className}"` : ""}
106
+ style="${buttonBlock.contentStyleConfig.desktop}">
107
+ <a ${buttonBlock.styleConfig.mobile ? `class="${buttonBlock.styleConfig.className}"` : ""}
108
+ style="${buttonBlock.styleConfig.desktop}" target="_black" href="https://${buttonBlock.linkURL}">${buttonBlock.text}</a>
109
+ </div>`;
110
+ };
111
+
112
+ const createDividerString = (dividerBLock) => {
113
+ return `<div ${dividerBLock.contentStyleConfig.mobile ? `class="${dividerBLock.contentStyleConfig.className}"` : ""}
114
+ style="${dividerBLock.contentStyleConfig.desktop}">
115
+ <div ${dividerBLock.styleConfig.mobile ? `class="${dividerBLock.styleConfig.className}"` : ""}
116
+ style="${dividerBLock.styleConfig.desktop}"></div>
117
+ </div>`;
118
+ };
119
+
120
+ const createSpacerString = (spacerBlock) => {
121
+ return `<div ${spacerBlock.contentStyleConfig?.mobile ? `class="${spacerBlock.contentStyleConfig.className}"` : ""}
122
+ style="${spacerBlock.contentStyleConfig?.desktop || ""}">
123
+ <div ${spacerBlock.styleConfig?.mobile ? `class="${spacerBlock.styleConfig.className}"` : ""}
124
+ style="${spacerBlock.styleConfig?.desktop || ""}"></div>
125
+ </div>`;
126
+ };
127
+
128
+ const createMenuString = (menuBlock) => {
129
+ const items = menuBlock.list || [];
130
+ const sep = menuBlock.separator != null ? String(menuBlock.separator) : " | ";
131
+ const linkStyle = menuBlock.styleConfig?.desktop || "";
132
+ const wrapStyle = menuBlock.contentStyleConfig?.desktop || "";
133
+ const content = items
134
+ .map((item) => {
135
+ const href = item.url ? (item.url.startsWith("http") ? item.url : "https://" + item.url) : "#";
136
+ const target = item.target === "_blank" ? "_blank" : "_self";
137
+ const label = item.label || "Link";
138
+ return `<a target="${target}" href="${href}" style="${linkStyle};text-decoration:none;display:inline-block;">${label}</a>`;
139
+ })
140
+ .join(sep);
141
+ return `<div ${menuBlock.contentStyleConfig?.mobile ? `class="${menuBlock.contentStyleConfig.className}"` : ""} style="${wrapStyle}">${content}</div>`;
142
+ };
143
+
144
+ const createSocialLinkString = (socialLinkBlock) => {
145
+ return `<div ${socialLinkBlock.contentStyleConfig.mobile ? `class="${socialLinkBlock.contentStyleConfig.className}"` : ""}
146
+ style="${socialLinkBlock.contentStyleConfig.desktop}">
147
+ ${socialLinkBlock.list
148
+ .map((socialLinkItem) => {
149
+ const { image, title, linkURL } = socialLinkItem;
150
+ return `<a target="_black" href="https://${linkURL}" style="${socialLinkBlock.styleConfig.desktop};display:inline-block;">
151
+ <img src="${image}" alt="${title}" style="width:${socialLinkBlock.imageWidth}px;"
152
+ ${socialLinkBlock.styleConfig.mobile ? `class="${socialLinkBlock.styleConfig.className}"` : ""}/>
153
+ </a>`;
154
+ })
155
+ .join("")}
156
+ </div>`;
157
+ };
158
+
159
+ const blockListToHtml = (blockList, bodySettings) => {
160
+ let content = "";
161
+ blockList.forEach((item) => {
162
+ if (item.key === "column") {
163
+ content += `<div ${item.styleConfig.mobile ? `class="${item.styleConfig.className}"` : ""}
164
+ style="${item.styleConfig.desktop};width:100%;display:block;">
165
+ <table ${item.contentStyleConfig.mobile ? `class="${item.contentStyleConfig.className}"` : ""}
166
+ style="width:100%;max-width:${bodySettings.contentWidth}px;margin:0 auto;${item.contentStyleConfig.desktop}">
167
+ <tbody><tr>${blockListToHtml(item.children)}</tr></tbody>
168
+ </table></div>`;
169
+ }
170
+
171
+ if (item.key === "content") {
172
+ content += `<td ${item.styleConfig.mobile ? `class="${item.styleConfig.className}"` : ""}
173
+ style="width:${item.width}; ${item.styleConfig.desktop}">${blockListToHtml(item.children)}</td>`;
174
+ }
175
+
176
+ if (item.key === "text") {
177
+ content += createTextString(item);
178
+ }
179
+
180
+ if (item.key === "heading") {
181
+ content += createHeaderString(item);
182
+ }
183
+
184
+ if (item.key === "image") {
185
+ content += createImageString(item);
186
+ }
187
+
188
+ if (item.key === "button") {
189
+ content += createButtonString(item);
190
+ }
191
+
192
+ if (item.key === "divider") {
193
+ content += createDividerString(item);
194
+ }
195
+
196
+ if (item.key === "spacer") {
197
+ content += createSpacerString(item);
198
+ }
199
+
200
+ if (item.key === "menu") {
201
+ content += createMenuString(item);
202
+ }
203
+
204
+ if (item.key === "social_link") {
205
+ content += createSocialLinkString(item);
206
+ }
207
+ });
208
+
209
+ return content;
210
+ };
211
+
212
+ const DEFAULT_BODY_SETTINGS = {
213
+ preHeader: "",
214
+ contentWidth: 600,
215
+ styles: {
216
+ backgroundColor: "#f5f5f5",
217
+ color: "#152b2a",
218
+ fontFamily: "Arial",
219
+ },
220
+ };
221
+
222
+ const dataToHtml = ({ bodySettings, blockList }) => {
223
+ let content = "";
224
+ const { newBlockList, styles } = createStyleTag(blockList);
225
+ content = blockListToHtml(newBlockList, bodySettings);
226
+ return `<html>
227
+ <head>
228
+ <meta charset="UTF-8">
229
+ <title>email</title>
230
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
231
+ <style type="text/css">
232
+ *{
233
+ margin: 0;
234
+ padding: 0;
235
+ border: none;
236
+ box-sizing: border-box;
237
+ }
238
+
239
+ html,body {
240
+ height:100%;
241
+ overflow-y:auto;
242
+ }
243
+
244
+ table {
245
+ width: 100%;
246
+ color:unset;
247
+ }
248
+
249
+ table, tr, td {
250
+ vertical-align: top;
251
+ border-collapse: collapse;
252
+ }
253
+
254
+ h1,h2,h3,h4 {
255
+ display: block;
256
+ margin-block-start: 0px;
257
+ margin-block-end: 0px;
258
+ margin-inline-start: 0px;
259
+ margin-inline-end: 0px;
260
+ font-weight: bold;
261
+ }
262
+
263
+ @media(max-width:620px){
264
+ td {
265
+ display:inline-block;
266
+ width:100% !important;
267
+ }
268
+ }
269
+ ${styles}
270
+ </style>
271
+ </head>
272
+ <body>
273
+ <div style="opacity:0;">${bodySettings.preHeader}</div>
274
+ <div style="background-color:${bodySettings.styles.backgroundColor};color:${bodySettings.styles.color}; font-family:${bodySettings.styles.fontFamily};"> ${content}</div>
275
+ </body>
276
+ </html>`;
277
+ };
278
+
279
+ /**
280
+ * Export normalizer: accepts exported template JSON and returns a complete HTML file string.
281
+ * @param {string|object} input - Template JSON as string or parsed object. Supports:
282
+ * - Legacy: array of column blocks (blockList only)
283
+ * - Full: { blockList, bodySettings }
284
+ * @returns {string} Complete HTML document (with doctype, head, styles, body)
285
+ */
286
+ export function exportNormalizerToHtml(input) {
287
+ let data = input;
288
+ if (typeof input === "string") {
289
+ try {
290
+ data = JSON.parse(input);
291
+ } catch (e) {
292
+ throw new Error("exportNormalizerToHtml: invalid JSON");
293
+ }
294
+ }
295
+ if (data == null) {
296
+ throw new Error("exportNormalizerToHtml: input is null or undefined");
297
+ }
298
+
299
+ let blockList = [];
300
+ let bodySettings = { ...DEFAULT_BODY_SETTINGS, styles: { ...DEFAULT_BODY_SETTINGS.styles } };
301
+
302
+ if (Array.isArray(data)) {
303
+ blockList = data;
304
+ } else if (typeof data === "object") {
305
+ blockList = Array.isArray(data.blockList) ? data.blockList : [];
306
+ if (data.bodySettings && typeof data.bodySettings === "object") {
307
+ bodySettings = {
308
+ preHeader: data.bodySettings.preHeader != null ? data.bodySettings.preHeader : DEFAULT_BODY_SETTINGS.preHeader,
309
+ contentWidth: Number(data.bodySettings.contentWidth) || DEFAULT_BODY_SETTINGS.contentWidth,
310
+ styles: {
311
+ backgroundColor: data.bodySettings.styles?.backgroundColor ?? DEFAULT_BODY_SETTINGS.styles.backgroundColor,
312
+ color: data.bodySettings.styles?.color ?? DEFAULT_BODY_SETTINGS.styles.color,
313
+ fontFamily: data.bodySettings.styles?.fontFamily ?? DEFAULT_BODY_SETTINGS.styles.fontFamily,
314
+ },
315
+ };
316
+ }
317
+ }
318
+
319
+ const html = dataToHtml({ bodySettings, blockList });
320
+ return `<!DOCTYPE html>\n${html}`;
321
+ }
322
+
323
+ export default dataToHtml;